Skip to main content

forge_lint/sol/high/
unprotected_initializer.rs

1use super::UnprotectedInitializer;
2use crate::{
3    linter::{LateLintPass, LintContext},
4    sol::{Severity, SolLint},
5};
6use solar::{
7    ast::{ContractKind, DataLocation, FunctionKind, StateMutability, Visibility},
8    interface::{Symbol, kw, sym},
9    sema::{
10        Gcx,
11        builtins::Builtin,
12        hir::{self, ContractId, ExprKind, FunctionId, ItemId, Res, StmtKind, VariableId},
13    },
14};
15use std::collections::HashSet;
16
17declare_forge_lint!(
18    UNPROTECTED_INITIALIZER,
19    Severity::High,
20    "unprotected-initializer",
21    "upgradeable initializer is not protected against direct implementation calls"
22);
23
24impl<'hir> LateLintPass<'hir> for UnprotectedInitializer {
25    fn check_nested_contract(
26        &mut self,
27        ctx: &LintContext,
28        gcx: Gcx<'hir>,
29        hir: &'hir hir::Hir<'hir>,
30        contract_id: ContractId,
31    ) {
32        let contract = hir.contract(contract_id);
33        if !matches!(contract.kind, ContractKind::Contract) || contract.linearization_failed() {
34            return;
35        }
36
37        let upgradeable = contract
38            .linearized_bases
39            .iter()
40            .any(|&base_id| hir.contract(base_id).name.as_str() == "Initializable");
41        let runtime_entries = effective_runtime_dispatch_surface(hir, contract.linearized_bases);
42        if !upgradeable
43            && !runtime_entries.iter().any(|&fid| has_initializer_modifier(hir, hir.function(fid)))
44        {
45            return;
46        }
47
48        if initializers_disabled_in_constructor(hir, contract) {
49            return;
50        }
51
52        if !has_destructive_entrypoint(hir, contract, &runtime_entries) {
53            return;
54        }
55
56        for fid in runtime_entries {
57            let func = hir.function(fid);
58            if !is_public_initializer(hir, func) || has_modifier_named(hir, func, "onlyProxy") {
59                continue;
60            }
61
62            let Some(body) = func.body else { continue };
63            let mut analyzer = StateWriteAnalyzer {
64                gcx,
65                hir,
66                bases: contract.linearized_bases,
67                stack: Vec::new(),
68            };
69            if analyzer.block_writes_state(body) {
70                ctx.emit(&UNPROTECTED_INITIALIZER, func.name.map_or(func.span, |name| name.span));
71            }
72        }
73    }
74}
75
76fn is_public_initializer(hir: &hir::Hir<'_>, func: &hir::Function<'_>) -> bool {
77    func.kind.is_function()
78        && matches!(func.visibility, Visibility::Public | Visibility::External)
79        && !matches!(func.state_mutability, StateMutability::Pure | StateMutability::View)
80        && has_initializer_modifier(hir, func)
81}
82
83fn initializers_disabled_in_constructor<'hir>(
84    hir: &'hir hir::Hir<'hir>,
85    contract: &hir::Contract<'hir>,
86) -> bool {
87    contract.linearized_bases.iter().filter_map(|&cid| hir.contract(cid).ctor).any(|ctor_id| {
88        let ctor = hir.function(ctor_id);
89        function_calls_named(hir, ctor, contract.linearized_bases, "_disableInitializers")
90    })
91}
92
93fn has_destructive_entrypoint<'hir>(
94    hir: &'hir hir::Hir<'hir>,
95    contract: &hir::Contract<'hir>,
96    runtime_entries: &[FunctionId],
97) -> bool {
98    runtime_entries.iter().copied().any(|fid| {
99        let func = hir.function(fid);
100        if has_modifier_named(hir, func, "onlyProxy") {
101            return false;
102        }
103
104        let Some(body) = func.body else { return false };
105        let mut finder =
106            DestructiveSinkFinder { hir, bases: contract.linearized_bases, stack: vec![fid] };
107        finder.block_has_destructive_sink(body)
108    })
109}
110
111fn effective_runtime_dispatch_surface(hir: &hir::Hir<'_>, bases: &[ContractId]) -> Vec<FunctionId> {
112    let mut seen_functions: HashSet<(Symbol, String)> = HashSet::new();
113    let mut seen_fallback = false;
114    let mut seen_receive = false;
115    let mut entries = Vec::new();
116
117    for &cid in bases {
118        for fid in hir.contract(cid).all_functions() {
119            let func = hir.function(fid);
120            match func.kind {
121                FunctionKind::Function => {
122                    if !matches!(func.visibility, Visibility::Public | Visibility::External) {
123                        continue;
124                    }
125                    let Some(name) = func.name else { continue };
126                    if seen_functions.insert((name.name, parameter_signature(hir, func.parameters)))
127                    {
128                        entries.push(fid);
129                    }
130                }
131                FunctionKind::Fallback => {
132                    if !seen_fallback {
133                        seen_fallback = true;
134                        entries.push(fid);
135                    }
136                }
137                FunctionKind::Receive => {
138                    if !seen_receive {
139                        seen_receive = true;
140                        entries.push(fid);
141                    }
142                }
143                FunctionKind::Constructor | FunctionKind::Modifier => {}
144            }
145        }
146    }
147
148    entries
149}
150
151fn parameter_signature(hir: &hir::Hir<'_>, params: &[VariableId]) -> String {
152    params
153        .iter()
154        .map(|&param| format!("{:?}", hir.variable(param).ty.kind))
155        .collect::<Vec<_>>()
156        .join(",")
157}
158
159struct DestructiveSinkFinder<'hir> {
160    hir: &'hir hir::Hir<'hir>,
161    bases: &'hir [ContractId],
162    stack: Vec<FunctionId>,
163}
164
165impl<'hir> DestructiveSinkFinder<'hir> {
166    fn block_has_destructive_sink(&mut self, block: hir::Block<'hir>) -> bool {
167        block.stmts.iter().any(|stmt| self.stmt_has_destructive_sink(stmt))
168    }
169
170    fn stmt_has_destructive_sink(&mut self, stmt: &'hir hir::Stmt<'hir>) -> bool {
171        match &stmt.kind {
172            StmtKind::DeclSingle(var_id) => self
173                .hir
174                .variable(*var_id)
175                .initializer
176                .is_some_and(|init| self.expr_has_destructive_sink(init)),
177            StmtKind::DeclMulti(_, expr)
178            | StmtKind::Emit(expr)
179            | StmtKind::Revert(expr)
180            | StmtKind::Return(Some(expr))
181            | StmtKind::Expr(expr) => self.expr_has_destructive_sink(expr),
182            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
183                self.block_has_destructive_sink(*block)
184            }
185            StmtKind::If(condition, then_stmt, else_stmt) => {
186                self.expr_has_destructive_sink(condition)
187                    || self.stmt_has_destructive_sink(then_stmt)
188                    || else_stmt.is_some_and(|stmt| self.stmt_has_destructive_sink(stmt))
189            }
190            StmtKind::Try(stmt_try) => {
191                self.expr_has_destructive_sink(&stmt_try.expr)
192                    || stmt_try
193                        .clauses
194                        .iter()
195                        .any(|clause| self.block_has_destructive_sink(clause.block))
196            }
197            StmtKind::Return(None)
198            | StmtKind::Break
199            | StmtKind::Continue
200            | StmtKind::Placeholder
201            | StmtKind::AssemblyBlock(_)
202            | StmtKind::Switch(_)
203            | StmtKind::Err(_) => false,
204        }
205    }
206
207    fn expr_has_destructive_sink(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
208        match &expr.kind {
209            ExprKind::Call(callee, args, opts) => {
210                if is_destructive_call(callee) {
211                    return true;
212                }
213
214                if self.expr_has_destructive_sink(callee)
215                    || opts.is_some_and(|opts| {
216                        opts.args.iter().any(|opt| self.expr_has_destructive_sink(&opt.value))
217                    })
218                    || args.exprs().any(|arg| self.expr_has_destructive_sink(arg))
219                {
220                    return true;
221                }
222
223                resolved_internal_function_ids(self.hir, callee, self.bases)
224                    .into_iter()
225                    .any(|func_id| self.function_has_destructive_sink(func_id))
226            }
227            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
228                self.expr_has_destructive_sink(lhs) || self.expr_has_destructive_sink(rhs)
229            }
230            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
231                self.expr_has_destructive_sink(inner)
232            }
233            ExprKind::Index(base, index) => {
234                self.expr_has_destructive_sink(base)
235                    || index.is_some_and(|index| self.expr_has_destructive_sink(index))
236            }
237            ExprKind::Slice(base, start, end) => {
238                self.expr_has_destructive_sink(base)
239                    || start.is_some_and(|start| self.expr_has_destructive_sink(start))
240                    || end.is_some_and(|end| self.expr_has_destructive_sink(end))
241            }
242            ExprKind::Member(base, _) => self.expr_has_destructive_sink(base),
243            ExprKind::Ternary(condition, if_true, if_false) => {
244                self.expr_has_destructive_sink(condition)
245                    || self.expr_has_destructive_sink(if_true)
246                    || self.expr_has_destructive_sink(if_false)
247            }
248            ExprKind::Array(exprs) => exprs.iter().any(|expr| self.expr_has_destructive_sink(expr)),
249            ExprKind::Tuple(exprs) => {
250                exprs.iter().flatten().any(|expr| self.expr_has_destructive_sink(expr))
251            }
252            ExprKind::Lit(_)
253            | ExprKind::Ident(_)
254            | ExprKind::New(_)
255            | ExprKind::TypeCall(_)
256            | ExprKind::Type(_)
257            | ExprKind::YulMember(..)
258            | ExprKind::Err(_) => false,
259        }
260    }
261
262    fn function_has_destructive_sink(&mut self, func_id: FunctionId) -> bool {
263        if self.stack.contains(&func_id) {
264            return false;
265        }
266
267        let func = self.hir.function(func_id);
268        let Some(body) = func.body else { return false };
269        self.stack.push(func_id);
270        let found = self.block_has_destructive_sink(body);
271        self.stack.pop();
272        found
273    }
274}
275
276fn is_destructive_call(callee: &hir::Expr<'_>) -> bool {
277    match &callee.peel_parens().kind {
278        ExprKind::Member(_, member) => matches!(member.name, kw::Delegatecall | kw::Callcode),
279        ExprKind::Ident(resolutions) => {
280            resolutions.iter().any(|res| matches!(res, Res::Builtin(Builtin::Selfdestruct)))
281        }
282        _ => false,
283    }
284}
285
286fn has_initializer_modifier(hir: &hir::Hir<'_>, func: &hir::Function<'_>) -> bool {
287    has_modifier_named(hir, func, "initializer") || has_modifier_named(hir, func, "reinitializer")
288}
289
290fn has_modifier_named(hir: &hir::Hir<'_>, func: &hir::Function<'_>, name: &str) -> bool {
291    func.modifiers.iter().any(|modifier| modifier_name_is(hir, modifier, name))
292}
293
294fn modifier_name_is(hir: &hir::Hir<'_>, modifier: &hir::Modifier<'_>, name: &str) -> bool {
295    match modifier.id {
296        ItemId::Function(fid) => hir.function(fid).name.is_some_and(|ident| ident.as_str() == name),
297        ItemId::Contract(cid) => hir.contract(cid).name.as_str() == name,
298        _ => false,
299    }
300}
301
302fn function_calls_named<'hir>(
303    hir: &'hir hir::Hir<'hir>,
304    func: &hir::Function<'hir>,
305    bases: &'hir [ContractId],
306    name: &str,
307) -> bool {
308    let Some(body) = func.body else { return false };
309    let mut finder = CallNameFinder { hir, name, bases, stack: vec![] };
310    finder.block_calls_named(body)
311}
312
313struct CallNameFinder<'a, 'hir> {
314    hir: &'hir hir::Hir<'hir>,
315    name: &'a str,
316    bases: &'hir [ContractId],
317    stack: Vec<FunctionId>,
318}
319
320impl<'hir> CallNameFinder<'_, 'hir> {
321    fn block_calls_named(&mut self, block: hir::Block<'hir>) -> bool {
322        block.stmts.iter().any(|stmt| self.stmt_calls_named(stmt))
323    }
324
325    fn stmt_calls_named(&mut self, stmt: &'hir hir::Stmt<'hir>) -> bool {
326        match &stmt.kind {
327            StmtKind::DeclSingle(var_id) => self
328                .hir
329                .variable(*var_id)
330                .initializer
331                .is_some_and(|init| self.expr_calls_named(init)),
332            StmtKind::DeclMulti(_, expr)
333            | StmtKind::Emit(expr)
334            | StmtKind::Revert(expr)
335            | StmtKind::Return(Some(expr))
336            | StmtKind::Expr(expr) => self.expr_calls_named(expr),
337            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
338                self.block_calls_named(*block)
339            }
340            StmtKind::If(condition, then_stmt, else_stmt) => {
341                self.expr_calls_named(condition)
342                    || self.stmt_calls_named(then_stmt)
343                    || else_stmt.is_some_and(|stmt| self.stmt_calls_named(stmt))
344            }
345            StmtKind::Try(stmt_try) => {
346                self.expr_calls_named(&stmt_try.expr)
347                    || stmt_try.clauses.iter().any(|clause| self.block_calls_named(clause.block))
348            }
349            StmtKind::Return(None)
350            | StmtKind::Break
351            | StmtKind::Continue
352            | StmtKind::Placeholder
353            | StmtKind::AssemblyBlock(_)
354            | StmtKind::Switch(_)
355            | StmtKind::Err(_) => false,
356        }
357    }
358
359    fn expr_calls_named(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
360        match &expr.kind {
361            ExprKind::Call(callee, args, opts) => {
362                let called_functions = resolved_internal_function_ids(self.hir, callee, self.bases);
363                if called_functions
364                    .iter()
365                    .copied()
366                    .any(|func_id| self.function_matches_name(func_id))
367                {
368                    return true;
369                }
370
371                if let Some(opts) = opts
372                    && opts.args.iter().any(|opt| self.expr_calls_named(&opt.value))
373                {
374                    return true;
375                }
376
377                if args.exprs().any(|arg| self.expr_calls_named(arg)) {
378                    return true;
379                }
380
381                for func_id in called_functions {
382                    if self.function_belongs_to_bases(func_id) && self.function_calls_named(func_id)
383                    {
384                        return true;
385                    }
386                }
387
388                false
389            }
390            ExprKind::Assign(lhs, _, rhs) | ExprKind::Binary(lhs, _, rhs) => {
391                self.expr_calls_named(lhs) || self.expr_calls_named(rhs)
392            }
393            ExprKind::Unary(_, inner) | ExprKind::Delete(inner) | ExprKind::Payable(inner) => {
394                self.expr_calls_named(inner)
395            }
396            ExprKind::Index(base, index) => {
397                self.expr_calls_named(base)
398                    || index.is_some_and(|index| self.expr_calls_named(index))
399            }
400            ExprKind::Slice(base, start, end) => {
401                self.expr_calls_named(base)
402                    || start.is_some_and(|start| self.expr_calls_named(start))
403                    || end.is_some_and(|end| self.expr_calls_named(end))
404            }
405            ExprKind::Member(base, _) => self.expr_calls_named(base),
406            ExprKind::Ternary(condition, if_true, if_false) => {
407                self.expr_calls_named(condition)
408                    || self.expr_calls_named(if_true)
409                    || self.expr_calls_named(if_false)
410            }
411            ExprKind::Array(exprs) => exprs.iter().any(|expr| self.expr_calls_named(expr)),
412            ExprKind::Tuple(exprs) => {
413                exprs.iter().flatten().any(|expr| self.expr_calls_named(expr))
414            }
415            ExprKind::Lit(_)
416            | ExprKind::Ident(_)
417            | ExprKind::New(_)
418            | ExprKind::TypeCall(_)
419            | ExprKind::Type(_)
420            | ExprKind::YulMember(..)
421            | ExprKind::Err(_) => false,
422        }
423    }
424
425    fn function_calls_named(&mut self, func_id: FunctionId) -> bool {
426        if self.stack.contains(&func_id) {
427            return false;
428        }
429
430        let func = self.hir.function(func_id);
431        let Some(body) = func.body else { return false };
432        self.stack.push(func_id);
433        let found = self.block_calls_named(body);
434        self.stack.pop();
435        found
436    }
437
438    fn function_matches_name(&self, func_id: FunctionId) -> bool {
439        self.function_belongs_to_bases(func_id)
440            && self.hir.function(func_id).name.is_some_and(|ident| ident.as_str() == self.name)
441    }
442
443    fn function_belongs_to_bases(&self, func_id: FunctionId) -> bool {
444        self.hir
445            .function(func_id)
446            .contract
447            .is_some_and(|contract_id| self.bases.contains(&contract_id))
448    }
449}
450
451struct StateWriteAnalyzer<'hir> {
452    gcx: Gcx<'hir>,
453    hir: &'hir hir::Hir<'hir>,
454    bases: &'hir [ContractId],
455    stack: Vec<FunctionId>,
456}
457
458impl<'hir> StateWriteAnalyzer<'hir> {
459    fn block_writes_state(&mut self, block: hir::Block<'hir>) -> bool {
460        block.stmts.iter().any(|stmt| self.stmt_writes_state(stmt))
461    }
462
463    fn stmt_writes_state(&mut self, stmt: &'hir hir::Stmt<'hir>) -> bool {
464        match &stmt.kind {
465            StmtKind::DeclSingle(var_id) => self
466                .hir
467                .variable(*var_id)
468                .initializer
469                .is_some_and(|init| self.expr_writes_state(init)),
470            StmtKind::DeclMulti(_, expr)
471            | StmtKind::Emit(expr)
472            | StmtKind::Revert(expr)
473            | StmtKind::Return(Some(expr))
474            | StmtKind::Expr(expr) => self.expr_writes_state(expr),
475            StmtKind::Block(block) | StmtKind::UncheckedBlock(block) | StmtKind::Loop(block, _) => {
476                self.block_writes_state(*block)
477            }
478            StmtKind::If(condition, then_stmt, else_stmt) => {
479                self.expr_writes_state(condition)
480                    || self.stmt_writes_state(then_stmt)
481                    || else_stmt.is_some_and(|stmt| self.stmt_writes_state(stmt))
482            }
483            StmtKind::Try(stmt_try) => {
484                self.expr_writes_state(&stmt_try.expr)
485                    || stmt_try.clauses.iter().any(|clause| self.block_writes_state(clause.block))
486            }
487            StmtKind::Return(None)
488            | StmtKind::Break
489            | StmtKind::Continue
490            | StmtKind::Placeholder
491            | StmtKind::AssemblyBlock(_)
492            | StmtKind::Switch(_)
493            | StmtKind::Err(_) => false,
494        }
495    }
496
497    fn expr_writes_state(&mut self, expr: &'hir hir::Expr<'hir>) -> bool {
498        match &expr.kind {
499            ExprKind::Assign(lhs, _, rhs) => {
500                lhs_writes_state(self.gcx, self.hir, lhs)
501                    || self.expr_writes_state(lhs)
502                    || self.expr_writes_state(rhs)
503            }
504            ExprKind::Delete(inner) => {
505                lhs_writes_state(self.gcx, self.hir, inner) || self.expr_writes_state(inner)
506            }
507            ExprKind::Unary(op, inner) => {
508                (op.kind.has_side_effects() && lhs_writes_state(self.gcx, self.hir, inner))
509                    || self.expr_writes_state(inner)
510            }
511            ExprKind::Call(callee, args, opts) => {
512                if member_call_writes_state(self.gcx, self.hir, callee) {
513                    return true;
514                }
515
516                if self.expr_writes_state(callee)
517                    || opts.is_some_and(|opts| {
518                        opts.args.iter().any(|opt| self.expr_writes_state(&opt.value))
519                    })
520                    || args.exprs().any(|arg| self.expr_writes_state(arg))
521                {
522                    return true;
523                }
524
525                resolved_internal_function_ids(self.hir, callee, self.bases)
526                    .into_iter()
527                    .any(|func_id| self.function_writes_state(func_id))
528            }
529            ExprKind::Binary(lhs, _, rhs) => {
530                self.expr_writes_state(lhs) || self.expr_writes_state(rhs)
531            }
532            ExprKind::Index(base, index) => {
533                self.expr_writes_state(base)
534                    || index.is_some_and(|index| self.expr_writes_state(index))
535            }
536            ExprKind::Slice(base, start, end) => {
537                self.expr_writes_state(base)
538                    || start.is_some_and(|start| self.expr_writes_state(start))
539                    || end.is_some_and(|end| self.expr_writes_state(end))
540            }
541            ExprKind::Member(base, _) | ExprKind::Payable(base) => self.expr_writes_state(base),
542            ExprKind::Ternary(condition, if_true, if_false) => {
543                self.expr_writes_state(condition)
544                    || self.expr_writes_state(if_true)
545                    || self.expr_writes_state(if_false)
546            }
547            ExprKind::Array(exprs) => exprs.iter().any(|expr| self.expr_writes_state(expr)),
548            ExprKind::Tuple(exprs) => {
549                exprs.iter().flatten().any(|expr| self.expr_writes_state(expr))
550            }
551            ExprKind::Lit(_)
552            | ExprKind::Ident(_)
553            | ExprKind::New(_)
554            | ExprKind::TypeCall(_)
555            | ExprKind::Type(_)
556            | ExprKind::YulMember(..)
557            | ExprKind::Err(_) => false,
558        }
559    }
560
561    fn function_writes_state(&mut self, func_id: FunctionId) -> bool {
562        if self.stack.contains(&func_id) {
563            return false;
564        }
565
566        let func = self.hir.function(func_id);
567        let Some(body) = func.body else { return false };
568        self.stack.push(func_id);
569        let writes = self.block_writes_state(body);
570        self.stack.pop();
571        writes
572    }
573}
574
575fn member_call_writes_state(gcx: Gcx<'_>, hir: &hir::Hir<'_>, callee: &hir::Expr<'_>) -> bool {
576    let ExprKind::Member(base, member) = &callee.peel_parens().kind else { return false };
577    matches!(member.as_str(), "push" | "pop") && lhs_writes_state(gcx, hir, base)
578}
579
580fn lhs_writes_state(gcx: Gcx<'_>, hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
581    match &expr.peel_parens().kind {
582        ExprKind::Ident(resolutions) => {
583            resolutions.iter().any(|res| matches!(res, Res::Item(ItemId::Variable(var_id)) if hir.variable(*var_id).kind.is_state()))
584        }
585        ExprKind::Index(base, _) | ExprKind::Slice(base, _, _) | ExprKind::Member(base, _) => {
586            expr_references_storage(gcx, hir, base)
587        }
588        ExprKind::Call(..) => expr_references_storage(gcx, hir, expr),
589        ExprKind::Tuple(exprs) => {
590            exprs.iter().flatten().any(|expr| lhs_writes_state(gcx, hir, expr))
591        }
592        _ => false,
593    }
594}
595
596fn expr_references_storage(gcx: Gcx<'_>, hir: &hir::Hir<'_>, expr: &hir::Expr<'_>) -> bool {
597    match &expr.peel_parens().kind {
598        ExprKind::Ident(resolutions) => resolutions.iter().any(|res| {
599            matches!(res, Res::Item(ItemId::Variable(var_id)) if variable_references_storage(hir.variable(*var_id)))
600        }),
601        ExprKind::Index(base, _) | ExprKind::Slice(base, _, _) | ExprKind::Member(base, _) => {
602            expr_references_storage(gcx, hir, base)
603        }
604        ExprKind::Call(..) | ExprKind::Ternary(..) => gcx
605            .type_of_expr(expr.peel_parens().id)
606            .is_some_and(|ty| ty.loc() == Some(DataLocation::Storage)),
607        _ => false,
608    }
609}
610
611fn variable_references_storage(var: &hir::Variable<'_>) -> bool {
612    var.kind.is_state() || var.data_location == Some(DataLocation::Storage)
613}
614
615fn resolved_internal_function_ids(
616    hir: &hir::Hir<'_>,
617    callee: &hir::Expr<'_>,
618    bases: &[ContractId],
619) -> Vec<FunctionId> {
620    match &callee.peel_parens().kind {
621        ExprKind::Ident(resolutions) => resolutions
622            .iter()
623            .filter_map(|res| match res {
624                Res::Item(ItemId::Function(func_id)) => Some(*func_id),
625                _ => None,
626            })
627            .collect(),
628        ExprKind::Member(base, method) => {
629            let ExprKind::Ident(resolutions) = &base.peel_parens().kind else { return vec![] };
630            let is_super = resolutions
631                .iter()
632                .any(|res| matches!(res, Res::Builtin(builtin) if builtin.name() == sym::super_));
633
634            let contracts: Vec<_> = if is_super {
635                bases.get(1..).unwrap_or_default().to_vec()
636            } else {
637                resolutions
638                    .iter()
639                    .filter_map(|res| match res {
640                        Res::Item(ItemId::Contract(cid)) => Some(*cid),
641                        _ => None,
642                    })
643                    .collect()
644            };
645
646            contracts
647                .into_iter()
648                .flat_map(|cid| hir.contract(cid).all_functions())
649                .filter(|&fid| {
650                    hir.function(fid).name.is_some_and(|name| name.as_str() == method.as_str())
651                })
652                .collect()
653        }
654        _ => vec![],
655    }
656}