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 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
55fn 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
69fn 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 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 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 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
188fn 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}