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