forge_lint/sol/gas/
custom_errors.rs

1use super::CustomErrors;
2use crate::{
3    linter::{EarlyLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::ast::{CallArgsKind, Expr, ExprKind};
7
8declare_forge_lint!(
9    CUSTOM_ERRORS,
10    Severity::Gas,
11    "custom-errors",
12    "prefer using custom errors on revert and require calls"
13);
14
15impl<'ast> EarlyLintPass<'ast> for CustomErrors {
16    fn check_expr(&mut self, ctx: &LintContext, expr: &'ast Expr<'ast>) {
17        if let ExprKind::Call(callee, args) = &expr.kind
18            && ((is_require_call(callee) && should_lint_require(args))
19                || (is_revert_call(callee) && should_lint_revert(args)))
20        {
21            ctx.emit(&CUSTOM_ERRORS, expr.span);
22        }
23    }
24}
25
26/// Checks if an expression is a call to the `require` builtin function.
27fn is_require_call(callee: &Expr<'_>) -> bool {
28    matches!(&callee.kind, ExprKind::Ident(ident) if ident.as_str() == "require")
29}
30
31/// Checks if an expression is a call to the `revert` builtin function.
32fn is_revert_call(callee: &Expr<'_>) -> bool {
33    matches!(&callee.kind, ExprKind::Ident(ident) if ident.as_str() == "revert")
34}
35
36/// Checks if a revert call should be linted: `revert()` or `revert("message")`.
37fn should_lint_revert(args: &solar::ast::CallArgs<'_>) -> bool {
38    matches!(&args.kind, CallArgsKind::Unnamed(arg_exprs) if {
39        arg_exprs.is_empty() || arg_exprs.first().is_some_and(|e| is_string_literal(e))
40    })
41}
42
43/// Checks if a require call should be linted: has string literal as second argument.
44fn should_lint_require(args: &solar::ast::CallArgs<'_>) -> bool {
45    matches!(&args.kind, CallArgsKind::Unnamed(arg_exprs) if {
46        arg_exprs.get(1).is_some_and(|e| is_string_literal(e))
47    })
48}
49
50/// Checks if an expression is a string literal.
51fn is_string_literal(expr: &Expr<'_>) -> bool {
52    matches!(&expr.kind, ExprKind::Lit(lit, _) if matches!(lit.kind, solar::ast::LitKind::Str(..)))
53}