@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
125 lines • 4.94 kB
JavaScript
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
arguments: [PositionalArguments]
]]
]
`);
const GET_REVERT_FUNCTION_ARGS_QUERY = Query.create(`
[RevertStatement
[RevertKeyword]
.
[ArgumentsDeclaration [PositionalArgumentsDeclaration
arguments: [PositionalArguments]
]]
]
`);
const GET_REVERT_STATEMENT_ARGS_QUERY = Query.create(`
[RevertStatement
[RevertKeyword]
[IdentifierPath]
[ArgumentsDeclaration [PositionalArgumentsDeclaration
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