Repository: protofire/solhint
Pull request #545 (2024-01-23 15:22:23)

GC: Increment by one


Patch:
diff --git a/conf/rulesets/solhint-all.js b/conf/rulesets/solhint-all.js
index 9b664a8..d2fc240 100644
--- a/conf/rulesets/solhint-all.js
+++ b/conf/rulesets/solhint-all.js
@@ -26,6 +26,7 @@ module.exports = Object.freeze({
     ],
     'constructor-syntax': 'warn',
     'gas-calldata-parameters': 'warn',
+    'gas-increment-by-one': 'warn',
     'gas-indexed-events': 'warn',
     'gas-multitoken1155': 'warn',
     'gas-small-strings': 'warn',
diff --git a/docs/rules.md b/docs/rules.md
index 6f6cfcb..bb220f3 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -27,12 +27,13 @@ title:       "Rule Index of Solhint"
 
 ## Gas Consumption Rules
 
-| Rule Id                                                                       | Error                                                          | Recommended | Deprecated |
-| ----------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------- | ---------- |
-| [gas-calldata-parameters](./rules/gas-consumption/gas-calldata-parameters.md) | Suggest calldata keyword on function arguments when read only  |             |            |
-| [gas-indexed-events](./rules/gas-consumption/gas-indexed-events.md)           | Suggest indexed arguments on events for uint, bool and address |             |            |
-| [gas-multitoken1155](./rules/gas-consumption/gas-multitoken1155.md)           | ERC1155 is a cheaper non-fungible token than ERC721            |             |            |
-| [gas-small-strings](./rules/gas-consumption/gas-small-strings.md)             | Keep strings smaller than 32 bytes                             |             |            |
+| Rule Id                                                                       | Error                                                             | Recommended | Deprecated |
+| ----------------------------------------------------------------------------- | ----------------------------------------------------------------- | ----------- | ---------- |
+| [gas-calldata-parameters](./rules/gas-consumption/gas-calldata-parameters.md) | Suggest calldata keyword on function arguments when read only     |             |            |
+| [gas-increment-by-one](./rules/gas-consumption/gas-increment-by-one.md)       | Suggest incrementation by one like this ++i instead of other type |             |            |
+| [gas-indexed-events](./rules/gas-consumption/gas-indexed-events.md)           | Suggest indexed arguments on events for uint, bool and address    |             |            |
+| [gas-multitoken1155](./rules/gas-consumption/gas-multitoken1155.md)           | ERC1155 is a cheaper non-fungible token than ERC721               |             |            |
+| [gas-small-strings](./rules/gas-consumption/gas-small-strings.md)             | Keep strings smaller than 32 bytes                                |             |            |
         
 
 ## Miscellaneous
diff --git a/docs/rules/gas-consumption/gas-calldata-parameters.md b/docs/rules/gas-consumption/gas-calldata-parameters.md
index 907d514..09ad20a 100644
--- a/docs/rules/gas-consumption/gas-calldata-parameters.md
+++ b/docs/rules/gas-consumption/gas-calldata-parameters.md
@@ -26,8 +26,8 @@ This rule accepts a string option of rule severity. Must be one of "error", "war
 ### Notes
 - Only applies for external functions when receiving arguments with [memory] keyword
 - This rule makes a soft check to see if argument is readOnly to make the suggestion. Check it manually before changing it.
-- [source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiciative (see Calldata vs Memory)
-- [source 2](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-6acr7) of the rule initiciative
+- [source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (see Calldata vs Memory)
+- [source 2](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-6acr7) of the rule initiative
 
 ## Examples
 This rule does not have examples.
diff --git a/docs/rules/gas-consumption/gas-increment-by-one.md b/docs/rules/gas-consumption/gas-increment-by-one.md
new file mode 100644
index 0000000..3d63fd7
--- /dev/null
+++ b/docs/rules/gas-consumption/gas-increment-by-one.md
@@ -0,0 +1,40 @@
+---
+warning:     "This is a dynamically generated file. Do not edit manually."
+layout:      "default"
+title:       "gas-increment-by-one | Solhint"
+---
+
+# gas-increment-by-one
+![Category Badge](https://img.shields.io/badge/-Gas%20Consumption%20Rules-informational)
+![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow)
+
+## Description
+Suggest incrementation by one like this ++i instead of other type
+
+## Options
+This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Default to warn.
+
+### Example Config
+```json
+{
+  "rules": {
+    "gas-increment-by-one": "warn"
+  }
+}
+```
+
+### Notes
+- This rule only works for expressions like this: [ j = j + 1 ] but will fail is the code is written like this: [ j = 1 + j ]
+- [source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (Incrementing/Decrementing By 1)
+- [source 2](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-8rekj) of the rule initiative
+
+## Examples
+This rule does not have examples.
+
+## Version
+This rule is introduced in the latest version.
+
+## Resources
+- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/gas-consumption/gas-increment-by-one.js)
+- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/gas-consumption/gas-increment-by-one.md)
+- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/gas-consumption/gas-increment-by-one.js)
diff --git a/docs/rules/gas-consumption/gas-indexed-events.md b/docs/rules/gas-consumption/gas-indexed-events.md
index 62dd17d..bfcb5f8 100644
--- a/docs/rules/gas-consumption/gas-indexed-events.md
+++ b/docs/rules/gas-consumption/gas-indexed-events.md
@@ -24,7 +24,7 @@ This rule accepts a string option of rule severity. Must be one of "error", "war
 ```
 
 ### Notes
-- [source](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiciative (see Indexed Events)
+- [source](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (see Indexed Events)
 
 ## Examples
 This rule does not have examples.
diff --git a/docs/rules/gas-consumption/gas-multitoken1155.md b/docs/rules/gas-consumption/gas-multitoken1155.md
index 535cd0b..e06f96c 100644
--- a/docs/rules/gas-consumption/gas-multitoken1155.md
+++ b/docs/rules/gas-consumption/gas-multitoken1155.md
@@ -24,7 +24,7 @@ This rule accepts a string option of rule severity. Must be one of "error", "war
 ```
 
 ### Notes
-- [source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-8v8t9) of the rule initiciative
+- [source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-8v8t9) of the rule initiative
 
 ## Examples
 This rule does not have examples.
diff --git a/docs/rules/gas-consumption/gas-small-strings.md b/docs/rules/gas-consumption/gas-small-strings.md
index 2121d29..7a3d852 100644
--- a/docs/rules/gas-consumption/gas-small-strings.md
+++ b/docs/rules/gas-consumption/gas-small-strings.md
@@ -24,7 +24,7 @@ This rule accepts a string option of rule severity. Must be one of "error", "war
 ```
 
 ### Notes
-- [source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-ck1vq) of the rule initiciative
+- [source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-ck1vq) of the rule initiative
 
 ## Examples
 This rule does not have examples.
diff --git a/lib/rules/gas-consumption/gas-calldata-parameters.js b/lib/rules/gas-consumption/gas-calldata-parameters.js
index c53aa3d..a5e5740 100644
--- a/lib/rules/gas-consumption/gas-calldata-parameters.js
+++ b/lib/rules/gas-consumption/gas-calldata-parameters.js
@@ -30,10 +30,10 @@ const meta = {
         note: 'This rule makes a soft check to see if argument is readOnly to make the suggestion. Check it manually before changing it.',
       },
       {
-        note: '[source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiciative (see Calldata vs Memory)',
+        note: '[source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (see Calldata vs Memory)',
       },
       {
-        note: '[source 2](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-6acr7) of the rule initiciative',
+        note: '[source 2](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-6acr7) of the rule initiative',
       },
     ],
   },
diff --git a/lib/rules/gas-consumption/gas-increment-by-one.js b/lib/rules/gas-consumption/gas-increment-by-one.js
new file mode 100644
index 0000000..d134046
--- /dev/null
+++ b/lib/rules/gas-consumption/gas-increment-by-one.js
@@ -0,0 +1,152 @@
+/* eslint-disable */
+const { isArray } = require('lodash')
+const BaseChecker = require('../base-checker')
+
+const operators = ['+', '-', '++', '--']
+const binaryOperators = ['+', '-', '+=', '-=']
+
+const ruleId = 'gas-increment-by-one'
+const meta = {
+  type: 'gas-consumption',
+
+  docs: {
+    description: 'Suggest incrementation by one like this ++i instead of other type',
+    category: 'Gas Consumption Rules',
+    notes: [
+      {
+        note: 'This rule only works for expressions like this: [ j = j + 1 ] but will fail is the code is written like this: [ j = 1 + j ]',
+      },
+      {
+        note: '[source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (Incrementing/Decrementing By 1)',
+      },
+      {
+        note: '[source 2](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-8rekj) of the rule initiative',
+      },
+    ],
+  },
+
+  isDefault: false,
+  recommended: false,
+  defaultSetup: 'warn',
+
+  schema: null,
+}
+
+class GasIncrementByOne extends BaseChecker {
+  constructor(reporter) {
+    super(reporter, ruleId, meta)
+  }
+
+  UnaryOperation(node) {
+    if (node.isPrefix) {
+      // Ignore pre-increment and pre-decrement
+      return
+    }
+
+    if (node.operator === '++' || node.operator === '--') {
+      const variableName = this.extractVariableName(node.subExpression)
+      if (variableName) {
+        this.reportError(node, variableName)
+      }
+    }
+  }
+
+  BinaryOperation(node) {
+    if (node.operator === '=' || node.operator === '+=' || node.operator === '-=') {
+      const resultObject = this.isVariableIncrementOrDecrement(node.left, node.right, node.operator)
+      if (resultObject.result) {
+        this.reportError(node, resultObject.varName)
+      }
+    }
+  }
+
+  isVariableIncrementOrDecrement(left, right, operator) {
+    const leftVar = this.extractVariableName(left)
+    let rightValue
+
+    // asignment and operation
+    if (operator === '+=' || operator === '-=') {
+      rightValue = right.type === 'NumberLiteral' && parseInt(right.number) === 1
+      return { result: leftVar && rightValue, varName: leftVar }
+    }
+    // regular asignment
+    else if (operator === '=') {
+      const rightVar = this.extractVariableName(right.left)
+      rightValue =
+        right.right && right.right.type === 'NumberLiteral' && parseInt(right.right.number) === 1
+      return { result: leftVar === rightVar && rightValue, varName: leftVar }
+    }
+
+    return false
+  }
+
+  extractVariableName(node) {
+    if (node.type === 'Identifier') {
+      return node.name
+    } else if (node.type === 'MemberAccess') {
+      return this.extractVariableName(node.expression) + '.' + node.memberName
+    } else if (node.type === 'IndexAccess') {
+      return this.extractVariableName(node.base)
+    }
+    return null
+  }
+
+  reportError(node, fullVariableName) {
+    const variableName = fullVariableName.split('.')[0]
+    this.error(
+      node,
+      `GC: For [ ${variableName} ] variable, increment/decrement by 1 using: [ ++variable ] to save gas`
+    )
+  }
+}
+
+module.exports = GasIncrementByOne
+
+// SourceUnit(node) {
+//   this.findVariableUpdates(node)
+// }
+
+// findVariableUpdates(node) {
+//   if (node === null || typeof node !== 'object') {
+//     return
+//   }
+
+//   // Check for assignment in a function body
+//   if (node.type === 'FunctionDefinition') {
+//     this.findVariableUpdates(node.body)
+//     return
+//   }
+
+//   if (node.type === 'ExpressionStatement' && node.expression.type === 'BinaryOperation') {
+//     const expr = node.expression
+//     if (expr.operator === '=' || expr.operator === '+=' || expr.operator === '-=') {
+//       if (this.isVariableIncrementOrDecrement(expr.left, expr.right, expr.operator)) {
+//         this.reportError(node, this.extractVariableName(expr.left))
+//       }
+//     }
+//   }
+
+//   Object.values(node).forEach((value) => {
+//     if (typeof value === 'object') {
+//       this.findVariableUpdates(value)
+//     }
+//   })
+// }
+
+// isVariableIncrementOrDecrement(left, right, operator) {
+//   const leftVar = this.extractVariableName(left)
+//   let rightVar, rightValue
+
+//   if (operator === '+=' || operator === '-=') {
+//     // For compound assignment, the operation is directly on the variable
+//     rightValue = right.type === 'NumberLiteral' && parseInt(right.number) === 1
+//     return leftVar && rightValue
+//   } else if (operator === '=') {
+//     rightVar = this.extractVariableName(right.left)
+//     rightValue =
+//       right.right && right.right.type === 'NumberLiteral' && parseInt(right.right.number) === 1
+//     return leftVar === rightVar && rightValue
+//   }
+
+//   return false
+// }
diff --git a/lib/rules/gas-consumption/gas-indexed-events.js b/lib/rules/gas-consumption/gas-indexed-events.js
index ca07d54..847cb1c 100644
--- a/lib/rules/gas-consumption/gas-indexed-events.js
+++ b/lib/rules/gas-consumption/gas-indexed-events.js
@@ -10,7 +10,7 @@ const meta = {
     category: 'Gas Consumption Rules',
     notes: [
       {
-        note: '[source](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiciative (see Indexed Events)',
+        note: '[source](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (see Indexed Events)',
       },
     ],
   },
diff --git a/lib/rules/gas-consumption/gas-multitoken1155.js b/lib/rules/gas-consumption/gas-multitoken1155.js
index e137e66..4ee1ab4 100644
--- a/lib/rules/gas-consumption/gas-multitoken1155.js
+++ b/lib/rules/gas-consumption/gas-multitoken1155.js
@@ -9,7 +9,7 @@ const meta = {
     category: 'Gas Consumption Rules',
     notes: [
       {
-        note: '[source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-8v8t9) of the rule initiciative',
+        note: '[source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-8v8t9) of the rule initiative',
       },
     ],
   },
diff --git a/lib/rules/gas-consumption/gas-small-strings.js b/lib/rules/gas-consumption/gas-small-strings.js
index 0fc24a3..d2008ce 100644
--- a/lib/rules/gas-consumption/gas-small-strings.js
+++ b/lib/rules/gas-consumption/gas-small-strings.js
@@ -9,7 +9,7 @@ const meta = {
     category: 'Gas Consumption Rules',
     notes: [
       {
-        note: '[source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-ck1vq) of the rule initiciative',
+        note: '[source](https://www.rareskills.io/post/gas-optimization?postId=c9db474a-ff97-4fa3-a51d-fe13ccb8fe3b#viewer-ck1vq) of the rule initiative',
       },
     ],
   },
diff --git a/lib/rules/gas-consumption/index.js b/lib/rules/gas-consumption/index.js
index 41fc27c..4c7a5ee 100644
--- a/lib/rules/gas-consumption/index.js
+++ b/lib/rules/gas-consumption/index.js
@@ -2,6 +2,7 @@ const GasMultitoken1155 = require('./gas-multitoken1155')
 const GasSmallStrings = require('./gas-small-strings')
 const GasIndexedEvents = require('./gas-indexed-events')
 const GasCalldataParameters = require('./gas-calldata-parameters')
+const IncrementByOne = require('./gas-increment-by-one')
 
 // module.exports = function checkers(reporter, config, tokens) {
 module.exports = function checkers(reporter, config) {
@@ -10,5 +11,6 @@ module.exports = function checkers(reporter, config) {
     new GasSmallStrings(reporter, config),
     new GasIndexedEvents(reporter, config),
     new GasCalldataParameters(reporter, config),
+    new IncrementByOne(reporter, config),
   ]
 }


Test patch:
diff --git a/test/rules/gas-consumption/gas-increment-by-one.js b/test/rules/gas-consumption/gas-increment-by-one.js
new file mode 100644
index 0000000..a5c5b79
--- /dev/null
+++ b/test/rules/gas-consumption/gas-increment-by-one.js
@@ -0,0 +1,108 @@
+const assert = require('assert')
+const linter = require('../../../lib/index')
+const { funcWith } = require('../../common/contract-builder')
+
+const replaceErrorMsg = (variableName) => {
+  const errMsg = `GC: For [ ${variableName} ] variable, increment/decrement by 1 using: [ ++variable ] to save gas`
+  return errMsg
+}
+
+const RAISE_ERRORS = [
+  {
+    statement: 'myStringArray[0] = myStringArray[0] - 1;',
+    varName: 'myStringArray',
+  },
+  {
+    statement: 'customStruct.value = customStruct.value + 1;',
+    varName: 'customStruct',
+  },
+  {
+    statement: 'testMapping2[account].value = testMapping2[account].value + 1;',
+    varName: 'testMapping2',
+  },
+  {
+    statement: 'testMapping3[account][token].value = testMapping3[account][token].value + 1;',
+    varName: 'testMapping3',
+  },
+  {
+    statement: 'someArray[index]--;',
+    varName: 'someArray',
+  },
+  {
+    statement: 'anotherCounter++;',
+    varName: 'anotherCounter',
+  },
+  {
+    statement: 'balance1 += 1;',
+    varName: 'balance1',
+  },
+  {
+    statement: 'anotherVar -= 1;',
+    varName: 'anotherVar',
+  },
+  // {
+  //   statement:
+  //     'complexMapping[user].balances[asset] = complexMapping[user].balances[asset] + 1 + someFunctionCall();',
+  //   varName: 'complexMapping',
+  // },
+  // {
+  //   statement: 'anotherMapping[user][token].count = anotherMapping[user][token].count - 1 + 5;',
+  //   varName: 'anotherMapping',
+  // },
+]
+
+const NOT_RAISE_ERRORS = [
+  {
+    statement: 'edgeCaseVar = edgeCaseVar * 2 + 1;',
+    varName: 'edgeCaseVar',
+  },
+  {
+    statement: 'bytes32 byteArray = byteArray + bytes32(1);',
+    varName: 'byteArray',
+  },
+  {
+    statement: '--balance1;',
+    varName: 'balance1',
+  },
+  {
+    statement: '++balance2;',
+    varName: 'balance2',
+  },
+  {
+    statement: 'someVar = someVar + 2;',
+    varName: 'someVar',
+  },
+  {
+    statement: 'unrelatedVar = someVar + 1;',
+    varName: 'unrelatedVar',
+  },
+  {
+    statement: 'uint256 result = someVar + 1;',
+    varName: 'result',
+  },
+]
+
+describe('Linter - gas-increment-by-one', () => {
+  for (const result of RAISE_ERRORS) {
+    it(`should raise error for ${result.varName}`, () => {
+      const code = funcWith(result.statement)
+      const report = linter.processStr(code, {
+        rules: { 'gas-increment-by-one': 'error' },
+      })
+
+      assert.equal(report.errorCount, 1)
+      assert.equal(report.messages[0].message, replaceErrorMsg(result.varName))
+    })
+  }
+
+  for (const result of NOT_RAISE_ERRORS) {
+    it(`should NOT raise error for ${result.varName}`, () => {
+      const code = funcWith(result.statement)
+      const report = linter.processStr(code, {
+        rules: { 'gas-increment-by-one': 'error' },
+      })
+
+      assert.equal(report.errorCount, 0)
+    })
+  }
+})
diff --git a/test/rules/naming/foundry-test-functions.js b/test/rules/naming/foundry-test-functions.js
index 53c70ca..ee5687f 100644
--- a/test/rules/naming/foundry-test-functions.js
+++ b/test/rules/naming/foundry-test-functions.js
@@ -39,7 +39,6 @@ describe('Linter - foundry-test-functions', () => {
       const functionDefinition = composeFunctionName(prefix, 'FunctionName()', 'public')
       const code = contractWith(functionDefinition)
 
-      console.log('`code` :>> ', code)
       const report = linter.processStr(code, {
         rules: { 'foundry-test-functions': ['error', ['setUp', 'finish']] },
       })