Skip to main content

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