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
155fn 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}