UNPKG

@slippy-lint/slippy

Version:

A simple but powerful linter for Solidity

209 lines 9.11 kB
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