1use super::EnumerableLoopRemoval;
2use crate::{
3 linter::{LateLintPass, LintContext},
4 sol::{
5 Severity, SolLint,
6 analysis::{branch_always_exits, loop_update, write_target},
7 },
8};
9use alloy_primitives::U256;
10use solar::{
11 ast::{LitKind, UnOpKind},
12 interface::Symbol,
13 sema::{
14 Gcx,
15 hir::{
16 self, BinOpKind, Expr, ExprKind, Hir, LoopSource, Stmt, StmtKind, VarKind, VariableId,
17 Visit,
18 },
19 },
20};
21use std::{convert::Infallible, ops::ControlFlow};
22
23declare_forge_lint!(
24 ENUMERABLE_LOOP_REMOVAL,
25 Severity::High,
26 "enumerable-loop-removal",
27 "`remove` on an `EnumerableSet` inside a loop that iterates it with `at` can corrupt the iteration"
28);
29
30impl<'gcx> LateLintPass<'gcx> for EnumerableLoopRemoval {
37 fn check_function(
38 &mut self,
39 ctx: &LintContext,
40 gcx: Gcx<'gcx>,
41 func: &'gcx hir::Function<'gcx>,
42 ) {
43 if let Some(body) = func.body {
44 LoopFinder { gcx, ctx, bindings: Vec::new() }.walk_body(body.stmts);
45 }
46 }
47}
48
49struct LoopFinder<'ctx, 's, 'c, 'gcx> {
54 gcx: Gcx<'gcx>,
55 ctx: &'ctx LintContext<'s, 'c>,
56 bindings: Vec<(VariableId, Option<SetPath>)>,
60}
61
62impl<'gcx> LoopFinder<'_, '_, '_, 'gcx> {
63 fn walk_body(&mut self, stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>) {
64 for stmt in stmts {
65 self.walk_stmt(stmt);
66 }
67 }
68
69 fn walk_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) {
70 if let StmtKind::Block(block) = &stmt.kind
73 && let Some((last, init)) = block.stmts.split_last()
74 && let StmtKind::Loop(body, source @ LoopSource::For { .. }) = &last.kind
75 {
76 self.walk_body(init);
77 return self.enter_loop(init, body.stmts, loop_update(*source));
78 }
79 match &stmt.kind {
80 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => self.walk_body(block.stmts),
82 StmtKind::Loop(body, source) => self.enter_loop(&[], body.stmts, loop_update(*source)),
83 StmtKind::If(_, then, else_) => {
86 self.poison_writes(std::slice::from_ref(stmt));
87 let mark = self.bindings.len();
88 self.walk_stmt(then);
89 self.bindings.truncate(mark);
90 if let Some(else_) = else_ {
91 self.walk_stmt(else_);
92 self.bindings.truncate(mark);
93 }
94 }
95 StmtKind::Try(try_) => {
96 self.poison_writes(std::slice::from_ref(stmt));
97 let mark = self.bindings.len();
98 for clause in try_.clauses {
99 self.walk_body(clause.block.stmts);
100 self.bindings.truncate(mark);
101 }
102 }
103 _ => self.apply_bindings(stmt),
104 }
105 }
106
107 fn enter_loop(
112 &mut self,
113 init: &'gcx [Stmt<'gcx>],
114 body: &'gcx [Stmt<'gcx>],
115 update: Option<&'gcx Stmt<'gcx>>,
116 ) {
117 self.poison_writes(init);
118 self.poison_writes(body.iter().chain(update));
119 self.analyze_loop(user_body(body).iter().chain(update));
120 let mark = self.bindings.len();
121 self.walk_body(body.iter().chain(update));
122 self.bindings.truncate(mark);
123 }
124
125 fn apply_bindings(&mut self, stmt: &'gcx Stmt<'gcx>) {
130 self.poison_writes(std::slice::from_ref(stmt));
131 let bindings = &mut self.bindings;
132 let mut bind = |var: VariableId, value: &Expr<'_>| {
133 let path = set_path(self.gcx, value, bindings, &mut Vec::new());
134 bindings.push((var, path));
135 };
136 match &stmt.kind {
137 StmtKind::DeclSingle(var) => {
138 if let Some(init) = self.gcx.hir.variable(*var).initializer {
139 bind(*var, init);
140 }
141 }
142 StmtKind::Expr(expr) => {
143 if let ExprKind::Assign(target, None, value) = &expr.peel_parens().kind
144 && let ExprKind::Ident(_) = &target.peel_parens().kind
145 && let Some(var) = self.gcx.resolved_variable(target)
146 {
147 bind(var, value);
148 }
149 }
150 _ => {}
151 }
152 }
153
154 fn poison_writes(&mut self, stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>) {
156 let mut written = Vec::new();
157 collect_writes(self.gcx, stmts, &mut written);
158 self.bindings.extend(written.into_iter().map(|var| (var, None)));
159 }
160
161 fn analyze_loop(&mut self, body: impl Iterator<Item = &'gcx Stmt<'gcx>> + Clone) {
164 if !body_is_straight_line(self.gcx, body.clone()) {
167 return;
168 }
169 let cadence = ascending_cadence(self.gcx, body.clone());
170 if cadence.is_empty() {
171 return;
172 }
173 let (mut iterated, mut removes) = (Vec::new(), Vec::new());
174 let mut calls = ExprWalker {
175 hir: &self.gcx.hir,
176 prune_unreachable: true,
177 f: |expr: &'gcx Expr<'gcx>| {
178 let Some(call) = enumerable_set_call(self.gcx, &self.bindings, expr) else {
179 return;
180 };
181 match call.op {
182 SetOp::At => {
183 if call
184 .index
185 .and_then(Expr::as_variable)
186 .is_some_and(|i| cadence.contains(&i))
187 {
188 iterated.push(call.set);
189 }
190 }
191 SetOp::Remove => removes.push((call.set, expr.span)),
192 }
193 },
194 };
195 for stmt in body {
196 let _ = calls.visit_stmt(stmt);
197 }
198 for (removed, span) in removes {
199 let corrupts = iterated.iter().any(|iterated| {
201 removed.as_ref().zip(iterated.as_ref()).is_none_or(|(a, b)| a == b)
202 });
203 if corrupts {
204 self.ctx.emit(&ENUMERABLE_LOOP_REMOVAL, span);
205 }
206 }
207 }
208}
209
210struct ExprWalker<'gcx, F> {
213 hir: &'gcx Hir<'gcx>,
214 prune_unreachable: bool,
215 f: F,
216}
217
218impl<'gcx, F: FnMut(&'gcx Expr<'gcx>)> Visit<'gcx> for ExprWalker<'gcx, F> {
219 type BreakValue = Infallible;
220
221 fn hir(&self) -> &'gcx Hir<'gcx> {
222 self.hir
223 }
224
225 fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Infallible> {
226 (self.f)(expr);
227 if !self.prune_unreachable {
228 return self.walk_expr(expr);
229 }
230 match &expr.kind {
231 ExprKind::Binary(left, op, right)
232 if matches!(op.kind, BinOpKind::And | BinOpKind::Or) =>
233 {
234 self.visit_expr(left)?;
235 let short_circuits = matches!(
236 (op.kind, literal_bool(left)),
237 (BinOpKind::And, Some(false)) | (BinOpKind::Or, Some(true))
238 );
239 if !short_circuits {
240 self.visit_expr(right)?;
241 }
242 ControlFlow::Continue(())
243 }
244 ExprKind::Ternary(condition, true_expr, false_expr) => {
245 self.visit_expr(condition)?;
246 match literal_bool(condition) {
247 Some(true) => self.visit_expr(true_expr),
248 Some(false) => self.visit_expr(false_expr),
249 None => {
250 self.visit_expr(true_expr)?;
251 self.visit_expr(false_expr)
252 }
253 }
254 }
255 _ => self.walk_expr(expr),
256 }
257 }
258}
259
260fn user_body<'gcx>(body: &'gcx [Stmt<'gcx>]) -> &'gcx [Stmt<'gcx>] {
265 let is_break = |stmt: &Stmt<'_>| matches!(stmt.kind, StmtKind::Break);
266 match body {
267 [only] => match &only.kind {
268 StmtKind::If(_, then, Some(else_)) if is_break(else_) => std::slice::from_ref(*then),
269 _ => body,
270 },
271 [rest @ .., last] => match &last.kind {
272 StmtKind::If(_, then, Some(else_))
273 if matches!(then.kind, StmtKind::Continue) && is_break(else_) =>
274 {
275 rest
276 }
277 _ => body,
278 },
279 [] => body,
280 }
281}
282
283fn body_is_straight_line<'gcx>(
288 gcx: Gcx<'_>,
289 stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>,
290) -> bool {
291 stmts.into_iter().all(|stmt| {
292 !branch_always_exits(gcx, stmt)
293 && match &stmt.kind {
294 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
295 body_is_straight_line(gcx, block.stmts)
296 }
297 StmtKind::If(..)
298 | StmtKind::Try(..)
299 | StmtKind::Loop(..)
300 | StmtKind::AssemblyBlock(..)
301 | StmtKind::Break
302 | StmtKind::Continue => false,
303 _ => true,
304 }
305 })
306}
307
308fn ascending_cadence<'gcx>(
312 gcx: Gcx<'gcx>,
313 body: impl IntoIterator<Item = &'gcx Stmt<'gcx>>,
314) -> Vec<VariableId> {
315 let (mut cadence, mut other_writes) = (Vec::new(), Vec::new());
316 collect_cadence_writes(gcx, body, &mut cadence, &mut other_writes);
317 cadence.retain(|var| !other_writes.contains(var));
318 cadence
319}
320
321fn collect_cadence_writes<'gcx>(
322 gcx: Gcx<'gcx>,
323 stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>,
324 cadence: &mut Vec<VariableId>,
325 other_writes: &mut Vec<VariableId>,
326) {
327 for stmt in stmts {
328 let mut written = match &stmt.kind {
329 StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
330 collect_cadence_writes(gcx, block.stmts, cadence, other_writes);
331 continue;
332 }
333 StmtKind::DeclSingle(var) => vec![*var],
334 StmtKind::DeclMulti(vars, _) => vars.iter().flatten().copied().collect(),
335 _ => Vec::new(),
336 };
337 collect_writes(gcx, std::slice::from_ref(stmt), &mut written);
338 let ascending = match &stmt.kind {
339 StmtKind::Expr(expr) => ascending_step(gcx, expr.peel_parens()),
340 _ => None,
341 };
342 for var in written {
343 if ascending != Some(var) {
344 other_writes.push(var);
345 } else if !cadence.contains(&var) {
346 cadence.push(var);
347 }
348 }
349 }
350}
351
352fn ascending_step<'gcx>(gcx: Gcx<'gcx>, expr: &'gcx Expr<'gcx>) -> Option<VariableId> {
355 let variable = |expr: &Expr<'_>| {
356 gcx.resolved_variable(expr)
357 .filter(|_| matches!(expr.peel_parens().kind, ExprKind::Ident(_)))
358 };
359 match &expr.kind {
360 ExprKind::Unary(op, operand) if matches!(op.kind, UnOpKind::PreInc | UnOpKind::PostInc) => {
361 variable(operand)
362 }
363 ExprKind::Assign(lhs, Some(op), rhs)
364 if op.kind == BinOpKind::Add && is_positive_literal(rhs) =>
365 {
366 variable(lhs)
367 }
368 ExprKind::Assign(lhs, None, rhs) => {
369 let target = variable(lhs)?;
370 let ExprKind::Binary(left, op, right) = &rhs.peel_parens().kind else { return None };
371 (op.kind == BinOpKind::Add
372 && ((variable(left) == Some(target) && is_positive_literal(right))
373 || (is_positive_literal(left) && variable(right) == Some(target))))
374 .then_some(target)
375 }
376 _ => None,
377 }
378}
379
380fn is_positive_literal(expr: &Expr<'_>) -> bool {
381 matches!(&expr.peel_parens().kind, ExprKind::Lit(lit)
382 if matches!(&lit.kind, LitKind::Number(value) if !value.is_zero()))
383}
384
385fn literal_bool(expr: &Expr<'_>) -> Option<bool> {
386 match &expr.peel_parens().kind {
387 ExprKind::Lit(lit) => match lit.kind {
388 LitKind::Bool(value) => Some(value),
389 _ => None,
390 },
391 _ => None,
392 }
393}
394
395fn collect_writes<'gcx>(
399 gcx: Gcx<'gcx>,
400 stmts: impl IntoIterator<Item = &'gcx Stmt<'gcx>>,
401 out: &mut Vec<VariableId>,
402) {
403 fn lvalue_variables(gcx: Gcx<'_>, expr: &Expr<'_>, out: &mut Vec<VariableId>) {
404 match &expr.peel_parens().kind {
405 ExprKind::Ident(_) => out.extend(gcx.resolved_variable(expr)),
406 ExprKind::Tuple(exprs) => {
407 exprs.iter().flatten().for_each(|expr| lvalue_variables(gcx, expr, out));
408 }
409 _ => {}
410 }
411 }
412 let mut writes = ExprWalker {
413 hir: &gcx.hir,
414 prune_unreachable: false,
415 f: |expr: &Expr<'_>| {
416 if let Some(target) = write_target(expr) {
417 lvalue_variables(gcx, target, out)
418 }
419 },
420 };
421 for stmt in stmts {
422 let _ = writes.visit_stmt(stmt);
423 }
424}
425
426#[derive(PartialEq, Eq, Clone, Copy)]
427enum SetOp {
428 At,
429 Remove,
430}
431
432struct SetCall<'gcx> {
434 op: SetOp,
435 set: Option<SetPath>,
436 index: Option<&'gcx Expr<'gcx>>,
438}
439
440fn enumerable_set_call<'gcx>(
444 gcx: Gcx<'gcx>,
445 bindings: &Bindings,
446 expr: &'gcx Expr<'gcx>,
447) -> Option<SetCall<'gcx>> {
448 let ExprKind::Call(callee, ..) = &expr.kind else { return None };
449 let function_id = gcx.resolved_function(callee)?;
450 let function = gcx.hir.function(function_id);
451 let contract = gcx.hir.contract(function.contract?);
452 if !contract.kind.is_library() || contract.name.as_str() != "EnumerableSet" {
453 return None;
454 }
455 let op = match function.name?.as_str() {
456 "at" => SetOp::At,
457 "remove" => SetOp::Remove,
458 _ => return None,
459 };
460 let (set_expr, index_arg) = match &callee.peel_parens().kind {
463 ExprKind::Member(receiver, _)
464 if gcx.resolved_call(expr).is_some_and(|resolved| resolved.attached) =>
465 {
466 (Some(&**receiver), 0)
467 }
468 _ => (gcx.call_arg(expr, 0), 1),
469 };
470 Some(SetCall {
471 op,
472 set: set_expr.and_then(|expr| set_path(gcx, expr, bindings, &mut Vec::new())),
473 index: gcx.call_arg(expr, index_arg),
474 })
475}
476
477#[derive(PartialEq, Eq, Clone, Copy)]
479enum Step {
480 Field(Symbol),
481 Key(U256),
482}
483
484#[derive(PartialEq, Eq, Clone)]
487struct SetPath {
488 base: VariableId,
489 steps: Vec<Step>,
490}
491
492type Bindings = [(VariableId, Option<SetPath>)];
495
496fn set_path(
500 gcx: Gcx<'_>,
501 expr: &Expr<'_>,
502 bindings: &Bindings,
503 seen: &mut Vec<VariableId>,
504) -> Option<SetPath> {
505 match &expr.peel_parens().kind {
506 ExprKind::Ident(_) => {
507 let var = gcx.resolved_variable(expr)?;
508 if seen.contains(&var) {
509 return None;
510 }
511 seen.push(var);
512 let variable = gcx.hir.variable(var);
513 if !matches!(variable.kind, VarKind::Statement) {
514 return Some(SetPath { base: var, steps: Vec::new() });
515 }
516 match bindings.iter().rev().find(|(bound, _)| *bound == var) {
520 Some((_, binding)) => binding.clone(),
521 None => set_path(gcx, variable.initializer?, bindings, seen),
522 }
523 }
524 ExprKind::Member(base, field) => {
525 let mut path = set_path(gcx, base, bindings, seen)?;
526 path.steps.push(Step::Field(field.name));
527 Some(path)
528 }
529 ExprKind::Index(base, Some(index)) => {
530 let ExprKind::Lit(lit) = &index.peel_parens().kind else { return None };
531 let LitKind::Number(key) = &lit.kind else { return None };
532 let mut path = set_path(gcx, base, bindings, seen)?;
533 path.steps.push(Step::Key(*key));
534 Some(path)
535 }
536 _ => None,
537 }
538}