Skip to main content

forge_lint/sol/analysis/
modifier_outcome.rs

1//! Control-flow summary of modifier bodies with respect to the `_` placeholder.
2
3use super::is_literal_false;
4use solar::sema::{
5    Gcx,
6    builtins::Builtin,
7    hir::{Block, Expr, ExprKind, LoopSource, Stmt, StmtKind},
8};
9
10/// Summary of how control flow can leave a statement or block *without* having executed the
11/// placeholder (`_`) or reverted.
12///
13/// Each flag tracks whether there is at least one such path. If every path reaches `_` or reverts,
14/// all flags are `false` (`Outcome::COVERED`).
15#[derive(Clone, Copy)]
16pub struct Outcome {
17    /// Control can reach the end of the construct normally and continue to the next statement.
18    falls_through: bool,
19    /// Control can exit the modifier via `return` before reaching `_`.
20    returns: bool,
21    /// Control can exit the enclosing loop via `break` before reaching `_`.
22    breaks: bool,
23    /// Control can jump to the enclosing loop's next iteration via `continue` before reaching `_`.
24    continues: bool,
25}
26
27impl Outcome {
28    /// Every path reaches `_` or reverts.
29    const COVERED: Self =
30        Self { falls_through: false, returns: false, breaks: false, continues: false };
31    const FALLTHROUGH: Self = Self { falls_through: true, ..Self::COVERED };
32    const RETURNS: Self = Self { returns: true, ..Self::COVERED };
33    const BREAKS: Self = Self { breaks: true, ..Self::COVERED };
34    const CONTINUES: Self = Self { continues: true, ..Self::COVERED };
35
36    /// Whether the modifier body can finish without executing `_`. Only fall-through and `return`
37    /// reach the modifier's end; `break`/`continue` are always consumed by an enclosing loop.
38    pub const fn can_skip_placeholder(self) -> bool {
39        self.falls_through || self.returns
40    }
41
42    const fn merge(self, other: Self) -> Self {
43        Self {
44            falls_through: self.falls_through || other.falls_through,
45            returns: self.returns || other.returns,
46            breaks: self.breaks || other.breaks,
47            continues: self.continues || other.continues,
48        }
49    }
50}
51
52pub fn block_outcome(gcx: Gcx<'_>, block: Block<'_>) -> Outcome {
53    let mut outcome = Outcome::FALLTHROUGH;
54    for stmt in block.stmts {
55        // Once a statement cannot fall through, the rest of the block is unreachable.
56        if !outcome.falls_through {
57            return outcome;
58        }
59        let stmt_outcome = stmt_outcome(gcx, stmt);
60        outcome = Outcome {
61            falls_through: stmt_outcome.falls_through,
62            returns: outcome.returns || stmt_outcome.returns,
63            breaks: outcome.breaks || stmt_outcome.breaks,
64            continues: outcome.continues || stmt_outcome.continues,
65        };
66    }
67    outcome
68}
69
70fn stmt_outcome(gcx: Gcx<'_>, stmt: &Stmt<'_>) -> Outcome {
71    match &stmt.kind {
72        StmtKind::Placeholder => Outcome::COVERED,
73        StmtKind::Return(_) => Outcome::RETURNS,
74        StmtKind::Break => Outcome::BREAKS,
75        StmtKind::Continue => Outcome::CONTINUES,
76        StmtKind::Expr(expr) => call_outcome(gcx, expr).unwrap_or(Outcome::FALLTHROUGH),
77        StmtKind::Revert(_) => Outcome::COVERED,
78        StmtKind::Block(block)
79        | StmtKind::UncheckedBlock(block)
80        | StmtKind::AssemblyBlock(block) => block_outcome(gcx, *block),
81        StmtKind::If(_, then_stmt, else_stmt) => {
82            let then_outcome = stmt_outcome(gcx, then_stmt);
83            let else_outcome =
84                else_stmt.map_or(Outcome::FALLTHROUGH, |stmt| stmt_outcome(gcx, stmt));
85            then_outcome.merge(else_outcome)
86        }
87        StmtKind::Loop(block, source) => {
88            // `for`/`while`/`do-while` are all desugared to a `Loop` whose body holds the condition
89            // as a synthetic `else break`. The loop can be left (and thus fall through to the
90            // following statement) via a `break`, including that synthetic condition break; a loop
91            // without any `break` (e.g. `for (;;)`) never falls through. For `do-while` the
92            // condition sits *after* the body, so a `continue` in the body also reaches it and can
93            // exit the loop. `break`/`continue` are otherwise consumed by the loop; only `return`
94            // keeps escaping toward the modifier's end.
95            let body = block_outcome(gcx, *block);
96            let falls_through =
97                body.breaks || (matches!(source, LoopSource::DoWhile) && body.continues);
98            Outcome { falls_through, returns: body.returns, ..Outcome::COVERED }
99        }
100        StmtKind::Try(try_stmt) => {
101            // Every execution enters exactly one clause (the `returns` clause on success or a
102            // matching `catch`), or the call reverts uncaught. There is no implicit fall-through
103            // path that skips all clauses, so start from `COVERED`.
104            let mut outcome = Outcome::COVERED;
105            for clause in try_stmt.clauses {
106                outcome = outcome.merge(block_outcome(gcx, clause.block));
107            }
108            outcome
109        }
110        StmtKind::Switch(switch) => {
111            // A Yul `switch` value that matches no `case` falls through unless a `default` clause
112            // is present (stored last, with no constant).
113            let has_default = switch.cases.last().is_some_and(|case| case.constant.is_none());
114            let mut outcome = if has_default { Outcome::COVERED } else { Outcome::FALLTHROUGH };
115            for case in switch.cases {
116                outcome = outcome.merge(block_outcome(gcx, case.body));
117            }
118            outcome
119        }
120        StmtKind::DeclSingle(_)
121        | StmtKind::DeclMulti(_, _)
122        | StmtKind::Emit(_)
123        | StmtKind::Err(_) => Outcome::FALLTHROUGH,
124    }
125}
126
127/// Classifies a statement-level call expression that terminates the current path before reaching
128/// `_`, if any. Covers both the Solidity `revert`/`revert(...)` builtins and the Yul halting
129/// builtins reachable when recursing into an `assembly { .. }` block.
130///
131/// - Failing halts (`revert`, Yul `revert`/`invalid`) leave every path either reverting or reaching
132///   `_`, so they are [`Outcome::COVERED`] (not flagged).
133/// - Successful halts (Yul `return`/`stop`, `selfdestruct`) let the surrounding call finish
134///   *without* running the modified function body, which is exactly what this lint flags, so they
135///   behave like a `return` ([`Outcome::RETURNS`]).
136fn call_outcome(gcx: Gcx<'_>, expr: &Expr<'_>) -> Option<Outcome> {
137    let ExprKind::Call(callee, args, _) = &expr.peel_parens().kind else { return None };
138    match gcx.resolved_builtin(callee)? {
139        Builtin::Revert | Builtin::RevertMsg | Builtin::YulRevert | Builtin::YulInvalid => {
140            Some(Outcome::COVERED)
141        }
142        Builtin::Require | Builtin::Assert if args.exprs().next().is_some_and(is_literal_false) => {
143            Some(Outcome::COVERED)
144        }
145        Builtin::YulReturn
146        | Builtin::YulStop
147        | Builtin::YulSelfdestruct
148        | Builtin::Selfdestruct => Some(Outcome::RETURNS),
149        _ => None,
150    }
151}