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