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 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
57fn 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
71fn 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 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 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 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
185fn 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}