Skip to content

Commit 8921f94

Browse files
committed
Sort state variables modifiers
1 parent dc4e9ae commit 8921f94

File tree

3 files changed

+187
-25
lines changed

3 files changed

+187
-25
lines changed

docs/rules/sort-modifiers.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
# sort-modifiers
22

3-
Enforces a specific order for modifiers.
3+
Enforces a specific order for functions and state variable modifiers.
44

55
## Rule details
66

7-
The enforced order is:
7+
The enforced order for functions is:
88

99
1. Visibility modifiers (`public`, `private`, `internal`, `external`)
1010
2. State mutability modifiers (`pure`, `view`, `payable`)
1111
3. The `virtual` modifier
1212
4. The `override` modifier
1313
5. Custom modifiers
1414

15+
The enforced order for state variables is:
16+
17+
1. Visibility modifiers (`public`, `private`, `internal`)
18+
2. Storage location (`constant`, `immutable`, `transient`)
19+
1520
Examples of **correct** code for this rule:
1621

1722
```solidity
1823
contract Example {
24+
uint public constant x = 1;
25+
1926
function f() public pure virtual override myModifier {}
2027
}
2128
```
@@ -25,6 +32,8 @@ Examples of **incorrect** code for this rule:
2532
<!-- prettier-ignore-start -->
2633
```solidity
2734
contract Example {
35+
uint immutable private x = 1;
36+
2837
function f() myModifier public pure virtual override {}
2938
}
3039
```

src/rules/sort-modifiers.ts

Lines changed: 146 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,27 @@ import {
1414
TerminalKindExtensions,
1515
TextRange,
1616
} from "@nomicfoundation/slang/cst";
17-
import { FunctionAttribute } from "@nomicfoundation/slang/ast";
17+
import {
18+
FunctionAttribute,
19+
StateVariableAttribute,
20+
} from "@nomicfoundation/slang/ast";
1821

19-
type ModifierKind =
22+
type FunctionModifierKind =
2023
| "visibility"
2124
| "mutability"
2225
| "virtual"
2326
| "override"
2427
| "custom";
2528

26-
interface ModifierPosition {
27-
kind: ModifierKind;
29+
interface FunctionModifierPosition {
30+
kind: FunctionModifierKind;
31+
textRange: TextRange;
32+
}
33+
34+
type StateVarModifierKind = "visibility" | "mutability";
35+
36+
interface StateVarModifierPosition {
37+
kind: StateVarModifierKind;
2838
textRange: TextRange;
2939
}
3040

@@ -44,6 +54,27 @@ class SortModifiersRule implements RuleWithoutConfig {
4454

4555
const cursor = file.createTreeCursor();
4656

57+
const functionModifiersResults = this._checkFunctionModifiers(
58+
file,
59+
cursor.clone(),
60+
);
61+
results.push(...functionModifiersResults);
62+
63+
const stateVarModifiersResults = this._checkStateVarModifiers(
64+
file,
65+
cursor.clone(),
66+
);
67+
results.push(...stateVarModifiersResults);
68+
69+
return results;
70+
}
71+
72+
private _checkFunctionModifiers(
73+
file: SlangFile,
74+
cursor: Cursor,
75+
): LintResult[] {
76+
const results: LintResult[] = [];
77+
4778
while (
4879
cursor.goToNextNonterminalWithKinds([
4980
NonterminalKind.FunctionDefinition,
@@ -61,7 +92,7 @@ class SortModifiersRule implements RuleWithoutConfig {
6192
}
6293
const functionCursor = cursor.spawn();
6394

64-
const modifiers: ModifierPosition[] = [];
95+
const modifiers: FunctionModifierPosition[] = [];
6596

6697
while (
6798
functionCursor.goToNextNonterminalWithKind(
@@ -147,27 +178,119 @@ class SortModifiersRule implements RuleWithoutConfig {
147178
},
148179
];
149180

150-
for (let i = 0; i < modifiersIndices.length - 1; i++) {
151-
for (let k = i + 1; k < modifiersIndices.length; k++) {
152-
const first = modifiersIndices[i];
153-
const second = modifiersIndices[k];
154-
155-
if (
156-
first.index !== -1 &&
157-
second.index !== -1 &&
158-
first.index > second.index
159-
) {
160-
return [
161-
this._buildError(
162-
file,
163-
first.kind,
164-
second.kind,
165-
modifiers[second.index].textRange,
166-
),
167-
];
181+
results.push(
182+
...this._checkModifiersOrder(file, modifiers, modifiersIndices),
183+
);
184+
}
185+
186+
return results;
187+
}
188+
189+
private _checkStateVarModifiers(
190+
file: SlangFile,
191+
cursor: Cursor,
192+
): LintResult[] {
193+
const results: LintResult[] = [];
194+
195+
while (
196+
cursor.goToNextNonterminalWithKind(
197+
NonterminalKind.StateVariableDefinition,
198+
)
199+
) {
200+
const stateVarTextRangeCursor = cursor.spawn();
201+
while (
202+
stateVarTextRangeCursor.goToNextTerminal() &&
203+
stateVarTextRangeCursor.node.isTerminalNode() &&
204+
TerminalKindExtensions.isTrivia(stateVarTextRangeCursor.node.kind)
205+
) {
206+
// skip trivia nodes
207+
}
208+
const stateVarCursor = cursor.spawn();
209+
210+
const modifiers: StateVarModifierPosition[] = [];
211+
212+
while (
213+
stateVarCursor.goToNextNonterminalWithKind(
214+
NonterminalKind.StateVariableAttribute,
215+
)
216+
) {
217+
assertNonterminalNode(stateVarCursor.node);
218+
const variant = new StateVariableAttribute(stateVarCursor.node).variant;
219+
220+
if ("kind" in variant) {
221+
// we know it's a terminal node
222+
assertTerminalNode(variant);
223+
switch (variant.kind) {
224+
case TerminalKind.InternalKeyword:
225+
case TerminalKind.PublicKeyword:
226+
case TerminalKind.PrivateKeyword:
227+
ignoreTrivia(stateVarCursor);
228+
modifiers.push({
229+
kind: "visibility",
230+
textRange: stateVarCursor.textRange,
231+
});
232+
break;
233+
case TerminalKind.ConstantKeyword:
234+
case TerminalKind.ImmutableKeyword:
235+
case TerminalKind.TransientKeyword:
236+
ignoreTrivia(stateVarCursor);
237+
modifiers.push({
238+
kind: "mutability",
239+
textRange: stateVarCursor.textRange,
240+
});
241+
break;
168242
}
169243
}
170244
}
245+
246+
const modifiersIndices = [
247+
{
248+
kind: "visibility",
249+
index: modifiers.findIndex((m) => m.kind === "visibility"),
250+
},
251+
{
252+
kind: "mutability",
253+
index: modifiers.findIndex((m) => m.kind === "mutability"),
254+
},
255+
];
256+
257+
results.push(
258+
...this._checkModifiersOrder(file, modifiers, modifiersIndices),
259+
);
260+
}
261+
262+
return results;
263+
}
264+
265+
private _checkModifiersOrder(
266+
file: SlangFile,
267+
modifiers:
268+
| Array<FunctionModifierPosition>
269+
| Array<StateVarModifierPosition>,
270+
modifiersIndices: Array<{ kind: string; index: number }>,
271+
): LintResult[] {
272+
const results: LintResult[] = [];
273+
274+
for (let i = 0; i < modifiersIndices.length - 1; i++) {
275+
for (let k = i + 1; k < modifiersIndices.length; k++) {
276+
const first = modifiersIndices[i];
277+
const second = modifiersIndices[k];
278+
279+
if (
280+
first.index !== -1 &&
281+
second.index !== -1 &&
282+
first.index > second.index
283+
) {
284+
return [
285+
this._buildError(
286+
file,
287+
first.kind,
288+
second.kind,
289+
modifiers[second.index].textRange,
290+
),
291+
];
292+
}
293+
}
171294
}
172295

173296
return results;

test/rules/sort-modifiers.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,36 @@ const fixtures: RuleTestFixture[] = [
111111
}
112112
`,
113113
},
114+
{
115+
description: "should report constant before visibility",
116+
content: `
117+
contract Foo {
118+
uint constant public x = 1;
119+
^^^^^^^^
120+
uint public constant y = 1;
121+
}
122+
`,
123+
},
124+
{
125+
description: "should report immutable before visibility",
126+
content: `
127+
contract Foo {
128+
uint immutable private x = 1;
129+
^^^^^^^^^
130+
uint private immutable y = 1;
131+
}
132+
`,
133+
},
134+
{
135+
description: "should report transient before visibility",
136+
content: `
137+
contract Foo {
138+
uint transient internal x;
139+
^^^^^^^^^
140+
uint internal transient y;
141+
}
142+
`,
143+
},
114144
];
115145

116146
describe(ruleName, () => {

0 commit comments

Comments
 (0)