Skip to main content

forge_lint/sol/gas/
cache_array_length.rs

1use super::CacheArrayLength;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint, analysis::for_each_lhs_var},
5};
6use solar::{
7    ast::ElementaryType,
8    interface::{Span, data_structures::Never, sym},
9    sema::{
10        Gcx,
11        builtins::Builtin,
12        hir::{
13            self, BinOpKind, Expr, ExprKind, LoopSource, StateMutability, Stmt, StmtKind,
14            VariableId, Visit as _,
15        },
16        ty::TyKind,
17    },
18};
19use std::ops::ControlFlow;
20
21declare_forge_lint!(
22    CACHE_ARRAY_LENGTH,
23    Severity::Gas,
24    "cache-array-length",
25    "array length is read on every loop iteration; cache it outside the loop"
26);
27
28impl<'gcx> LateLintPass<'gcx> for CacheArrayLength {
29    fn check_stmt(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, stmt: &'gcx Stmt<'gcx>) {
30        let StmtKind::Loop(block, LoopSource::For { .. }) = &stmt.kind else { return };
31        // `for (init; cond; update) body` lowers to `loop { if (cond) { body } else break }` with
32        // the update kept on the loop source.
33        let Some(Stmt { kind: StmtKind::If(condition, _, Some(else_stmt)), .. }) =
34            block.stmts.first()
35        else {
36            return;
37        };
38        if !matches!(else_stmt.kind, StmtKind::Break) {
39            return;
40        }
41
42        let mut reads = Vec::new();
43        collect_length_reads(gcx, condition, &mut reads);
44        if reads.is_empty() {
45            return;
46        }
47
48        let mut facts = LoopFacts { gcx, written: Vec::new(), skip: false };
49        let _ = facts.visit_stmt(stmt);
50        if facts.skip {
51            return;
52        }
53        for (span, var) in reads {
54            if !facts.written.contains(&var) {
55                ctx.emit(&CACHE_ARRAY_LENGTH, span);
56            }
57        }
58    }
59}
60
61/// Collects `<state dynamic array>.length` reads compared against an identifier in `expr`.
62fn collect_length_reads<'gcx>(
63    gcx: Gcx<'gcx>,
64    expr: &'gcx Expr<'gcx>,
65    reads: &mut Vec<(Span, VariableId)>,
66) {
67    let ExprKind::Binary(lhs, op, rhs) = &expr.peel_parens().kind else { return };
68    match op.kind {
69        BinOpKind::And | BinOpKind::Or => {
70            collect_length_reads(gcx, lhs, reads);
71            collect_length_reads(gcx, rhs, reads);
72        }
73        kind if kind.is_cmp() => {
74            for (side, other) in [(lhs, rhs), (rhs, lhs)] {
75                let side = side.peel_parens();
76                if matches!(other.peel_parens().kind, ExprKind::Ident(_))
77                    && let ExprKind::Member(base, member) = &side.kind
78                    && member.name == sym::length
79                    && let Some(var) = state_dyn_array(gcx, base)
80                {
81                    reads.push((side.span, var));
82                }
83            }
84        }
85        _ => {}
86    }
87}
88
89/// Loop body facts that make caching unsafe: variables written, array length mutations and
90/// calls that may mutate state.
91struct LoopFacts<'gcx> {
92    gcx: Gcx<'gcx>,
93    written: Vec<VariableId>,
94    skip: bool,
95}
96
97impl<'gcx> hir::Visit<'gcx> for LoopFacts<'gcx> {
98    type BreakValue = Never;
99
100    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
101        &self.gcx.hir
102    }
103
104    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Self::BreakValue> {
105        match &expr.kind {
106            ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => {
107                self.skip |= is_array_like(self.gcx, lhs);
108                for_each_lhs_var(self.gcx, lhs, &mut |v| self.written.push(v));
109            }
110            ExprKind::Unary(op, inner) if op.kind.has_side_effects() => {
111                for_each_lhs_var(self.gcx, inner, &mut |v| self.written.push(v));
112            }
113            ExprKind::Call(callee, ..) => {
114                self.skip |= call_may_mutate_state(self.gcx, callee);
115            }
116            _ => {}
117        }
118        self.walk_expr(expr)
119    }
120}
121
122/// Whether a call may write storage; array `push`/`pop` count since they change the length.
123fn call_may_mutate_state<'gcx>(gcx: Gcx<'gcx>, callee: &'gcx Expr<'gcx>) -> bool {
124    let callee = callee.peel_parens();
125    match &callee.kind {
126        ExprKind::Type(_) => false,
127        ExprKind::Member(..)
128            if matches!(
129                gcx.resolved_builtin(callee),
130                Some(Builtin::ArrayPush0 | Builtin::ArrayPush | Builtin::ArrayPop)
131            ) =>
132        {
133            true
134        }
135        _ => !matches!(
136            gcx.type_of_expr(callee.id).map(|ty| ty.peel_refs().kind),
137            Some(TyKind::Fn(f)) if f.state_mutability <= StateMutability::View
138        ),
139    }
140}
141
142fn is_array_like<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> bool {
143    gcx.type_of_expr(expr.peel_parens().id).is_some_and(|ty| {
144        matches!(
145            ty.peel_refs().kind,
146            TyKind::DynArray(_) | TyKind::Elementary(ElementaryType::Bytes)
147        )
148    })
149}
150
151/// The state variable `expr` names, if it is a dynamic array.
152fn state_dyn_array<'gcx>(gcx: Gcx<'gcx>, expr: &Expr<'gcx>) -> Option<VariableId> {
153    let expr = expr.peel_parens();
154    let var = gcx.resolved_variable(expr)?;
155    (gcx.hir.variable(var).is_state_variable()
156        && matches!(
157            gcx.type_of_expr(expr.id).map(|ty| ty.peel_refs().kind),
158            Some(TyKind::DynArray(_))
159        ))
160    .then_some(var)
161}