@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
209 lines • 9.11 kB
JavaScript
import { assertNonterminalNode, assertTerminalNode, NonterminalKind, TerminalKind, TerminalKindExtensions, } from "@nomicfoundation/slang/cst";
import { FunctionAttribute, StateVariableAttribute, } from "@nomicfoundation/slang/ast";
export const SortModifiers = {
name: "sort-modifiers",
recommended: true,
create: function () {
return new SortModifiersRule(this.name);
},
};
class SortModifiersRule {
constructor(name) {
this.name = name;
}
run({ file }) {
const diagnostics = [];
const cursor = file.createTreeCursor();
const functionModifiersDiagnostics = this._checkFunctionModifiers(file, cursor.clone());
diagnostics.push(...functionModifiersDiagnostics);
const stateVarModifiersDiagnostics = this._checkStateVarModifiers(file, cursor.clone());
diagnostics.push(...stateVarModifiersDiagnostics);
return diagnostics;
}
_checkFunctionModifiers(file, cursor) {
const diagnostics = [];
while (cursor.goToNextNonterminalWithKinds([
NonterminalKind.FunctionDefinition,
NonterminalKind.FallbackFunctionDefinition,
NonterminalKind.ReceiveFunctionDefinition,
])) {
const functionTextRangeCursor = cursor.spawn();
while (functionTextRangeCursor.goToNextTerminal() &&
functionTextRangeCursor.node.isTerminalNode() &&
TerminalKindExtensions.isTrivia(functionTextRangeCursor.node.kind)) {
// skip trivia nodes
}
const functionCursor = cursor.spawn();
const modifiers = [];
while (functionCursor.goToNextNonterminalWithKind(NonterminalKind.FunctionAttribute)) {
assertNonterminalNode(functionCursor.node);
const variant = new FunctionAttribute(functionCursor.node).variant;
if ("kind" in variant) {
// we know it's a terminal node
assertTerminalNode(variant);
switch (variant.kind) {
case TerminalKind.ExternalKeyword:
case TerminalKind.InternalKeyword:
case TerminalKind.PublicKeyword:
case TerminalKind.PrivateKeyword:
ignoreTrivia(functionCursor);
modifiers.push({
kind: "visibility",
textRange: functionCursor.textRange,
});
break;
case TerminalKind.ViewKeyword:
case TerminalKind.PureKeyword:
case TerminalKind.PayableKeyword:
ignoreTrivia(functionCursor);
modifiers.push({
kind: "mutability",
textRange: functionCursor.textRange,
});
break;
case TerminalKind.VirtualKeyword:
ignoreTrivia(functionCursor);
modifiers.push({
kind: "virtual",
textRange: functionCursor.textRange,
});
break;
case TerminalKind.OverrideKeyword:
ignoreTrivia(functionCursor);
modifiers.push({
kind: "override",
textRange: functionCursor.textRange,
});
break;
}
}
else if ("overrideKeyword" in variant) {
ignoreTrivia(functionCursor);
modifiers.push({
kind: "override",
textRange: functionCursor.textRange,
});
}
else {
ignoreTrivia(functionCursor);
modifiers.push({
kind: "custom",
textRange: functionCursor.textRange,
});
}
}
const modifiersIndices = [
{
kind: "visibility",
index: modifiers.findIndex((m) => m.kind === "visibility"),
},
{
kind: "mutability",
index: modifiers.findIndex((m) => m.kind === "mutability"),
},
{
kind: "virtual",
index: modifiers.findIndex((m) => m.kind === "virtual"),
},
{
kind: "override",
index: modifiers.findIndex((m) => m.kind === "override"),
},
{
kind: "custom",
index: modifiers.findIndex((m) => m.kind === "custom"),
},
];
diagnostics.push(...this._checkModifiersOrder(file, modifiers, modifiersIndices));
}
return diagnostics;
}
_checkStateVarModifiers(file, cursor) {
const diagnostics = [];
while (cursor.goToNextNonterminalWithKind(NonterminalKind.StateVariableDefinition)) {
const stateVarTextRangeCursor = cursor.spawn();
while (stateVarTextRangeCursor.goToNextTerminal() &&
stateVarTextRangeCursor.node.isTerminalNode() &&
TerminalKindExtensions.isTrivia(stateVarTextRangeCursor.node.kind)) {
// skip trivia nodes
}
const stateVarCursor = cursor.spawn();
const modifiers = [];
while (stateVarCursor.goToNextNonterminalWithKind(NonterminalKind.StateVariableAttribute)) {
assertNonterminalNode(stateVarCursor.node);
const variant = new StateVariableAttribute(stateVarCursor.node).variant;
if ("kind" in variant) {
// we know it's a terminal node
assertTerminalNode(variant);
switch (variant.kind) {
case TerminalKind.InternalKeyword:
case TerminalKind.PublicKeyword:
case TerminalKind.PrivateKeyword:
ignoreTrivia(stateVarCursor);
modifiers.push({
kind: "visibility",
textRange: stateVarCursor.textRange,
});
break;
case TerminalKind.ConstantKeyword:
case TerminalKind.ImmutableKeyword:
case TerminalKind.TransientKeyword:
ignoreTrivia(stateVarCursor);
modifiers.push({
kind: "mutability",
textRange: stateVarCursor.textRange,
});
break;
}
}
}
const modifiersIndices = [
{
kind: "visibility",
index: modifiers.findIndex((m) => m.kind === "visibility"),
},
{
kind: "mutability",
index: modifiers.findIndex((m) => m.kind === "mutability"),
},
];
diagnostics.push(...this._checkModifiersOrder(file, modifiers, modifiersIndices));
}
return diagnostics;
}
_checkModifiersOrder(file, modifiers, modifiersIndices) {
const diagnostics = [];
for (let i = 0; i < modifiersIndices.length - 1; i++) {
for (let k = i + 1; k < modifiersIndices.length; k++) {
const first = modifiersIndices[i];
const second = modifiersIndices[k];
if (first.index !== -1 &&
second.index !== -1 &&
first.index > second.index) {
return [
this._buildError(file, first.kind, second.kind, modifiers[second.index].textRange),
];
}
}
}
return diagnostics;
}
_buildError(file, first, second, textRange) {
return {
rule: this.name,
sourceId: file.id,
message: `${first} modifier should come before ${second} modifier`,
line: textRange.start.line,
column: textRange.start.column,
};
}
}
function ignoreTrivia(cursor) {
cursor.goToNextTerminal();
while (cursor.node.isTerminalNode() &&
TerminalKindExtensions.isTrivia(cursor.node.kind) &&
cursor.goToNext()) {
// skip trivia nodes
}
}
//# sourceMappingURL=sort-modifiers.js.map