1use super::UnwrappedModifierLogic;
2use crate::{
3 linter::{LateLintPass, LintContext, Suggestion},
4 sol::{Severity, SolLint, low::incorrect_modifier},
5};
6use solar::{
7 ast,
8 sema::hir::{self, Res, Visit as _},
9};
10use std::ops::ControlFlow;
11
12declare_forge_lint!(
13 UNWRAPPED_MODIFIER_LOGIC,
14 Severity::CodeSize,
15 "unwrapped-modifier-logic",
16 "wrap modifier logic to reduce code size"
17);
18
19impl<'hir> LateLintPass<'hir> for UnwrappedModifierLogic {
20 fn check_function(
21 &mut self,
22 ctx: &LintContext,
23 _gcx: solar::sema::Gcx<'hir>,
24 hir: &'hir hir::Hir<'hir>,
25 func: &'hir hir::Function<'hir>,
26 ) {
27 let body = match (func.kind, &func.body, func.name) {
29 (ast::FunctionKind::Modifier, Some(body), Some(_)) => body,
30 _ => return,
31 };
32
33 if incorrect_modifier::block_outcome(*body).can_skip_placeholder() {
34 return;
35 }
36
37 if count_placeholders(body.stmts) != 1 {
42 return;
43 }
44 let Some(idx) =
45 body.stmts.iter().position(|s| matches!(s.kind, hir::StmtKind::Placeholder))
46 else {
47 return;
49 };
50
51 let stmts = body.stmts[..].as_ref();
53 let (before, after) = (&stmts[..idx], &stmts[idx + 1..]);
54
55 if let Some(suggestion) = self.get_snippet(ctx, hir, func, before, after) {
57 ctx.emit_with_suggestion(
58 &UNWRAPPED_MODIFIER_LOGIC,
59 func.span.to(func.body_span),
60 suggestion,
61 );
62 }
63 }
64}
65
66impl UnwrappedModifierLogic {
67 fn is_valid_expr(&self, hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
69 if let hir::ExprKind::Call(func_expr, _, _) = &expr.kind {
70 if let hir::ExprKind::Ident(resolutions) = &func_expr.kind {
71 return !resolutions.iter().any(|r| matches!(r, Res::Builtin(_)));
72 }
73
74 if let hir::ExprKind::Member(base, _) = &func_expr.kind
75 && let hir::ExprKind::Ident(resolutions) = &base.kind
76 {
77 return resolutions.iter().any(|r| {
78 matches!(r, Res::Item(hir::ItemId::Contract(id)) if hir.contract(*id).kind == ast::ContractKind::Library)
79 });
80 }
81 }
82
83 false
84 }
85
86 fn stmts_require_wrapping(&self, hir: &hir::Hir<'_>, stmts: &[hir::Stmt<'_>]) -> bool {
96 let (mut res, mut has_valid_stmt) = (false, false);
97 for stmt in stmts {
98 match &stmt.kind {
99 hir::StmtKind::Placeholder => {}
100 hir::StmtKind::Expr(expr) => {
101 if !self.is_valid_expr(hir, expr) || has_valid_stmt {
102 res = true;
103 }
104 has_valid_stmt = true;
105 }
106 hir::StmtKind::AssemblyBlock(_)
108 | hir::StmtKind::Switch(_)
109 | hir::StmtKind::Err(_) => return false,
110 _ => res = true,
111 }
112 }
113
114 res
115 }
116
117 fn get_snippet<'hir>(
118 &self,
119 ctx: &LintContext,
120 hir: &'hir hir::Hir<'hir>,
121 func: &'hir hir::Function<'hir>,
122 before: &'hir [hir::Stmt<'hir>],
123 after: &'hir [hir::Stmt<'hir>],
124 ) -> Option<Suggestion> {
125 let wrap_before = !before.is_empty() && self.stmts_require_wrapping(hir, before);
126 let wrap_after = !after.is_empty() && self.stmts_require_wrapping(hir, after);
127
128 if !(wrap_before || wrap_after) {
129 return None;
130 }
131
132 if has_shared_locals(hir, before, after)
136 || (wrap_before && has_written_params_used_after(hir, func.parameters, before, after))
137 {
138 return None;
139 }
140
141 let binding = func.name.unwrap();
142 let modifier_name = binding.name.as_str();
143 let mut param_list = vec![];
144 let mut param_decls = vec![];
145
146 for var_id in func.parameters {
147 let var = hir.variable(*var_id);
148 let ty = ctx
149 .span_to_snippet(var.ty.span)
150 .unwrap_or_else(|| "/* unknown type */".to_string());
151
152 if let Some(ident) = var.name {
154 param_list.push(ident.to_string());
155 param_decls.push(format!("{ty} {}", ident.to_string()));
156 }
157 }
158
159 let param_list = param_list.join(", ");
160 let param_decls = param_decls.join(", ");
161
162 let body_indent = " ".repeat(ctx.get_span_indentation(
163 before.first().or(after.first()).map(|stmt| stmt.span).unwrap_or(func.span),
164 ));
165 let mut body_lines = Vec::new();
168 if wrap_before {
169 let suffix = if wrap_after { "Before" } else { "" };
170 body_lines.push(format!("{body_indent}_{modifier_name}{suffix}({param_list});"));
171 } else {
172 for stmt in before {
173 body_lines.push(format!("{body_indent}{}", ctx.span_to_snippet(stmt.span)?));
174 }
175 }
176 body_lines.push(format!("{body_indent}_;"));
177 if wrap_after {
178 let suffix = if wrap_before { "After" } else { "" };
179 body_lines.push(format!("{body_indent}_{modifier_name}{suffix}({param_list});"));
180 } else {
181 for stmt in after {
182 body_lines.push(format!("{body_indent}{}", ctx.span_to_snippet(stmt.span)?));
183 }
184 }
185 let body = body_lines.join("\n");
186
187 let mod_indent = " ".repeat(ctx.get_span_indentation(func.span));
188 let mut replacement =
189 format!("modifier {modifier_name}({param_decls}) {{\n{body}\n{mod_indent}}}");
190
191 let build_func = |stmts: &[hir::Stmt<'_>], suffix: &str| {
192 let body_stmts = stmts
193 .iter()
194 .map(|s| ctx.span_to_snippet(s.span).map(|code| format!("\n{body_indent}{code}")))
195 .collect::<Option<String>>()?;
196 Some(format!(
197 "\n\n{mod_indent}function _{modifier_name}{suffix}({param_decls}) internal {{{body_stmts}\n{mod_indent}}}"
198 ))
199 };
200
201 if wrap_before {
202 replacement.push_str(&build_func(before, if wrap_after { "Before" } else { "" })?);
203 }
204 if wrap_after {
205 replacement.push_str(&build_func(after, if wrap_before { "After" } else { "" })?);
206 }
207
208 Some(
209 Suggestion::fix(
210 replacement,
211 ast::interface::diagnostics::Applicability::MachineApplicable,
212 )
213 .with_desc("wrap modifier logic to reduce code size"),
214 )
215 }
216}
217
218struct SharedLocalFinder<'a, 'hir> {
220 hir: &'hir hir::Hir<'hir>,
221 locals: &'a [hir::VariableId],
222}
223
224impl<'hir> hir::Visit<'hir> for SharedLocalFinder<'_, 'hir> {
225 type BreakValue = ();
226
227 fn hir(&self) -> &'hir hir::Hir<'hir> {
228 self.hir
229 }
230
231 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
232 if let hir::ExprKind::Ident(resolutions) = &expr.kind
233 && resolutions.iter().any(
234 |r| matches!(r, Res::Item(hir::ItemId::Variable(id)) if self.locals.contains(id)),
235 )
236 {
237 return ControlFlow::Break(());
238 }
239 self.walk_expr(expr)
240 }
241}
242
243fn has_shared_locals<'hir>(
249 hir: &'hir hir::Hir<'hir>,
250 before: &'hir [hir::Stmt<'hir>],
251 after: &'hir [hir::Stmt<'hir>],
252) -> bool {
253 let mut declared = Vec::new();
254 for stmt in before {
255 match &stmt.kind {
256 hir::StmtKind::DeclSingle(id) => declared.push(*id),
257 hir::StmtKind::DeclMulti(ids, _) => declared.extend(ids.iter().copied().flatten()),
258 _ => {}
259 }
260 }
261 if declared.is_empty() {
262 return false;
263 }
264
265 let mut finder = SharedLocalFinder { hir, locals: &declared };
266 after.iter().any(|stmt| finder.visit_stmt(stmt).is_break())
267}
268
269fn has_written_params_used_after<'hir>(
272 hir: &'hir hir::Hir<'hir>,
273 params: &'hir [hir::VariableId],
274 before: &'hir [hir::Stmt<'hir>],
275 after: &'hir [hir::Stmt<'hir>],
276) -> bool {
277 let mut written = Vec::new();
278 let mut finder = ParamWriteFinder { hir, params, written: &mut written };
279 for stmt in before {
280 let _ = finder.visit_stmt(stmt);
281 }
282
283 if written.is_empty() {
284 return false;
285 }
286
287 let mut finder = SharedLocalFinder { hir, locals: &written };
288 after.iter().any(|stmt| finder.visit_stmt(stmt).is_break())
289}
290
291struct ParamWriteFinder<'a, 'hir> {
293 hir: &'hir hir::Hir<'hir>,
294 params: &'a [hir::VariableId],
295 written: &'a mut Vec<hir::VariableId>,
296}
297
298impl<'hir> hir::Visit<'hir> for ParamWriteFinder<'_, 'hir> {
299 type BreakValue = ();
300
301 fn hir(&self) -> &'hir hir::Hir<'hir> {
302 self.hir
303 }
304
305 fn visit_expr(&mut self, expr: &'hir hir::Expr<'hir>) -> ControlFlow<Self::BreakValue> {
306 match &expr.kind {
307 hir::ExprKind::Assign(lhs, _, _) | hir::ExprKind::Delete(lhs) => {
308 collect_written_params(lhs, self.params, self.written);
309 }
310 hir::ExprKind::Unary(op, inner)
311 if matches!(
312 op.kind,
313 ast::UnOpKind::PreInc
314 | ast::UnOpKind::PreDec
315 | ast::UnOpKind::PostInc
316 | ast::UnOpKind::PostDec
317 ) =>
318 {
319 collect_written_params(inner, self.params, self.written);
320 }
321 _ => {}
322 }
323
324 self.walk_expr(expr)
325 }
326}
327
328fn collect_written_params(
329 expr: &hir::Expr<'_>,
330 params: &[hir::VariableId],
331 written: &mut Vec<hir::VariableId>,
332) {
333 match &expr.kind {
334 hir::ExprKind::Ident(resolutions) => {
335 for resolution in *resolutions {
336 if let Res::Item(hir::ItemId::Variable(id)) = resolution
337 && params.contains(id)
338 && !written.contains(id)
339 {
340 written.push(*id);
341 }
342 }
343 }
344 hir::ExprKind::Tuple(items) => {
345 for item in items.iter().flatten() {
346 collect_written_params(item, params, written);
347 }
348 }
349 hir::ExprKind::Index(base, _)
350 | hir::ExprKind::Slice(base, _, _)
351 | hir::ExprKind::Member(base, _)
352 | hir::ExprKind::YulMember(base, _) => collect_written_params(base, params, written),
353 _ => {}
354 }
355}
356
357fn count_placeholders(stmts: &[hir::Stmt<'_>]) -> usize {
360 stmts.iter().map(count_placeholders_in_stmt).sum()
361}
362
363fn count_placeholders_in_stmt(stmt: &hir::Stmt<'_>) -> usize {
364 match &stmt.kind {
365 hir::StmtKind::Placeholder => 1,
366 hir::StmtKind::Block(block)
367 | hir::StmtKind::UncheckedBlock(block)
368 | hir::StmtKind::AssemblyBlock(block)
369 | hir::StmtKind::Loop(block, _) => count_placeholders(block.stmts),
370 hir::StmtKind::If(_, then_stmt, else_stmt) => {
371 count_placeholders_in_stmt(then_stmt)
372 + else_stmt.map_or(0, |s| count_placeholders_in_stmt(s))
373 }
374 hir::StmtKind::Try(try_stmt) => {
375 try_stmt.clauses.iter().map(|clause| count_placeholders(clause.block.stmts)).sum()
376 }
377 hir::StmtKind::Switch(switch) => {
378 switch.cases.iter().map(|case| count_placeholders(case.body.stmts)).sum()
379 }
380 _ => 0,
381 }
382}