forge_lint/sol/gas/
costly_loop.rs1use 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
62fn 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}