UNPKG

@slippy-lint/slippy

Version:

A simple but powerful linter for Solidity

125 lines 4.94 kB
import { PositionalArguments, StringExpression, } from "@nomicfoundation/slang/ast"; import { assertNonterminalNode, Query, TerminalKindExtensions, } from "@nomicfoundation/slang/cst"; import * as z from "zod"; const GET_REQUIRE_ARGS_QUERY = Query.create(` [FunctionCallExpression operand: [Expression ["require"]] arguments: [ArgumentsDeclaration [PositionalArgumentsDeclaration @requireArgs arguments: [PositionalArguments] ]] ] `); const GET_REVERT_FUNCTION_ARGS_QUERY = Query.create(` [RevertStatement [RevertKeyword] . [ArgumentsDeclaration [PositionalArgumentsDeclaration @revertFunctionArgs arguments: [PositionalArguments] ]] ] `); const GET_REVERT_STATEMENT_ARGS_QUERY = Query.create(` [RevertStatement [RevertKeyword] [IdentifierPath] [ArgumentsDeclaration [PositionalArgumentsDeclaration @revertStatementArgs arguments: [PositionalArguments] ]] ] `); const Schema = z.enum(["any", "string", "customError"]).default("any"); export const RequireRevertReason = { name: "require-revert-reason", recommended: true, parseConfig: (config) => Schema.parse(config), create: function (config) { return new RequireRevertReasonRule(this.name, config); }, }; class RequireRevertReasonRule { constructor(name, config) { this.name = name; this.config = config; } run({ file }) { const diagnostics = []; const cursor = file.createTreeCursor(); const matches = cursor.query([ GET_REQUIRE_ARGS_QUERY, GET_REVERT_FUNCTION_ARGS_QUERY, GET_REVERT_STATEMENT_ARGS_QUERY, ]); for (const match of matches) { const requireArgs = match.captures.requireArgs?.[0]; const revertFunctionArgs = match.captures.revertFunctionArgs?.[0]; const revertStatementArgs = match.captures.revertStatementArgs?.[0]; const node = (requireArgs ?? revertFunctionArgs ?? revertStatementArgs) .node; assertNonterminalNode(node); const args = new PositionalArguments(node); if (requireArgs !== undefined) { if (args.items.length === 1) { diagnostics.push(this.makeResult(file, match.root, "require", "missingReason")); } else if (args.items.length === 2) { const isStringReason = args.items[1].variant instanceof StringExpression; if (this.config === "customError" && isStringReason) { diagnostics.push(this.makeResult(file, match.root, "require", "notError")); } else if (this.config === "string" && !isStringReason) { diagnostics.push(this.makeResult(file, match.root, "require", "notString")); } } } else if (revertFunctionArgs !== undefined) { if (args.items.length === 0) { diagnostics.push(this.makeResult(file, match.root, "revert", "missingReason")); } else if (args.items.length === 1) { const isStringReason = args.items[0].variant instanceof StringExpression; if (this.config === "customError" && isStringReason) { diagnostics.push(this.makeResult(file, match.root, "revert", "notError")); } else if (this.config === "string" && !isStringReason) { diagnostics.push(this.makeResult(file, match.root, "revert", "notString")); } } } else if (revertStatementArgs !== undefined) { // revert statements always have a reason of error kind if (this.config === "string") { diagnostics.push(this.makeResult(file, match.root, "revert", "notString")); } } } return diagnostics; } makeResult(file, cursor, name, cause) { ignoreLeadingTrivia(cursor); let message; if (cause === "missingReason") { message = `Missing revert reason in ${name} statement`; } else if (cause === "notError") { message = `Revert reason in ${name} should be an error`; } else { message = `Revert reason in ${name} should be a string`; } return { rule: this.name, sourceId: file.id, message, line: cursor.textRange.start.line, column: cursor.textRange.start.column, }; } } function ignoreLeadingTrivia(cursor) { while (cursor.goToNextTerminal() && cursor.node.isTerminalNode() && TerminalKindExtensions.isTrivia(cursor.node.kind)) { // ignore trivia nodes } } //# sourceMappingURL=require-revert-reason.js.map