1use crate::linter::LintContext;
2use solar::{
3 ast::{
4 DataLocation, ElementaryType, ItemKind as AstItemKind, LitKind, StateMutability, StrKind,
5 TypeSize, UsingList as AstUsingList, Visibility,
6 },
7 interface::sym,
8 sema::{
9 Gcx, Ty,
10 hir::{
11 Block, CallArgs, CallArgsKind, ContractId, Expr, ExprKind, Function, FunctionId,
12 FunctionKind, Hir, ItemId, Modifier, Res, Stmt, StmtKind, VariableId, Visit,
13 },
14 ty::TyKind,
15 },
16};
17use std::ops::ControlFlow;
18
19type LoopExprCallback<'ctx, 's, 'hir, 'cb> =
20 dyn FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Expr<'hir>) + 'cb;
21type LoopStmtCallback<'ctx, 's, 'hir, 'cb> =
22 dyn FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Stmt<'hir>) + 'cb;
23
24struct LoopContextChecker<'ctx, 's, 'hir, 'cb> {
25 ctx: &'ctx LintContext<'s, 'ctx>,
26 hir: &'hir Hir<'hir>,
27 gcx: Gcx<'hir>,
28 loop_depth: usize,
29 placeholder: Option<ModifierContinuation<'hir>>,
30 modifier_stack: Vec<FunctionId>,
31 call_stack: Vec<FunctionId>,
32 internal_call_loop_depths: Vec<usize>,
33 dispatch_contract: Option<ContractId>,
34 current_contract: Option<ContractId>,
35 follow_calls_outside_loop: bool,
36 report_local_loops_in_internal_calls: bool,
37 stmt_f: &'cb mut LoopStmtCallback<'ctx, 's, 'hir, 'cb>,
38 expr_f: &'cb mut LoopExprCallback<'ctx, 's, 'hir, 'cb>,
39}
40
41type ModifierContinuation<'hir> = (&'hir [Modifier<'hir>], usize, Block<'hir>, Option<ContractId>);
42
43impl<'ctx, 's, 'hir, 'cb> LoopContextChecker<'ctx, 's, 'hir, 'cb> {
44 fn visit_modifier_chain(
45 &mut self,
46 modifiers: &'hir [Modifier<'hir>],
47 index: usize,
48 body: Block<'hir>,
49 body_contract: Option<ContractId>,
50 ) {
51 let Some(modifier) = modifiers.get(index) else {
52 self.visit_block_with_placeholder(body, None, body_contract);
53 return;
54 };
55
56 let _ = self.visit_call_args(&modifier.args);
57
58 let Some(modifier_id) = modifier.id.as_function() else {
59 self.visit_modifier_chain(modifiers, index + 1, body, body_contract);
60 return;
61 };
62
63 if self.modifier_stack.contains(&modifier_id) {
64 self.visit_modifier_chain(modifiers, index + 1, body, body_contract);
65 return;
66 }
67
68 let modifier_func = self.hir.function(modifier_id);
69 let Some(modifier_body) = modifier_func.body else {
70 self.visit_modifier_chain(modifiers, index + 1, body, body_contract);
71 return;
72 };
73
74 self.modifier_stack.push(modifier_id);
75 self.visit_block_with_placeholder(
76 modifier_body,
77 Some((modifiers, index + 1, body, body_contract)),
78 modifier_func.contract,
79 );
80 self.modifier_stack.pop();
81 }
82
83 fn visit_block_stmts(&mut self, block: Block<'hir>) {
84 for stmt in block.stmts {
85 let _ = self.visit_stmt(stmt);
86 }
87 }
88
89 fn visit_block_with_placeholder(
90 &mut self,
91 block: Block<'hir>,
92 placeholder: Option<ModifierContinuation<'hir>>,
93 current_contract: Option<ContractId>,
94 ) {
95 let previous = self.placeholder;
96 let previous_contract = self.current_contract;
97 self.placeholder = placeholder;
98 self.current_contract = current_contract;
99 self.visit_block_stmts(block);
100 self.current_contract = previous_contract;
101 self.placeholder = previous;
102 }
103
104 fn visit_loop_block(&mut self, block: Block<'hir>) -> ControlFlow<()> {
105 self.loop_depth += 1;
106 self.visit_block_stmts(block);
107 self.loop_depth -= 1;
108 ControlFlow::Continue(())
109 }
110
111 fn visit_internal_call(&mut self, func_id: FunctionId) {
112 if self.call_stack.contains(&func_id) {
113 return;
114 }
115
116 let func = self.hir.function(func_id);
117 let Some(body) = func.body else { return };
118
119 self.call_stack.push(func_id);
120 self.internal_call_loop_depths.push(self.loop_depth);
121 self.visit_modifier_chain(func.modifiers, 0, body, func.contract);
122 self.internal_call_loop_depths.pop();
123 self.call_stack.pop();
124 }
125
126 fn current_loop_context_is_reportable(&self) -> bool {
127 if self.loop_depth == 0 {
128 return false;
129 }
130 self.report_local_loops_in_internal_calls
131 || self.internal_call_loop_depths.last().is_none_or(|&depth| self.loop_depth <= depth)
132 }
133
134 fn resolved_internal_function_ids(
135 &self,
136 callee: &'hir Expr<'hir>,
137 args: &CallArgs<'hir>,
138 ) -> Vec<FunctionId> {
139 match &callee.peel_parens().kind {
140 ExprKind::Ident(reses) => unique(
141 reses
142 .iter()
143 .filter_map(|res| match res {
144 Res::Item(ItemId::Function(func_id)) => Some(*func_id),
145 _ => None,
146 })
147 .filter(|&func_id| self.is_followable_call(func_id, args)),
148 )
149 .into_iter()
150 .collect(),
151 ExprKind::Member(base, member) => {
152 unique(self.member_function_ids(base, member.name, args).into_iter())
153 .into_iter()
154 .collect()
155 }
156 _ => Vec::new(),
157 }
158 }
159
160 fn member_function_ids(
161 &self,
162 base: &'hir Expr<'hir>,
163 member_name: solar::interface::Symbol,
164 args: &CallArgs<'hir>,
165 ) -> Vec<FunctionId> {
166 if is_builtin(base, sym::super_) {
167 return self.super_function_ids(member_name, args);
168 }
169
170 let contract_functions = match &base.peel_parens().kind {
171 ExprKind::Ident(reses) => reses
172 .iter()
173 .filter_map(|res| match res {
174 Res::Item(ItemId::Contract(contract_id)) => Some(*contract_id),
175 _ => None,
176 })
177 .flat_map(|contract_id| self.contract_function_ids(contract_id, member_name))
178 .filter(|&func_id| self.is_followable_call(func_id, args))
179 .collect(),
180 _ => Vec::new(),
181 };
182 if !contract_functions.is_empty() {
183 return contract_functions;
184 }
185
186 let Some(base_ty) = expr_ty(self.gcx, self.hir, base) else { return Vec::new() };
187
188 if matches!(base_ty.peel_refs().kind, TyKind::Contract(_)) {
189 return self.library_extension_function_ids(member_name, args, base);
190 }
191
192 let member_functions: Vec<_> = self
193 .gcx
194 .members_of(base_ty, base_item_source(self.hir, base), base_contract(self.hir, base))
195 .filter(|member| member.name == member_name)
196 .filter_map(|member| match (member.res, member.ty.kind) {
197 (Some(Res::Item(ItemId::Function(func_id))), _) => Some(func_id),
198 (_, TyKind::Fn(func)) => func.function_id,
199 _ => None,
200 })
201 .filter(|&func_id| self.is_followable_member_call(func_id, args, base))
202 .collect();
203 if !member_functions.is_empty() {
204 return member_functions;
205 }
206
207 self.library_extension_function_ids(member_name, args, base)
208 }
209
210 fn super_function_ids(
211 &self,
212 member_name: solar::interface::Symbol,
213 args: &CallArgs<'hir>,
214 ) -> Vec<FunctionId> {
215 let (Some(dispatch_contract), Some(current_contract)) =
216 (self.dispatch_contract, self.current_contract)
217 else {
218 return Vec::new();
219 };
220
221 let linearized_bases = self.hir.contract(dispatch_contract).linearized_bases;
222 let Some(current_index) = linearized_bases.iter().position(|&id| id == current_contract)
223 else {
224 return Vec::new();
225 };
226
227 for &base_id in linearized_bases.iter().skip(current_index + 1) {
228 let funcs: Vec<_> = self
229 .contract_function_ids(base_id, member_name)
230 .into_iter()
231 .filter(|&func_id| self.is_followable_call(func_id, args))
232 .collect();
233 if !funcs.is_empty() {
234 return funcs;
235 }
236 }
237
238 Vec::new()
239 }
240
241 fn contract_function_ids(
242 &self,
243 contract_id: ContractId,
244 member_name: solar::interface::Symbol,
245 ) -> Vec<FunctionId> {
246 self.hir
247 .contract(contract_id)
248 .functions()
249 .filter(|&func_id| {
250 let func = self.hir.function(func_id);
251 func.name.is_some_and(|name| name.name == member_name)
252 })
253 .collect()
254 }
255
256 fn is_followable_call(&self, func_id: FunctionId, args: &CallArgs<'hir>) -> bool {
257 let func = self.hir.function(func_id);
258 is_current_context_helper(func)
259 && args_match_function(self.gcx, self.hir, args, func.parameters)
260 }
261
262 fn is_followable_member_call(
263 &self,
264 func_id: FunctionId,
265 args: &CallArgs<'hir>,
266 receiver: &Expr<'hir>,
267 ) -> bool {
268 let func = self.hir.function(func_id);
269 is_current_context_helper(func)
270 && (args_match_function(self.gcx, self.hir, args, func.parameters)
271 || args_match_extension_function(self.gcx, self.hir, args, receiver, func))
272 }
273
274 fn library_extension_function_ids(
275 &self,
276 member_name: solar::interface::Symbol,
277 args: &CallArgs<'hir>,
278 receiver: &Expr<'hir>,
279 ) -> Vec<FunctionId> {
280 self.hir
281 .function_ids()
282 .filter(|&func_id| {
283 let func = self.hir.function(func_id);
284 func.contract
285 .is_some_and(|contract_id| self.hir.contract(contract_id).kind.is_library())
286 && func.name.is_some_and(|name| name.name == member_name)
287 && self.using_allows_extension(func_id, member_name)
288 && self.is_followable_member_call(func_id, args, receiver)
289 })
290 .collect()
291 }
292
293 fn using_allows_extension(
294 &self,
295 func_id: FunctionId,
296 member_name: solar::interface::Symbol,
297 ) -> bool {
298 let func = self.hir.function(func_id);
299 let Some(library_id) = func.contract else { return false };
300 let library_name = self.hir.contract(library_id).name.name;
301
302 let current_contract = self.current_contract.map(|id| self.hir.contract(id));
303 let source_id = current_contract.map(|contract| contract.source).unwrap_or(func.source);
304 let Some(source) = self.gcx.sources.get(source_id).and_then(|source| source.ast.as_ref())
305 else {
306 return false;
307 };
308
309 source.items.iter().any(|item| {
310 if let AstItemKind::Using(using) = &item.kind {
311 return using_list_allows_extension(&using.list, library_name, member_name);
312 }
313
314 let Some(current_contract) = current_contract else { return false };
315 let AstItemKind::Contract(contract) = &item.kind else { return false };
316 if contract.name.name != current_contract.name.name
317 || !item.span.contains(current_contract.span)
318 {
319 return false;
320 }
321
322 contract.body.iter().any(|item| match &item.kind {
323 AstItemKind::Using(using) => {
324 using_list_allows_extension(&using.list, library_name, member_name)
325 }
326 _ => false,
327 })
328 })
329 }
330}
331
332impl<'hir> Visit<'hir> for LoopContextChecker<'_, '_, 'hir, '_> {
333 type BreakValue = ();
334
335 fn hir(&self) -> &'hir Hir<'hir> {
336 self.hir
337 }
338
339 fn visit_stmt(&mut self, stmt: &'hir Stmt<'hir>) -> ControlFlow<Self::BreakValue> {
340 if self.current_loop_context_is_reportable() {
341 (self.stmt_f)(self.ctx, self.gcx, self.hir, stmt);
342 }
343
344 match stmt.kind {
345 StmtKind::Loop(block, _) => self.visit_loop_block(block),
346 StmtKind::Placeholder => {
347 if let Some((modifiers, index, body, body_contract)) = self.placeholder {
348 self.visit_modifier_chain(modifiers, index, body, body_contract);
349 }
350 ControlFlow::Continue(())
351 }
352 _ => self.walk_stmt(stmt),
353 }
354 }
355
356 fn visit_expr(&mut self, expr: &'hir Expr<'hir>) -> ControlFlow<Self::BreakValue> {
357 let reportable_loop_context = self.current_loop_context_is_reportable();
358 if reportable_loop_context {
359 (self.expr_f)(self.ctx, self.gcx, self.hir, expr);
360 }
361
362 let result = self.walk_expr(expr);
363 if result.is_break() {
364 return result;
365 }
366
367 if (self.follow_calls_outside_loop || reportable_loop_context)
368 && let ExprKind::Call(callee, args, _) = &expr.kind
369 {
370 for func_id in self.resolved_internal_function_ids(callee, args) {
371 self.visit_internal_call(func_id);
372 }
373 }
374
375 ControlFlow::Continue(())
376 }
377}
378
379fn is_current_context_helper(func: &Function<'_>) -> bool {
380 func.kind.is_ordinary()
381 && matches!(
382 func.visibility,
383 Visibility::Public | Visibility::Internal | Visibility::Private
384 )
385}
386
387pub(super) fn is_builtin(expr: &Expr<'_>, symbol: solar::interface::Symbol) -> bool {
388 let ExprKind::Ident(reses) = &expr.peel_parens().kind else { return false };
389 let mut iter = reses.iter().filter(|res| !matches!(res, Res::Err(_)));
390 matches!(
391 (iter.next(), iter.next()),
392 (Some(Res::Builtin(builtin)), None) if builtin.name() == symbol
393 )
394}
395
396pub(super) fn is_this_or_super(expr: &Expr<'_>) -> bool {
397 is_builtin(expr, sym::this) || is_builtin(expr, sym::super_)
398}
399
400fn unique<T>(mut iter: impl Iterator<Item = T>) -> Option<T> {
401 let first = iter.next()?;
402 iter.next().is_none().then_some(first)
403}
404
405fn using_list_allows_extension(
406 using_list: &AstUsingList<'_>,
407 library_name: solar::interface::Symbol,
408 member_name: solar::interface::Symbol,
409) -> bool {
410 match using_list {
411 AstUsingList::Single(path) => {
412 path.segments().last().is_some_and(|id| id.name == library_name)
413 }
414 AstUsingList::Multiple(paths) => paths.iter().any(|(path, _)| {
415 let segments = path.segments();
416 matches!(
417 segments,
418 [.., library, member] if library.name == library_name && member.name == member_name
419 )
420 }),
421 }
422}
423
424fn args_match_function<'gcx>(
425 gcx: Gcx<'gcx>,
426 hir: &Hir<'gcx>,
427 args: &CallArgs<'gcx>,
428 params: &'gcx [VariableId],
429) -> bool {
430 if args.len() != params.len() {
431 return false;
432 }
433
434 match args.kind {
435 CallArgsKind::Unnamed(exprs) => {
436 exprs.iter().zip(params).all(|(arg, ¶m)| arg_matches_param(gcx, hir, arg, param))
437 }
438 CallArgsKind::Named(named_args) => named_args.iter().all(|arg| {
439 params
440 .iter()
441 .copied()
442 .find(|¶m| {
443 hir.variable(param).name.is_some_and(|name| name.name == arg.name.name)
444 })
445 .is_some_and(|param| arg_matches_param(gcx, hir, &arg.value, param))
446 }),
447 }
448}
449
450fn args_match_extension_function<'gcx>(
451 gcx: Gcx<'gcx>,
452 hir: &Hir<'gcx>,
453 args: &CallArgs<'gcx>,
454 receiver: &Expr<'gcx>,
455 func: &Function<'gcx>,
456) -> bool {
457 let Some(params) = func.parameters.split_first() else { return false };
458 let (self_param, params) = params;
459 args.len() == params.len()
460 && receiver_matches_param(gcx, hir, receiver, *self_param)
461 && args_match_function(gcx, hir, args, params)
462}
463
464fn receiver_matches_param<'gcx>(
465 gcx: Gcx<'gcx>,
466 hir: &Hir<'gcx>,
467 receiver: &Expr<'gcx>,
468 param: VariableId,
469) -> bool {
470 let Some(receiver_ty) = expr_ty(gcx, hir, receiver) else {
471 return true;
472 };
473 let param_var = hir.variable(param);
474 let param_ty = gcx.type_of_item(param.into()).with_loc_if_ref_opt(gcx, param_var.data_location);
475 receiver_ty.convert_implicit_to(param_ty, gcx)
476}
477
478fn arg_matches_param<'gcx>(
479 gcx: Gcx<'gcx>,
480 hir: &Hir<'gcx>,
481 arg: &Expr<'gcx>,
482 param: VariableId,
483) -> bool {
484 let Some(arg_ty) = expr_ty(gcx, hir, arg) else {
485 return true;
486 };
487 let param_var = hir.variable(param);
488 let param_ty = gcx.type_of_item(param.into()).with_loc_if_ref_opt(gcx, param_var.data_location);
489 arg_ty.convert_implicit_to(param_ty, gcx)
490}
491
492pub(super) fn expr_ty<'gcx>(
493 gcx: Gcx<'gcx>,
494 hir: &Hir<'gcx>,
495 expr: &Expr<'gcx>,
496) -> Option<Ty<'gcx>> {
497 match &expr.peel_parens().kind {
498 ExprKind::Array(_) => None,
499 ExprKind::Call(callee, args, _) => {
500 let callee_ty = expr_ty(gcx, hir, callee)?;
501 match callee_ty.kind {
502 TyKind::Fn(func) => fn_call_return_type(gcx, func.returns),
503 TyKind::Type(to) => Some(explicit_cast_ty(gcx, to, args)),
504 _ => None,
505 }
506 }
507 ExprKind::Ident(reses) => {
508 let res = unique(reses.iter().filter(|res| !matches!(res, Res::Err(_))).copied())?;
509 match res {
510 Res::Builtin(builtin)
511 if matches!(
512 builtin.name(),
513 solar::interface::sym::this | solar::interface::sym::super_
514 ) =>
515 {
516 None
517 }
518 Res::Item(ItemId::Variable(var_id)) => Some(
519 gcx.type_of_res(res)
520 .with_loc_if_ref_opt(gcx, variable_data_location(hir, var_id)),
521 ),
522 _ => Some(gcx.type_of_res(res)),
523 }
524 }
525 ExprKind::Index(lhs, index) => {
526 let lhs_ty = expr_ty(gcx, hir, lhs)?;
527 if let Some(index) = index
528 && !expr_ty(gcx, hir, index)?.convert_implicit_to(gcx.types.uint(256), gcx)
529 {
530 return None;
531 }
532 index_ty(gcx, lhs_ty)
533 }
534 ExprKind::Lit(lit) => Some(match &lit.kind {
535 LitKind::Str(StrKind::Hex, s, _) => {
536 let size = TypeSize::try_new_fb_bytes(s.as_byte_str().len().min(32) as u8)?;
537 gcx.types.fixed_bytes(size.bytes())
538 }
539 LitKind::Str(_, s, _) => gcx.mk_ty_string_literal(s.as_byte_str()),
540 LitKind::Number(int) => gcx.mk_ty_int_literal(false, int.bit_len() as _)?,
541 LitKind::Rational(_) | LitKind::Err(_) => return None,
542 LitKind::Address(_) => gcx.types.address,
543 LitKind::Bool(_) => gcx.types.bool,
544 }),
545 ExprKind::Member(base, member) => member_ty(gcx, hir, base, member.name),
546 ExprKind::New(ty) => {
547 let ty = gcx.type_of_hir_ty(ty);
548 Some(gcx.mk_ty(TyKind::Type(ty)))
549 }
550 ExprKind::Payable(inner) => {
551 let inner_ty = expr_ty(gcx, hir, inner)?;
552 inner_ty
553 .convert_explicit_to(gcx.types.address_payable, gcx)
554 .then_some(gcx.types.address_payable)
555 }
556 ExprKind::Slice(lhs, ..) => {
557 let lhs_ty = expr_ty(gcx, hir, lhs)?;
558 lhs_ty.is_sliceable().then_some(gcx.mk_ty(TyKind::Slice(lhs_ty)))
559 }
560 ExprKind::Tuple(exprs) => {
561 let tys = exprs
562 .iter()
563 .map(|expr| expr.and_then(|expr| expr_ty(gcx, hir, expr)))
564 .collect::<Option<Vec<_>>>()?;
565 Some(gcx.mk_ty_tuple(gcx.mk_tys(&tys)))
566 }
567 ExprKind::Ternary(_, true_expr, false_expr) => {
568 let true_ty = expr_ty(gcx, hir, true_expr)?;
569 let false_ty = expr_ty(gcx, hir, false_expr)?;
570 common_ty(gcx, true_ty, false_ty)
571 }
572 ExprKind::Type(ty) | ExprKind::TypeCall(ty) => {
573 let ty = gcx.type_of_hir_ty(ty);
574 Some(gcx.mk_ty(TyKind::Type(ty)))
575 }
576 ExprKind::Unary(_, inner) => expr_ty(gcx, hir, inner),
577 ExprKind::Assign(..)
578 | ExprKind::Binary(..)
579 | ExprKind::Delete(..)
580 | ExprKind::YulMember(..)
581 | ExprKind::Err(_) => None,
582 }
583}
584
585fn common_ty<'gcx>(gcx: Gcx<'gcx>, lhs: Ty<'gcx>, rhs: Ty<'gcx>) -> Option<Ty<'gcx>> {
586 if lhs.convert_implicit_to(rhs, gcx) {
587 Some(rhs)
588 } else {
589 rhs.convert_implicit_to(lhs, gcx).then_some(lhs)
590 }
591}
592
593fn fn_call_return_type<'gcx>(gcx: Gcx<'gcx>, returns: &'gcx [Ty<'gcx>]) -> Option<Ty<'gcx>> {
594 Some(match returns {
595 [] => gcx.types.unit,
596 [ret] => *ret,
597 _ => gcx.mk_ty_tuple(returns),
598 })
599}
600
601fn explicit_cast_ty<'gcx>(gcx: Gcx<'gcx>, to: Ty<'gcx>, args: &CallArgs<'gcx>) -> Ty<'gcx> {
602 match args.exprs().next().and_then(|arg| expr_ty(gcx, &gcx.hir, arg)) {
603 Some(from) => from.try_convert_explicit_to(to, gcx).unwrap_or(to),
604 None => to,
605 }
606}
607
608fn index_ty<'gcx>(gcx: Gcx<'gcx>, base_ty: Ty<'gcx>) -> Option<Ty<'gcx>> {
609 let loc = indexed_base_data_location(base_ty);
610 match base_ty.peel_refs().kind {
611 TyKind::Mapping(_, value) => Some(value.with_loc_if_ref_opt(gcx, loc)),
612 _ => base_ty.base_type(gcx),
613 }
614}
615
616fn indexed_base_data_location(ty: Ty<'_>) -> Option<DataLocation> {
617 ty.loc().or_else(|| matches!(ty.kind, TyKind::Mapping(..)).then_some(DataLocation::Storage))
618}
619
620fn member_ty<'gcx>(
621 gcx: Gcx<'gcx>,
622 hir: &Hir<'gcx>,
623 base: &Expr<'gcx>,
624 member_name: solar::interface::Symbol,
625) -> Option<Ty<'gcx>> {
626 let base_ty = match &base.peel_parens().kind {
627 ExprKind::Ident(_) if is_this_or_super(base) => {
628 return None;
629 }
630 _ => expr_ty(gcx, hir, base)?,
631 };
632
633 unique(
634 gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
635 .filter(|member| member.name == member_name)
636 .map(|member| member.ty),
637 )
638}
639
640fn base_item_source(hir: &Hir<'_>, expr: &Expr<'_>) -> solar::sema::hir::SourceId {
641 referenced_item(expr)
642 .map(|id| hir.item(id).source())
643 .unwrap_or_else(|| hir.sources_enumerated().next().expect("HIR has a source").0)
644}
645
646fn base_contract(hir: &Hir<'_>, expr: &Expr<'_>) -> Option<solar::sema::hir::ContractId> {
647 referenced_item(expr).and_then(|id| hir.item(id).contract())
648}
649
650fn referenced_item(expr: &Expr<'_>) -> Option<ItemId> {
651 match &expr.peel_parens().kind {
652 ExprKind::Ident([Res::Item(id), ..]) => Some(*id),
653 _ => None,
654 }
655}
656
657fn variable_data_location(hir: &Hir<'_>, var_id: VariableId) -> Option<DataLocation> {
658 let var = hir.variable(var_id);
659 var.data_location.or_else(|| {
660 (var.parent.is_none() && var.contract.is_some()).then_some(DataLocation::Storage)
661 })
662}
663
664pub(super) fn is_address_ty(ty: Ty<'_>) -> bool {
665 matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
666}
667
668pub(super) fn visit_payable_loop_expressions<'ctx, 's, 'hir, 'cb>(
669 ctx: &'ctx LintContext<'s, 'ctx>,
670 gcx: Gcx<'hir>,
671 hir: &'hir Hir<'hir>,
672 func: &'hir Function<'hir>,
673 f: impl FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Expr<'hir>) + 'cb,
674) {
675 if !is_payable_entry_point(func) {
676 return;
677 }
678
679 visit_loop_statements_and_expressions_with_options(
680 ctx,
681 gcx,
682 hir,
683 func,
684 true,
685 true,
686 |_, _, _, _| {},
687 f,
688 );
689}
690
691pub(super) fn visit_loop_statements_and_expressions<'ctx, 's, 'hir, 'cb>(
692 ctx: &'ctx LintContext<'s, 'ctx>,
693 gcx: Gcx<'hir>,
694 hir: &'hir Hir<'hir>,
695 func: &'hir Function<'hir>,
696 mut stmt_f: impl FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Stmt<'hir>)
697 + 'cb,
698 mut expr_f: impl FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Expr<'hir>)
699 + 'cb,
700) {
701 visit_loop_statements_and_expressions_with_options(
702 ctx,
703 gcx,
704 hir,
705 func,
706 false,
707 false,
708 &mut stmt_f,
709 &mut expr_f,
710 );
711}
712
713#[allow(clippy::too_many_arguments)]
714fn visit_loop_statements_and_expressions_with_options<'ctx, 's, 'hir, 'cb>(
715 ctx: &'ctx LintContext<'s, 'ctx>,
716 gcx: Gcx<'hir>,
717 hir: &'hir Hir<'hir>,
718 func: &'hir Function<'hir>,
719 follow_calls_outside_loop: bool,
720 report_local_loops_in_internal_calls: bool,
721 mut stmt_f: impl FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Stmt<'hir>)
722 + 'cb,
723 mut expr_f: impl FnMut(&'ctx LintContext<'s, 'ctx>, Gcx<'hir>, &'hir Hir<'hir>, &'hir Expr<'hir>)
724 + 'cb,
725) {
726 let Some(body) = func.body else { return };
727
728 let mut checker = LoopContextChecker {
729 ctx,
730 hir,
731 gcx,
732 loop_depth: 0,
733 placeholder: None,
734 modifier_stack: Vec::new(),
735 call_stack: Vec::new(),
736 internal_call_loop_depths: Vec::new(),
737 dispatch_contract: func.contract,
738 current_contract: func.contract,
739 follow_calls_outside_loop,
740 report_local_loops_in_internal_calls,
741 stmt_f: &mut stmt_f,
742 expr_f: &mut expr_f,
743 };
744 checker.visit_modifier_chain(func.modifiers, 0, body, func.contract);
745}
746
747fn is_payable_entry_point(func: &Function<'_>) -> bool {
748 !matches!(func.kind, FunctionKind::Constructor | FunctionKind::Modifier)
749 && func.state_mutability == StateMutability::Payable
750 && matches!(func.visibility, Visibility::Public | Visibility::External)
751}