diff --git a/.gitignore b/.gitignore index 77dd2e5e..063bb153 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,4 @@ convertLib.sol antlr4.jar /docs/.sass-cache/ _temp/ -.solhint.json + diff --git a/CHANGELOG.md b/CHANGELOG.md index ecc118fb..92070ed1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,43 @@ +## [3.6.1] - 2023-08-11 + +### BREAKING CHANGE +- RULE: `not-rely-on-time` was REMOVED from RECOMMENDED ruleset
+This was long overdue.
+Beware!! If you are relying on this rule and it is not explicitly configured (meaning there's only `solhint:recommended` option).
+You should add this rule manually: +```json + { + "extends": "solhint:recommended", + "rules": { + "not-rely-on-time": "warn", + "compiler-version": "off" + }, + } +``` +If not explicitly added, this rule will not be executed. + +### SPECIAL ATTENTION +- RULE: `compiler-version` default was updated from ^0.5.2 to ^0.8.0 + +### Added +- New Rule: Enforces the use of Custom Errors over Require and Revert statements [#475](https://github.com/protofire/solhint/pull/475) +- New Rule: Enforces the test_ prefix on a file for Foundry users [#476](https://github.com/protofire/solhint/pull/476) +- New Rule: Enforces the naming of function return values [#478](https://github.com/protofire/solhint/pull/478) +- `Notes` option on docs to add more information of each rule. See `foundry-test-functions`. [#476](https://github.com/protofire/solhint/pull/476) + +### Fixed +- `func-named-parameters` - false positives on builtin functions [#472](https://github.com/protofire/solhint/pull/472) +- `ordering` - treat initializer weight same as constructor [#474](https://github.com/protofire/solhint/pull/474) +- `check-send-result` - false positive on `erc777.send()`` function [#477](https://github.com/protofire/solhint/pull/477) +- `explicit-types` - default value is now taking into account when no value is specified in config [#481](https://github.com/protofire/solhint/pull/481) +- `compiler-version` - default value is now taking into account when no value is specified in config [#483](https://github.com/protofire/solhint/pull/483) + +### Updates +- Rule: `check-send-result` added config clarification in the new `Notes` section [#482](https://github.com/protofire/solhint/pull/482) +- Rule: `compiler-version` default was updated from ^0.5.2 to ^0.8.0 [#483](https://github.com/protofire/solhint/pull/483) + + + ## [3.5.1] - 2023-08-04 ### Updated - Support `ignoreConstructors` option for `no-empty-blocks` [#418](https://github.com/protofire/solhint/pull/418) @@ -30,7 +70,6 @@ -

## [3.4.1] - 2023-03-06 ### Updated - Updated solidity parser to 0.16.0 [#420](https://github.com/protofire/solhint/pull/420) @@ -46,10 +85,6 @@ - Fix named-parameters-mapping to not enforce on nested mappings [#421](https://github.com/protofire/solhint/pull/421) - - - -

## [3.4.0] - 2023-02-17 ### Updated - Solhint dependencies to support newer versions [#380](https://github.com/protofire/solhint/pull/380) @@ -73,10 +108,6 @@ - False positive on no-unused-vars for payable arguments without name [#399](https://github.com/protofire/solhint/pull/399) - - - -

## [3.3.8] - 2023-01-17 ### Fixed Docs and Typos - [#292](https://github.com/protofire/solhint/pull/292) diff --git a/conf/rulesets/solhint-all.js b/conf/rulesets/solhint-all.js index 6ceced1e..eab50c55 100644 --- a/conf/rulesets/solhint-all.js +++ b/conf/rulesets/solhint-all.js @@ -6,6 +6,7 @@ module.exports = Object.freeze({ rules: { 'code-complexity': ['warn', 7], + 'custom-errors': 'warn', 'explicit-types': ['warn', 'explicit'], 'function-max-lines': ['warn', 50], 'max-line-length': ['error', 120], @@ -28,8 +29,9 @@ module.exports = Object.freeze({ 'const-name-snakecase': 'warn', 'contract-name-camelcase': 'warn', 'event-name-camelcase': 'warn', + 'foundry-test-functions': ['off', ['setUp']], 'func-name-mixedcase': 'warn', - 'func-named-parameters': ['warn', 2], + 'func-named-parameters': ['warn', 4], 'func-param-name-mixedcase': 'warn', 'immutable-vars-naming': [ 'warn', @@ -39,6 +41,7 @@ module.exports = Object.freeze({ ], 'modifier-name-mixedcase': 'warn', 'named-parameters-mapping': 'off', + 'named-return-values': 'warn', 'private-vars-leading-underscore': [ 'warn', { @@ -57,7 +60,7 @@ module.exports = Object.freeze({ 'avoid-throw': 'warn', 'avoid-tx-origin': 'warn', 'check-send-result': 'warn', - 'compiler-version': ['error', '^0.5.8'], + 'compiler-version': ['error', '^0.8.0'], 'func-visibility': [ 'warn', { diff --git a/conf/rulesets/solhint-recommended.js b/conf/rulesets/solhint-recommended.js index dbd05e77..072ea68e 100644 --- a/conf/rulesets/solhint-recommended.js +++ b/conf/rulesets/solhint-recommended.js @@ -5,6 +5,7 @@ module.exports = Object.freeze({ rules: { + 'custom-errors': 'warn', 'explicit-types': ['warn', 'explicit'], 'max-states-count': ['warn', 15], 'no-console': 'error', @@ -24,7 +25,6 @@ module.exports = Object.freeze({ 'contract-name-camelcase': 'warn', 'event-name-camelcase': 'warn', 'func-name-mixedcase': 'warn', - 'func-named-parameters': ['warn', 2], 'immutable-vars-naming': [ 'warn', { @@ -42,7 +42,7 @@ module.exports = Object.freeze({ 'avoid-throw': 'warn', 'avoid-tx-origin': 'warn', 'check-send-result': 'warn', - 'compiler-version': ['error', '^0.5.8'], + 'compiler-version': ['error', '^0.8.0'], 'func-visibility': [ 'warn', { @@ -53,7 +53,6 @@ module.exports = Object.freeze({ 'no-complex-fallback': 'warn', 'no-inline-assembly': 'warn', 'not-rely-on-block-hash': 'warn', - 'not-rely-on-time': 'warn', reentrancy: 'warn', 'state-visibility': 'warn', }, diff --git a/docs/rules.md b/docs/rules.md index 24d799a8..00b344b9 100644 --- a/docs/rules.md +++ b/docs/rules.md @@ -9,6 +9,7 @@ title: "Rule Index of Solhint" | Rule Id | Error | Recommended | Deprecated | | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------- | ------------ | ---------- | | [code-complexity](./rules/best-practises/code-complexity.md) | Function has cyclomatic complexity "current" but allowed no more than maxcompl. | | | +| [custom-errors](./rules/best-practises/custom-errors.md) | Enforces the use of Custom Errors over Require and Revert statements | $~~~~~~~~$✔️ | | | [explicit-types](./rules/best-practises/explicit-types.md) | Forbid or enforce explicit types (like uint256) that have an alias (like uint). | $~~~~~~~~$✔️ | | | [function-max-lines](./rules/best-practises/function-max-lines.md) | Function body contains "count" lines but allowed no more than maxlines. | | | | [max-line-length](./rules/best-practises/max-line-length.md) | Line length must be no more than maxlen. | | | @@ -33,24 +34,26 @@ title: "Rule Index of Solhint" ## Style Guide Rules -| Rule Id | Error | Recommended | Deprecated | -| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ------------ | ----------- | -| [const-name-snakecase](./rules/naming/const-name-snakecase.md) | Constant name must be in capitalized SNAKE_CASE. (Does not check IMMUTABLES, use immutable-vars-naming) | $~~~~~~~~$✔️ | | -| [contract-name-camelcase](./rules/naming/contract-name-camelcase.md) | Contract name must be in CamelCase. | $~~~~~~~~$✔️ | | -| [event-name-camelcase](./rules/naming/event-name-camelcase.md) | Event name must be in CamelCase. | $~~~~~~~~$✔️ | | -| [func-name-mixedcase](./rules/naming/func-name-mixedcase.md) | Function name must be in mixedCase. | $~~~~~~~~$✔️ | | -| [func-named-parameters](./rules/naming/func-named-parameters.md) | Enforce function calls with Named Parameters when containing more than the configured qty | $~~~~~~~~$✔️ | | -| [func-param-name-mixedcase](./rules/naming/func-param-name-mixedcase.md) | Function param name must be in mixedCase. | | | -| [immutable-vars-naming](./rules/naming/immutable-vars-naming.md) | Check Immutable variables. Capitalized SNAKE_CASE or mixedCase depending on configuration. | $~~~~~~~~$✔️ | | -| [modifier-name-mixedcase](./rules/naming/modifier-name-mixedcase.md) | Modifier name must be in mixedCase. | | | -| [named-parameters-mapping](./rules/naming/named-parameters-mapping.md) | Solidity v0.8.18 introduced named parameters on the mappings definition. | | | -| [private-vars-leading-underscore](./rules/naming/private-vars-leading-underscore.md) | Private and internal names must start with a single underscore. | | | -| [use-forbidden-name](./rules/naming/use-forbidden-name.md) | Avoid to use letters 'I', 'l', 'O' as identifiers. | $~~~~~~~~$✔️ | | -| [var-name-mixedcase](./rules/naming/var-name-mixedcase.md) | Variable name must be in mixedCase. (Does not check IMMUTABLES, use immutable-vars-naming) | $~~~~~~~~$✔️ | | -| [func-order](./rules/order/func-order.md) | Function order is incorrect. | | $~~~~~~~$✔️ | -| [imports-on-top](./rules/order/imports-on-top.md) | Import statements must be on top. | $~~~~~~~~$✔️ | | -| [ordering](./rules/order/ordering.md) | Check order of elements in file and inside each contract, according to the style guide. | | | -| [visibility-modifier-order](./rules/order/visibility-modifier-order.md) | Visibility modifier must be first in list of modifiers. | $~~~~~~~~$✔️ | | +| Rule Id | Error | Recommended | Deprecated | +| ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------ | ----------- | +| [const-name-snakecase](./rules/naming/const-name-snakecase.md) | Constant name must be in capitalized SNAKE_CASE. (Does not check IMMUTABLES, use immutable-vars-naming) | $~~~~~~~~$✔️ | | +| [contract-name-camelcase](./rules/naming/contract-name-camelcase.md) | Contract name must be in CamelCase. | $~~~~~~~~$✔️ | | +| [event-name-camelcase](./rules/naming/event-name-camelcase.md) | Event name must be in CamelCase. | $~~~~~~~~$✔️ | | +| [foundry-test-functions](./rules/naming/foundry-test-functions.md) | Enforce naming convention on functions for Foundry test cases | | | +| [func-name-mixedcase](./rules/naming/func-name-mixedcase.md) | Function name must be in mixedCase. | $~~~~~~~~$✔️ | | +| [func-named-parameters](./rules/naming/func-named-parameters.md) | Enforce named parameters for function calls with 4 or more arguments. This rule may have some false positives | | | +| [func-param-name-mixedcase](./rules/naming/func-param-name-mixedcase.md) | Function param name must be in mixedCase. | | | +| [immutable-vars-naming](./rules/naming/immutable-vars-naming.md) | Check Immutable variables. Capitalized SNAKE_CASE or mixedCase depending on configuration. | $~~~~~~~~$✔️ | | +| [modifier-name-mixedcase](./rules/naming/modifier-name-mixedcase.md) | Modifier name must be in mixedCase. | | | +| [named-parameters-mapping](./rules/naming/named-parameters-mapping.md) | Solidity v0.8.18 introduced named parameters on the mappings definition. | | | +| [named-return-values](./rules/naming/named-return-values.md) | Enforce the return values of a function to be named | | | +| [private-vars-leading-underscore](./rules/naming/private-vars-leading-underscore.md) | Private and internal names must start with a single underscore. | | | +| [use-forbidden-name](./rules/naming/use-forbidden-name.md) | Avoid to use letters 'I', 'l', 'O' as identifiers. | $~~~~~~~~$✔️ | | +| [var-name-mixedcase](./rules/naming/var-name-mixedcase.md) | Variable name must be in mixedCase. (Does not check IMMUTABLES, use immutable-vars-naming) | $~~~~~~~~$✔️ | | +| [func-order](./rules/order/func-order.md) | Function order is incorrect. | | $~~~~~~~$✔️ | +| [imports-on-top](./rules/order/imports-on-top.md) | Import statements must be on top. | $~~~~~~~~$✔️ | | +| [ordering](./rules/order/ordering.md) | Check order of elements in file and inside each contract, according to the style guide. | | | +| [visibility-modifier-order](./rules/order/visibility-modifier-order.md) | Visibility modifier must be first in list of modifiers. | $~~~~~~~~$✔️ | | ## Security Rules @@ -71,7 +74,7 @@ title: "Rule Index of Solhint" | [no-complex-fallback](./rules/security/no-complex-fallback.md) | Fallback function must be simple. | $~~~~~~~~$✔️ | | | [no-inline-assembly](./rules/security/no-inline-assembly.md) | Avoid to use inline assembly. It is acceptable only in rare cases. | $~~~~~~~~$✔️ | | | [not-rely-on-block-hash](./rules/security/not-rely-on-block-hash.md) | Do not rely on "block.blockhash". Miners can influence its value. | $~~~~~~~~$✔️ | | -| [not-rely-on-time](./rules/security/not-rely-on-time.md) | Avoid making time-based decisions in your business logic. | $~~~~~~~~$✔️ | | +| [not-rely-on-time](./rules/security/not-rely-on-time.md) | Avoid making time-based decisions in your business logic. | | | | [reentrancy](./rules/security/reentrancy.md) | Possible reentrancy vulnerabilities. Avoid state changes after transfer. | $~~~~~~~~$✔️ | | | [state-visibility](./rules/security/state-visibility.md) | Explicitly mark visibility of state. | $~~~~~~~~$✔️ | | diff --git a/docs/rules/best-practises/custom-errors.md b/docs/rules/best-practises/custom-errors.md new file mode 100644 index 00000000..afe5b3c7 --- /dev/null +++ b/docs/rules/best-practises/custom-errors.md @@ -0,0 +1,71 @@ +--- +warning: "This is a dynamically generated file. Do not edit manually." +layout: "default" +title: "custom-errors | Solhint" +--- + +# custom-errors +![Recommended Badge](https://img.shields.io/badge/-Recommended-brightgreen) +![Category Badge](https://img.shields.io/badge/-Best%20Practise%20Rules-informational) +![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow) +> The {"extends": "solhint:recommended"} property in a configuration file enables this rule. + + +## Description +Enforces the use of Custom Errors over Require and Revert statements + +## Options +This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Default to warn. + +### Example Config +```json +{ + "rules": { + "custom-errors": "warn" + } +} +``` + + +## Examples +### 👍 Examples of **correct** code for this rule + +#### Use of Custom Errors + +```solidity +revert CustomErrorFunction(); +``` + +#### Use of Custom Errors with arguments + +```solidity +revert CustomErrorFunction({ msg: "Insufficient Balance" }); +``` + +### 👎 Examples of **incorrect** code for this rule + +#### Use of require statement + +```solidity +require(userBalance >= availableAmount, "Insufficient Balance"); +``` + +#### Use of plain revert statement + +```solidity +revert(); +``` + +#### Use of revert statement with message + +```solidity +revert("Insufficient Balance"); +``` + +## Version +This rule is introduced in the latest version. + +## Resources +- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/best-practises/custom-errors.js) +- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/best-practises/custom-errors.md) +- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/best-practises/custom-errors.js) diff --git a/docs/rules/best-practises/explicit-types.md b/docs/rules/best-practises/explicit-types.md index 96cb06a5..8448bc6e 100644 --- a/docs/rules/best-practises/explicit-types.md +++ b/docs/rules/best-practises/explicit-types.md @@ -63,7 +63,7 @@ uint256 public variableName ``` ## Version -This rule is introduced in the latest version. +This rule was introduced in [Solhint 3.5.1](https://github.com/protofire/solhint/tree/v3.5.1) ## Resources - [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/best-practises/explicit-types.js) diff --git a/docs/rules/best-practises/no-unused-import.md b/docs/rules/best-practises/no-unused-import.md index 8d13e897..8d567c91 100644 --- a/docs/rules/best-practises/no-unused-import.md +++ b/docs/rules/best-practises/no-unused-import.md @@ -51,7 +51,7 @@ This rule accepts a string option of rule severity. Must be one of "error", "war ``` ## Version -This rule is introduced in the latest version. +This rule was introduced in [Solhint 3.5.1](https://github.com/protofire/solhint/tree/v3.5.1) ## Resources - [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/best-practises/no-unused-import.js) diff --git a/docs/rules/naming/foundry-test-functions.md b/docs/rules/naming/foundry-test-functions.md new file mode 100644 index 00000000..534ad6e5 --- /dev/null +++ b/docs/rules/naming/foundry-test-functions.md @@ -0,0 +1,73 @@ +--- +warning: "This is a dynamically generated file. Do not edit manually." +layout: "default" +title: "foundry-test-functions | Solhint" +--- + +# foundry-test-functions +![Category Badge](https://img.shields.io/badge/-Style%20Guide%20Rules-informational) +![Default Severity Badge off](https://img.shields.io/badge/Default%20Severity-off-undefined) + +## Description +Enforce naming convention on functions for Foundry test cases + +## Options +This rule accepts an array of options: + +| Index | Description | Default Value | +| ----- | ----------------------------------------------------- | ------------- | +| 0 | Rule severity. Must be one of "error", "warn", "off". | off | + + +### Example Config +```json +{ + "rules": { + "foundry-test-functions": ["off",["setUp"]] + } +} +``` + +### Notes +- This rule can be configured to skip certain function names in the SKIP array. In Example Config. ```setUp``` function will be skipped +- Supported Regex: ```test(Fork)?(Fuzz)?(Fail)?_(Revert(If_|When_){1})?\w{1,}``` +- This rule should be executed in a separate folder with a separate .solhint.json => ```solhint --config .solhint.json testFolder/**/*.sol``` +- This rule applies only to `external` and `public` functions +- This rule skips the `setUp()` function + +## Examples +### 👍 Examples of **correct** code for this rule + +#### Foundry test case with correct Function declaration + +```solidity +function test_NumberIs42() public {} +``` + +#### Foundry test case with correct Function declaration + +```solidity +function testFail_Subtract43() public {} +``` + +#### Foundry test case with correct Function declaration + +```solidity +function testFuzz_FuzzyTest() public {} +``` + +### 👎 Examples of **incorrect** code for this rule + +#### Foundry test case with incorrect Function declaration + +```solidity +function numberIs42() public {} +``` + +## Version +This rule is introduced in the latest version. + +## Resources +- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/naming/foundry-test-functions.js) +- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/naming/foundry-test-functions.md) +- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/naming/foundry-test-functions.js) diff --git a/docs/rules/naming/func-named-parameters.md b/docs/rules/naming/func-named-parameters.md index 3c6710ce..44fe7da6 100644 --- a/docs/rules/naming/func-named-parameters.md +++ b/docs/rules/naming/func-named-parameters.md @@ -5,29 +5,26 @@ title: "func-named-parameters | Solhint" --- # func-named-parameters -![Recommended Badge](https://img.shields.io/badge/-Recommended-brightgreen) ![Category Badge](https://img.shields.io/badge/-Style%20Guide%20Rules-informational) ![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow) -> The {"extends": "solhint:recommended"} property in a configuration file enables this rule. - ## Description -Enforce function calls with Named Parameters when containing more than the configured qty +Enforce named parameters for function calls with 4 or more arguments. This rule may have some false positives ## Options This rule accepts an array of options: -| Index | Description | Default Value | -| ----- | ------------------------------------------------------ | ------------- | -| 0 | Rule severity. Must be one of "error", "warn", "off". | warn | -| 1 | Maximum qty of unnamed parameters for a function call. | 2 | +| Index | Description | Default Value | +| ----- | -------------------------------------------------------------------------------------------------------- | ------------- | +| 0 | Rule severity. Must be one of "error", "warn", "off". | warn | +| 1 | Minimum qty of unnamed parameters for a function call (to prevent false positives on builtin functions). | 4 | ### Example Config ```json { "rules": { - "func-named-parameters": ["warn",2] + "func-named-parameters": ["warn",4] } } ``` @@ -36,7 +33,7 @@ This rule accepts an array of options: ## Examples ### 👍 Examples of **correct** code for this rule -#### Function call with two UNNAMED parameters (default is max 2) +#### Function call with two UNNAMED parameters (default is 4) ```solidity functionName('0xA81705c8C247C413a19A244938ae7f4A0393944e', 1e18) @@ -56,14 +53,14 @@ functionName({ sender: _senderAddress, amount: 1e18, token: _tokenAddress, recei ### 👎 Examples of **incorrect** code for this rule -#### Function call with four UNNAMED parameters (default is max 2) +#### Function call with four UNNAMED parameters (default 4) ```solidity functionName(_senderAddress, 1e18, _tokenAddress, _receiverAddress ) ``` ## Version -This rule is introduced in the latest version. +This rule was introduced in [Solhint 3.5.1](https://github.com/protofire/solhint/tree/v3.5.1) ## Resources - [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/naming/func-named-parameters.js) diff --git a/docs/rules/naming/immutable-vars-naming.md b/docs/rules/naming/immutable-vars-naming.md index 8705d7ec..a12c2291 100644 --- a/docs/rules/naming/immutable-vars-naming.md +++ b/docs/rules/naming/immutable-vars-naming.md @@ -37,7 +37,7 @@ This rule accepts an array of options: This rule does not have examples. ## Version -This rule is introduced in the latest version. +This rule was introduced in [Solhint 3.5.1](https://github.com/protofire/solhint/tree/v3.5.1) ## Resources - [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/naming/immutable-vars-naming.js) diff --git a/docs/rules/naming/named-return-values.md b/docs/rules/naming/named-return-values.md new file mode 100644 index 00000000..be9ce471 --- /dev/null +++ b/docs/rules/naming/named-return-values.md @@ -0,0 +1,50 @@ +--- +warning: "This is a dynamically generated file. Do not edit manually." +layout: "default" +title: "named-return-values | Solhint" +--- + +# named-return-values +![Category Badge](https://img.shields.io/badge/-Style%20Guide%20Rules-informational) +![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow) + +## Description +Enforce the return values of a function to be named + +## Options +This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Default to warn. + +### Example Config +```json +{ + "rules": { + "named-return-values": "warn" + } +} +``` + + +## Examples +### 👍 Examples of **correct** code for this rule + +#### Function definition with named return values + +```solidity +function checkBalance(address wallet) external view returns(uint256 retBalance) {} +``` + +### 👎 Examples of **incorrect** code for this rule + +#### Function definition with UNNAMED return values + +```solidity +function checkBalance(address wallet) external view returns(uint256) {} +``` + +## Version +This rule is introduced in the latest version. + +## Resources +- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/naming/named-return-values.js) +- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/naming/named-return-values.md) +- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/naming/named-return-values.js) diff --git a/docs/rules/security/check-send-result.md b/docs/rules/security/check-send-result.md index 7f8521bb..18db9a94 100644 --- a/docs/rules/security/check-send-result.md +++ b/docs/rules/security/check-send-result.md @@ -26,6 +26,9 @@ This rule accepts a string option of rule severity. Must be one of "error", "war } ``` +### Notes +- Rule will rise false positive on this: `bool success = walletAddress.send(amount); require(success, "Failed to send"); ` +- Rule will skip ERC777 "send" function to prevent false positives ## Examples ### 👍 Examples of **correct** code for this rule @@ -36,6 +39,12 @@ This rule accepts a string option of rule severity. Must be one of "error", "war if(x.send(55)) {} ``` +#### result of "send" call checked within a require + +```solidity +require(payable(walletAddress).send(moneyAmount), "Failed to send moneyAmount"); +``` + ### 👎 Examples of **incorrect** code for this rule #### result of "send" call ignored diff --git a/docs/rules/security/compiler-version.md b/docs/rules/security/compiler-version.md index e930e93d..d7bc39de 100644 --- a/docs/rules/security/compiler-version.md +++ b/docs/rules/security/compiler-version.md @@ -20,14 +20,14 @@ This rule accepts an array of options: | Index | Description | Default Value | | ----- | ----------------------------------------------------- | ------------- | | 0 | Rule severity. Must be one of "error", "warn", "off". | error | -| 1 | Semver requirement | ^0.5.8 | +| 1 | Semver requirement | ^0.8.0 | ### Example Config ```json { "rules": { - "compiler-version": ["error","^0.5.8"] + "compiler-version": ["error","^0.8.0"] } } ``` diff --git a/docs/rules/security/not-rely-on-time.md b/docs/rules/security/not-rely-on-time.md index f9998d73..dcc24251 100644 --- a/docs/rules/security/not-rely-on-time.md +++ b/docs/rules/security/not-rely-on-time.md @@ -5,11 +5,8 @@ title: "not-rely-on-time | Solhint" --- # not-rely-on-time -![Recommended Badge](https://img.shields.io/badge/-Recommended-brightgreen) ![Category Badge](https://img.shields.io/badge/-Security%20Rules-informational) ![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow) -> The {"extends": "solhint:recommended"} property in a configuration file enables this rule. - ## Description Avoid making time-based decisions in your business logic. diff --git a/e2e/05-max-warnings/contracts/Foo.sol b/e2e/05-max-warnings/contracts/Foo.sol index 659755d7..1c045747 100644 --- a/e2e/05-max-warnings/contracts/Foo.sol +++ b/e2e/05-max-warnings/contracts/Foo.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.5.8; +pragma solidity >=0.8.0; contract Foo { uint256 public constant test1 = 1; diff --git a/e2e/06-formatters/contracts/Foo2.sol b/e2e/06-formatters/contracts/Foo2.sol index 9fb98013..4a5c2af2 100644 --- a/e2e/06-formatters/contracts/Foo2.sol +++ b/e2e/06-formatters/contracts/Foo2.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.5.8; +pragma solidity >=0.8.0; contract Foo { uint256 public constant test1 = 1; diff --git a/e2e/06-formatters/contracts/Foo3.sol b/e2e/06-formatters/contracts/Foo3.sol index f89c6912..8c1c1e0a 100644 --- a/e2e/06-formatters/contracts/Foo3.sol +++ b/e2e/06-formatters/contracts/Foo3.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -pragma solidity >=0.5.8; +pragma solidity >=0.8.0; contract Foo { uint256 public constant TEST1 = 1; diff --git a/e2e/06-formatters/helpers/helpers.js b/e2e/06-formatters/helpers/helpers.js index 05089917..85d43c6e 100644 --- a/e2e/06-formatters/helpers/helpers.js +++ b/e2e/06-formatters/helpers/helpers.js @@ -3,7 +3,7 @@ const foo1Output = [ line: 2, column: 1, severity: 'Error', - message: 'Compiler version >=0.6.0 does not satisfy the ^0.5.8 semver requirement', + message: 'Compiler version >=0.6.0 does not satisfy the ^0.8.0 semver requirement', ruleId: 'compiler-version', fix: null, filePath: 'contracts/Foo.sol', diff --git a/e2e/07-foundry-test/.solhint.json b/e2e/07-foundry-test/.solhint.json new file mode 100644 index 00000000..92277f3d --- /dev/null +++ b/e2e/07-foundry-test/.solhint.json @@ -0,0 +1,7 @@ +{ + "extends": "solhint:recommended", + "rules": { + "compiler-version": "off", + "func-visibility": "off" + } +} diff --git a/e2e/07-foundry-test/contracts/Foo.sol b/e2e/07-foundry-test/contracts/Foo.sol new file mode 100644 index 00000000..3ffecf60 --- /dev/null +++ b/e2e/07-foundry-test/contracts/Foo.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +contract Foo { + uint256 public constant TEST = 1; + + constructor() { + + } +} diff --git a/e2e/07-foundry-test/test/.solhint.json b/e2e/07-foundry-test/test/.solhint.json new file mode 100644 index 00000000..da87cc47 --- /dev/null +++ b/e2e/07-foundry-test/test/.solhint.json @@ -0,0 +1,5 @@ +{ + "rules": { + "foundry-test-functions": ["error", ["setUp", "finish"]] + } +} diff --git a/e2e/07-foundry-test/test/FooTest.sol b/e2e/07-foundry-test/test/FooTest.sol new file mode 100644 index 00000000..be603e53 --- /dev/null +++ b/e2e/07-foundry-test/test/FooTest.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0; + +import "./Test.sol"; + +contract FooTest is Test { + uint256 testNumber; + + function setUp() public { + testNumber = 42; + } + + function testFail_Subtract43() public { + testNumber -= 43; + } + + function wrongFunctionDefinitionName() public { + testNumber -= 43; + } +} \ No newline at end of file diff --git a/e2e/07-foundry-test/test/Test.sol b/e2e/07-foundry-test/test/Test.sol new file mode 100644 index 00000000..d7d16242 --- /dev/null +++ b/e2e/07-foundry-test/test/Test.sol @@ -0,0 +1,4 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.6.0; + +contract Test {} diff --git a/e2e/test.js b/e2e/test.js index bf3910f3..c218dae1 100644 --- a/e2e/test.js +++ b/e2e/test.js @@ -147,4 +147,26 @@ describe('e2e', function () { expect(stdout.trim()).to.not.contain(errorFound) }) }) + + describe('Linter - foundry-test-functions with shell', () => { + // Foo contract has 1 warning + // FooTest contract has 1 error + useFixture('07-foundry-test') + + it(`should raise error for empty blocks only`, () => { + const { code, stdout } = shell.exec('solhint contracts/Foo.sol') + + expect(code).to.equal(0) + expect(stdout.trim()).to.contain('Code contains empty blocks') + }) + + it(`should raise error for wrongFunctionDefinitionName() only`, () => { + const { code, stdout } = shell.exec('solhint -c test/.solhint.json test/FooTest.sol') + + expect(code).to.equal(1) + expect(stdout.trim()).to.contain( + 'Function wrongFunctionDefinitionName() must match Foundry test naming convention' + ) + }) + }) }) diff --git a/lib/common/identifier-naming.js b/lib/common/identifier-naming.js index 1444d398..efc7e3f5 100644 --- a/lib/common/identifier-naming.js +++ b/lib/common/identifier-naming.js @@ -30,4 +30,9 @@ module.exports = { hasLeadingUnderscore(text) { return text && text[0] === '_' }, + + isNotFoundryTestCase(text) { + const regex = /^test(Fork)?(Fuzz)?(Fail)?_(Revert(If_|When_){1})?\w{1,}$/ + return !match(text, regex) + }, } diff --git a/lib/config.js b/lib/config.js index 44b4c420..69b3dad0 100644 --- a/lib/config.js +++ b/lib/config.js @@ -15,11 +15,6 @@ module.exports = { return _.isBoolean(configVal) ? configVal : defaultValue }, - getStringByPath(path, defaultValue) { - const configVal = _.get(this, path) - return _.isString(configVal) ? configVal : defaultValue - }, - getNumber(ruleName, defaultValue) { return this.getNumberByPath(`rules["${ruleName}"][1]`, defaultValue) }, @@ -32,7 +27,18 @@ module.exports = { return this.getBooleanByPath(`rules["${ruleName}"][1][${ruleProperty}]`, defaultValue) }, - getString(ruleName, defaultValue) { - return this.getStringByPath(`rules["${ruleName}"][1]`, defaultValue) + getStringFromArray(ruleName, defaultValue) { + if (this.rules && this.rules[ruleName]) { + const ruleValue = this.rules[ruleName] + return Array.isArray(ruleValue) ? ruleValue[1] : defaultValue + } else { + return defaultValue + } + }, + + getArray(ruleName, defaultValue) { + const path = `rules["${ruleName}"][1]` + const configVal = _.get(this, path) + return _.isArray(configVal) ? configVal : defaultValue }, } diff --git a/lib/rules/best-practises/custom-errors.js b/lib/rules/best-practises/custom-errors.js new file mode 100644 index 00000000..ce4304af --- /dev/null +++ b/lib/rules/best-practises/custom-errors.js @@ -0,0 +1,76 @@ +const BaseChecker = require('../base-checker') +const { severityDescription } = require('../../doc/utils') + +const DEFAULT_SEVERITY = 'warn' + +const ruleId = 'custom-errors' +const meta = { + type: 'best-practises', + + docs: { + description: 'Enforces the use of Custom Errors over Require and Revert statements', + category: 'Best Practise Rules', + options: [ + { + description: severityDescription, + default: DEFAULT_SEVERITY, + }, + ], + examples: { + good: [ + { + description: 'Use of Custom Errors', + code: 'revert CustomErrorFunction();', + }, + { + description: 'Use of Custom Errors with arguments', + code: 'revert CustomErrorFunction({ msg: "Insufficient Balance" });', + }, + ], + bad: [ + { + description: 'Use of require statement', + code: 'require(userBalance >= availableAmount, "Insufficient Balance");', + }, + { + description: 'Use of plain revert statement', + code: 'revert();', + }, + { + description: 'Use of revert statement with message', + code: 'revert("Insufficient Balance");', + }, + ], + }, + }, + + isDefault: false, + recommended: true, + defaultSetup: DEFAULT_SEVERITY, + + schema: null, +} + +class CustomErrorsChecker extends BaseChecker { + constructor(reporter) { + super(reporter, ruleId, meta) + } + + FunctionCall(node) { + let errorStr = '' + if (node.expression.name === 'require') { + errorStr = 'require' + } else if ( + node.expression.name === 'revert' && + (node.arguments.length === 0 || node.arguments[0].type === 'StringLiteral') + ) { + errorStr = 'revert' + } + + if (errorStr !== '') { + this.error(node, `Use Custom Errors instead of ${errorStr} statements`) + } + } +} + +module.exports = CustomErrorsChecker diff --git a/lib/rules/best-practises/explicit-types.js b/lib/rules/best-practises/explicit-types.js index 4f2a6149..ebf7b60d 100644 --- a/lib/rules/best-practises/explicit-types.js +++ b/lib/rules/best-practises/explicit-types.js @@ -62,13 +62,17 @@ const meta = { class ExplicitTypesChecker extends BaseChecker { constructor(reporter, config) { super(reporter, ruleId, meta) - this.configOption = (config && config.getString(ruleId, DEFAULT_OPTION)) || DEFAULT_OPTION + this.configOption = + (config && config.getStringFromArray(ruleId, DEFAULT_OPTION)) || DEFAULT_OPTION this.isExplicit = this.configOption === 'explicit' - - this.validateConfigOption(this.configOption) } VariableDeclaration(node) { + if (!VALID_CONFIGURATION_OPTIONS.includes(this.configOption)) { + this.error(node, 'Error: Config error on [explicit-types]. See explicit-types.md.') + return + } + const typeToFind = 'ElementaryTypeName' const onlyTypes = this.findNamesOfElementaryTypeName(node, typeToFind) @@ -89,12 +93,6 @@ class ExplicitTypesChecker extends BaseChecker { } } - validateConfigOption(configOption) { - const configOk = VALID_CONFIGURATION_OPTIONS.includes(configOption) - if (!configOk) - throw new Error('Error: Config error on [explicit-types]. See explicit-types.md.') - } - validateVariables(configType, node, varsToBeChecked) { const errorVars = varsToBeChecked.filter((type) => configType.includes(type)) diff --git a/lib/rules/best-practises/index.js b/lib/rules/best-practises/index.js index 6b2ac780..e7f5228d 100644 --- a/lib/rules/best-practises/index.js +++ b/lib/rules/best-practises/index.js @@ -10,8 +10,9 @@ const NoConsoleLogChecker = require('./no-console') const NoGlobalImportsChecker = require('./no-global-import') const NoUnusedImportsChecker = require('./no-unused-import') const ExplicitTypesChecker = require('./explicit-types') +const CustomErrorsChecker = require('./custom-errors') -module.exports = function checkers(reporter, config, inputSrc) { +module.exports = function checkers(reporter, config, inputSrc, tokens) { return [ new CodeComplexityChecker(reporter, config), new FunctionMaxLinesChecker(reporter, config), @@ -23,7 +24,8 @@ module.exports = function checkers(reporter, config, inputSrc) { new ReasonStringChecker(reporter, config), new NoConsoleLogChecker(reporter), new NoGlobalImportsChecker(reporter), - new NoUnusedImportsChecker(reporter), + new NoUnusedImportsChecker(reporter, tokens), new ExplicitTypesChecker(reporter, config), + new CustomErrorsChecker(reporter), ] } diff --git a/lib/rules/best-practises/no-unused-import.js b/lib/rules/best-practises/no-unused-import.js index 993ae144..297875ff 100644 --- a/lib/rules/best-practises/no-unused-import.js +++ b/lib/rules/best-practises/no-unused-import.js @@ -43,9 +43,10 @@ function isImportIdentifier(node) { } class NoUnusedImportsChecker extends BaseChecker { - constructor(reporter) { + constructor(reporter, tokens) { super(reporter, ruleId, meta) this.importedNames = {} + this.tokens = tokens } registerUsage(rawName) { @@ -60,14 +61,6 @@ class NoUnusedImportsChecker extends BaseChecker { } } - ArrayTypeName(node) { - this.registerUsage(node.baseTypeName.namePath) - } - - NewExpression(node) { - this.registerUsage(node.typeName.namePath) - } - Identifier(node) { if (!isImportIdentifier(node)) { this.registerUsage(node.name) @@ -75,9 +68,6 @@ class NoUnusedImportsChecker extends BaseChecker { } FunctionDefinition(node) { - if (node.override) { - node.override.forEach((it) => this.registerUsage(it.namePath)) - } if (node.isConstructor) { node.modifiers.forEach((it) => this.registerUsage(it.name)) } @@ -93,18 +83,8 @@ class NoUnusedImportsChecker extends BaseChecker { } } - VariableDeclaration(node) { - // this ignores builtin types, the check inside registerUsage ignores types - // defined in the same file - if (node.typeName.type === 'UserDefinedTypeName') { - this.registerUsage(node.typeName.namePath) - } - } - - ContractDefinition(node) { - node.baseContracts.forEach((inheritanceSpecifier) => { - this.registerUsage(inheritanceSpecifier.baseName.namePath) - }) + UserDefinedTypeName(node) { + this.registerUsage(node.namePath) } UsingForDeclaration(node) { @@ -113,6 +93,16 @@ class NoUnusedImportsChecker extends BaseChecker { } 'SourceUnit:exit'() { + const keywords = this.tokens.filter((it) => it.type === 'Keyword') + const inheritdocStatements = keywords.filter( + ({ value }) => /^\/\/\/ *@inheritdoc/.test(value) || /^\/\*\* *@inheritdoc/.test(value) + ) + inheritdocStatements.forEach(({ value }) => { + const match = value.match(/@inheritdoc *([a-zA-Z0-9_]*)/) + if (match && match[1]) { + this.registerUsage(match[1]) + } + }) forIn(this.importedNames, (value, key) => { if (!value.used) { this.error(value.node, `imported name ${key} is not used`) diff --git a/lib/rules/index.js b/lib/rules/index.js index d7e78ab8..7e8a3b55 100644 --- a/lib/rules/index.js +++ b/lib/rules/index.js @@ -58,7 +58,7 @@ function coreRules(meta) { const { reporter, config, inputSrc, tokens } = meta return [ - ...bestPractises(reporter, config, inputSrc), + ...bestPractises(reporter, config, inputSrc, tokens), ...deprecations(reporter), ...miscellaneous(reporter, config, tokens), ...naming(reporter, config), diff --git a/lib/rules/naming/foundry-test-functions.js b/lib/rules/naming/foundry-test-functions.js new file mode 100644 index 00000000..1cc367a6 --- /dev/null +++ b/lib/rules/naming/foundry-test-functions.js @@ -0,0 +1,93 @@ +const BaseChecker = require('../base-checker') +const naming = require('../../common/identifier-naming') +const { severityDescription } = require('../../doc/utils') + +const DEFAULT_SEVERITY = 'off' +const DEFAULT_SKIP_FUNCTIONS = ['setUp'] + +const ruleId = 'foundry-test-functions' +const meta = { + type: 'naming', + + docs: { + description: `Enforce naming convention on functions for Foundry test cases`, + category: 'Style Guide Rules', + options: [ + { + description: severityDescription, + default: DEFAULT_SEVERITY, + }, + ], + examples: { + good: [ + { + description: 'Foundry test case with correct Function declaration', + code: 'function test_NumberIs42() public {}', + }, + { + description: 'Foundry test case with correct Function declaration', + code: 'function testFail_Subtract43() public {}', + }, + { + description: 'Foundry test case with correct Function declaration', + code: 'function testFuzz_FuzzyTest() public {}', + }, + ], + bad: [ + { + description: 'Foundry test case with incorrect Function declaration', + code: 'function numberIs42() public {}', + }, + ], + }, + notes: [ + { + note: 'This rule can be configured to skip certain function names in the SKIP array. In Example Config. ```setUp``` function will be skipped', + }, + { note: 'Supported Regex: ```test(Fork)?(Fuzz)?(Fail)?_(Revert(If_|When_){1})?\\w{1,}```' }, + { + note: 'This rule should be executed in a separate folder with a separate .solhint.json => ```solhint --config .solhint.json testFolder/**/*.sol```', + }, + { + note: 'This rule applies only to `external` and `public` functions', + }, + { + note: 'This rule skips the `setUp()` function', + }, + ], + }, + + isDefault: false, + recommended: false, + defaultSetup: [DEFAULT_SEVERITY, DEFAULT_SKIP_FUNCTIONS], + + schema: { type: 'array' }, +} + +class FoundryTestFunctionsChecker extends BaseChecker { + constructor(reporter, config) { + super(reporter, ruleId, meta) + this.skippedFunctions = config + ? config.getArray(ruleId, DEFAULT_SKIP_FUNCTIONS) + : DEFAULT_SKIP_FUNCTIONS + } + + FunctionDefinition(node) { + // function name should not be in skipped functions array + // should be external or public + if ( + !this.searchInArray(this.skippedFunctions, node.name) && + (node.visibility === 'public' || node.visibility === 'external') + ) { + if (naming.isNotFoundryTestCase(node.name)) { + this.error(node, `Function ${node.name}() must match Foundry test naming convention`) + } + } + } + + searchInArray(array, searchString) { + return array.indexOf(searchString) !== -1 + } +} + +module.exports = FoundryTestFunctionsChecker diff --git a/lib/rules/naming/func-named-parameters.js b/lib/rules/naming/func-named-parameters.js index 24d71d13..170ea4ba 100644 --- a/lib/rules/naming/func-named-parameters.js +++ b/lib/rules/naming/func-named-parameters.js @@ -2,15 +2,14 @@ const BaseChecker = require('../base-checker') const { severityDescription } = require('../../doc/utils') const DEFAULT_SEVERITY = 'warn' -const DEFAULT_MAX_UNNAMED_ARGUMENTS = 2 +const DEFAULT_MIN_UNNAMED_ARGUMENTS = 4 const ruleId = 'func-named-parameters' const meta = { type: 'naming', docs: { - description: - 'Enforce function calls with Named Parameters when containing more than the configured qty', + description: `Enforce named parameters for function calls with ${DEFAULT_MIN_UNNAMED_ARGUMENTS} or more arguments. This rule may have some false positives`, category: 'Style Guide Rules', options: [ { @@ -18,14 +17,15 @@ const meta = { default: DEFAULT_SEVERITY, }, { - description: 'Maximum qty of unnamed parameters for a function call.', - default: JSON.stringify(DEFAULT_MAX_UNNAMED_ARGUMENTS), + description: + 'Minimum qty of unnamed parameters for a function call (to prevent false positives on builtin functions).', + default: JSON.stringify(DEFAULT_MIN_UNNAMED_ARGUMENTS), }, ], examples: { good: [ { - description: 'Function call with two UNNAMED parameters (default is max 2)', + description: 'Function call with two UNNAMED parameters (default is 4)', code: "functionName('0xA81705c8C247C413a19A244938ae7f4A0393944e', 1e18)", }, { @@ -39,7 +39,7 @@ const meta = { ], bad: [ { - description: 'Function call with four UNNAMED parameters (default is max 2)', + description: 'Function call with four UNNAMED parameters (default 4)', code: 'functionName(_senderAddress, 1e18, _tokenAddress, _receiverAddress )', }, ], @@ -47,8 +47,8 @@ const meta = { }, isDefault: false, - recommended: true, - defaultSetup: [DEFAULT_SEVERITY, DEFAULT_MAX_UNNAMED_ARGUMENTS], + recommended: false, + defaultSetup: [DEFAULT_SEVERITY, DEFAULT_MIN_UNNAMED_ARGUMENTS], schema: { type: 'integer' }, } @@ -56,8 +56,11 @@ const meta = { class FunctionNamedParametersChecker extends BaseChecker { constructor(reporter, config) { super(reporter, ruleId, meta) - this.maxUnnamedArguments = config && config.getNumber(ruleId, DEFAULT_MAX_UNNAMED_ARGUMENTS) - if (!(this.maxUnnamedArguments >= 0)) this.maxUnnamedArguments = DEFAULT_MAX_UNNAMED_ARGUMENTS + this.maxUnnamedArguments = + (config && config.getNumber(ruleId, DEFAULT_MIN_UNNAMED_ARGUMENTS)) || + DEFAULT_MIN_UNNAMED_ARGUMENTS + if (this.maxUnnamedArguments < DEFAULT_MIN_UNNAMED_ARGUMENTS) + this.maxUnnamedArguments = DEFAULT_MIN_UNNAMED_ARGUMENTS } FunctionCall(node) { @@ -68,7 +71,7 @@ class FunctionNamedParametersChecker extends BaseChecker { if (qtyNamed === 0 && qtyArgs > this.maxUnnamedArguments) { this.error( node, - `Missing named parameters. Max unnamed parameters value is ${this.maxUnnamedArguments}` + `Named parameters missing. MIN unnamed argumenst is ${this.maxUnnamedArguments}` ) } } diff --git a/lib/rules/naming/index.js b/lib/rules/naming/index.js index 1dffa3f8..efb3901f 100644 --- a/lib/rules/naming/index.js +++ b/lib/rules/naming/index.js @@ -10,6 +10,8 @@ const VarNameMixedcaseChecker = require('./var-name-mixedcase') const NamedParametersMappingChecker = require('./named-parameters-mapping') const ImmutableVarsNamingChecker = require('./immutable-vars-naming') const FunctionNamedParametersChecker = require('./func-named-parameters') +const FoundryTestFunctionsChecker = require('./foundry-test-functions') +const NamedReturnValuesChecker = require('./named-return-values') module.exports = function checkers(reporter, config) { return [ @@ -25,5 +27,7 @@ module.exports = function checkers(reporter, config) { new NamedParametersMappingChecker(reporter), new ImmutableVarsNamingChecker(reporter, config), new FunctionNamedParametersChecker(reporter, config), + new FoundryTestFunctionsChecker(reporter, config), + new NamedReturnValuesChecker(reporter), ] } diff --git a/lib/rules/naming/named-return-values.js b/lib/rules/naming/named-return-values.js new file mode 100644 index 00000000..492a81ce --- /dev/null +++ b/lib/rules/naming/named-return-values.js @@ -0,0 +1,60 @@ +const BaseChecker = require('../base-checker') +const { severityDescription } = require('../../doc/utils') + +const DEFAULT_SEVERITY = 'warn' + +const ruleId = 'named-return-values' +const meta = { + type: 'naming', + + docs: { + description: `Enforce the return values of a function to be named`, + category: 'Style Guide Rules', + options: [ + { + description: severityDescription, + default: DEFAULT_SEVERITY, + }, + ], + examples: { + good: [ + { + description: 'Function definition with named return values', + code: 'function checkBalance(address wallet) external view returns(uint256 retBalance) {}', + }, + ], + bad: [ + { + description: 'Function definition with UNNAMED return values', + code: 'function checkBalance(address wallet) external view returns(uint256) {}', + }, + ], + }, + }, + + isDefault: false, + recommended: false, + defaultSetup: DEFAULT_SEVERITY, + + schema: null, +} + +class NamedReturnValuesChecker extends BaseChecker { + constructor(reporter) { + super(reporter, ruleId, meta) + } + + FunctionDefinition(node) { + if (node.returnParameters) { + let index = 0 + for (const returnValue of node.returnParameters) { + if (!returnValue.name) { + this.error(node, `Named return value is missing - Index ${index}`) + } + index++ + } + } + } +} + +module.exports = NamedReturnValuesChecker diff --git a/lib/rules/order/ordering.js b/lib/rules/order/ordering.js index 494f3ad5..6d0e8a07 100644 --- a/lib/rules/order/ordering.js +++ b/lib/rules/order/ordering.js @@ -122,6 +122,13 @@ function sourceUnitPartOrder(node) { throw new Error('Unrecognized source unit part, please report this issue') } +function isInitializerModifier(modifiers, targetName, targetArguments) { + // search the modifiers array with the name === 'initializer' + return modifiers.some( + (modifier) => modifier.name === targetName && modifier.arguments === targetArguments + ) +} + function contractPartOrder(node) { if (node.type === 'UsingForDeclaration') { return [0, 'using for declaration'] @@ -154,8 +161,13 @@ function contractPartOrder(node) { return [40, 'modifier definition'] } - if (node.isConstructor) { - return [50, 'constructor'] + if ( + node.isConstructor || + (node.type === 'FunctionDefinition' && + node.name === 'initialize' && + isInitializerModifier(node.modifiers, 'initializer', null)) + ) { + return [50, 'constructor/initializer'] } if (isReceiveFunction(node)) { diff --git a/lib/rules/security/check-send-result.js b/lib/rules/security/check-send-result.js index a20ee2ed..e6381f0f 100644 --- a/lib/rules/security/check-send-result.js +++ b/lib/rules/security/check-send-result.js @@ -14,16 +14,26 @@ const meta = { good: [ { description: 'result of "send" call checked with if statement', - code: require('../../../test/fixtures/security/send-result-checked'), + code: 'if(x.send(55)) {}', + }, + { + description: 'result of "send" call checked within a require', + code: 'require(payable(walletAddress).send(moneyAmount), "Failed to send moneyAmount");', }, ], bad: [ { description: 'result of "send" call ignored', - code: require('../../../test/fixtures/security/send-result-ignored'), + code: 'x.send(55);', }, ], }, + notes: [ + { + note: 'Rule will rise false positive on this: `bool success = walletAddress.send(amount); require(success, "Failed to send"); ` ', + }, + { note: 'Rule will skip ERC777 "send" function to prevent false positives' }, + ], }, isDefault: false, @@ -44,17 +54,24 @@ class CheckSendResultChecker extends BaseChecker { validateSend(node) { if (node.memberName === 'send') { - const hasVarDeclaration = traversing.statementNotContains(node, 'VariableDeclaration') - const hasIfStatement = traversing.statementNotContains(node, 'IfStatement') - const hasRequire = traversing.someParent(node, this.isRequire) - const hasAssert = traversing.someParent(node, this.isAssert) + if (this.isNotErc777Token(node)) { + const hasVarDeclaration = traversing.statementNotContains(node, 'VariableDeclaration') + const hasIfStatement = traversing.statementNotContains(node, 'IfStatement') + const hasRequire = traversing.someParent(node, this.isRequire) + const hasAssert = traversing.someParent(node, this.isAssert) - if (!hasIfStatement && !hasVarDeclaration && !hasRequire && !hasAssert) { - this.error(node, 'Check result of "send" call') + if (!hasIfStatement && !hasVarDeclaration && !hasRequire && !hasAssert) { + this.error(node, 'Check result of "send" call') + } } } } + isNotErc777Token(node) { + const isErc777 = node.parent.type === 'FunctionCall' && node.parent.arguments.length >= 3 + return !isErc777 + } + isRequire(node) { return node.type === 'FunctionCall' && node.expression.name === 'require' } diff --git a/lib/rules/security/compiler-version.js b/lib/rules/security/compiler-version.js index 3bb51324..955a53bd 100644 --- a/lib/rules/security/compiler-version.js +++ b/lib/rules/security/compiler-version.js @@ -4,7 +4,7 @@ const { severityDescription } = require('../../doc/utils') const ruleId = 'compiler-version' const DEFAULT_SEVERITY = 'error' -const DEFAULT_SEMVER = '^0.5.8' +const DEFAULT_SEMVER = '^0.8.0' const meta = { type: 'security', @@ -36,7 +36,8 @@ class CompilerVersionChecker extends BaseChecker { constructor(reporter, config) { super(reporter, ruleId, meta) - this.requirement = (config && config.getString(ruleId, DEFAULT_SEMVER)) || DEFAULT_SEMVER + this.requirement = + (config && config.getStringFromArray(ruleId, DEFAULT_SEMVER)) || DEFAULT_SEMVER } SourceUnit(node) { diff --git a/lib/rules/security/not-rely-on-time.js b/lib/rules/security/not-rely-on-time.js index f3420c71..00a96098 100644 --- a/lib/rules/security/not-rely-on-time.js +++ b/lib/rules/security/not-rely-on-time.js @@ -10,7 +10,7 @@ const meta = { }, isDefault: false, - recommended: true, + recommended: false, defaultSetup: 'warn', schema: null, diff --git a/package.json b/package.json index dabbe266..7c53d289 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "solhint", - "version": "3.5.1", + "version": "3.6.1", "description": "Solidity Code Linter", "main": "lib/index.js", "keywords": [ diff --git a/scripts/generate-rule-docs.js b/scripts/generate-rule-docs.js index 1e7a02e1..c170bb74 100755 --- a/scripts/generate-rule-docs.js +++ b/scripts/generate-rule-docs.js @@ -1,75 +1,77 @@ #!env node const { loadRules } = require('../lib/load-rules') -const fs = require('fs'); -const { exec, mkdir } = require("shelljs"); -const semver = require('semver'); -const path = require('path'); -const table = require('markdown-table'); -const { ruleSeverityEnum } = require('../lib/doc/utils'); +const fs = require('fs') +const { exec, mkdir } = require('shelljs') +const semver = require('semver') +const path = require('path') +const table = require('markdown-table') +const { ruleSeverityEnum } = require('../lib/doc/utils') /** * Borrowed from https://github.com/eslint/eslint/blob/master/Makefile.js */ class GitHelper { - /** - * Gets the tag name where a given file was introduced first. - * @param {string} filePath The file path to check. - * @returns {string} The tag name. - */ - static getFirstVersionOfFile(filePath) { - const firstCommit = GitHelper.getFirstCommitOfFile(filePath); - let tags = GitHelper.execSilent(`git tag --contains ${firstCommit}`); - - tags = GitHelper.splitCommandResultToLines(tags); - return tags.reduce((list, version) => { - const validatedVersion = semver.valid(version.trim()); - - if (validatedVersion) { - list.push(validatedVersion); - } - return list; - }, []).sort(semver.compare)[0]; - } + /** + * Gets the tag name where a given file was introduced first. + * @param {string} filePath The file path to check. + * @returns {string} The tag name. + */ + static getFirstVersionOfFile(filePath) { + const firstCommit = GitHelper.getFirstCommitOfFile(filePath) + let tags = GitHelper.execSilent(`git tag --contains ${firstCommit}`) + + tags = GitHelper.splitCommandResultToLines(tags) + return tags + .reduce((list, version) => { + const validatedVersion = semver.valid(version.trim()) + + if (validatedVersion) { + list.push(validatedVersion) + } + return list + }, []) + .sort(semver.compare)[0] + } - /** - * Gets the first commit sha of the given file. - * @param {string} filePath The file path which should be checked. - * @returns {string} The commit sha. - */ - static getFirstCommitOfFile(filePath) { - let commits = GitHelper.execSilent(`git rev-list HEAD -- ${filePath}`); + /** + * Gets the first commit sha of the given file. + * @param {string} filePath The file path which should be checked. + * @returns {string} The commit sha. + */ + static getFirstCommitOfFile(filePath) { + let commits = GitHelper.execSilent(`git rev-list HEAD -- ${filePath}`) - commits = GitHelper.splitCommandResultToLines(commits); - return commits[commits.length - 1].trim(); - } + commits = GitHelper.splitCommandResultToLines(commits) + return commits[commits.length - 1].trim() + } - /** - * Splits a command result to separate lines. - * @param {string} result The command result string. - * @returns {Array} The separated lines. - */ - static splitCommandResultToLines(result) { - return result.trim().split("\n"); - } + /** + * Splits a command result to separate lines. + * @param {string} result The command result string. + * @returns {Array} The separated lines. + */ + static splitCommandResultToLines(result) { + return result.trim().split('\n') + } - /** - * Executes a command and returns the output instead of printing it to stdout. - * @param {string} cmd The command string to execute. - * @returns {string} The result of the executed command. - */ - static execSilent(cmd) { - return exec(cmd, {silent: true}).stdout; - } + /** + * Executes a command and returns the output instead of printing it to stdout. + * @param {string} cmd The command string to execute. + * @returns {string} The result of the executed command. + */ + static execSilent(cmd) { + return exec(cmd, { silent: true }).stdout + } } function generateRuleDoc(rule) { - const isDefault = !rule.meta.deprecated && rule.meta.isDefault; - const isRecommended = !rule.meta.deprecated && rule.meta.recommended; - const isDeprecated = rule.meta.deprecated; - const version = GitHelper.getFirstVersionOfFile(rule.file); - const defaultSeverity = getDefaultSeverity(rule); + const isDefault = !rule.meta.deprecated && rule.meta.isDefault + const isRecommended = !rule.meta.deprecated && rule.meta.recommended + const isDeprecated = rule.meta.deprecated + const version = GitHelper.getFirstVersionOfFile(rule.file) + const defaultSeverity = getDefaultSeverity(rule) - return `--- + return `--- warning: "This is a dynamically generated file. Do not edit manually." layout: "default" title: "${rule.ruleId} | Solhint" @@ -77,14 +79,20 @@ title: "${rule.ruleId} | Solhint" # ${rule.ruleId} ${[ - recommendedBadge(isRecommended), - deprecatedBadge(isDeprecated), - categoryBadge(rule.meta.docs.category), - defaultSeverityBadge(defaultSeverity), - isDefault ? '> The {"extends": "solhint:default"} property in a configuration file enables this rule.\n' : '', - isRecommended ? '> The {"extends": "solhint:recommended"} property in a configuration file enables this rule.\n' : '', - isDeprecated ? '> This rule is **deprecated**\n' : '' -].filter(s => s !== '').join("\n")} + recommendedBadge(isRecommended), + deprecatedBadge(isDeprecated), + categoryBadge(rule.meta.docs.category), + defaultSeverityBadge(defaultSeverity), + isDefault + ? '> The {"extends": "solhint:default"} property in a configuration file enables this rule.\n' + : '', + isRecommended + ? '> The {"extends": "solhint:recommended"} property in a configuration file enables this rule.\n' + : '', + isDeprecated ? '> This rule is **deprecated**\n' : '', +] + .filter((s) => s !== '') + .join('\n')} ## Description ${rule.meta.docs.description} @@ -94,7 +102,7 @@ ${loadOptions(rule)} ### Example Config ${loadExampleConfig(rule)} - +${loadNotes(rule)} ## Examples ${loadExamples(rule)} @@ -109,44 +117,48 @@ ${linkToVersion(version)} } function categoryBadge(category) { - return `![Category Badge](https://img.shields.io/badge/-${encodeURIComponent(category)}-informational)`; + return `![Category Badge](https://img.shields.io/badge/-${encodeURIComponent( + category + )}-informational)` } function recommendedBadge(isRecommended) { - return isRecommended ? `![Recommended Badge](https://img.shields.io/badge/-Recommended-brightgreen)` : ''; + return isRecommended + ? `![Recommended Badge](https://img.shields.io/badge/-Recommended-brightgreen)` + : '' } function deprecatedBadge(isDeprecated) { - return isDeprecated ? `![Deprecated Badge](https://img.shields.io/badge/-Deprecated-yellow)` : ''; + return isDeprecated ? `![Deprecated Badge](https://img.shields.io/badge/-Deprecated-yellow)` : '' } function defaultSeverityBadge(severity) { - const colors = { - 'warn': 'yellow', - 'error': 'red' - }; - return `![Default Severity Badge ${severity}](https://img.shields.io/badge/Default%20Severity-${severity}-${colors[severity]})`; + const colors = { + warn: 'yellow', + error: 'red', + } + return `![Default Severity Badge ${severity}](https://img.shields.io/badge/Default%20Severity-${severity}-${colors[severity]})` } function loadOptions(rule) { - if (Array.isArray(rule.meta.defaultSetup)) { - const optionsTable = [['Index', 'Description', 'Default Value']]; - rule.meta.docs.options.forEach((option, index) => { - optionsTable.push([index, option.description, option.default]); - }); - return `This rule accepts an array of options: + if (Array.isArray(rule.meta.defaultSetup)) { + const optionsTable = [['Index', 'Description', 'Default Value']] + rule.meta.docs.options.forEach((option, index) => { + optionsTable.push([index, option.description, option.default]) + }) + return `This rule accepts an array of options: ${table(optionsTable)} -`; - } else if (typeof rule.meta.defaultSetup === 'string') { - return `This rule accepts a string option of rule severity. Must be one of ${ruleSeverityEnum}. Default to ${rule.meta.defaultSetup}.` - } else { - throw new Error(`Unhandled type of rule.meta.defaultSetup from rule ${rule.ruleId}`); - } +` + } else if (typeof rule.meta.defaultSetup === 'string') { + return `This rule accepts a string option of rule severity. Must be one of ${ruleSeverityEnum}. Default to ${rule.meta.defaultSetup}.` + } else { + throw new Error(`Unhandled type of rule.meta.defaultSetup from rule ${rule.ruleId}`) + } } function loadExampleConfig(rule) { - return `\`\`\`json + return `\`\`\`json { "rules": { "${rule.ruleId}": ${JSON.stringify(rule.meta.defaultSetup)} @@ -156,77 +168,109 @@ function loadExampleConfig(rule) { ` } -function linkToVersion(version) { - if (version) { - return `This rule was introduced in [Solhint ${version}](https://github.com/protofire/solhint/tree/v${version})`; - } else { - return `This rule is introduced in the latest version.`; +function loadNotes(rule) { + let textToReturn = '' + let noteValue = '' + if (rule.meta.docs.notes) { + textToReturn = `### Notes\n` + for (let i = 0; i < rule.meta.docs.notes.length; i++) { + noteValue = rule.meta.docs.notes[i].note + textToReturn = textToReturn + `- ${noteValue}\n` } + } + + return textToReturn +} + +function linkToVersion(version) { + if (version) { + return `This rule was introduced in [Solhint ${version}](https://github.com/protofire/solhint/tree/v${version})` + } else { + return `This rule is introduced in the latest version.` + } } function linkToSource(rule) { - const link = rule.file.replace(path.resolve(path.join(__dirname, '..')), ''); - return `https://github.com/protofire/solhint/tree/master${link}`; + const link = rule.file.replace(path.resolve(path.join(__dirname, '..')), '') + return `https://github.com/protofire/solhint/tree/master${link}` } function linkToDocumentSource(rule) { - const link = rule.file.replace(path.resolve(path.join(__dirname, '..')), '').replace("lib/rules", "docs/rules").replace(/\.js$/, ".md"); - return `https://github.com/protofire/solhint/tree/master${link}`; + const link = rule.file + .replace(path.resolve(path.join(__dirname, '..')), '') + .replace('lib/rules', 'docs/rules') + .replace(/\.js$/, '.md') + return `https://github.com/protofire/solhint/tree/master${link}` } function linkToTestCase(rule) { - const link = rule.file.replace(path.resolve(path.join(__dirname, '..', 'lib', 'rules')), ''); - return `https://github.com/protofire/solhint/tree/master/test/rules${link}`; + const link = rule.file.replace(path.resolve(path.join(__dirname, '..', 'lib', 'rules')), '') + return `https://github.com/protofire/solhint/tree/master/test/rules${link}` } function loadExamples(rule) { - if (!rule.meta.docs.examples) { - return "This rule does not have examples."; - } + if (!rule.meta.docs.examples) { + return 'This rule does not have examples.' + } - const examples = [loadCorrectExample(rule), loadIncorrectExample(rule)].filter(s => s !== '').join('\n\n'); - return examples === '' ? 'This rule does not have examples.' : examples; + const examples = [loadCorrectExample(rule), loadIncorrectExample(rule)] + .filter((s) => s !== '') + .join('\n\n') + return examples === '' ? 'This rule does not have examples.' : examples } function loadIncorrectExample(rule) { - if (rule.meta.docs.examples.bad && rule.meta.docs.examples.bad.length) { - return `### 👎 Examples of **incorrect** code for this rule - -${rule.meta.docs.examples.bad.map(ex => `#### ${ex.description}\n\n\`\`\`solidity\n${ex.code}\n\`\`\``).join("\n\n")}`; - } else { - return ''; - } + if (rule.meta.docs.examples.bad && rule.meta.docs.examples.bad.length) { + return `### 👎 Examples of **incorrect** code for this rule + +${rule.meta.docs.examples.bad + .map((ex) => `#### ${ex.description}\n\n\`\`\`solidity\n${ex.code}\n\`\`\``) + .join('\n\n')}` + } else { + return '' + } } function loadCorrectExample(rule) { - if (rule.meta.docs.examples.good && rule.meta.docs.examples.good.length) { - return `### 👍 Examples of **correct** code for this rule - -${rule.meta.docs.examples.good.map(ex => `#### ${ex.description}\n\n\`\`\`solidity\n${ex.code}\n\`\`\``).join("\n\n")}`; - } else { - return ''; - } + if (rule.meta.docs.examples.good && rule.meta.docs.examples.good.length) { + return `### 👍 Examples of **correct** code for this rule + +${rule.meta.docs.examples.good + .map((ex) => `#### ${ex.description}\n\n\`\`\`solidity\n${ex.code}\n\`\`\``) + .join('\n\n')}` + } else { + return '' + } } function getDefaultSeverity(rule) { - if (Array.isArray(rule.meta.defaultSetup)) { - return rule.meta.defaultSetup[0]; - } else { - return rule.meta.defaultSetup; - } + if (Array.isArray(rule.meta.defaultSetup)) { + return rule.meta.defaultSetup[0] + } else { + return rule.meta.defaultSetup + } } function generateRuleIndex(rulesIndexed) { - const contents = Object.keys(rulesIndexed).map(category => { - const rows = [["Rule Id", "Error", "Recommended", "Deprecated"]]; - rulesIndexed[category].map(rule => [`[${rule.ruleId}](./rules/${rule.meta.type}/${rule.ruleId}.md)`, rule.meta.docs.description, (rule.meta.recommended && !rule.meta.deprecated) ? '$~~~~~~~~$✔️' : '', rule.meta.deprecated ? '$~~~~~~~$✔️' : '']).forEach(row => rows.push(row)); - return `## ${category} + const contents = Object.keys(rulesIndexed) + .map((category) => { + const rows = [['Rule Id', 'Error', 'Recommended', 'Deprecated']] + rulesIndexed[category] + .map((rule) => [ + `[${rule.ruleId}](./rules/${rule.meta.type}/${rule.ruleId}.md)`, + rule.meta.docs.description, + rule.meta.recommended && !rule.meta.deprecated ? '$~~~~~~~~$✔️' : '', + rule.meta.deprecated ? '$~~~~~~~$✔️' : '', + ]) + .forEach((row) => rows.push(row)) + return `## ${category} ${table(rows)} - `; - }).join("\n\n"); + ` + }) + .join('\n\n') - return `--- + return `--- warning: "This is a dynamically generated file. Do not edit manually." layout: "default" title: "Rule Index of Solhint" @@ -238,61 +282,63 @@ ${contents} - [ConsenSys Guide for Smart Contracts](https://consensys.github.io/smart-contract-best-practices/recommendations/) - [Solidity Style Guide](http://solidity.readthedocs.io/en/develop/style-guide.html) -`; +` } function main() { - const program = require('commander'); - program.option('--rule-id ', 'rule id'); - program.option('--index-only', 'only generate rule index'); - program.parse(process.argv); - - const rules = loadRules(); - if (!program.indexOnly) { - let ruleIds = []; - if (program.ruleId) { - ruleIds = program.ruleId.split(","); - } - - rules.filter(rule => { - if (ruleIds.length) { - return ruleIds.includes(rule.ruleId); - } else { - return true; - } - }).forEach(rule => { - const ruleDoc = generateRuleDoc(rule); - const dir = path.resolve(path.join(__dirname, '..', 'docs', 'rules', rule.meta.type)); - const fileName = `${rule.ruleId}.md`; - const filePath = path.resolve(path.join(dir, fileName)); - mkdir('-p', dir); - fs.writeFile(filePath, ruleDoc, function (err) { - if (err) { - console.error(err); - } else { - console.log(`Wrote ${filePath}`) - } - }); - }); + const program = require('commander') + program.option('--rule-id ', 'rule id') + program.option('--index-only', 'only generate rule index') + program.parse(process.argv) + + const rules = loadRules() + if (!program.indexOnly) { + let ruleIds = [] + if (program.ruleId) { + ruleIds = program.ruleId.split(',') } - const rulesIndexed = {}; - rules.forEach(rule => { - if (!rulesIndexed[rule.meta.docs.category]) { - rulesIndexed[rule.meta.docs.category] = [rule]; + rules + .filter((rule) => { + if (ruleIds.length) { + return ruleIds.includes(rule.ruleId) } else { - rulesIndexed[rule.meta.docs.category].push(rule); + return true } - }); - const ruleIndexDoc = generateRuleIndex(rulesIndexed); - const ruleIndexDocPath = path.resolve(path.join(__dirname, '..', 'docs', 'rules.md')); - fs.writeFile(ruleIndexDocPath, ruleIndexDoc, function (err) { - if (err) { - console.error(err); - } else { - console.log(`Wrote ${ruleIndexDocPath}`) - } - }); + }) + .forEach((rule) => { + const ruleDoc = generateRuleDoc(rule) + const dir = path.resolve(path.join(__dirname, '..', 'docs', 'rules', rule.meta.type)) + const fileName = `${rule.ruleId}.md` + const filePath = path.resolve(path.join(dir, fileName)) + mkdir('-p', dir) + fs.writeFile(filePath, ruleDoc, function (err) { + if (err) { + console.error(err) + } else { + console.log(`Wrote ${filePath}`) + } + }) + }) + } + + const rulesIndexed = {} + rules.forEach((rule) => { + if (!rulesIndexed[rule.meta.docs.category]) { + rulesIndexed[rule.meta.docs.category] = [rule] + } else { + rulesIndexed[rule.meta.docs.category].push(rule) + } + }) + const ruleIndexDoc = generateRuleIndex(rulesIndexed) + const ruleIndexDocPath = path.resolve(path.join(__dirname, '..', 'docs', 'rules.md')) + fs.writeFile(ruleIndexDocPath, ruleIndexDoc, function (err) { + if (err) { + console.error(err) + } else { + console.log(`Wrote ${ruleIndexDocPath}`) + } + }) } -main(); +main() diff --git a/test/fixtures/naming/func-named-parameters.js b/test/fixtures/naming/func-named-parameters.js index d4ceb7ac..ef0203c6 100644 --- a/test/fixtures/naming/func-named-parameters.js +++ b/test/fixtures/naming/func-named-parameters.js @@ -1,53 +1,64 @@ const FUNCTION_CALLS_ERRORS = { - maxUnnamed_0_1u: { - code: 'funcName(_sender);', - maxUnnamed: 0, + err1: { + code: 'funcName(_sender, amount, receiver, token1, token2, token3);', + minUnnamed: 5, }, - maxUnnamed_1_3u: { - code: 'funcName(_sender, amount, receiver);', - maxUnnamed: 1, + err2: { + code: 'funcName(_sender, amount, receiver, token1, token2);', + minUnnamed: 4, }, - maxUnnamed_2_3u: { - code: 'funcName(_sender, amount, receiver);', - maxUnnamed: 2, - }, - - maxUnnamed_3_4u: { - code: 'funcName(_sender, amount, receiver, token);', - maxUnnamed: 3, + err3: { + code: 'funcName(_sender, amount, receiver, token1, token2);', + minUnnamed: 0, }, } const FUNCTION_CALLS_OK = { - maxUnnamed_0_0u: { + ok1: { code: 'funcName();', - maxUnnamed: 0, + minUnnamed: 0, }, - maxUnnamed_0_0uB: { - code: 'funcName();', - maxUnnamed: 10, + ok2: { + code: 'address(0);', + minUnnamed: 10, }, - maxUnnamed_1_0u: { + ok3: { code: 'funcName({ sender: _sender, amount: _amount, receiver: _receiver });', - maxUnnamed: 1, + minUnnamed: 1, }, - maxUnnamed_1_1u: { - code: 'funcName(sender);', - maxUnnamed: 1, + ok4: { + code: 'assert(1 == 3);', + minUnnamed: 1, }, - maxUnnamed_2_2u: { - code: 'funcName(sender, amount);', - maxUnnamed: 2, + + ok5: { + code: 'bytes foo = abi.encodeWithSelector(hex"0102030405060708", uint16(0xff00));', + minUnnamed: 2, }, - maxUnnamed3_0u: { - code: 'funcName({ sender: _sender, amount: _amount, receiver: _receiver });', - maxUnnamed: 3, + ok6: { + code: 'funcName({ sender: _sender, amount: _amount, receiver: _receiver, token1: _token1, token2: _token2 });', + minUnnamed: 5, + }, + + ok7: { + code: 'new BeaconProxy(address(0),abi.encodeWithSelector(bytes4(""),address(0),address(0),address(0)));', + minUnnamed: 2, + }, + + ok8: { + code: 'salt = keccak256(abi.encode(msg.sender, block.chainid, salt));', + minUnnamed: 0, + }, + + ok9: { + code: 'require(foobar != address(0), "foobar must a valid address");', + minUnnamed: 0, }, } diff --git a/test/fixtures/security/send-result-checked.js b/test/fixtures/security/send-result-checked.js deleted file mode 100644 index 6b4f3848..00000000 --- a/test/fixtures/security/send-result-checked.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'if(x.send(55)) {}' diff --git a/test/fixtures/security/send-result-ignored.js b/test/fixtures/security/send-result-ignored.js deleted file mode 100644 index 040959e6..00000000 --- a/test/fixtures/security/send-result-ignored.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'x.send(55);' diff --git a/test/rules/best-practises/custom-errors.js b/test/rules/best-practises/custom-errors.js new file mode 100644 index 00000000..1904a3ad --- /dev/null +++ b/test/rules/best-practises/custom-errors.js @@ -0,0 +1,103 @@ +const assert = require('assert') +const { + assertNoWarnings, + assertNoErrors, + assertErrorMessage, + assertErrorCount, + assertWarnsCount, +} = require('../../common/asserts') +const linter = require('../../../lib/index') +const { funcWith } = require('../../common/contract-builder') + +describe('Linter - custom-errors', () => { + it('should raise error for revert()', () => { + const code = funcWith(`revert();`) + const report = linter.processStr(code, { + rules: { 'custom-errors': 'error' }, + }) + + assertErrorCount(report, 1) + assertErrorMessage(report, 'Use Custom Errors instead of revert statement') + }) + + it('should raise error for revert([string])', () => { + const code = funcWith(`revert("Insufficent funds");`) + const report = linter.processStr(code, { + rules: { 'custom-errors': 'error' }, + }) + + assertErrorCount(report, 1) + assertErrorMessage(report, 'Use Custom Errors instead of revert statement') + }) + + it('should NOT raise error for revert ErrorFunction()', () => { + const code = funcWith(`revert ErrorFunction();`) + const report = linter.processStr(code, { + rules: { 'custom-errors': 'error' }, + }) + + assertNoWarnings(report) + assertNoErrors(report) + }) + + it('should NOT raise error for revert ErrorFunction() with arguments', () => { + const code = funcWith(`revert ErrorFunction({ msg: "Insufficent funds msg" });`) + const report = linter.processStr(code, { + rules: { 'custom-errors': 'error' }, + }) + + assertNoWarnings(report) + assertNoErrors(report) + }) + + it('should raise 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: { 'custom-errors': 'error' }, + }) + + assertErrorCount(report, 1) + assertErrorMessage(report, 'Use Custom Errors instead of require statement') + }) + + it('should NOT raise error for regular function call', () => { + const code = funcWith(`callOtherFunction(); + role.bearer[account] = true;role.bearer[account] = true; + `) + const report = linter.processStr(code, { + rules: { 'custom-errors': 'error' }, + }) + assertNoWarnings(report) + assertNoErrors(report) + }) + + it('should raise error for require, revert message and not for revert CustomError() for [recommended] config', () => { + const code = funcWith(`require(!has(role, account), "Roles: account already has role"); + revert("RevertMessage"); + revert CustomError(); + `) + const report = linter.processStr(code, { + extends: 'solhint:recommended', + rules: { 'compiler-version': 'off' }, + }) + + assertWarnsCount(report, 2) + assert.equal(report.reports[0].message, 'Use Custom Errors instead of require statements') + assert.equal(report.reports[1].message, 'Use Custom Errors instead of revert statements') + }) + + it('should NOT raise error for default config', () => { + const code = funcWith(`require(!has(role, account), "Roles: account already has role"); + revert("RevertMessage"); + revert CustomError(); + `) + const report = linter.processStr(code, { + extends: 'solhint:default', + }) + + assertWarnsCount(report, 0) + assertErrorCount(report, 0) + }) +}) diff --git a/test/rules/best-practises/explicit-types.js b/test/rules/best-practises/explicit-types.js index 842329c6..2ff4a296 100644 --- a/test/rules/best-practises/explicit-types.js +++ b/test/rules/best-practises/explicit-types.js @@ -1,4 +1,3 @@ -const assert = require('assert') const linter = require('../../../lib/index') const contractWith = require('../../common/contract-builder').contractWith const { assertErrorCount, assertNoErrors, assertErrorMessage } = require('../../common/asserts') @@ -21,30 +20,24 @@ const getZeroErrosObject = () => { } describe('Linter - explicit-types rule', () => { - it('should throw an error with a wrong configuration rule example 1', () => { + it('should throw an error with a wrong configuration rule', () => { const code = contractWith('uint256 public constant SNAKE_CASE = 1;') - try { - linter.processStr(code, { - rules: { 'explicit-types': ['error', 'implicito'] }, - }) - } catch (error) { - assert.ok( - error.toString().includes('Error: Config error on [explicit-types]. See explicit-types.md.') - ) - } + + const report = linter.processStr(code, { + rules: { 'explicit-types': ['error', 'implicito'] }, + }) + + assertErrorCount(report, 1) + assertErrorMessage(report, `Error: Config error on [explicit-types]. See explicit-types.md.`) }) - it('should throw an error with a wrong configuration rule example 2', () => { + it('should NOT throw error without the config array and default should take place', () => { const code = contractWith('uint256 public constant SNAKE_CASE = 1;') - try { - linter.processStr(code, { - rules: { 'explicit-types': 'error' }, - }) - } catch (error) { - assert.ok( - error.toString().includes('Error: Config error on [explicit-types]. See explicit-types.md.') - ) - } + const report = linter.processStr(code, { + rules: { 'explicit-types': 'error' }, + }) + + assertNoErrors(report) }) for (const key in VAR_DECLARATIONS) { diff --git a/test/rules/best-practises/no-global-import.js b/test/rules/best-practises/no-global-import.js index 3ab26a1b..7a357e18 100644 --- a/test/rules/best-practises/no-global-import.js +++ b/test/rules/best-practises/no-global-import.js @@ -28,7 +28,7 @@ describe('Linter - no-global-import', () => { assertErrorMessage(report, 'Specify names to import individually') }) it('should raise warning when using solhint:recommended', () => { - const code = `pragma solidity ^0.5.8; import "./A.sol";` + const code = `pragma solidity ^0.8.0; import "./A.sol";` const report = linter.processStr(code, { extends: 'solhint:recommended', diff --git a/test/rules/best-practises/no-unused-import.js b/test/rules/best-practises/no-unused-import.js index 49a2c496..f3666891 100644 --- a/test/rules/best-practises/no-unused-import.js +++ b/test/rules/best-practises/no-unused-import.js @@ -27,8 +27,28 @@ describe('Linter - no-unused-import', () => { assertErrorMessage(report, 'imported name A is not used') }) + it('should not crash on, but also not recognize, malformed inheritdoc statements', () => { + const code = ` + import {A} from './A.sol'; + // inheritdoc A + // @inheritdoc A + /// inheritdoc A + /// @ inherit A + /// @ inheritdoc A + /// @inheritdoc somethingelse + /// @inheritdoc + /// @inheritdoc + ` + + const report = linter.processStr(code, { + rules: { 'no-unused-import': 'error' }, + }) + assertErrorCount(report, 1) + assertErrorMessage(report, 'imported name A is not used') + }) + it('should raise error when using solhint:recommended', () => { - const code = `pragma solidity ^0.5.8; import {A} from "./A.sol";` + const code = `pragma solidity ^0.8.0; import {A} from "./A.sol";` const report = linter.processStr(code, { extends: 'solhint:recommended', @@ -171,10 +191,11 @@ describe('Linter - no-unused-import', () => { description: 'Name is used in an override', code: ` pragma solidity >=0.8.19; + import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; import { IERC721Metadata } from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; + contract MyContract { - /// @inheritdoc ERC721 function tokenURI(uint256 streamId) public view override(IERC721Metadata, ERC721) returns (string memory uri) { uri = "example.com"; } @@ -185,7 +206,9 @@ describe('Linter - no-unused-import', () => { description: 'Type is used in a constructor initializator', code: ` pragma solidity >=0.8.19; + import { ERC721 } from "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + contract MyContract { constructor() ERC721("Sablier V2 Lockup Dynamic NFT", "SAB-V2-LOCKUP-DYN") { } } @@ -195,7 +218,9 @@ describe('Linter - no-unused-import', () => { description: 'Import is used as value in a function parameter', code: ` pragma solidity >=0.8.19 <0.9.0; + import { UD60x18, ZERO } from "@prb/math/UD60x18.sol"; + contract SetProtocolFee_Integration_Fuzz_Test { function testFuzz_SetProtocolFee(UD60x18 newProtocolFee) external { newProtocolFee = _bound(newProtocolFee, 1, MAX_FEE); @@ -209,6 +234,7 @@ describe('Linter - no-unused-import', () => { description: 'Import is used as value in a binary expression', code: ` import { ZERO } from "@prb/math/UD60x18.sol"; + contract Foo { function returnFifteen() public returns (uint){ return ZERO + 15; @@ -220,6 +246,7 @@ describe('Linter - no-unused-import', () => { description: 'Import is used as value in an assignment', code: ` import { ZERO } from "@prb/math/UD60x18.sol"; + contract Foo { uint256 public howMuch; constructor () { @@ -228,6 +255,69 @@ describe('Linter - no-unused-import', () => { } `, }, + { + description: 'Import is used in the left side of a using for statement', + code: ` + pragma solidity >=0.8.19; + + import {EnumerableSetUD60x18, EnumerableSet} from "./libraries/EnumerableSetUD60x18.sol"; + + contract SolhintTest { + using EnumerableSetUD60x18 for EnumerableSet.Bytes32Set; + } + `, + }, + { + description: 'Import is used as the type value of a mapping', + code: ` + pragma solidity >=0.8.19; + + import {UD60x18} from "@prb/math/UD60x18.sol"; + + contract SolhintTest { + mapping(address user => UD60x18 amount) internal balance; + } + `, + }, + { + description: 'Import is used as the type value of a nested mapping', + code: ` + pragma solidity >=0.8.19; + import {SD59x18} from "@prb/math/SD59x18.sol"; + + contract SolhintTest { + mapping(address user => mapping(address relayer => SD59x18 amount)) internal balance; + } + `, + }, + { + description: 'Import is used in /// @inheritdoc', + code: ` + import { IPRBProxyPlugin } from "@prb/proxy/interfaces/IPRBProxyPlugin.sol"; + + contract SablierV2ProxyPlugin { + /// @inheritdoc IPRBProxyPlugin + function getMethods() external pure returns (bytes4[] memory methods) { + methods = new bytes4[](1); + methods[0] = this.onStreamCanceled.selector; + } + } + `, + }, + { + description: 'Import is used in /** @inheritdoc', + code: ` + import { IPRBProxyPlugin } from "@prb/proxy/interfaces/IPRBProxyPlugin.sol"; + + contract SablierV2ProxyPlugin { + /** @inheritdoc IPRBProxyPlugin */ + function getMethods() external pure returns (bytes4[] memory methods) { + methods = new bytes4[](1); + methods[0] = this.onStreamCanceled.selector; + } + } + `, + }, ].forEach(({ description, code }) => { it(`should not raise when ${description}`, () => { const report = linter.processStr(code, { diff --git a/test/rules/naming/foundry-test-functions.js b/test/rules/naming/foundry-test-functions.js new file mode 100644 index 00000000..9a0feeb9 --- /dev/null +++ b/test/rules/naming/foundry-test-functions.js @@ -0,0 +1,224 @@ +const assert = require('assert') +const linter = require('../../../lib/index') +const contractWith = require('../../common/contract-builder').contractWith +const { assertErrorCount, assertNoErrors, assertErrorMessage } = require('../../common/asserts') + +const ALLOWED_FUNCTION_NAMES = [ + 'test_', + 'testFork_', + 'testFuzz_', + 'testFail_', + 'test_Revert_', + 'test_If_', + 'test_When_', + 'testFail_Revert_', + 'testFail_If_', + 'testFail_When_', + 'testFork_Revert_', + 'testFork_If_', + 'testFork_When_', + 'testFuzz_Revert_', + 'testFuzz_If_', + 'testFuzz_When_', +] + +const DISALLOWED_FUNCTION_NAMES = ['Test_', 'Test', 'test', 'testo_', '', 'any', 'setUp', 'other'] + +const composeFunctionName = (prefix, name, visibility) => + 'function ' + prefix + name + ' ' + visibility + ' { testNumber = 42; }' + +describe('Linter - foundry-test-functions', () => { + for (const prefix of DISALLOWED_FUNCTION_NAMES) { + it(`should raise error for DISALLOWED_FUNCTION_NAMES [${prefix}] when PUBLIC`, () => { + const functionDefinition = composeFunctionName(prefix, 'FunctionName()', 'public') + const code = contractWith(functionDefinition) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertErrorCount(report, 1) + assertErrorMessage( + report, + `Function ${prefix + 'FunctionName()'} must match Foundry test naming convention` + ) + }) + } + + for (const prefix of DISALLOWED_FUNCTION_NAMES) { + it(`should NOT raise error for DISALLOWED_FUNCTION_NAMES [${prefix}] when INTERNAL`, () => { + const functionDefinition = composeFunctionName(prefix, 'FunctionName()', 'internal') + const code = contractWith(functionDefinition) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertNoErrors(report) + }) + } + + for (const prefix of ALLOWED_FUNCTION_NAMES) { + it(`should NOT raise error for ALLOWED_FUNCTION_NAMES [${prefix}] when PUBLIC`, () => { + const functionDefinition = composeFunctionName(prefix, 'FunctionName()', 'public') + const code = contractWith(functionDefinition) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertNoErrors(report) + }) + } + + for (const prefix of ALLOWED_FUNCTION_NAMES) { + it(`should NOT raise error for ALLOWED_FUNCTION_NAMES [${prefix}] when EXTERNAL`, () => { + const functionDefinition = composeFunctionName(prefix, 'FunctionName()', 'external') + const code = contractWith(functionDefinition) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertNoErrors(report) + }) + } + + for (const prefix of ALLOWED_FUNCTION_NAMES) { + it(`should NOT raise error for ALLOWED_FUNCTION_NAMES [${prefix}] when INTERNAL`, () => { + const functionDefinition = composeFunctionName(prefix, 'FunctionName()', 'external') + const code = contractWith(functionDefinition) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertNoErrors(report) + }) + } + + it(`should NOT raise error for setUp function, since is confired as SKIPPED`, () => { + const code = contractWith( + 'function setUp() public { testNumber = 42; } function finish() public { testNumber = 42; }' + ) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertNoErrors(report) + }) + + it(`should NOT raise error for setUp and finish functions but RAISE for the other two functions`, () => { + const code = contractWith(` + function setUp() public { testNumber = 42; } + function finish() public { testNumber = 43; } + function invalidFunction1() external { testNumber = 44; } + function invalidFunction2() external { testNumber = 45; }`) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] }, + }) + + assertErrorCount(report, 2) + assert.equal( + report.reports[0].message, + `Function invalidFunction1() must match Foundry test naming convention` + ) + assert.equal( + report.reports[1].message, + 'Function invalidFunction2() must match Foundry test naming convention' + ) + }) + + it('should NOT raise error when recommended rules are configured', () => { + const code = contractWith(` + function setUp() public { testNumber = 42; } + function finish() public { testNumber = 43; } + function invalidFunction1() external { testNumber = 44; } + function invalidFunction2() external { testNumber = 45; }`) + + const report = linter.processStr(code, { + extends: 'solhint:recommended', + rules: { 'compiler-version': 'off' }, + }) + + assertNoErrors(report) + }) + + it('should raise 2 errors when all rules are configured and setUp is skipped', () => { + const code = contractWith(` + function setUp() public { testNumber = 42; } + function invalidFunction1() external { testNumber = 44; } + function invalidFunction2() external { testNumber = 45; }`) + + const report = linter.processStr(code, { + extends: 'solhint:recommended', + rules: { + 'compiler-version': 'off', + 'foundry-test-functions': ['error', ['setUp', 'finish']], + }, + }) + + assertErrorCount(report, 2) + assert.equal( + report.reports[0].message, + `Function invalidFunction1() must match Foundry test naming convention` + ) + assert.equal( + report.reports[1].message, + 'Function invalidFunction2() must match Foundry test naming convention' + ) + }) + + it(`should NOT raise error only for setUp when rule is just on 'error'`, () => { + const code = contractWith(` + function setUp() public { testNumber = 42; } + function finish() public { testNumber = 43; } + function invalidFunction1() external { testNumber = 44; } + function invalidFunction2() external { testNumber = 45; }`) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': 'error' }, + }) + + assertErrorCount(report, 3) + assert.equal( + report.reports[0].message, + 'Function finish() must match Foundry test naming convention' + ) + assert.equal( + report.reports[1].message, + `Function invalidFunction1() must match Foundry test naming convention` + ) + assert.equal( + report.reports[2].message, + 'Function invalidFunction2() must match Foundry test naming convention' + ) + }) + + it(`should raise error for all functions when rule SKIP array is empty`, () => { + const code = contractWith(` + function setUp() public { testNumber = 42; } + function finish() public { testNumber = 43; } + function invalidFunction() external { testNumber = 44; }`) + + const report = linter.processStr(code, { + rules: { 'foundry-test-functions': ['error', []] }, + }) + + assertErrorCount(report, 3) + assert.equal( + report.reports[0].message, + 'Function setUp() must match Foundry test naming convention' + ) + assert.equal( + report.reports[1].message, + `Function finish() must match Foundry test naming convention` + ) + assert.equal( + report.reports[2].message, + 'Function invalidFunction() must match Foundry test naming convention' + ) + }) +}) diff --git a/test/rules/naming/func-named-parameters.js b/test/rules/naming/func-named-parameters.js index 9fe90566..9591e434 100644 --- a/test/rules/naming/func-named-parameters.js +++ b/test/rules/naming/func-named-parameters.js @@ -11,33 +11,37 @@ const { FUNCTION_CALLS_OK, } = require('../../fixtures/naming/func-named-parameters') +const DEFAULT_MIN_UNNAMED_ARGUMENTS = 4 + describe('Linter - func-named-parameters', () => { for (const key in FUNCTION_CALLS_ERRORS) { it(`should raise error for FUNCTION_CALLS_ERRORS [${key}]`, () => { - const { code, maxUnnamed } = FUNCTION_CALLS_ERRORS[key] + const { code, minUnnamed } = FUNCTION_CALLS_ERRORS[key] const sourceCode = contractWith('function callerFunction() public { ' + code + ' }') const report = linter.processStr(sourceCode, { - rules: { 'func-named-parameters': ['error', maxUnnamed] }, + rules: { 'func-named-parameters': ['error', minUnnamed] }, }) assertErrorCount(report, 1) assertErrorMessage( report, - `Missing named parameters. Max unnamed parameters value is ${maxUnnamed}` + `Named parameters missing. MIN unnamed argumenst is ${ + minUnnamed < DEFAULT_MIN_UNNAMED_ARGUMENTS ? DEFAULT_MIN_UNNAMED_ARGUMENTS : minUnnamed + }` ) }) } for (const key in FUNCTION_CALLS_OK) { it(`should NOT raise error for FUNCTION_CALLS_OK [${key}]`, () => { - const { code, maxUnnamed } = FUNCTION_CALLS_OK[key] + const { code, minUnnamed } = FUNCTION_CALLS_OK[key] const sourceCode = contractWith('function callerFunction() public { ' + code + ' }') const report = linter.processStr(sourceCode, { - rules: { 'func-named-parameters': ['error', maxUnnamed] }, + rules: { 'func-named-parameters': ['error', minUnnamed] }, }) assertNoErrors(report) @@ -46,7 +50,7 @@ describe('Linter - func-named-parameters', () => { it('should NOT raise error when default rules are configured', () => { const code = contractWith( - `function callerFunction() public { funcName(sender, amount, receiver); }` + `function callerFunction() public { funcName(sender, amount, receiver, token1, token2); }` ) const report = linter.processStr(code, { extends: 'solhint:default', @@ -56,22 +60,39 @@ describe('Linter - func-named-parameters', () => { assertNoErrors(report) }) - it('should raise error when recommended rules are configured', () => { + it('should NOT raise error when recommended rules are configured', () => { const code = contractWith( - `function callerFunction() public { funcName(sender, amount, receiver); }` + `function callerFunction() public { funcName(sender, amount, receiver, token1, token2); }` ) const report = linter.processStr(code, { extends: 'solhint:recommended', rules: { 'compiler-version': 'off' }, }) + assertNoErrors(report) + }) + + it('should raise warning when all rules are configured (no min value)', () => { + const code = contractWith( + `function callerFunction() public { funcName(sender, amount, receiver, token1, token2, token3); }` + ) + + const report = linter.processStr(code, { + extends: 'solhint:all', + rules: { 'compiler-version': 'off', 'comprehensive-interface': 'off' }, + }) + assertWarnsCount(report, 1) - assertErrorMessage(report, `Missing named parameters. Max unnamed parameters value is 2`) + + assertErrorMessage( + report, + `Named parameters missing. MIN unnamed argumenst is ${DEFAULT_MIN_UNNAMED_ARGUMENTS}` + ) }) - it('should raise error when rule has no maxUnnamed arguments is set and default value takes over', () => { + it('should raise error when rule has no minUnnamed arguments is set and default value takes over', () => { const code = contractWith( - `function callerFunction() public { funcName(sender, amount, receiver); }` + `function callerFunction() public { funcName(sender, amount, receiver, token1, token2, token3); }` ) const report = linter.processStr(code, { rules: { 'func-named-parameters': 'error' }, @@ -79,7 +100,9 @@ describe('Linter - func-named-parameters', () => { assertErrorCount(report, 1) - // default value is 2 for this rule - assertErrorMessage(report, `Missing named parameters. Max unnamed parameters value is 2`) + assertErrorMessage( + report, + `Named parameters missing. MIN unnamed argumenst is ${DEFAULT_MIN_UNNAMED_ARGUMENTS}` + ) }) }) diff --git a/test/rules/naming/named-return-values.js b/test/rules/naming/named-return-values.js new file mode 100644 index 00000000..b93d1349 --- /dev/null +++ b/test/rules/naming/named-return-values.js @@ -0,0 +1,92 @@ +const assert = require('assert') +const linter = require('../../../lib/index') +const contractWith = require('../../common/contract-builder').contractWith +const { assertErrorCount, assertNoErrors, assertWarnsCount } = require('../../common/asserts') + +describe('Linter - named-return-values', () => { + it('should NOT raise error for named return values', () => { + const code = contractWith( + `function getBalanceFromTokens(address wallet) public returns(address token1, address token2, uint256 balance1, uint256 balance2) { balance = 1; }` + ) + const report = linter.processStr(code, { + rules: { 'named-return-values': 'error' }, + }) + assertNoErrors(report) + }) + + it('should raise error for unnamed return values', () => { + const code = contractWith( + `function getBalanceFromTokens(address wallet) public returns(address, address, uint256, uint256) { balance = 1; }` + ) + const report = linter.processStr(code, { + rules: { 'named-return-values': 'error' }, + }) + + assertErrorCount(report, 4) + for (let index = 0; index < report.reports.length; index++) { + assert.equal(report.reports[index].message, `Named return value is missing - Index ${index}`) + } + }) + + it('should NOT raise error for functions without return values', () => { + const code = contractWith(`function writeOnStorage(address wallet) public { balance = 1; }`) + const report = linter.processStr(code, { + rules: { 'named-return-values': 'error' }, + }) + assertNoErrors(report) + }) + + it('should raise error for 2 unnamed return values', () => { + const code = contractWith( + `function getBalanceFromTokens(address wallet) public returns(address user, address, uint256 amount, uint256) { balance = 1; }` + ) + const report = linter.processStr(code, { + rules: { 'named-return-values': 'error' }, + }) + + assertErrorCount(report, 2) + assert.equal(report.reports[0].message, `Named return value is missing - Index 1`) + assert.equal(report.reports[1].message, `Named return value is missing - Index 3`) + }) + + it('should NOT raise error for solhint:recommended setup', () => { + const code = contractWith( + `function getBalanceFromTokens(address wallet) public returns(address, address, uint256, uint256) { balance = 1; }` + ) + + const report = linter.processStr(code, { + extends: 'solhint:recommended', + rules: { 'compiler-version': 'off' }, + }) + + assertNoErrors(report) + }) + + it('should NOT raise error for solhint:default setup', () => { + const code = contractWith( + `function getBalance(address wallet) public returns(uint256) { balance = 1; }` + ) + + const report = linter.processStr(code, { + extends: 'solhint:default', + }) + + assertNoErrors(report) + }) + + it('should raise error for solhint:all setup', () => { + const code = contractWith( + `function getBalance(uint256 wallet) public override returns(uint256, address) { wallet = 1; }` + ) + + const report = linter.processStr(code, { + extends: 'solhint:all', + rules: { 'compiler-version': 'off' }, + }) + + assertWarnsCount(report, 2) + for (let index = 0; index < report.reports.length; index++) { + assert.equal(report.reports[index].message, `Named return value is missing - Index ${index}`) + } + }) +}) diff --git a/test/rules/order/ordering.js b/test/rules/order/ordering.js index d83d73f4..1ff356ef 100644 --- a/test/rules/order/ordering.js +++ b/test/rules/order/ordering.js @@ -427,4 +427,63 @@ describe('Linter - ordering', () => { assert.equal(report.errorCount, 0) }) + + it('should raise error when INITIALIZER is NOT well located', () => { + const code = ` + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + import "@openzeppelin/contracts/ownership/Ownable.sol"; + uint256 constant oneNiceConstant = 1; + struct OneNiceStruct { uint256 a; uint256 b; } + enum MyEnum { A, B } + error Unauthorized(); + function freeFunction() pure returns (uint256) { + return 1; + } + contract OneNiceContract { + function initialize() initializer { + oneNiceConstant; + } + struct OneNiceStruct { uint256 a; uint256 b; } + } + ` + + const report = linter.processStr(code, { + rules: { ordering: 'error' }, + }) + + assert.equal(report.errorCount, 1) + assert.ok( + report.messages[0].message.includes( + 'Function order is incorrect, struct definition can not go after constructor/initializer' + ) + ) + }) + + it('should NOT raise error when INITIALIZER is well located', () => { + const code = ` + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.0; + import "@openzeppelin/contracts/ownership/Ownable.sol"; + uint256 constant oneNiceConstant = 1; + enum MyEnum { A, B } + struct OneNiceStruct { uint256 a; uint256 b; } + error Unauthorized(); + function freeFunction() pure returns (uint256) { + return 1; + } + contract OneNiceContract { + struct OneNiceStruct { uint256 a; uint256 b; } + function initialize() initializer { + oneNiceConstant; + } + } + ` + + const report = linter.processStr(code, { + rules: { ordering: 'error' }, + }) + + assert.equal(report.errorCount, 0) + }) }) diff --git a/test/rules/security/check-send-result.js b/test/rules/security/check-send-result.js index c2d75d8b..69f0875d 100644 --- a/test/rules/security/check-send-result.js +++ b/test/rules/security/check-send-result.js @@ -4,7 +4,7 @@ const funcWith = require('../../common/contract-builder').funcWith describe('Linter - check-send-result', () => { it('should return "send" call verification error', () => { - const code = funcWith(require('../../fixtures/security/send-result-ignored')) + const code = funcWith('x.send(55);') const report = linter.processStr(code, { rules: { 'check-send-result': 'error' }, @@ -15,7 +15,7 @@ describe('Linter - check-send-result', () => { }) it('should not return "send" call verification error', () => { - const code = funcWith(require('../../fixtures/security/send-result-checked')) + const code = funcWith('if(x.send(55)) {}') const report = linter.processStr(code, { rules: { 'check-send-result': 'error' }, @@ -73,4 +73,14 @@ describe('Linter - check-send-result', () => { assert.equal(report.errorCount, 1) }) + + it('should not emit error when the send() is used for an ERC777', () => { + const code = funcWith('erc777.send(recipient, amount, "");') + + const report = linter.processStr(code, { + rules: { 'check-send-result': 'error' }, + }) + + assert.equal(report.errorCount, 0) + }) }) diff --git a/test/rules/security/compiler-version.js b/test/rules/security/compiler-version.js index 3f18d6fd..b3c51feb 100644 --- a/test/rules/security/compiler-version.js +++ b/test/rules/security/compiler-version.js @@ -2,6 +2,8 @@ const assert = require('assert') const { assertNoErrors, assertErrorCount, assertErrorMessage } = require('../../common/asserts') const linter = require('../../../lib/index') +const DEFAULT_SEMVER = '^0.8.0' + describe('Linter - compiler-version', () => { it('should disable only one compiler error on next line', () => { const report = linter.processStr( @@ -11,7 +13,7 @@ describe('Linter - compiler-version', () => { pragma solidity 0.3.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) @@ -26,7 +28,7 @@ describe('Linter - compiler-version', () => { pragma solidity 0.3.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) @@ -41,7 +43,7 @@ describe('Linter - compiler-version', () => { pragma solidity 0.3.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) @@ -56,7 +58,7 @@ describe('Linter - compiler-version', () => { pragma solidity 0.3.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) @@ -72,12 +74,12 @@ describe('Linter - compiler-version', () => { pragma solidity 0.3.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) assertErrorCount(report, 1) - assertErrorMessage(report, '0.5.2') + assertErrorMessage(report, DEFAULT_SEMVER) }) it('should disable all errors', () => { @@ -88,7 +90,7 @@ describe('Linter - compiler-version', () => { pragma solidity 0.3.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) assertNoErrors(report) @@ -103,58 +105,58 @@ describe('Linter - compiler-version', () => { pragma solidity ^0.4.4; `, { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, } ) assertErrorCount(report, 1) - assertErrorMessage(report, '0.5.2') + assertErrorMessage(report, DEFAULT_SEMVER) }) it('should return compiler version error', () => { const report = linter.processStr('pragma solidity 0.3.4;', { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, }) assert.equal(report.errorCount, 1) - assert.ok(report.reports[0].message.includes('0.5.2')) + assert.ok(report.reports[0].message.includes(DEFAULT_SEMVER)) }) it('should not report compiler version error on exact match', () => { - const report = linter.processStr('pragma solidity 0.5.2;', { - rules: { 'compiler-version': ['error', '0.5.2'] }, + const report = linter.processStr('pragma solidity 0.8.0;', { + rules: { 'compiler-version': ['error', '0.8.0'] }, }) assert.equal(report.errorCount, 0) }) it('should not report compiler version error on range match', () => { - const report = linter.processStr('pragma solidity ^0.5.2;', { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + const report = linter.processStr('pragma solidity ^0.8.0;', { + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, }) assert.equal(report.errorCount, 0) }) it('should not report compiler version error on patch bump', () => { - const report = linter.processStr('pragma solidity 0.5.3;', { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + const report = linter.processStr('pragma solidity 0.8.1;', { + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, }) assert.equal(report.errorCount, 0) }) it('should not report compiler version error on range match', () => { - const report = linter.processStr('pragma solidity ^0.5.3;', { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + const report = linter.processStr('pragma solidity ^0.8.2;', { + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, }) assert.equal(report.errorCount, 0) }) it('should report compiler version error on range not matching', () => { - const report = linter.processStr('pragma solidity ^0.5.2;', { - rules: { 'compiler-version': ['error', '^0.5.3'] }, + const report = linter.processStr('pragma solidity ^0.8.1;', { + rules: { 'compiler-version': ['error', '^0.8.3'] }, }) assert.equal(report.errorCount, 1) @@ -162,7 +164,7 @@ describe('Linter - compiler-version', () => { it('should report compiler version error on minor bump', () => { const report = linter.processStr('pragma solidity 0.6.0;', { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, }) assert.equal(report.errorCount, 1) @@ -170,7 +172,7 @@ describe('Linter - compiler-version', () => { it(`should report compiler version error if pragma doesn't exist`, () => { const report = linter.processStr('contract Foo {}', { - rules: { 'compiler-version': ['error', '^0.5.2'] }, + rules: { 'compiler-version': ['error', DEFAULT_SEMVER] }, }) assert.equal(report.errorCount, 1) @@ -178,10 +180,10 @@ describe('Linter - compiler-version', () => { it(`should not report compiler version error if pragma exist`, () => { const report = linter.processStr( - `pragma solidity 0.6.0; + `pragma solidity 0.8.2; contract Foo {}`, { - rules: { 'compiler-version': ['error', '^0.6.0'] }, + rules: { 'compiler-version': ['error', '^0.8.2'] }, } ) @@ -202,4 +204,62 @@ describe('Linter - compiler-version', () => { assert.equal(report.errorCount, 0) }) + + it(`should not report compiler version error using default and correct pragma`, () => { + const report = linter.processStr( + `pragma solidity ^0.8.1; + pragma experimental ABIEncoderV2; + + contract Main { + }`, + { + rules: { 'compiler-version': 'error' }, + } + ) + + assert.equal(report.errorCount, 0) + }) + + it(`should report compiler version error using default and lower pragma`, () => { + const report = linter.processStr( + `pragma solidity ^0.7.4; + + contract Main { + }`, + { + rules: { 'compiler-version': 'error' }, + } + ) + + assert.equal(report.errorCount, 1) + assertErrorMessage(report, DEFAULT_SEMVER) + }) + + it(`should not report compiler version error using >= and default and correct pragma`, () => { + const report = linter.processStr( + `pragma solidity >=0.8.0; + + contract Main { + }`, + { + rules: { 'compiler-version': 'error' }, + } + ) + + assert.equal(report.errorCount, 0) + }) + + it(`should report compiler version error using >= and default and lower pragma`, () => { + const report = linter.processStr( + `pragma solidity >=0.7.4; + + contract Main { + }`, + { + rules: { 'compiler-version': 'error' }, + } + ) + assert.equal(report.errorCount, 1) + assertErrorMessage(report, DEFAULT_SEMVER) + }) })