Skip to main content

forge_lint/sol/gas/
costly_loop.rs

1use super::CostlyLoop;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::DataLocation,
8    interface::data_structures::Never,
9    sema::{
10        Gcx, Hir,
11        builtins::Builtin,
12        hir::{self, Expr, ExprKind, Function, Stmt, StmtKind, Visit as _},
13    },
14};
15use std::ops::ControlFlow;
16
17declare_forge_lint!(COSTLY_LOOP, Severity::Gas, "costly-loop", "storage write inside a loop");
18
19impl<'gcx> LateLintPass<'gcx> for CostlyLoop {
20    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
21        let mut finder = LoopWriteFinder { ctx, gcx, loop_depth: 0 };
22        let _ = finder.visit_function(func);
23    }
24}
25
26struct LoopWriteFinder<'a, 'gcx> {
27    ctx: &'a LintContext<'a, 'a>,
28    gcx: Gcx<'gcx>,
29    loop_depth: u32,
30}
31
32impl<'gcx> hir::Visit<'gcx> for LoopWriteFinder<'_, 'gcx> {
33    type BreakValue = Never;
34
35    fn hir(&self) -> &'gcx Hir<'gcx> {
36        &self.gcx.hir
37    }
38
39    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Self::BreakValue> {
40        let is_loop = matches!(stmt.kind, StmtKind::Loop(..));
41        self.loop_depth += is_loop as u32;
42        let flow = self.walk_stmt(stmt);
43        self.loop_depth -= is_loop as u32;
44        flow
45    }
46
47    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
48        if self.loop_depth > 0 {
49            let lvalue = match &expr.kind {
50                ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => Some(lhs),
51                ExprKind::Unary(op, inner) if op.kind.has_side_effects() => Some(inner),
52                _ => None,
53            };
54            if lvalue.is_some_and(|lvalue| lvalue_is_state_var(self.gcx, lvalue)) {
55                self.ctx.emit(&COSTLY_LOOP, expr.span);
56            }
57        }
58        self.walk_expr(expr)
59    }
60}
61
62/// Returns `true` if the lvalue expression ultimately writes to a storage variable.
63///
64/// Peels through index accesses, member accesses, and slices to find a state variable or an
65/// expression that returns a storage reference.
66fn lvalue_is_state_var(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
67    let expr = expr.peel_parens();
68    match &expr.kind {
69        ExprKind::Ident(_) => {
70            gcx.resolved_variable(expr).is_some_and(|id| gcx.hir.variable(id).is_state_variable())
71        }
72        ExprKind::Call(callee, ..) => {
73            gcx.resolved_builtin(callee) == Some(Builtin::ArrayPush0)
74                || gcx
75                    .type_of_expr(expr.id)
76                    .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage))
77        }
78        ExprKind::Index(base, _)
79        | ExprKind::Slice(base, _, _)
80        | ExprKind::Member(base, _)
81        | ExprKind::Payable(base) => lvalue_is_state_var(gcx, base),
82        ExprKind::Tuple(exprs) => exprs.iter().flatten().any(|e| lvalue_is_state_var(gcx, e)),
83        _ => false,
84    }
85}