forge_lint/sol/gas/
cache_array_length.rs1use 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 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
61fn 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
89struct 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
122fn 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
151fn 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}