forge_lint/sol/codesize/
unwrapped_modifier_logic.rs

1use super::UnwrappedModifierLogic;
2use crate::{
3    linter::{LateLintPass, LintContext, Suggestion},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast,
8    sema::hir::{self, Res},
9};
10
11declare_forge_lint!(
12    UNWRAPPED_MODIFIER_LOGIC,
13    Severity::CodeSize,
14    "unwrapped-modifier-logic",
15    "wrap modifier logic to reduce code size"
16);
17
18impl<'hir> LateLintPass<'hir> for UnwrappedModifierLogic {
19    fn check_function(
20        &mut self,
21        ctx: &LintContext,
22        hir: &'hir hir::Hir<'hir>,
23        func: &'hir hir::Function<'hir>,
24    ) {
25        // Only check modifiers with a body and a name
26        let body = match (func.kind, &func.body, func.name) {
27            (ast::FunctionKind::Modifier, Some(body), Some(_)) => body,
28            _ => return,
29        };
30
31        // Split statements into before and after the placeholder `_`.
32        let stmts = body.stmts[..].as_ref();
33        let (before, after) = stmts
34            .iter()
35            .position(|s| matches!(s.kind, hir::StmtKind::Placeholder))
36            .map_or((stmts, &[][..]), |idx| (&stmts[..idx], &stmts[idx + 1..]));
37
38        // Generate a fix suggestion if the modifier logic should be wrapped.
39        if let Some(suggestion) = self.get_snippet(ctx, hir, func, before, after) {
40            ctx.emit_with_suggestion(
41                &UNWRAPPED_MODIFIER_LOGIC,
42                func.span.to(func.body_span),
43                suggestion,
44            );
45        }
46    }
47}
48
49impl UnwrappedModifierLogic {
50    /// Returns `true` if an expr is not a built-in ('require' or 'assert') call or a lib function.
51    fn is_valid_expr(&self, hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
52        if let hir::ExprKind::Call(func_expr, _, _) = &expr.kind {
53            if let hir::ExprKind::Ident(resolutions) = &func_expr.kind {
54                return !resolutions.iter().any(|r| matches!(r, Res::Builtin(_)));
55            }
56
57            if let hir::ExprKind::Member(base, _) = &func_expr.kind
58                && let hir::ExprKind::Ident(resolutions) = &base.kind
59            {
60                return resolutions.iter().any(|r| {
61                    matches!(r, Res::Item(hir::ItemId::Contract(id)) if hir.contract(*id).kind == ast::ContractKind::Library)
62                });
63            }
64        }
65
66        false
67    }
68
69    /// Checks if a block of statements is complex and should be wrapped in a helper function.
70    ///
71    /// This always is 'false' the modifier contains assembly. We assume that if devs know how to
72    /// use assembly, they will also know how to reduce the codesize of their contracts and they
73    /// have a good reason to use it on their modifiers.
74    ///
75    /// This is 'true' if the block contains:
76    /// 1. Any statement that is not a placeholder or a valid expression.
77    /// 2. More than one simple call expression.
78    fn stmts_require_wrapping(&self, hir: &hir::Hir<'_>, stmts: &[hir::Stmt<'_>]) -> bool {
79        let (mut res, mut has_valid_stmt) = (false, false);
80        for stmt in stmts {
81            match &stmt.kind {
82                hir::StmtKind::Placeholder => continue,
83                hir::StmtKind::Expr(expr) => {
84                    if !self.is_valid_expr(hir, expr) || has_valid_stmt {
85                        res = true;
86                    }
87                    has_valid_stmt = true;
88                }
89                // HIR doesn't support assembly yet:
90                // <https://github.com/paradigmxyz/solar/blob/d25bf38a5accd11409318e023f701313d98b9e1e/crates/sema/src/hir/mod.rs#L977-L982>
91                hir::StmtKind::Err(_) => return false,
92                _ => res = true,
93            }
94        }
95
96        res
97    }
98
99    fn get_snippet<'a>(
100        &self,
101        ctx: &LintContext,
102        hir: &hir::Hir<'_>,
103        func: &hir::Function<'_>,
104        before: &'a [hir::Stmt<'a>],
105        after: &'a [hir::Stmt<'a>],
106    ) -> Option<Suggestion> {
107        let wrap_before = !before.is_empty() && self.stmts_require_wrapping(hir, before);
108        let wrap_after = !after.is_empty() && self.stmts_require_wrapping(hir, after);
109
110        if !(wrap_before || wrap_after) {
111            return None;
112        }
113
114        let binding = func.name.unwrap();
115        let modifier_name = binding.name.as_str();
116        let mut param_list = vec![];
117        let mut param_decls = vec![];
118
119        for var_id in func.parameters {
120            let var = hir.variable(*var_id);
121            let ty = ctx
122                .span_to_snippet(var.ty.span)
123                .unwrap_or_else(|| "/* unknown type */".to_string());
124
125            // solidity functions should always have named parameters
126            if let Some(ident) = var.name {
127                param_list.push(ident.to_string());
128                param_decls.push(format!("{ty} {}", ident.to_string()));
129            }
130        }
131
132        let param_list = param_list.join(", ");
133        let param_decls = param_decls.join(", ");
134
135        let body_indent = " ".repeat(ctx.get_span_indentation(
136            before.first().or(after.first()).map(|stmt| stmt.span).unwrap_or(func.span),
137        ));
138        let body = match (wrap_before, wrap_after) {
139            (true, true) => format!(
140                "{body_indent}_{modifier_name}Before({param_list});\n{body_indent}_;\n{body_indent}_{modifier_name}After({param_list});"
141            ),
142            (true, false) => {
143                format!("{body_indent}_{modifier_name}({param_list});\n{body_indent}_;")
144            }
145            (false, true) => {
146                format!("{body_indent}_;\n{body_indent}_{modifier_name}({param_list});")
147            }
148            _ => unreachable!(),
149        };
150
151        let mod_indent = " ".repeat(ctx.get_span_indentation(func.span));
152        let mut replacement =
153            format!("modifier {modifier_name}({param_decls}) {{\n{body}\n{mod_indent}}}");
154
155        let build_func = |stmts: &[hir::Stmt<'_>], suffix: &str| {
156            let body_stmts = stmts
157                .iter()
158                .filter_map(|s| ctx.span_to_snippet(s.span))
159                .map(|code| format!("\n{body_indent}{code}"))
160                .collect::<String>();
161            format!(
162                "\n\n{mod_indent}function _{modifier_name}{suffix}({param_decls}) internal {{{body_stmts}\n{mod_indent}}}"
163            )
164        };
165
166        if wrap_before {
167            replacement.push_str(&build_func(before, if wrap_after { "Before" } else { "" }));
168        }
169        if wrap_after {
170            replacement.push_str(&build_func(after, if wrap_before { "After" } else { "" }));
171        }
172
173        Some(
174            Suggestion::fix(
175                replacement,
176                ast::interface::diagnostics::Applicability::MachineApplicable,
177            )
178            .with_desc("wrap modifier logic to reduce code size"),
179        )
180    }
181}