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