@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
325 lines (291 loc) • 8.84 kB
text/typescript
import { File as SlangFile } from "@nomicfoundation/slang/compilation";
import {
Diagnostic,
RuleContext,
RuleWithoutConfig,
RuleDefinitionWithoutConfig,
} from "./types.js";
import {
assertNonterminalNode,
assertTerminalNode,
Cursor,
NonterminalKind,
TerminalKind,
TerminalKindExtensions,
TextRange,
} from "@nomicfoundation/slang/cst";
import {
FunctionAttribute,
StateVariableAttribute,
} from "@nomicfoundation/slang/ast";
type FunctionModifierKind =
| "visibility"
| "mutability"
| "virtual"
| "override"
| "custom";
interface FunctionModifierPosition {
kind: FunctionModifierKind;
textRange: TextRange;
}
type StateVarModifierKind = "visibility" | "mutability";
interface StateVarModifierPosition {
kind: StateVarModifierKind;
textRange: TextRange;
}
export const SortModifiers: RuleDefinitionWithoutConfig = {
name: "sort-modifiers",
recommended: true,
create: function () {
return new SortModifiersRule(this.name);
},
};
class SortModifiersRule implements RuleWithoutConfig {
constructor(public name: string) {}
public run({ file }: RuleContext): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
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;
}
private _checkFunctionModifiers(
file: SlangFile,
cursor: Cursor,
): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
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: FunctionModifierPosition[] = [];
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;
}
private _checkStateVarModifiers(
file: SlangFile,
cursor: Cursor,
): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
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: StateVarModifierPosition[] = [];
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;
}
private _checkModifiersOrder(
file: SlangFile,
modifiers:
| Array<FunctionModifierPosition>
| Array<StateVarModifierPosition>,
modifiersIndices: Array<{ kind: string; index: number }>,
): Diagnostic[] {
const diagnostics: Diagnostic[] = [];
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;
}
private _buildError(
file: SlangFile,
first: string,
second: string,
textRange: TextRange,
): Diagnostic {
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) {
cursor.goToNextTerminal();
while (
cursor.node.isTerminalNode() &&
TerminalKindExtensions.isTrivia(cursor.node.kind) &&
cursor.goToNext()
) {
// skip trivia nodes
}
}