Skip to main content

forge_lint/sol/low/
calls_loop.rs

1use super::CallsLoop;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{DataLocation, ElementaryType, StateMutability, Visibility},
8    interface::{kw, sym},
9    sema::{
10        Gcx, Ty,
11        builtins::Builtin,
12        hir::{
13            self, Block, ContractId, Expr, ExprKind, Function, FunctionId, Hir, ItemId, Res, Stmt,
14            StmtKind, TypeKind,
15        },
16        ty::{TyFn, TyKind},
17    },
18};
19use std::collections::HashSet;
20
21declare_forge_lint!(CALLS_LOOP, Severity::Low, "calls-loop", "external call inside a loop");
22
23impl<'hir> LateLintPass<'hir> for CallsLoop {
24    fn check_function(
25        &mut self,
26        ctx: &LintContext,
27        gcx: Gcx<'hir>,
28        hir: &'hir Hir<'hir>,
29        func: &'hir Function<'hir>,
30    ) {
31        let Some(body) = func.body else { return };
32
33        let mut analyzer = Analyzer::new(ctx, gcx, hir);
34        analyzer.analyze_callable(func, body, 0);
35    }
36}
37
38type Placeholder<'hir> = Option<(&'hir [hir::Modifier<'hir>], usize, Block<'hir>)>;
39
40struct Analyzer<'ctx, 's, 'c, 'hir> {
41    ctx: &'ctx LintContext<'s, 'c>,
42    gcx: Gcx<'hir>,
43    hir: &'hir Hir<'hir>,
44    call_stack: Vec<FunctionId>,
45    analyzed_loop_calls: HashSet<FunctionId>,
46    emitted: HashSet<solar::interface::Span>,
47}
48
49impl<'ctx, 's, 'c, 'hir> Analyzer<'ctx, 's, 'c, 'hir> {
50    fn new(ctx: &'ctx LintContext<'s, 'c>, gcx: Gcx<'hir>, hir: &'hir Hir<'hir>) -> Self {
51        Self {
52            ctx,
53            gcx,
54            hir,
55            call_stack: Vec::new(),
56            analyzed_loop_calls: HashSet::new(),
57            emitted: HashSet::new(),
58        }
59    }
60
61    fn analyze_callable(&mut self, func: &'hir Function<'hir>, body: Block<'hir>, loop_depth: u32) {
62        self.analyze_modifier_chain(func.modifiers, 0, body, loop_depth);
63    }
64
65    fn analyze_modifier_chain(
66        &mut self,
67        modifiers: &'hir [hir::Modifier<'hir>],
68        index: usize,
69        body: Block<'hir>,
70        loop_depth: u32,
71    ) {
72        let Some(modifier) = modifiers.get(index) else {
73            return self.analyze_block(body, None, loop_depth);
74        };
75
76        for arg in modifier.args.exprs() {
77            self.analyze_expr(arg, loop_depth);
78        }
79
80        let Some(modifier_id) = modifier.id.as_function() else {
81            return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
82        };
83
84        if self.call_stack.contains(&modifier_id) {
85            return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
86        }
87
88        let modifier_func = self.hir.function(modifier_id);
89        let Some(modifier_body) = modifier_func.body else {
90            return self.analyze_modifier_chain(modifiers, index + 1, body, loop_depth);
91        };
92
93        self.call_stack.push(modifier_id);
94        self.analyze_block(modifier_body, Some((modifiers, index + 1, body)), loop_depth);
95        self.call_stack.pop();
96    }
97
98    fn analyze_block(
99        &mut self,
100        block: Block<'hir>,
101        placeholder: Placeholder<'hir>,
102        loop_depth: u32,
103    ) {
104        for stmt in block.stmts {
105            self.analyze_stmt(stmt, placeholder, loop_depth);
106        }
107    }
108
109    fn analyze_stmt(
110        &mut self,
111        stmt: &'hir Stmt<'hir>,
112        placeholder: Placeholder<'hir>,
113        loop_depth: u32,
114    ) {
115        match stmt.kind {
116            StmtKind::DeclSingle(var_id) => {
117                if let Some(init) = self.hir.variable(var_id).initializer {
118                    self.analyze_expr(init, loop_depth);
119                }
120            }
121            StmtKind::DeclMulti(_, expr) | StmtKind::Expr(expr) => {
122                self.analyze_expr(expr, loop_depth);
123            }
124            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) => {
125                self.analyze_block(block, placeholder, loop_depth);
126            }
127            StmtKind::Emit(expr) | StmtKind::Revert(expr) => {
128                self.analyze_expr(expr, loop_depth);
129            }
130            StmtKind::Return(Some(expr)) => {
131                self.analyze_expr(expr, loop_depth);
132            }
133            StmtKind::Loop(block, _) => {
134                self.analyze_block(block, placeholder, loop_depth + 1);
135            }
136            StmtKind::If(cond, then_stmt, else_stmt) => {
137                self.analyze_expr(cond, loop_depth);
138                self.analyze_stmt(then_stmt, placeholder, loop_depth);
139                if let Some(else_stmt) = else_stmt {
140                    self.analyze_stmt(else_stmt, placeholder, loop_depth);
141                }
142            }
143            StmtKind::Try(try_stmt) => {
144                self.analyze_expr(&try_stmt.expr, loop_depth);
145                for clause in try_stmt.clauses {
146                    self.analyze_block(clause.block, placeholder, loop_depth);
147                }
148            }
149            StmtKind::Placeholder => {
150                if let Some((modifiers, index, body)) = placeholder {
151                    self.analyze_modifier_chain(modifiers, index, body, loop_depth);
152                }
153            }
154            StmtKind::Return(None)
155            | StmtKind::Break
156            | StmtKind::Continue
157            | StmtKind::AssemblyBlock(_)
158            | StmtKind::Switch(_)
159            | StmtKind::Err(_) => {}
160        }
161    }
162
163    fn analyze_expr(&mut self, expr: &'hir Expr<'hir>, loop_depth: u32) {
164        match &expr.kind {
165            ExprKind::Call(callee, args, opts) => {
166                self.analyze_expr(callee, loop_depth);
167                if let Some(opts) = opts {
168                    for opt in opts.args {
169                        self.analyze_expr(&opt.value, loop_depth);
170                    }
171                }
172                for arg in args.exprs() {
173                    self.analyze_expr(arg, loop_depth);
174                }
175
176                if loop_depth > 0 {
177                    if is_external_call(self.gcx, self.hir, callee, args.len()) {
178                        self.emit(expr);
179                    }
180                    for func_id in resolved_internal_function_ids(self.hir, callee) {
181                        self.analyze_internal_call(func_id, loop_depth);
182                    }
183                }
184            }
185            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
186                self.analyze_expr(lhs, loop_depth);
187                self.analyze_expr(rhs, loop_depth);
188            }
189            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
190                self.analyze_expr(inner, loop_depth);
191            }
192            ExprKind::Index(base, index) => {
193                self.analyze_expr(base, loop_depth);
194                if let Some(index) = index {
195                    self.analyze_expr(index, loop_depth);
196                }
197            }
198            ExprKind::Slice(base, start, end) => {
199                self.analyze_expr(base, loop_depth);
200                if let Some(start) = start {
201                    self.analyze_expr(start, loop_depth);
202                }
203                if let Some(end) = end {
204                    self.analyze_expr(end, loop_depth);
205                }
206            }
207            ExprKind::Ternary(cond, then_expr, else_expr) => {
208                self.analyze_expr(cond, loop_depth);
209                self.analyze_expr(then_expr, loop_depth);
210                self.analyze_expr(else_expr, loop_depth);
211            }
212            ExprKind::Array(exprs) => {
213                for expr in *exprs {
214                    self.analyze_expr(expr, loop_depth);
215                }
216            }
217            ExprKind::Tuple(exprs) => {
218                for expr in exprs.iter().copied().flatten() {
219                    self.analyze_expr(expr, loop_depth);
220                }
221            }
222            ExprKind::Member(base, _) => self.analyze_expr(base, loop_depth),
223            ExprKind::Ident(_)
224            | ExprKind::Lit(_)
225            | ExprKind::New(_)
226            | ExprKind::TypeCall(_)
227            | ExprKind::Type(_)
228            | ExprKind::YulMember(..)
229            | ExprKind::Err(_) => {}
230        }
231    }
232
233    fn analyze_internal_call(&mut self, func_id: FunctionId, loop_depth: u32) {
234        if self.call_stack.contains(&func_id) {
235            return;
236        }
237        if !self.analyzed_loop_calls.insert(func_id) {
238            return;
239        }
240
241        let func = self.hir.function(func_id);
242        let Some(body) = func.body else { return };
243
244        self.call_stack.push(func_id);
245        self.analyze_callable(func, body, loop_depth);
246        self.call_stack.pop();
247    }
248
249    fn emit(&mut self, expr: &Expr<'_>) {
250        if self.emitted.insert(expr.span) {
251            self.ctx.emit(&CALLS_LOOP, expr.span);
252        }
253    }
254}
255
256pub(super) fn is_external_call<'gcx>(
257    gcx: Gcx<'gcx>,
258    hir: &'gcx Hir<'gcx>,
259    callee: &Expr<'gcx>,
260    explicit_arg_count: usize,
261) -> bool {
262    // `new Foo(...)` runs the deployed contract's constructor — an external interaction.
263    if matches!(callee.peel_parens().kind, ExprKind::New(_)) {
264        return true;
265    }
266    let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
267
268    if matches!(member.name, kw::Call | kw::Delegatecall | kw::Staticcall)
269        && is_address_like(gcx, base)
270    {
271        return true;
272    }
273
274    if matches!(member.name, sym::send | sym::transfer) && is_address_like(gcx, base) {
275        return true;
276    }
277
278    if is_this(base) {
279        return true;
280    }
281
282    // `super.<member>(...)` is internal base-chain dispatch, not an external call.
283    // Short-circuit before any code that would ask Solar for `super`'s type (panics).
284    if is_super(base) {
285        return false;
286    }
287
288    if resolves_to_internal_library_extension(gcx, hir, base, *member, explicit_arg_count) {
289        return false;
290    }
291
292    // Iterate matching members so overloads aren't dropped by a unique-name lookup.
293    external_member_signatures(gcx, hir, base, member.name, explicit_arg_count)
294        .into_iter()
295        .any(|(vis, _)| vis >= Visibility::Public)
296}
297
298/// Like [`is_external_call`], but excludes calls that cannot affect log ordering or
299/// observable state: `staticcall` and high-level `view`/`pure` callees (including `this.*`).
300pub(super) fn is_state_mutating_external_call<'gcx>(
301    gcx: Gcx<'gcx>,
302    hir: &'gcx Hir<'gcx>,
303    callee: &Expr<'gcx>,
304    explicit_arg_count: usize,
305    enclosing_contract: Option<ContractId>,
306) -> bool {
307    // Contract deployment: the constructor runs arbitrary code and can emit logs.
308    if matches!(callee.peel_parens().kind, ExprKind::New(_)) {
309        return true;
310    }
311    let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
312
313    // Low-level address calls: `call` and `delegatecall` are in scope; `staticcall` is not.
314    if matches!(member.name, kw::Call | kw::Delegatecall) && is_address_like(gcx, base) {
315        return true;
316    }
317
318    if member.name == kw::Staticcall && is_address_like(gcx, base) {
319        return false;
320    }
321
322    if matches!(member.name, sym::send | sym::transfer) && is_address_like(gcx, base) {
323        return true;
324    }
325
326    if is_this(base) {
327        // `this.<view|pure>()` compiles to a STATICCALL and cannot reorder events; only
328        // taint when the resolved self-call is state-mutating.
329        return self_call_is_state_mutating(
330            hir,
331            enclosing_contract,
332            member.name,
333            explicit_arg_count,
334        );
335    }
336
337    // `super.<member>(...)` is internal dispatch — not an external call.
338    if is_super(base) {
339        return false;
340    }
341
342    if resolves_to_internal_library_extension(gcx, hir, base, *member, explicit_arg_count) {
343        return false;
344    }
345
346    // Iterate overloads: conservatively flag if any matching public/external member is
347    // not `view`/`pure` (rather than silently dropping overloaded names).
348    external_member_signatures(gcx, hir, base, member.name, explicit_arg_count).into_iter().any(
349        |(vis, mut_)| {
350            vis >= Visibility::Public
351                && !matches!(mut_, StateMutability::View | StateMutability::Pure)
352        },
353    )
354}
355
356/// Returns `true` when a `this.<member>(...)` call may emit logs or mutate state.
357/// Conservative on unresolved/overloaded names (avoids `gcx.type_of_res` for the `this` builtin,
358/// which panics).
359fn self_call_is_state_mutating(
360    hir: &Hir<'_>,
361    enclosing_contract: Option<ContractId>,
362    member_name: solar::interface::Symbol,
363    explicit_arg_count: usize,
364) -> bool {
365    let Some(contract_id) = enclosing_contract else { return true };
366
367    let mut matched = false;
368    for item_id in hir.contract_item_ids(contract_id) {
369        let Some(func_id) = item_id.as_function() else { continue };
370        let func = hir.function(func_id);
371        if func.name.is_none_or(|name| name.name != member_name) {
372            continue;
373        }
374        if func.parameters.len() != explicit_arg_count {
375            continue;
376        }
377        // Only externally-callable functions can appear in a `this.<member>(...)` call.
378        if func.visibility < Visibility::Public {
379            continue;
380        }
381        matched = true;
382        if !matches!(func.state_mutability, StateMutability::View | StateMutability::Pure) {
383            return true;
384        }
385    }
386    // No public/external match resolved → conservatively taint.
387    !matched
388}
389
390fn resolves_to_internal_library_extension<'gcx>(
391    gcx: Gcx<'gcx>,
392    hir: &Hir<'gcx>,
393    base: &Expr<'gcx>,
394    member: solar::interface::Ident,
395    explicit_arg_count: usize,
396) -> bool {
397    member_function_ids(gcx, hir, base, member.name).into_iter().any(|func_id| {
398        let func = hir.function(func_id);
399        func.parameters.len() == explicit_arg_count + 1
400            && matches!(func.visibility, Visibility::Internal | Visibility::Private)
401            && func.contract.is_some_and(|contract_id| hir.contract(contract_id).kind.is_library())
402    })
403}
404
405fn member_function_ids<'gcx>(
406    gcx: Gcx<'gcx>,
407    hir: &Hir<'gcx>,
408    base: &Expr<'gcx>,
409    member_name: solar::interface::Symbol,
410) -> Vec<FunctionId> {
411    let Some(base_ty) = semantic_expr_ty(gcx, hir, base) else { return Vec::new() };
412
413    gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
414        .filter(|member| member.name == member_name)
415        .filter_map(|member| match (member.res, member.ty.kind) {
416            (Some(Res::Item(ItemId::Function(func_id))), _) => Some(func_id),
417            (_, TyKind::Fn(func)) => func.function_id,
418            _ => None,
419        })
420        .collect()
421}
422
423/// `(visibility, state_mutability)` for every callable member named `member_name`.
424/// When `explicit_arg_count` matches one or more overloads, narrows to those; otherwise
425/// returns the full set. Preserves overloads (no `unique` collapse).
426fn external_member_signatures<'gcx>(
427    gcx: Gcx<'gcx>,
428    hir: &Hir<'gcx>,
429    base: &Expr<'gcx>,
430    member_name: solar::interface::Symbol,
431    explicit_arg_count: usize,
432) -> Vec<(Visibility, StateMutability)> {
433    let Some(base_ty) = semantic_expr_ty(gcx, hir, base) else { return Vec::new() };
434
435    // (visibility, state_mutability, arity) for every matching callable member.
436    let all: Vec<(Visibility, StateMutability, usize)> = gcx
437        .members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
438        .filter(|member| member.name == member_name)
439        .filter_map(|member| match (member.res, member.ty.kind) {
440            (Some(Res::Item(ItemId::Function(func_id))), _) => {
441                let f = hir.function(func_id);
442                Some((f.visibility, f.state_mutability, f.parameters.len()))
443            }
444            (_, TyKind::Fn(func)) => Some(function_signature_from_ty_fn(hir, func)),
445            _ => None,
446        })
447        .collect();
448
449    // Prefer arity-matched overloads; fall back to the full set when none match.
450    let arity_matched: Vec<_> =
451        all.iter().filter(|(_, _, n)| *n == explicit_arg_count).copied().collect();
452    let chosen = if arity_matched.is_empty() { all } else { arity_matched };
453    chosen.into_iter().map(|(v, m, _)| (v, m)).collect()
454}
455
456fn function_signature_from_ty_fn(
457    hir: &Hir<'_>,
458    func: &TyFn<'_>,
459) -> (Visibility, StateMutability, usize) {
460    if let Some(func_id) = func.function_id {
461        let f = hir.function(func_id);
462        (f.visibility, f.state_mutability, f.parameters.len())
463    } else if func.is_internal() {
464        (Visibility::Internal, func.state_mutability, func.parameters.len())
465    } else {
466        (Visibility::External, func.state_mutability, func.parameters.len())
467    }
468}
469
470pub(super) fn resolved_internal_function_ids<'hir>(
471    hir: &'hir Hir<'hir>,
472    callee: &'hir Expr<'hir>,
473) -> impl Iterator<Item = FunctionId> + 'hir {
474    let reses = match &callee.peel_parens().kind {
475        ExprKind::Ident(reses) => *reses,
476        _ => &[],
477    };
478
479    reses.iter().filter_map(|res| match res {
480        Res::Item(ItemId::Function(func_id)) if is_internal_callable(hir.function(*func_id)) => {
481            Some(*func_id)
482        }
483        _ => None,
484    })
485}
486
487/// Resolves `super.<member>(...)` to the matching base-chain function(s) — for transitive
488/// analysis of external calls reached through C3 super dispatch.
489pub(super) fn resolved_super_function_ids<'hir>(
490    hir: &'hir Hir<'hir>,
491    enclosing_contract: Option<ContractId>,
492    callee: &'hir Expr<'hir>,
493    explicit_arg_count: usize,
494) -> Vec<FunctionId> {
495    let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return Vec::new() };
496    if !is_super(base) {
497        return Vec::new();
498    }
499    let Some(contract_id) = enclosing_contract else { return Vec::new() };
500
501    let mut out = Vec::new();
502    // Skip the contract itself; super dispatch starts at the next base in linearization order.
503    for base_id in hir.contract(contract_id).linearized_bases.iter().skip(1).copied() {
504        for item_id in hir.contract(base_id).items {
505            let Some(func_id) = item_id.as_function() else { continue };
506            let func = hir.function(func_id);
507            if func.name.is_some_and(|name| name.name == member.name)
508                && func.parameters.len() == explicit_arg_count
509                && matches!(func.visibility, Visibility::Internal | Visibility::Public)
510            {
511                out.push(func_id);
512                return out;
513            }
514        }
515    }
516    out
517}
518
519const fn is_internal_callable(func: &Function<'_>) -> bool {
520    func.kind.is_function()
521        && matches!(
522            func.visibility,
523            Visibility::Public | Visibility::Internal | Visibility::Private
524        )
525}
526
527fn is_this(expr: &Expr<'_>) -> bool {
528    matches!(
529        &expr.peel_parens().kind,
530        ExprKind::Ident(reses)
531            if reses.iter().any(|res| {
532                matches!(res, Res::Builtin(builtin) if builtin.name() == sym::this)
533            })
534    )
535}
536
537/// `super` is the C3-linearized base-chain dispatch builtin; `gcx.type_of_res` panics on it.
538fn is_super(expr: &Expr<'_>) -> bool {
539    matches!(
540        &expr.peel_parens().kind,
541        ExprKind::Ident(reses)
542            if reses.iter().any(|res| {
543                matches!(res, Res::Builtin(builtin) if builtin.name() == sym::super_)
544            })
545    )
546}
547
548fn is_address_like<'hir>(gcx: Gcx<'hir>, expr: &'hir Expr<'hir>) -> bool {
549    match &expr.peel_parens().kind {
550        ExprKind::Payable(_) => true,
551        ExprKind::Call(callee, _, _) if is_address_type_expr(callee) => true,
552        _ => semantic_expr_ty(gcx, &gcx.hir, expr).is_some_and(type_is_address_like),
553    }
554}
555
556fn is_address_type_expr(expr: &Expr<'_>) -> bool {
557    matches!(
558        &expr.peel_parens().kind,
559        ExprKind::Type(hir::Type { kind: TypeKind::Elementary(ElementaryType::Address(_)), .. })
560    )
561}
562
563fn type_is_address_like(ty: Ty<'_>) -> bool {
564    matches!(ty.peel_refs().kind, TyKind::Elementary(ElementaryType::Address(_)))
565}
566
567fn semantic_expr_ty<'gcx>(gcx: Gcx<'gcx>, hir: &Hir<'gcx>, expr: &Expr<'gcx>) -> Option<Ty<'gcx>> {
568    if !is_typeless_builtin_expr(expr)
569        && let Some(ty) = gcx.type_of_expr(expr.peel_parens().id)
570    {
571        return Some(ty);
572    }
573
574    match &expr.peel_parens().kind {
575        ExprKind::Ident(reses) => {
576            let res = unique(reses.iter().filter(|res| !matches!(res, Res::Err(_))).copied())
577                .or_else(|| {
578                    unique(reses.iter().filter_map(|res| {
579                        res.as_variable().map(|var_id| Res::Item(ItemId::Variable(var_id)))
580                    }))
581                })?;
582            if matches!(res, Res::Builtin(builtin) if is_typeless_builtin(builtin)) {
583                return None;
584            }
585            let ty = gcx.type_of_res(res);
586            Some(match res {
587                Res::Item(ItemId::Variable(var_id)) => {
588                    ty.with_loc_if_ref_opt(gcx, variable_data_location(hir, var_id))
589                }
590                _ => ty,
591            })
592        }
593        ExprKind::Index(base, _) => semantic_index_ty(gcx, hir, base),
594        ExprKind::Member(base, member) => semantic_member_ty(gcx, hir, base, member.name),
595        ExprKind::Call(callee, _, _) => {
596            let callee_ty = semantic_expr_ty(gcx, hir, callee)?;
597            match callee_ty.kind {
598                TyKind::Fn(func) => semantic_fn_call_return_ty(gcx, func.returns),
599                TyKind::Type(to) => Some(to),
600                _ => None,
601            }
602        }
603        ExprKind::New(ty) | ExprKind::Type(ty) | ExprKind::TypeCall(ty) => {
604            Some(gcx.mk_ty(TyKind::Type(gcx.type_of_hir_ty(ty))))
605        }
606        ExprKind::Payable(_) => Some(gcx.types.address_payable),
607        _ => None,
608    }
609}
610
611fn is_typeless_builtin_expr(expr: &Expr<'_>) -> bool {
612    matches!(
613        &expr.peel_parens().kind,
614        ExprKind::Ident(reses)
615            if reses.iter().any(|res| {
616                matches!(res, Res::Builtin(builtin) if is_typeless_builtin(*builtin))
617            })
618    )
619}
620
621const fn is_typeless_builtin(builtin: Builtin) -> bool {
622    matches!(
623        builtin,
624        Builtin::This
625            | Builtin::Super
626            | Builtin::ArrayPush0
627            | Builtin::ArrayPush
628            | Builtin::ArrayPop
629            | Builtin::TypeMin
630            | Builtin::TypeMax
631            | Builtin::UdvtWrap
632            | Builtin::UdvtUnwrap
633    )
634}
635
636fn semantic_index_ty<'gcx>(gcx: Gcx<'gcx>, hir: &Hir<'gcx>, base: &Expr<'gcx>) -> Option<Ty<'gcx>> {
637    let base_ty = semantic_expr_ty(gcx, hir, base)?;
638    let loc = indexed_base_data_location(base_ty);
639    match base_ty.peel_refs().kind {
640        TyKind::Mapping(_, value) => Some(value.with_loc_if_ref_opt(gcx, loc)),
641        _ => base_ty.base_type(gcx),
642    }
643}
644
645fn indexed_base_data_location(ty: Ty<'_>) -> Option<DataLocation> {
646    ty.loc().or_else(|| {
647        // Mappings can only live in storage, but Solar does not model `TyKind::Mapping`
648        // itself as a reference type.
649        matches!(ty.kind, TyKind::Mapping(..)).then_some(DataLocation::Storage)
650    })
651}
652
653fn semantic_member_ty<'gcx>(
654    gcx: Gcx<'gcx>,
655    hir: &Hir<'gcx>,
656    base: &Expr<'gcx>,
657    member_name: solar::interface::Symbol,
658) -> Option<Ty<'gcx>> {
659    let base_ty = semantic_expr_ty(gcx, hir, base)?;
660    unique(
661        gcx.members_of(base_ty, base_item_source(hir, base), base_contract(hir, base))
662            .filter(|member| member.name == member_name)
663            .map(|member| member.ty),
664    )
665}
666
667fn semantic_fn_call_return_ty<'gcx>(gcx: Gcx<'gcx>, returns: &'gcx [Ty<'gcx>]) -> Option<Ty<'gcx>> {
668    Some(match returns {
669        [] => gcx.types.unit,
670        [ret] => *ret,
671        _ => gcx.mk_ty_tuple(returns),
672    })
673}
674
675fn base_item_source(hir: &Hir<'_>, expr: &Expr<'_>) -> solar::sema::hir::SourceId {
676    referenced_item(expr)
677        .map(|id| hir.item(id).source())
678        .unwrap_or_else(|| hir.sources_enumerated().next().expect("HIR has a source").0)
679}
680
681fn base_contract(hir: &Hir<'_>, expr: &Expr<'_>) -> Option<ContractId> {
682    referenced_item(expr).and_then(|id| hir.item(id).contract())
683}
684
685fn referenced_item(expr: &Expr<'_>) -> Option<ItemId> {
686    match &expr.peel_parens().kind {
687        ExprKind::Ident([Res::Item(id), ..]) => Some(*id),
688        _ => None,
689    }
690}
691
692fn variable_data_location(hir: &Hir<'_>, var_id: hir::VariableId) -> Option<DataLocation> {
693    let var = hir.variable(var_id);
694    var.data_location.or_else(|| {
695        (var.parent.is_none() && var.contract.is_some()).then_some(DataLocation::Storage)
696    })
697}
698
699fn unique<T>(mut iter: impl Iterator<Item = T>) -> Option<T> {
700    let first = iter.next()?;
701    iter.next().is_none().then_some(first)
702}