Skip to content

Commit c0c6696

Browse files
Merge pull request #559 from protofire/gc-length-in-loops
GC: Dot Length in Loops
2 parents 53ab05e + bd8a092 commit c0c6696

File tree

6 files changed

+246
-0
lines changed

6 files changed

+246
-0
lines changed

conf/rulesets/solhint-all.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ module.exports = Object.freeze({
2828
'gas-custom-errors': 'warn',
2929
'gas-increment-by-one': 'warn',
3030
'gas-indexed-events': 'warn',
31+
'gas-length-in-loops': 'warn',
3132
'gas-multitoken1155': 'warn',
3233
'gas-named-return-values': 'warn',
3334
'gas-small-strings': 'warn',

docs/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ title: "Rule Index of Solhint"
3737
| [gas-small-strings](./rules/gas-consumption/gas-small-strings.md) | Keep strings smaller than 32 bytes | | |
3838
| [gas-strict-inequalities](./rules/gas-consumption/gas-strict-inequalities.md) | Suggest Strict Inequalities over non Strict ones | | |
3939
| [gas-struct-packing](./rules/gas-consumption/gas-struct-packing.md) | Suggest to re-arrange struct packing order when it is inefficient | | |
40+
| [gas-length-in-loops](./rules/gas-consumption/gas-length-in-loops.md) | Suggest replacing object.length in a loop condition to avoid calculation on each lap | | |
4041
4142

4243
## Miscellaneous
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
warning: "This is a dynamically generated file. Do not edit manually."
3+
layout: "default"
4+
title: "gas-length-in-loops | Solhint"
5+
---
6+
7+
# gas-length-in-loops
8+
![Category Badge](https://img.shields.io/badge/-Gas%20Consumption%20Rules-informational)
9+
![Default Severity Badge warn](https://img.shields.io/badge/Default%20Severity-warn-yellow)
10+
11+
## Description
12+
Suggest replacing object.length in a loop condition to avoid calculation on each lap
13+
14+
## Options
15+
This rule accepts a string option of rule severity. Must be one of "error", "warn", "off". Default to warn.
16+
17+
### Example Config
18+
```json
19+
{
20+
"rules": {
21+
"gas-length-in-loops": "warn"
22+
}
23+
}
24+
```
25+
26+
### Notes
27+
- [source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (see Array Length Caching)
28+
29+
## Examples
30+
This rule does not have examples.
31+
32+
## Version
33+
This rule is introduced in the latest version.
34+
35+
## Resources
36+
- [Rule source](https://github.com/protofire/solhint/tree/master/lib/rules/gas-consumption/gas-length-in-loops.js)
37+
- [Document source](https://github.com/protofire/solhint/tree/master/docs/rules/gas-consumption/gas-length-in-loops.md)
38+
- [Test cases](https://github.com/protofire/solhint/tree/master/test/rules/gas-consumption/gas-length-in-loops.js)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
const BaseChecker = require('../base-checker')
2+
3+
let found
4+
const ruleId = 'gas-length-in-loops'
5+
const meta = {
6+
type: 'gas-consumption',
7+
8+
docs: {
9+
description:
10+
'Suggest replacing object.length in a loop condition to avoid calculation on each lap',
11+
category: 'Gas Consumption Rules',
12+
notes: [
13+
{
14+
note: '[source 1](https://coinsbench.com/comprehensive-guide-tips-and-tricks-for-gas-optimization-in-solidity-5380db734404) of the rule initiative (see Array Length Caching)',
15+
},
16+
],
17+
},
18+
19+
isDefault: false,
20+
recommended: false,
21+
defaultSetup: 'warn',
22+
23+
schema: null,
24+
}
25+
26+
class GasLengthInLoops extends BaseChecker {
27+
constructor(reporter) {
28+
super(reporter, ruleId, meta)
29+
}
30+
31+
checkConditionForMemberAccessLength(node) {
32+
if (found) return // Return early if the condition has already been found
33+
if (typeof node === 'object' && node !== null) {
34+
if (node.type === 'MemberAccess' && node.memberName === 'length') {
35+
found = true // Update the flag if the condition is met
36+
return
37+
}
38+
// Recursively search through all object properties
39+
Object.values(node).forEach((value) => this.checkConditionForMemberAccessLength(value))
40+
}
41+
}
42+
43+
DoWhileStatement(node) {
44+
found = false
45+
this.checkConditionForMemberAccessLength(node.condition)
46+
if (found) {
47+
this.reportError(node)
48+
}
49+
}
50+
51+
WhileStatement(node) {
52+
found = false
53+
this.checkConditionForMemberAccessLength(node.condition)
54+
if (found) {
55+
this.reportError(node)
56+
}
57+
}
58+
59+
ForStatement(node) {
60+
found = false
61+
this.checkConditionForMemberAccessLength(node.conditionExpression)
62+
if (found) {
63+
this.reportError(node)
64+
}
65+
}
66+
67+
reportError(node) {
68+
this.error(
69+
node,
70+
`GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable`
71+
)
72+
}
73+
}
74+
75+
module.exports = GasLengthInLoops

lib/rules/gas-consumption/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const GasIndexedEvents = require('./gas-indexed-events')
44
const GasCalldataParameters = require('./gas-calldata-parameters')
55
const GasIncrementByOne = require('./gas-increment-by-one')
66
const GasStructPacking = require('./gas-struct-packing')
7+
const GasLengthInLoops = require('./gas-length-in-loops')
78
const GasStrictInequalities = require('./gas-strict-inequalities')
89
const GasNamedReturnValuesChecker = require('./gas-named-return-values')
910
const GasCustomErrorsChecker = require('./gas-custom-errors')
@@ -16,6 +17,7 @@ module.exports = function checkers(reporter, config) {
1617
new GasCalldataParameters(reporter, config),
1718
new GasIncrementByOne(reporter, config),
1819
new GasStructPacking(reporter, config),
20+
new GasLengthInLoops(reporter, config),
1921
new GasStrictInequalities(reporter, config),
2022
new GasNamedReturnValuesChecker(reporter),
2123
new GasCustomErrorsChecker(reporter),
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
const assert = require('assert')
2+
const linter = require('../../../lib/index')
3+
const funcWith = require('../../common/contract-builder').funcWith
4+
5+
const ERROR_MSG =
6+
'GC: Found [ .length ] property in Loop condition. Suggestion: assign it to a variable'
7+
8+
describe('Linter - gas-length-in-loops', () => {
9+
it('should raise error on ForLoop with .length in condition', () => {
10+
const code = funcWith(`
11+
for (uint256 length = 0; length > object.length; legnth++) {
12+
// code block to be executed
13+
}`)
14+
15+
const report = linter.processStr(code, {
16+
rules: { 'gas-length-in-loops': 'error' },
17+
})
18+
19+
assert.equal(report.errorCount, 1)
20+
assert.equal(report.messages[0].message, ERROR_MSG)
21+
})
22+
23+
it('should raise error on While with .length in condition', () => {
24+
const code = funcWith(`
25+
while (condition + 1 && boolIsTrue && arr.length > i) {
26+
// code block to be executed
27+
arr.length.push(1);
28+
}`)
29+
30+
const report = linter.processStr(code, {
31+
rules: { 'gas-length-in-loops': 'error' },
32+
})
33+
34+
assert.equal(report.errorCount, 1)
35+
assert.equal(report.messages[0].message, ERROR_MSG)
36+
})
37+
38+
it('should raise error on DoWhile with .length in condition', () => {
39+
const code = funcWith(`
40+
do {
41+
// code block to be executed
42+
} while (condition[5].member > 35 && length && arr.length < counter);
43+
`)
44+
45+
const report = linter.processStr(code, {
46+
rules: { 'gas-length-in-loops': 'error' },
47+
})
48+
49+
assert.equal(report.errorCount, 1)
50+
assert.equal(report.messages[0].message, ERROR_MSG)
51+
})
52+
53+
it('should raise error on DoWhile and While with .length in condition', () => {
54+
const code = funcWith(`
55+
for (uint256 length = 0; condition; length++) {
56+
// code block to be executed
57+
}
58+
59+
while (condition + 1 && boolIsTrue && arr.length > i) {
60+
// code block to be executed
61+
arr.length.push(1);
62+
}
63+
64+
do {
65+
// code block to be executed
66+
} while (condition[5].member > 35 && length && arr.length < counter);
67+
`)
68+
69+
const report = linter.processStr(code, {
70+
rules: { 'gas-length-in-loops': 'error' },
71+
})
72+
73+
assert.equal(report.errorCount, 2)
74+
assert.equal(report.messages[0].message, ERROR_MSG)
75+
assert.equal(report.messages[1].message, ERROR_MSG)
76+
})
77+
78+
it('should NOT raise error on ForLoop with none .length in condition', () => {
79+
const code = funcWith(`
80+
for (initialization; condition; iteration) {
81+
// code block to be executed
82+
}`)
83+
84+
const report = linter.processStr(code, {
85+
rules: { 'gas-length-in-loops': 'error' },
86+
})
87+
88+
assert.equal(report.errorCount, 0)
89+
})
90+
91+
it('should NOT raise error on ForLoop with none .length in condition', () => {
92+
const code = funcWith(`
93+
for (uint256 length = 0; condition; length++) {
94+
// code block to be executed
95+
}`)
96+
97+
const report = linter.processStr(code, {
98+
rules: { 'gas-length-in-loops': 'error' },
99+
})
100+
101+
assert.equal(report.errorCount, 0)
102+
})
103+
104+
it('should NOT raise error on While with none .length in condition', () => {
105+
const code = funcWith(`
106+
while (condition) {
107+
// code block to be executed
108+
}`)
109+
110+
const report = linter.processStr(code, {
111+
rules: { 'gas-length-in-loops': 'error' },
112+
})
113+
114+
assert.equal(report.errorCount, 0)
115+
})
116+
117+
it('should NOT raise error on DoWhile with none .length in condition', () => {
118+
const code = funcWith(`
119+
do {
120+
// code block to be executed
121+
} while (condition[5].member > 35 && length);`)
122+
123+
const report = linter.processStr(code, {
124+
rules: { 'gas-length-in-loops': 'error' },
125+
})
126+
127+
assert.equal(report.errorCount, 0)
128+
})
129+
})

0 commit comments

Comments
 (0)