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::sema::{
7    Hir,
8    hir::{Block, Expr, ExprKind, Function, ItemId, Res, Stmt, StmtKind},
9};
10
11declare_forge_lint!(COSTLY_LOOP, Severity::Gas, "costly-loop", "storage write inside a loop");
12
13impl<'hir> LateLintPass<'hir> for CostlyLoop {
14    fn check_function(
15        &mut self,
16        ctx: &LintContext,
17        _gcx: solar::sema::Gcx<'hir>,
18        hir: &'hir Hir<'hir>,
19        func: &'hir Function<'hir>,
20    ) {
21        if let Some(body) = func.body {
22            check_block(ctx, hir, body, 0);
23        }
24    }
25}
26
27fn check_block<'hir>(ctx: &LintContext, hir: &'hir Hir<'hir>, block: Block<'hir>, loop_depth: u32) {
28    for stmt in block.stmts {
29        check_stmt(ctx, hir, stmt, loop_depth);
30    }
31}
32
33fn check_stmt<'hir>(
34    ctx: &LintContext,
35    hir: &'hir Hir<'hir>,
36    stmt: &'hir Stmt<'hir>,
37    loop_depth: u32,
38) {
39    match &stmt.kind {
40        StmtKind::Loop(block, _) => check_block(ctx, hir, *block, loop_depth + 1),
41        StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
42            check_block(ctx, hir, *block, loop_depth);
43        }
44        StmtKind::If(_, then_stmt, else_stmt) => {
45            check_stmt(ctx, hir, then_stmt, loop_depth);
46            if let Some(else_stmt) = else_stmt {
47                check_stmt(ctx, hir, else_stmt, loop_depth);
48            }
49        }
50        StmtKind::Try(stmt_try) => {
51            for clause in stmt_try.clauses {
52                check_block(ctx, hir, clause.block, loop_depth);
53            }
54        }
55        StmtKind::Expr(expr) if loop_depth > 0 => {
56            check_expr_for_writes(ctx, hir, expr);
57        }
58        StmtKind::DeclSingle(var_id) if loop_depth > 0 => {
59            if let Some(init) = hir.variable(*var_id).initializer {
60                check_expr_for_writes(ctx, hir, init);
61            }
62        }
63        StmtKind::DeclMulti(_, expr) if loop_depth > 0 => {
64            check_expr_for_writes(ctx, hir, expr);
65        }
66        StmtKind::Return(Some(expr)) if loop_depth > 0 => {
67            check_expr_for_writes(ctx, hir, expr);
68        }
69        StmtKind::Emit(expr) | StmtKind::Revert(expr) if loop_depth > 0 => {
70            check_expr_for_writes(ctx, hir, expr);
71        }
72        _ => {}
73    }
74}
75
76fn check_expr_for_writes<'hir>(ctx: &LintContext, hir: &'hir Hir<'hir>, expr: &'hir Expr<'hir>) {
77    match &expr.kind {
78        ExprKind::Assign(lhs, _, rhs) => {
79            if lvalue_is_state_var(hir, lhs) {
80                ctx.emit(&COSTLY_LOOP, expr.span);
81            }
82            check_expr_for_writes(ctx, hir, lhs);
83            check_expr_for_writes(ctx, hir, rhs);
84        }
85        ExprKind::Unary(op, inner) => {
86            if op.kind.has_side_effects() && lvalue_is_state_var(hir, inner) {
87                ctx.emit(&COSTLY_LOOP, expr.span);
88            }
89            check_expr_for_writes(ctx, hir, inner);
90        }
91        ExprKind::Delete(inner) => {
92            if lvalue_is_state_var(hir, inner) {
93                ctx.emit(&COSTLY_LOOP, expr.span);
94            }
95            check_expr_for_writes(ctx, hir, inner);
96        }
97        ExprKind::Binary(lhs, _, rhs) => {
98            check_expr_for_writes(ctx, hir, lhs);
99            check_expr_for_writes(ctx, hir, rhs);
100        }
101        ExprKind::Ternary(cond, then_expr, else_expr) => {
102            check_expr_for_writes(ctx, hir, cond);
103            check_expr_for_writes(ctx, hir, then_expr);
104            check_expr_for_writes(ctx, hir, else_expr);
105        }
106        ExprKind::Call(callee, args, named_args) => {
107            check_expr_for_writes(ctx, hir, callee);
108            for arg in args.exprs() {
109                check_expr_for_writes(ctx, hir, arg);
110            }
111            if let Some(named_args) = named_args {
112                for arg in named_args.args {
113                    check_expr_for_writes(ctx, hir, &arg.value);
114                }
115            }
116        }
117        ExprKind::Index(base, index) => {
118            check_expr_for_writes(ctx, hir, base);
119            if let Some(index) = index {
120                check_expr_for_writes(ctx, hir, index);
121            }
122        }
123        ExprKind::Slice(base, start, end) => {
124            check_expr_for_writes(ctx, hir, base);
125            if let Some(start) = start {
126                check_expr_for_writes(ctx, hir, start);
127            }
128            if let Some(end) = end {
129                check_expr_for_writes(ctx, hir, end);
130            }
131        }
132        ExprKind::Member(base, _) | ExprKind::Payable(base) => {
133            check_expr_for_writes(ctx, hir, base);
134        }
135        ExprKind::Tuple(exprs) => {
136            for e in exprs.iter().flatten() {
137                check_expr_for_writes(ctx, hir, e);
138            }
139        }
140        ExprKind::Array(exprs) => {
141            for e in *exprs {
142                check_expr_for_writes(ctx, hir, e);
143            }
144        }
145        ExprKind::Ident(_)
146        | ExprKind::Lit(_)
147        | ExprKind::New(_)
148        | ExprKind::TypeCall(_)
149        | ExprKind::Type(_)
150        | ExprKind::YulMember(..)
151        | ExprKind::Err(_) => {}
152    }
153}
154
155/// Returns `true` if the lvalue expression ultimately writes to a storage variable.
156///
157/// Peels through index accesses, member accesses, and slices to find the root identifier.
158fn lvalue_is_state_var(hir: &Hir<'_>, expr: &Expr<'_>) -> bool {
159    match &expr.peel_parens().kind {
160        ExprKind::Ident([Res::Item(ItemId::Variable(id)), ..]) => {
161            hir.variable(*id).is_state_variable()
162        }
163        ExprKind::Index(base, _)
164        | ExprKind::Slice(base, _, _)
165        | ExprKind::Member(base, _)
166        | ExprKind::Payable(base) => lvalue_is_state_var(hir, base),
167        ExprKind::Tuple(exprs) => exprs.iter().flatten().any(|e| lvalue_is_state_var(hir, e)),
168        _ => false,
169    }
170}