Skip to main content

forge_lint/sol/low/
payable_loop.rs

1//! Loop-context walker shared by the `*-loop` lints.
2//!
3//! Visits every statement and expression that executes inside a loop of a function, following
4//! its modifier chain through `_` and inlining the internal helpers it calls (including `super`
5//! dispatch, resolved against the contract the entry point belongs to).
6
7use crate::sol::analysis::{is_builtin, loop_stmts};
8use solar::{
9    ast::{StateMutability, Visibility},
10    interface::sym,
11    sema::{
12        Gcx,
13        hir::{
14            Block, ContractId, Expr, ExprKind, Function, FunctionId, FunctionKind, Hir, Modifier,
15            Stmt, StmtKind, Visit,
16        },
17        ty::TyKind,
18    },
19};
20use std::{convert::Infallible, ops::ControlFlow};
21
22/// A statement or expression reached inside a loop.
23pub(super) enum LoopItem<'gcx> {
24    Stmt(&'gcx Stmt<'gcx>),
25    Expr(&'gcx Expr<'gcx>),
26}
27
28/// Calls `f` for every expression executing inside a loop of a payable entry point, including
29/// loops in the internal helpers it calls (whether the call itself sits in a loop or not).
30pub(super) fn for_each_payable_loop_expr<'gcx>(
31    gcx: Gcx<'gcx>,
32    func: &'gcx Function<'gcx>,
33    mut f: impl FnMut(&'gcx Expr<'gcx>),
34) {
35    if !matches!(func.kind, FunctionKind::Constructor | FunctionKind::Modifier)
36        && func.state_mutability == StateMutability::Payable
37        && matches!(func.visibility, Visibility::Public | Visibility::External)
38    {
39        for_each_loop_item(gcx, func, true, |item| {
40            if let LoopItem::Expr(expr) = item {
41                f(expr);
42            }
43        });
44    }
45}
46
47/// Calls `f` for every statement and expression executing inside a loop of `func`. Internal
48/// helpers called from a loop are inlined; with `follow_calls_outside_loop`, so are helpers
49/// called outside one, so that their own loops are reported too.
50pub(super) fn for_each_loop_item<'gcx>(
51    gcx: Gcx<'gcx>,
52    func: &'gcx Function<'gcx>,
53    follow_calls_outside_loop: bool,
54    f: impl FnMut(LoopItem<'gcx>),
55) {
56    let Some(body) = func.body else { return };
57    let mut walker = LoopWalker {
58        gcx,
59        f,
60        loop_depth: 0,
61        placeholder: None,
62        stack: Vec::new(),
63        dispatch: func.contract,
64        current: func.contract,
65        follow_calls_outside_loop,
66    };
67    walker.visit_modifiers(func.modifiers, 0, body, func.contract);
68}
69
70/// The rest of a modifier chain and the function body it wraps, executed at `_`.
71type Continuation<'gcx> = (&'gcx [Modifier<'gcx>], usize, Block<'gcx>, Option<ContractId>);
72
73struct LoopWalker<'gcx, F> {
74    gcx: Gcx<'gcx>,
75    f: F,
76    loop_depth: usize,
77    placeholder: Option<Continuation<'gcx>>,
78    /// Modifiers and helpers currently being inlined, to cut recursion.
79    stack: Vec<FunctionId>,
80    /// Contract whose linearization resolves `super`.
81    dispatch: Option<ContractId>,
82    /// Contract defining the code being walked.
83    current: Option<ContractId>,
84    follow_calls_outside_loop: bool,
85}
86
87impl<'gcx, F: FnMut(LoopItem<'gcx>)> LoopWalker<'gcx, F> {
88    fn visit_modifiers(
89        &mut self,
90        modifiers: &'gcx [Modifier<'gcx>],
91        index: usize,
92        body: Block<'gcx>,
93        contract: Option<ContractId>,
94    ) {
95        let Some(modifier) = modifiers.get(index) else {
96            return self.visit_scoped(body, None, contract);
97        };
98        let _ = self.visit_call_args(&modifier.args);
99        if let Some(id) = self.dispatch.map_or_else(
100            || modifier.id.as_function(),
101            |contract| self.gcx.resolve_modifier_target(contract, modifier),
102        ) && let Some(modifier_body) = self.gcx.hir.function(id).body
103            && !self.stack.contains(&id)
104        {
105            self.stack.push(id);
106            let continuation = Some((modifiers, index + 1, body, contract));
107            self.visit_scoped(modifier_body, continuation, self.gcx.hir.function(id).contract);
108            self.stack.pop();
109        } else {
110            self.visit_modifiers(modifiers, index + 1, body, contract);
111        }
112    }
113
114    fn visit_scoped(
115        &mut self,
116        block: Block<'gcx>,
117        placeholder: Option<Continuation<'gcx>>,
118        contract: Option<ContractId>,
119    ) {
120        let saved = (self.placeholder, self.current);
121        (self.placeholder, self.current) = (placeholder, contract);
122        for stmt in block.stmts {
123            let _ = self.visit_stmt(stmt);
124        }
125        (self.placeholder, self.current) = saved;
126    }
127
128    fn visit_call(&mut self, id: FunctionId) {
129        let func = self.gcx.hir.function(id);
130        if let Some(body) = func.body
131            && !self.stack.contains(&id)
132        {
133            self.stack.push(id);
134            self.visit_modifiers(func.modifiers, 0, body, func.contract);
135            self.stack.pop();
136        }
137    }
138
139    /// The internal function a call dispatches to, if it can be inlined: a helper called
140    /// directly, through a library/base qualifier or a `using for` binding, or via `super`.
141    /// Calls on a contract-typed value (`this` included) are external and are not followed.
142    fn callee(&self, callee: &'gcx Expr<'gcx>) -> Option<FunctionId> {
143        let callee = callee.peel_parens();
144        let TyKind::Fn(function) = self.gcx.type_of_expr(callee.id)?.kind else { return None };
145        if !function.is_internal() && !function.is_delegate_call() {
146            return None;
147        }
148        let func_id = self.gcx.resolved_function(callee)?;
149        let ExprKind::Member(base, _) = &callee.kind else {
150            return Some(
151                self.dispatch.map_or(func_id, |contract| {
152                    self.gcx.resolve_virtual_function(contract, func_id)
153                }),
154            );
155        };
156        if is_builtin(self.gcx, base, sym::super_) {
157            return Some(self.gcx.resolve_super_function(self.dispatch?, self.current?, func_id));
158        }
159        Some(func_id)
160    }
161}
162
163impl<'gcx, F: FnMut(LoopItem<'gcx>)> Visit<'gcx> for LoopWalker<'gcx, F> {
164    type BreakValue = Infallible;
165
166    fn hir(&self) -> &'gcx Hir<'gcx> {
167        &self.gcx.hir
168    }
169
170    fn visit_stmt(&mut self, stmt: &'gcx Stmt<'gcx>) -> ControlFlow<Infallible> {
171        if self.loop_depth > 0 {
172            (self.f)(LoopItem::Stmt(stmt));
173        }
174        match stmt.kind {
175            StmtKind::Loop(block, source) => {
176                self.loop_depth += 1;
177                for stmt in loop_stmts(block, source) {
178                    self.visit_stmt(stmt)?;
179                }
180                self.loop_depth -= 1;
181            }
182            StmtKind::Placeholder => {
183                if let Some((modifiers, index, body, contract)) = self.placeholder {
184                    self.visit_modifiers(modifiers, index, body, contract);
185                }
186            }
187            _ => self.walk_stmt(stmt)?,
188        }
189        ControlFlow::Continue(())
190    }
191
192    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<Infallible> {
193        self.walk_expr(expr)?;
194        let in_loop = self.loop_depth > 0;
195        if in_loop {
196            (self.f)(LoopItem::Expr(expr));
197        }
198        if (in_loop || self.follow_calls_outside_loop)
199            && let ExprKind::Call(callee, ..) = &expr.kind
200            && let Some(id) = self.callee(callee)
201        {
202            self.visit_call(id);
203        }
204        ControlFlow::Continue(())
205    }
206}