Skip to main content

forge_lint/sol/codesize/
unwrapped_modifier_logic.rs

1use super::UnwrappedModifierLogic;
2use crate::{
3    linter::{LateLintPass, LintContext, Suggestion},
4    sol::{
5        Severity, SolLint,
6        analysis::{block_outcome, count_placeholders, for_each_lhs_var, referenced_item},
7    },
8};
9use solar::{
10    ast::{ContractKind, FunctionKind},
11    interface::diagnostics::Applicability,
12    sema::{
13        Gcx,
14        hir::{self, Expr, ExprKind, Function, ItemId, Stmt, StmtKind, Visit as _},
15    },
16};
17use std::ops::ControlFlow;
18
19declare_forge_lint!(
20    UNWRAPPED_MODIFIER_LOGIC,
21    Severity::CodeSize,
22    "unwrapped-modifier-logic",
23    "modifier logic can be wrapped to reduce code size"
24);
25
26impl<'gcx> LateLintPass<'gcx> for UnwrappedModifierLogic {
27    fn check_function(&mut self, ctx: &LintContext, gcx: Gcx<'gcx>, func: &'gcx Function<'gcx>) {
28        let (FunctionKind::Modifier, Some(body), Some(name)) = (func.kind, func.body, func.name)
29        else {
30            return;
31        };
32        if block_outcome(gcx, body).can_skip_placeholder() {
33            return;
34        }
35        // Only a single, top-level placeholder can be split around: extracting a placeholder
36        // nested in an `if`/loop/`try` into a helper would change behavior.
37        if count_placeholders(body.stmts) != 1 {
38            return;
39        }
40        let Some(idx) = body.stmts.iter().position(|s| matches!(s.kind, StmtKind::Placeholder))
41        else {
42            return;
43        };
44        let (before, after) = (&body.stmts[..idx], &body.stmts[idx + 1..]);
45        if let Some(suggestion) = snippet(ctx, gcx, func, name.as_str(), before, after) {
46            ctx.emit_with_suggestion(
47                &UNWRAPPED_MODIFIER_LOGIC,
48                func.span.to(func.body_span),
49                suggestion,
50            );
51        }
52    }
53}
54
55/// A call to a non-builtin function or to a library function: the only statement cheap enough to
56/// leave inline.
57fn is_plain_call(gcx: Gcx<'_>, expr: &Expr<'_>) -> bool {
58    let ExprKind::Call(callee, ..) = &expr.kind else { return false };
59    match &callee.kind {
60        ExprKind::Ident(_) => gcx.resolved_builtin(callee).is_none(),
61        ExprKind::Member(base, _) => {
62            matches!(referenced_item(gcx, base), Some(ItemId::Contract(id))
63            if gcx.hir.contract(id).kind == ContractKind::Library)
64        }
65        _ => false,
66    }
67}
68
69/// Whether `stmts` should move into a helper: anything but a single plain call requires wrapping.
70/// Inline assembly is left alone; its authors know how to manage code size and have a reason to
71/// use it in a modifier.
72fn requires_wrapping(gcx: Gcx<'_>, stmts: &[Stmt<'_>]) -> bool {
73    let (mut calls, mut other) = (0, false);
74    for stmt in stmts {
75        match &stmt.kind {
76            StmtKind::Placeholder => {}
77            StmtKind::Expr(expr) if is_plain_call(gcx, expr) => calls += 1,
78            StmtKind::AssemblyBlock(_) | StmtKind::Switch(_) | StmtKind::Err(_) => return false,
79            _ => other = true,
80        }
81    }
82    other || calls > 1
83}
84
85fn snippet<'gcx>(
86    ctx: &LintContext,
87    gcx: Gcx<'gcx>,
88    func: &'gcx Function<'gcx>,
89    name: &str,
90    before: &'gcx [Stmt<'gcx>],
91    after: &'gcx [Stmt<'gcx>],
92) -> Option<Suggestion> {
93    let hir = &gcx.hir;
94    let (wrap_before, wrap_after) = (requires_wrapping(gcx, before), requires_wrapping(gcx, after));
95    if !(wrap_before || wrap_after) {
96        return None;
97    }
98
99    // Extracted helpers only receive the modifier's parameters, so a local declared before the
100    // placeholder and used after it, or a parameter mutated before it and read after it (the
101    // helper would only mutate its by-value copy), makes the rewrite unsafe. Only top-level
102    // declarations matter: nested ones are scoped to their block.
103    let mut shared = Vec::new();
104    for stmt in before {
105        match &stmt.kind {
106            StmtKind::DeclSingle(id) => shared.push(*id),
107            StmtKind::DeclMulti(ids, _) => shared.extend(ids.iter().flatten()),
108            _ => {}
109        }
110    }
111    if wrap_before {
112        any_expr(hir, before, |expr| {
113            let lvalue = match &expr.kind {
114                ExprKind::Assign(lhs, ..) | ExprKind::Delete(lhs) => Some(lhs),
115                ExprKind::Unary(op, inner) if op.kind.has_side_effects() => Some(inner),
116                _ => None,
117            };
118            if let Some(lvalue) = lvalue {
119                for_each_lhs_var(gcx, lvalue, &mut |v| {
120                    if func.parameters.contains(&v) && !shared.contains(&v) {
121                        shared.push(v);
122                    }
123                });
124            }
125            false
126        });
127    }
128    if any_expr(hir, after, |expr| gcx.resolved_variable(expr).is_some_and(|v| shared.contains(&v)))
129    {
130        return None;
131    }
132
133    let (mut param_list, mut param_decls) = (Vec::new(), Vec::new());
134    for &var_id in func.parameters {
135        let var = hir.variable(var_id);
136        // Unnamed parameters cannot be forwarded to the helper.
137        let Some(ident) = var.name else { continue };
138        let ty = ctx.span_to_snippet(var.ty.span).unwrap_or_else(|| "/* unknown type */".into());
139        param_list.push(ident.to_string());
140        param_decls.push(format!("{ty} {ident}"));
141    }
142    let (param_list, param_decls) = (param_list.join(", "), param_decls.join(", "));
143    let body_indent = " ".repeat(
144        ctx.get_span_indentation(before.first().or(after.first()).map_or(func.span, |s| s.span)),
145    );
146    let mod_indent = " ".repeat(ctx.get_span_indentation(func.span));
147    let (before_suffix, after_suffix) =
148        if wrap_before && wrap_after { ("Before", "After") } else { ("", "") };
149
150    // A side that needs wrapping becomes a helper call plus the helper definition; the other side
151    // is preserved verbatim so the rewrite never drops statements.
152    let side = |stmts: &[Stmt<'_>], wrap: bool, suffix: &str| -> Option<(Vec<String>, String)> {
153        if !wrap {
154            let lines = stmts
155                .iter()
156                .map(|s| Some(format!("{body_indent}{}", ctx.span_to_snippet(s.span)?)))
157                .collect::<Option<_>>()?;
158            return Some((lines, String::new()));
159        }
160        let body = stmts
161            .iter()
162            .map(|s| Some(format!("\n{body_indent}{}", ctx.span_to_snippet(s.span)?)))
163            .collect::<Option<String>>()?;
164        Some((
165            vec![format!("{body_indent}_{name}{suffix}({param_list});")],
166            format!(
167                "\n\n{mod_indent}function _{name}{suffix}({param_decls}) internal {{{body}\n{mod_indent}}}"
168            ),
169        ))
170    };
171    let (before_lines, before_helper) = side(before, wrap_before, before_suffix)?;
172    let (after_lines, after_helper) = side(after, wrap_after, after_suffix)?;
173    let body = before_lines
174        .into_iter()
175        .chain([format!("{body_indent}_;")])
176        .chain(after_lines)
177        .collect::<Vec<_>>()
178        .join("\n");
179    let replacement = format!(
180        "modifier {name}({param_decls}) {{\n{body}\n{mod_indent}}}{before_helper}{after_helper}"
181    );
182    Some(
183        Suggestion::fix(replacement, Applicability::MachineApplicable)
184            .with_desc("wrap modifier logic to reduce code size"),
185    )
186}
187
188/// Visits every expression under `stmts`, stopping as soon as `f` returns `true`.
189fn any_expr<'gcx>(
190    hir: &'gcx hir::Hir<'gcx>,
191    stmts: &'gcx [Stmt<'gcx>],
192    f: impl FnMut(&'gcx Expr<'gcx>) -> bool,
193) -> bool {
194    let mut finder = ExprFinder { hir, f };
195    stmts.iter().any(|stmt| finder.visit_stmt(stmt).is_break())
196}
197
198struct ExprFinder<'gcx, F> {
199    hir: &'gcx hir::Hir<'gcx>,
200    f: F,
201}
202
203impl<'gcx, F: FnMut(&'gcx Expr<'gcx>) -> bool> hir::Visit<'gcx> for ExprFinder<'gcx, F> {
204    type BreakValue = ();
205
206    fn hir(&self) -> &'gcx hir::Hir<'gcx> {
207        self.hir
208    }
209
210    fn visit_expr(&mut self, expr: &'gcx Expr<'gcx>) -> ControlFlow<()> {
211        if (self.f)(expr) {
212            return ControlFlow::Break(());
213        }
214        self.walk_expr(expr)
215    }
216}