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