Skip to main content

forge_lint/sol/low/
payable_loop.rs

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, &param)| 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(|&param| {
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}