Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
209 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
const _ = require('lodash') | ||
const BaseChecker = require('./../base-checker') | ||
|
||
const DEFAULT_MAX_CHARACTERS_LONG = 32 | ||
|
||
const ruleId = 'reason-string' | ||
const meta = { | ||
type: 'best-practises', | ||
|
||
docs: { | ||
description: | ||
'Require or revert statement must have a reason string and check that each reason string is at most N characters long.', | ||
category: 'Best Practise Rules' | ||
}, | ||
|
||
isDefault: false, | ||
recommended: false, | ||
defaultSetup: ['warn', { maxLength: DEFAULT_MAX_CHARACTERS_LONG }], | ||
|
||
schema: [ | ||
{ | ||
type: 'array', | ||
items: [ | ||
{ | ||
properties: { | ||
maxLength: { | ||
type: 'integer' | ||
} | ||
}, | ||
additionalProperties: false | ||
} | ||
], | ||
uniqueItems: true, | ||
minItems: 2 | ||
} | ||
] | ||
} | ||
|
||
class ReasonStringChecker extends BaseChecker { | ||
constructor(reporter, config) { | ||
super(reporter, ruleId, meta) | ||
|
||
this.maxCharactersLong = | ||
(config && | ||
config.getObjectPropertyNumber(ruleId, 'maxLength', DEFAULT_MAX_CHARACTERS_LONG)) || | ||
DEFAULT_MAX_CHARACTERS_LONG | ||
} | ||
|
||
enterFunctionCallArguments(ctx) { | ||
if (this.isReasonStringStatement(ctx)) { | ||
const functionParameters = this.getFunctionParameters(ctx) | ||
const functionName = this.getFunctionName(ctx) | ||
|
||
// Throw an error if have no message | ||
if (functionParameters.length <= 1) { | ||
this._errorHaveNoMessage(ctx, functionName) | ||
return | ||
} | ||
|
||
const lastFunctionParameter = _.last(functionParameters) | ||
const hasReasonMessage = this.hasReasonMessage(lastFunctionParameter) | ||
|
||
// If has reason message and is too long, throw an error | ||
if (hasReasonMessage) { | ||
const lastParameterWithoutQuotes = lastFunctionParameter.replace(/['"]+/g, '') | ||
if (lastParameterWithoutQuotes.length > this.maxCharactersLong) { | ||
this._errorMessageIsTooLong(ctx, functionName) | ||
} | ||
} | ||
} | ||
} | ||
|
||
isReasonStringStatement(ctx) { | ||
const name = this.getFunctionName(ctx) | ||
return name === 'revert' || name === 'require' | ||
} | ||
|
||
getFunctionName(ctx) { | ||
const parent = ctx.parentCtx | ||
const name = parent.children[0] | ||
return name.getText() | ||
} | ||
|
||
getFunctionParameters(ctx) { | ||
const parent = ctx.parentCtx | ||
const nodes = parent.children[2] | ||
const children = nodes.children[0].children | ||
const parameters = children | ||
.filter(value => value.getText() !== ',') | ||
.map(value => value.getText()) | ||
return parameters | ||
} | ||
|
||
hasReasonMessage(str) { | ||
return str.indexOf("'") >= 0 || str.indexOf('"') >= 0 | ||
} | ||
|
||
_errorHaveNoMessage(ctx, key) { | ||
this.error(ctx, `Provide an error message for ${key}`) | ||
} | ||
|
||
_errorMessageIsTooLong(ctx, key) { | ||
this.error(ctx, `Error message for ${key} is too long`) | ||
} | ||
} | ||
|
||
module.exports = ReasonStringChecker |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
const assert = require('assert') | ||
const { | ||
assertNoWarnings, | ||
assertNoErrors, | ||
assertErrorMessage, | ||
assertWarnsCount, | ||
assertErrorCount | ||
} = require('./../../common/asserts') | ||
const linter = require('./../../../lib/index') | ||
const { funcWith } = require('./../../common/contract-builder') | ||
|
||
describe('Linter - reason-string', () => { | ||
it('should raise reason string is mandatory for require', () => { | ||
const code = funcWith(`require(!has(role, account)); | ||
role.bearer[account] = true;role.bearer[account] = true; | ||
`) | ||
|
||
const report = linter.processStr(code, { | ||
rules: { 'reason-string': ['warn', { maxLength: 5 }] } | ||
}) | ||
|
||
assert.ok(report.warningCount > 0) | ||
assertWarnsCount(report, 1) | ||
assertErrorMessage(report, 'Provide an error message for require') | ||
}) | ||
|
||
it('should raise reason string is mandatory for revert', () => { | ||
const code = funcWith(`revert(!has(role, account)); | ||
role.bearer[account] = true;role.bearer[account] = true; | ||
`) | ||
|
||
const report = linter.processStr(code, { | ||
rules: { 'reason-string': ['error', { maxLength: 5 }] } | ||
}) | ||
|
||
assert.ok(report.errorCount > 0) | ||
assertErrorCount(report, 1) | ||
assertErrorMessage(report, 'Provide an error message for revert') | ||
}) | ||
|
||
it('should raise reason string maxLength error for require', () => { | ||
const code = funcWith(`require(!has(role, account), "Roles: account already has role"); | ||
role.bearer[account] = true;role.bearer[account] = true; | ||
`) | ||
|
||
const report = linter.processStr(code, { | ||
rules: { 'reason-string': ['warn', { maxLength: 5 }] } | ||
}) | ||
|
||
assert.ok(report.warningCount > 0) | ||
assertWarnsCount(report, 1) | ||
assertErrorMessage(report, 'Error message for require is too long') | ||
}) | ||
|
||
it('should raise reason string maxLength error for revert', () => { | ||
const code = funcWith(`revert(!has(role, account), "Roles: account already has role"); | ||
role.bearer[account] = true;role.bearer[account] = true; | ||
`) | ||
|
||
const report = linter.processStr(code, { | ||
rules: { 'reason-string': ['error', { maxLength: 5 }] } | ||
}) | ||
|
||
assert.ok(report.errorCount > 0) | ||
assertErrorCount(report, 1) | ||
assertErrorMessage(report, 'Error message for revert is too long') | ||
}) | ||
|
||
it('should not raise warning for require', () => { | ||
const code = funcWith(`require(!has(role, account), "Roles: account already has role"); | ||
role.bearer[account] = true;role.bearer[account] = true; | ||
`) | ||
|
||
const report = linter.processStr(code, { | ||
rules: { 'reason-string': ['warn', { maxLength: 31 }] } | ||
}) | ||
|
||
assertNoWarnings(report) | ||
assertNoErrors(report) | ||
}) | ||
|
||
it('should not raise error for revert', () => { | ||
const code = funcWith(`revert(!has(role, account), "Roles: account already has role"); | ||
role.bearer[account] = true;role.bearer[account] = true; | ||
`) | ||
|
||
const report = linter.processStr(code, { | ||
rules: { 'reason-string': ['error', { maxLength: 50 }] } | ||
}) | ||
|
||
assertNoWarnings(report) | ||
assertNoErrors(report) | ||
}) | ||
}) |