Skip to main content

brokk_bifrost_cpp/graph/
inverted.rs

1//! Whole-workspace inverted edge builder for C++.
2//!
3//! Walks each file once and resolves every reference to the callee fqn it names,
4//! via the shared `build_edges` driver in `brokk-bifrost-analysis`. C++ node fqns
5//! are dotted: a namespace +
6//! class + member reads `example.Service.run`, a free function `example.freeHelper`,
7//! and a class `example.Service`. References resolve through the forward scanner's
8//! visibility primitives ([`VisibilityIndex::resolve_type`] / `resolve_named`,
9//! which honor the include closure and namespaces) plus a [`LocalInferenceEngine`]
10//! (typed by [`CodeUnit`], like the forward scan) seeded with every local's and
11//! parameter's declared type so a method call's receiver can be typed:
12//!
13//! - a type reference (`Foo x`, `new Foo()`, a base class) resolves to the class;
14//! - `recv.m(..)` / `recv->m(..)` (`field_expression` under a call) types `recv`
15//!   and gives `Owner.m`;
16//! - `X::m(..)` (`qualified_identifier`) resolves `X` and gives `Owner.m`;
17//! - a bare `m(..)` is a free function (`Namespace.m`); `this->m(..)` and other
18//!   unqualified member calls attribute to the enclosing class;
19//! - a chained receiver (`p->get()->m()`) follows the uniquely resolved persisted
20//!   callable return type before recording `Owner.m`.
21//!
22//! The enclosing class is taken from a per-file class-range index (the analyzer's
23//! own fqns), so `this->`/unqualified calls attribute to the right class without
24//! re-deriving the namespace. Ambiguous receiver or return identities fail closed.
25
26use crate::declarations::{
27    CppSentinelRecoveredClass, cpp_sentinel_recovered_classes,
28    cpp_sentinel_recovered_scope_for_node, node_text, recovered_macro_return_type_node,
29};
30use crate::graph::CppGraphSource;
31use crate::graph::extractor::{
32    BareCallTargetResolution, LexicalScopeResolution, enclosing_lexical_scope_components,
33    has_persisted_global_field_identity, initialized_ordinary_type_imports,
34    ordinary_using_declaration_type_node, resolve_bare_call_target,
35    resolve_ordinary_using_declaration_owner, resolve_type_components_lexically_at,
36    resolve_type_node_lexically, resolve_using_enum_declaration_owner,
37    using_enum_declaration_type_node,
38};
39use crate::graph::resolver::field_declared_binding;
40use crate::graph::resolver::{
41    CppTemplateResolutionError, DesignatedInitializerOwner, EnclosingMemberOwnerResolution,
42    LexicalCallableValueResolution, LexicalTypeResolution, OrdinaryMacroReferenceResolution,
43    OrdinaryTypeImportCell, TargetKind, VisibilityIndex, VisibleMemberResolution,
44    anonymous_aggregate_owner, c_offsetof_member_parts, canonical_cpp_scope_components,
45    cast_expression_type_node, constructor_style_local_declaration, cpp_callable_arity,
46    cpp_template_reference_arguments, cpp_type_name_components, declarator_name_node,
47    designated_initializer_owner, extract_variable_name, first_type_child, function_terminal_node,
48    has_ancestor_kind, infer_cpp_initializer_binding, infer_cpp_initializer_type,
49    initialized_type_declaration_with_cast, is_c_offsetof_member_node,
50    is_c_sizeof_expression_type_candidate, is_cpp_template_argument_type_leaf, is_declaration_name,
51    is_declarator_node, is_globally_qualified_cpp_name, is_nested_type_node,
52    is_recovered_qualified_friend_class_type_reference, normalize_type_text,
53    out_of_line_member_definition_owner, parameter_belongs_to_callable_scope,
54    qualified_alias_reference_preserves_target, qualified_alias_reference_requires_terminal,
55    qualified_owner_components, recovered_c_new_expression_argument_at,
56    recovered_macro_decorated_type_node, recovered_relational_template_member_call,
57    resolve_declaring_callable_owner, resolve_declaring_member_owner, same_logical_symbol,
58    same_visible_global_field_symbol, same_visible_symbol, type_reference_hit_node,
59    unique_macro_replacement_type_candidate,
60};
61use crate::graph::syntax::{object_macro_replacement_type_references, qualified_callable_value};
62use brokk_bifrost_core::analyzer::tree_walk::{TreeWalkAction, walk_tree_iterative};
63use brokk_bifrost_core::analyzer::usages::common::same_node;
64use brokk_bifrost_core::analyzer::usages::inverted_edges::{
65    ClassRangeIndex, FileEdgeScanInput, PerFileEdges, classify_reference_node, first_precise,
66};
67use brokk_bifrost_core::analyzer::usages::local_inference::{
68    LocalInferenceConfig, LocalInferenceEngine,
69};
70use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile};
71use brokk_bifrost_core::hash::{HashMap, HashSet};
72use tree_sitter::Node;
73
74/// The C++ half of one file's inverted pass: seed the scan context from the
75/// already-parsed tree and walk it, recording every `caller -> callee` edge the
76/// file names.
77///
78/// The pass's fan-out -- `build_edge_output` plus `parse_and_collect`, the
79/// shared language-agnostic driver -- stays in `brokk-bifrost-analysis` and
80/// calls this once per kept file.
81pub fn scan_file(
82    analyzer: &CppGraphSource<'_>,
83    visibility: &VisibilityIndex<'_>,
84    file: &ProjectFile,
85    input: &FileEdgeScanInput<'_>,
86) -> PerFileEdges {
87    let ordinary_type_imports =
88        initialized_ordinary_type_imports(input.root(), analyzer, visibility, file, input.source);
89    let recovered_sentinel_classes = cpp_sentinel_recovered_classes(input.root(), input.source);
90    let mut ctx = CppScan {
91        analyzer: *analyzer,
92        visibility,
93        file,
94        source: input.source,
95        ordinary_type_imports,
96        recovered_sentinel_classes,
97        class_ranges: ClassRangeIndex::build(analyzer.index, file),
98        declaring_member_cache: HashMap::default(),
99        input,
100        edges: PerFileEdges::default(),
101    };
102    let mut bindings = LocalInferenceEngine::new(LocalInferenceConfig::default());
103    walk(input.root(), &mut ctx, &mut bindings);
104    ctx.edges
105}
106
107struct CppScan<'a> {
108    analyzer: CppGraphSource<'a>,
109    visibility: &'a VisibilityIndex<'a>,
110    file: &'a ProjectFile,
111    source: &'a str,
112    ordinary_type_imports: OrdinaryTypeImportCell,
113    recovered_sentinel_classes: Vec<CppSentinelRecoveredClass>,
114    class_ranges: ClassRangeIndex,
115    declaring_member_cache: HashMap<CodeUnit, HashMap<String, EnclosingMemberOwnerResolution>>,
116    input: &'a FileEdgeScanInput<'a>,
117    edges: PerFileEdges,
118}
119
120impl CppScan<'_> {
121    /// Resolve a type reference's text to a class `CodeUnit`.
122    fn resolve_type(&self, text: &str) -> Option<CodeUnit> {
123        self.visibility.resolve_type(self.file, text)
124    }
125
126    fn resolve_type_node_result(
127        &self,
128        node: Node<'_>,
129    ) -> std::result::Result<Option<CodeUnit>, CppTemplateResolutionError> {
130        self.visibility
131            .resolve_type_node_result(self.file, node, self.source)
132    }
133
134    /// The fqn of the smallest class declaration containing `byte`.
135    fn enclosing_class(&self, byte: usize) -> Option<&str> {
136        self.class_ranges.enclosing(byte)
137    }
138
139    /// Return the smallest recovered sentinel owner scope containing `node`.
140    /// Out-of-line definitions can sit beyond the recovered class range, so
141    /// prefer an owner span over the class itself.  For references in a class
142    /// body, append any parser-visible nested class names to the recovered
143    /// top-level class path; the declaration visitor uses the same AST nesting
144    /// when it re-owns those members.
145    fn recovered_sentinel_scope(&self, node: Node<'_>) -> Option<Vec<String>> {
146        cpp_sentinel_recovered_scope_for_node(node, self.source, &self.recovered_sentinel_classes)
147    }
148
149    fn record(&mut self, callee: String, node: Node<'_>) {
150        self.edges.record_kind(
151            self.input,
152            callee,
153            classify_reference_node(node),
154            node.start_byte(),
155            node.end_byte(),
156        );
157    }
158
159    fn record_range(&mut self, callee: String, anchor: Node<'_>, start: usize, end: usize) {
160        self.edges.record_kind(
161            self.input,
162            callee,
163            classify_reference_node(anchor),
164            start,
165            end,
166        );
167    }
168
169    fn record_unproven(&mut self, name: &str, node: Node<'_>) {
170        self.edges
171            .record_unproven_name(self.input, name, node.start_byte(), node.end_byte());
172    }
173}
174
175const SCOPE_NODES: &[&str] = &[
176    "compound_statement",
177    "field_declaration_list",
178    "function_definition",
179    "for_range_loop",
180    "lambda_expression",
181    "for_statement",
182    "while_statement",
183    "if_statement",
184];
185
186fn walk(node: Node<'_>, ctx: &mut CppScan<'_>, bindings: &mut LocalInferenceEngine<CodeUnit>) {
187    let mut state = (ctx, bindings);
188    walk_tree_iterative(
189        node,
190        &mut state,
191        |node, (ctx, bindings)| {
192            if walk_enter(node, ctx, bindings) {
193                TreeWalkAction::DescendWithExit
194            } else {
195                TreeWalkAction::Descend
196            }
197        },
198        |(_, bindings)| bindings.exit_scope(),
199    );
200}
201
202fn walk_enter(
203    node: Node<'_>,
204    ctx: &mut CppScan<'_>,
205    bindings: &mut LocalInferenceEngine<CodeUnit>,
206) -> bool {
207    let enters_scope = SCOPE_NODES.contains(&node.kind());
208    if enters_scope {
209        bindings.enter_scope();
210    }
211    seed_declaration(node, ctx, bindings);
212    record_reference(node, ctx, bindings);
213    enters_scope
214}
215
216fn record_reference(
217    node: Node<'_>,
218    ctx: &mut CppScan<'_>,
219    bindings: &LocalInferenceEngine<CodeUnit>,
220) {
221    if recovered_c_new_expression_argument_at(
222        node,
223        node.start_byte(),
224        node.end_byte(),
225        ctx.analyzer.reference_uses_c_semantics(ctx.file),
226    )
227    .is_some()
228    {
229        return;
230    }
231    if node.kind() == "preproc_arg" {
232        record_object_macro_replacement_type_references(node, ctx);
233    }
234    if node
235        .parent()
236        .is_some_and(|parent| parent.kind() == "destructor_name")
237    {
238        return;
239    }
240    match ctx
241        .visibility
242        .resolve_ordinary_macro_reference(&ctx.analyzer, ctx.file, node, ctx.source)
243    {
244        OrdinaryMacroReferenceResolution::Resolved(macro_unit) => {
245            ctx.record(macro_unit.fq_name(), node);
246            return;
247        }
248        OrdinaryMacroReferenceResolution::Ambiguous => {
249            ctx.record_unproven(node_text(node, ctx.source), node);
250            return;
251        }
252        OrdinaryMacroReferenceResolution::Missing => {}
253    }
254    if let Some(return_type) = recovered_macro_return_type_node(node, ctx.source) {
255        record_recovered_macro_return_type_reference(return_type, ctx);
256        return;
257    }
258    if let Some(call) = recovered_relational_template_member_call(node) {
259        record_recovered_relational_template_member_call(call, ctx, bindings);
260        return;
261    }
262    if node.kind() == "using_declaration" {
263        let (resolution, type_node) =
264            if let Some(type_node) = using_enum_declaration_type_node(node) {
265                (
266                    resolve_using_enum_declaration_owner(
267                        node,
268                        &ctx.analyzer,
269                        ctx.visibility,
270                        &ctx.ordinary_type_imports,
271                        ctx.file,
272                        ctx.source,
273                    ),
274                    type_node,
275                )
276            } else if let Some(type_node) = ordinary_using_declaration_type_node(node) {
277                (
278                    resolve_ordinary_using_declaration_owner(
279                        node,
280                        &ctx.analyzer,
281                        ctx.visibility,
282                        ctx.file,
283                        ctx.source,
284                    ),
285                    type_node,
286                )
287            } else {
288                return;
289            };
290        match resolution {
291            LexicalTypeResolution::Resolved { unit, .. } => ctx.record(unit.fq_name(), type_node),
292            LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => {
293                ctx.record_unproven(node_text(type_node, ctx.source), type_node);
294            }
295        }
296        return;
297    }
298    if has_ancestor_kind(node, "using_declaration") {
299        return;
300    }
301    if is_c_offsetof_member_node(node) {
302        record_c_offsetof_field_reference(node, ctx);
303        return;
304    }
305    if ctx.analyzer.reference_uses_c_semantics(ctx.file)
306        && node.kind() == "field_expression"
307        && !node.parent().is_some_and(|parent| {
308            parent.kind() == "call_expression"
309                && parent.child_by_field_name("function") == Some(node)
310        })
311    {
312        record_c_field_reference(node, ctx, bindings);
313        return;
314    }
315    if is_c_sizeof_expression_type_candidate(ctx.file, node) {
316        let name = node_text(node, ctx.source);
317        if bindings.is_shadowed(name) {
318            return;
319        }
320        match ctx.resolve_type_node_result(node) {
321            Ok(Some(unit)) => ctx.record(unit.fq_name(), node),
322            Ok(None) | Err(_) => ctx.record_unproven(name, node),
323        }
324        return;
325    }
326    if matches!(node.kind(), "qualified_identifier" | "scoped_identifier")
327        && is_declaration_name(node)
328        && !is_recovered_qualified_friend_class_type_reference(node, ctx.source)
329        && let Some(owners) = out_of_line_member_definition_owner(
330            &ctx.analyzer,
331            ctx.visibility,
332            ctx.file,
333            ctx.source,
334            node,
335        )
336    {
337        for (owner_node, owner) in owners.owners {
338            ctx.record(owner.fq_name(), owner_node);
339        }
340        return;
341    }
342    if let Some(value) = qualified_callable_value(node) {
343        record_qualified_callable_value(
344            value.qualified,
345            value.global,
346            &value.owner_components,
347            value.member,
348            ctx,
349        );
350        return;
351    }
352    if matches!(node.kind(), "identifier" | "field_identifier")
353        && let Some(designator_owner) =
354            designated_initializer_owner(ctx.visibility, ctx.file, ctx.source, node)
355    {
356        let name = node_text(node, ctx.source);
357        match designator_owner {
358            DesignatedInitializerOwner::Resolved(owner) => {
359                if let Some(field) = ctx
360                    .visibility
361                    .visible_members_for_owner_name(ctx.file, &owner, name)
362                    .into_iter()
363                    .find(|unit| unit.is_field())
364                {
365                    ctx.record(field.fq_name(), node);
366                }
367            }
368            DesignatedInitializerOwner::Unresolved => ctx.record_unproven(name, node),
369        }
370        return;
371    }
372    // C field reads are value references, not callable edges.  The C++ graph
373    // deliberately omits ordinary data-member edges, but C census sites need
374    // the same structured field identity that the forward resolver exposes.
375    // Let `record_call` own a field expression used as a call callee.
376    if ctx.analyzer.reference_uses_c_semantics(ctx.file)
377        && node.kind() == "field_expression"
378        && !node.parent().is_some_and(|parent| {
379            parent.kind() == "call_expression"
380                && parent.child_by_field_name("function") == Some(node)
381        })
382    {
383        record_c_field_reference(node, ctx, bindings);
384        return;
385    }
386    match node.kind() {
387        "namespace_identifier"
388            if let Some((type_node, _)) = recovered_macro_decorated_type_node(node) =>
389        {
390            record_recovered_macro_decorated_type_reference(node, type_node, ctx);
391        }
392        // A type reference (`Foo x`, base class, `new Foo()`'s type child) resolves
393        // to the class. `new Foo()` reaches its type via this case (its type child
394        // is itself one of these nodes), so there is no separate construction case.
395        "type_identifier" | "qualified_identifier" | "scoped_type_identifier" | "template_type" => {
396            if is_declaration_name(node)
397                && !is_recovered_qualified_friend_class_type_reference(node, ctx.source)
398            {
399                if let Some(owners) = out_of_line_member_definition_owner(
400                    &ctx.analyzer,
401                    ctx.visibility,
402                    ctx.file,
403                    ctx.source,
404                    node,
405                ) {
406                    for (owner_node, owner) in owners.owners {
407                        ctx.record(owner.fq_name(), owner_node);
408                    }
409                }
410                return;
411            }
412            if is_nested_type_node(node) && !is_cpp_template_argument_type_leaf(node) {
413                record_nested_type_terminal_reference(node, ctx);
414                return;
415            }
416            // A `X::m(..)` static/scoped call appears as a `qualified_identifier`
417            // function: resolve the `X` qualifier as a type and emit `Owner.m`.
418            if let Some(function) = scoped_free_function(node, ctx) {
419                ctx.record(function.fq_name(), function_terminal_node(node));
420                return;
421            }
422            if let Some(call) = node.parent().filter(|parent| {
423                parent.kind() == "call_expression"
424                    && parent.child_by_field_name("function") == Some(node)
425            }) && let LexicalTypeResolution::Resolved { unit, .. } = resolve_type_node_lexically(
426                node,
427                &ctx.analyzer,
428                ctx.visibility,
429                &ctx.ordinary_type_imports,
430                ctx.file,
431                ctx.source,
432            ) {
433                let Some(call_arity) = ctx
434                    .visibility
435                    .call_arity_evidence(ctx.file, call, ctx.source)
436                    .exact()
437                else {
438                    ctx.record_unproven(node_text(node, ctx.source), function_terminal_node(node));
439                    return;
440                };
441                if let VisibleMemberResolution::Callable(constructors) = ctx
442                    .visibility
443                    .visible_member_for_owner_name(ctx.file, &unit, unit.identifier())
444                    && let Some(constructor) = constructors.iter().find(|constructor| {
445                        cpp_callable_arity(&ctx.analyzer, constructor).accepts(call_arity)
446                    })
447                {
448                    ctx.record(constructor.fq_name(), node);
449                } else {
450                    ctx.record(unit.fq_name(), node);
451                }
452                return;
453            }
454            if let Some(owner) = scoped_call_owner(node, ctx) {
455                let member = scoped_call_member(node, ctx.source);
456                if !member.is_empty() {
457                    ctx.record(format!("{owner}.{member}"), function_terminal_node(node));
458                    return;
459                }
460            }
461            record_type_reference(node, ctx);
462        }
463        "call_expression" => record_call(node, ctx, bindings),
464        _ => {}
465    }
466}
467
468fn record_c_offsetof_field_reference(node: Node<'_>, ctx: &mut CppScan<'_>) {
469    let name = node_text(node, ctx.source);
470    let Some((type_reference, member)) = c_offsetof_member_parts(node) else {
471        ctx.record_unproven(name, node);
472        return;
473    };
474    let owner = match resolve_type_node_lexically(
475        type_reference,
476        &ctx.analyzer,
477        ctx.visibility,
478        &ctx.ordinary_type_imports,
479        ctx.file,
480        ctx.source,
481    ) {
482        LexicalTypeResolution::Resolved { unit, .. } if unit.is_class() => unit,
483        LexicalTypeResolution::Resolved { .. }
484        | LexicalTypeResolution::Ambiguous
485        | LexicalTypeResolution::Missing => {
486            ctx.record_unproven(name, member);
487            return;
488        }
489    };
490    let candidates = ctx
491        .visibility
492        .visible_members_for_owner_name(ctx.file, &owner, name)
493        .into_iter()
494        .filter(|candidate| candidate.is_field())
495        .collect::<Vec<_>>();
496    if candidates.len() == 1 {
497        ctx.record(candidates[0].fq_name(), member);
498    } else if candidates.len() > 1 {
499        ctx.record_unproven(name, member);
500    }
501}
502fn record_c_field_reference(
503    node: Node<'_>,
504    ctx: &mut CppScan<'_>,
505    bindings: &LocalInferenceEngine<CodeUnit>,
506) {
507    let Some(field) = node.child_by_field_name("field") else {
508        return;
509    };
510    let name = node_text(field, ctx.source);
511    if name.is_empty() {
512        return;
513    }
514    let Some(receiver) = node
515        .child_by_field_name("argument")
516        .or_else(|| node.child_by_field_name("object"))
517        .or_else(|| node.named_child(0))
518    else {
519        ctx.record_unproven(name, field);
520        return;
521    };
522    let Some(receiver_owner) = receiver_type_unit(receiver, ctx, bindings, 32) else {
523        ctx.record_unproven(name, field);
524        return;
525    };
526    let owner = match resolve_declaring_member_owner_cached(ctx, &receiver_owner, name) {
527        EnclosingMemberOwnerResolution::Owner(owner) => owner,
528        EnclosingMemberOwnerResolution::Ambiguous => {
529            ctx.record_unproven(name, field);
530            return;
531        }
532        EnclosingMemberOwnerResolution::Missing => return,
533    };
534    let fields = ctx
535        .visibility
536        .visible_members_for_owner_name(ctx.file, &owner, name)
537        .into_iter()
538        .filter(|candidate| {
539            candidate.is_field()
540                && ctx.visibility.declaration_visible_at_reference(
541                    &ctx.analyzer,
542                    ctx.file,
543                    candidate,
544                    field,
545                )
546        })
547        .collect::<Vec<_>>();
548    match fields.as_slice() {
549        [candidate] => ctx.record(candidate.fq_name(), field),
550        [] => {}
551        _ => {
552            if fields
553                .iter()
554                .skip(1)
555                .all(|candidate| same_visible_symbol(candidate, fields[0]))
556            {
557                ctx.record(fields[0].fq_name(), field);
558            } else {
559                ctx.record_unproven(name, field);
560            }
561        }
562    }
563}
564
565fn record_object_macro_replacement_type_references(node: Node<'_>, ctx: &mut CppScan<'_>) {
566    for reference in object_macro_replacement_type_references(node, ctx.source) {
567        for component_count in 1..=reference.components.len() {
568            let resolution = resolve_type_components_lexically_at(
569                node,
570                &reference.components[..component_count],
571                reference.global,
572                &ctx.analyzer,
573                ctx.visibility,
574                &ctx.ordinary_type_imports,
575                ctx.file,
576                ctx.source,
577            );
578            let unit = match resolution {
579                LexicalTypeResolution::Resolved { unit, .. } => unit,
580                LexicalTypeResolution::Missing => {
581                    let Some(unit) = unique_macro_replacement_type_candidate(
582                        &ctx.analyzer,
583                        ctx.visibility,
584                        ctx.file,
585                        &reference.components[..component_count],
586                    ) else {
587                        continue;
588                    };
589                    unit
590                }
591                LexicalTypeResolution::Ambiguous => continue,
592            };
593            let range = &reference.component_ranges[component_count - 1];
594            ctx.record_range(unit.fq_name(), node, range.start, range.end);
595        }
596    }
597}
598
599fn record_recovered_relational_template_member_call(
600    call: crate::graph::resolver::RecoveredRelationalTemplateMemberCall<'_>,
601    ctx: &mut CppScan<'_>,
602    bindings: &LocalInferenceEngine<CodeUnit>,
603) {
604    let name = node_text(call.member, ctx.source);
605    let Some(receiver_owner) = receiver_type_unit(call.receiver, ctx, bindings, 32) else {
606        ctx.record_unproven(name, call.member);
607        return;
608    };
609    let owner = match resolve_declaring_member_owner_cached(ctx, &receiver_owner, name) {
610        EnclosingMemberOwnerResolution::Owner(owner) => owner,
611        EnclosingMemberOwnerResolution::Ambiguous => {
612            ctx.record_unproven(name, call.member);
613            return;
614        }
615        EnclosingMemberOwnerResolution::Missing => return,
616    };
617    let VisibleMemberResolution::Callable(callables) = ctx
618        .visibility
619        .visible_member_for_owner_name(ctx.file, &owner, name)
620    else {
621        return;
622    };
623    let mut candidates = callables.into_iter().filter(|candidate| {
624        cpp_callable_arity(&ctx.analyzer, candidate).accepts(call.arity)
625            && ctx
626                .visibility
627                .callable_is_template_declaration(&ctx.analyzer, candidate)
628    });
629    let Some(candidate) = candidates.next() else {
630        return;
631    };
632    if candidates.next().is_some() {
633        ctx.record_unproven(name, call.member);
634        return;
635    }
636    ctx.record(candidate.fq_name(), call.member);
637}
638
639fn record_recovered_macro_return_type_reference(return_type: Node<'_>, ctx: &mut CppScan<'_>) {
640    let name = node_text(return_type, ctx.source);
641    let Some(scope) = recovered_or_indexed_lexical_scope(return_type, ctx) else {
642        ctx.record_unproven(name, return_type);
643        return;
644    };
645    let components = [name.to_string()];
646    if let LexicalTypeResolution::Resolved { unit, .. } = ctx
647        .visibility
648        .resolve_type_components_lexically(&ctx.analyzer, ctx.file, &components, false, &scope)
649    {
650        ctx.record(unit.fq_name(), return_type);
651        return;
652    }
653
654    let mut aliases = ctx
655        .visibility
656        .visible_identifier_candidates(ctx.file, name)
657        .filter(|candidate| {
658            ctx.analyzer
659                .type_alias_provider()
660                .is_some_and(|provider| provider.is_type_alias(candidate))
661                && ctx.visibility.external_type_candidate_visible_in_context(
662                    &ctx.analyzer,
663                    ctx.file,
664                    candidate,
665                    return_type,
666                )
667        })
668        .filter_map(|candidate| {
669            let owner = ctx.analyzer.parent_of(candidate)?;
670            let owner_components = canonical_cpp_scope_components(&owner);
671            scope
672                .starts_with(&owner_components)
673                .then_some((candidate, owner_components.len()))
674        })
675        .collect::<Vec<_>>();
676    let deepest = aliases.iter().map(|(_, depth)| *depth).max();
677    aliases.retain(|(_, depth)| Some(*depth) == deepest);
678    if aliases.len() == 1 {
679        ctx.record(aliases[0].0.fq_name(), return_type);
680    } else {
681        ctx.record_unproven(name, return_type);
682    }
683}
684
685/// A type node that is the direct type payload of a template argument.  The
686/// outer template-id is recorded separately, but these leaves can name class
687/// aliases (for example `expected<T, error_type>`) and therefore need their
688/// own inverse edge as well.
689fn record_type_reference(node: Node<'_>, ctx: &mut CppScan<'_>) {
690    let ordinary_resolution = resolve_type_node_lexically(
691        node,
692        &ctx.analyzer,
693        ctx.visibility,
694        &ctx.ordinary_type_imports,
695        ctx.file,
696        ctx.source,
697    );
698    let resolution = match recovered_sentinel_type_resolution(node, ctx) {
699        Some(recovered @ LexicalTypeResolution::Resolved { .. }) => recovered,
700        Some(LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing) | None => {
701            ordinary_resolution
702        }
703    };
704    let resolution = resolve_inverted_type_node(node, ctx, resolution);
705    match resolution {
706        LexicalTypeResolution::Resolved { unit, .. } => {
707            let callee = unit.fq_name();
708            let hit = type_reference_hit_node(node);
709            ctx.record(callee.clone(), hit);
710            if initialized_type_declaration_with_cast(node) {
711                ctx.record(callee.clone(), function_terminal_node(hit));
712            }
713            if qualified_alias_reference_requires_terminal(
714                qualified_alias_reference_preserves_target(
715                    node,
716                    &unit,
717                    &ctx.analyzer,
718                    ctx.visibility,
719                    ctx.file,
720                    ctx.source,
721                ),
722            ) {
723                let terminal = function_terminal_node(node);
724                if terminal.start_byte() != node.start_byte()
725                    || terminal.end_byte() != node.end_byte()
726                {
727                    ctx.record(callee, terminal);
728                }
729            }
730        }
731        LexicalTypeResolution::Missing => {
732            if let Some(unit) = ctx.visibility.unique_visible_parameter_type_fallback(
733                &ctx.analyzer,
734                ctx.file,
735                node,
736                ctx.source,
737            ) {
738                ctx.record(unit.fq_name(), node);
739            }
740        }
741        LexicalTypeResolution::Ambiguous => {}
742    }
743}
744
745fn record_nested_type_terminal_reference(node: Node<'_>, ctx: &mut CppScan<'_>) {
746    let Some(qualified) = node.parent().filter(|parent| {
747        matches!(
748            parent.kind(),
749            "qualified_identifier" | "scoped_type_identifier"
750        ) && parent.child_by_field_name("name") == Some(node)
751    }) else {
752        return;
753    };
754    let Some(owner) = qualified_owner_components(qualified, ctx.source) else {
755        return;
756    };
757    let mut complete = qualified;
758    while let Some(parent) = complete.parent().filter(|parent| {
759        matches!(
760            parent.kind(),
761            "qualified_identifier" | "scoped_type_identifier"
762        )
763    }) {
764        complete = parent;
765    }
766    if matches!(
767        resolve_type_node_lexically(
768            complete,
769            &ctx.analyzer,
770            ctx.visibility,
771            &ctx.ordinary_type_imports,
772            ctx.file,
773            ctx.source,
774        ),
775        LexicalTypeResolution::Resolved { .. }
776    ) {
777        return;
778    }
779
780    let Some(enclosing_owner) = enclosing_callable_owner(node, ctx) else {
781        return;
782    };
783    let lexical_scope = canonical_cpp_scope_components(&enclosing_owner);
784    let owner_resolution = ctx.visibility.resolve_type_components_lexically(
785        &ctx.analyzer,
786        ctx.file,
787        &owner.names,
788        owner.global,
789        &lexical_scope,
790    );
791    let owner_unit = match owner_resolution {
792        LexicalTypeResolution::Resolved { unit, .. } => unit,
793        LexicalTypeResolution::Missing if !owner.global && owner.names.len() == 1 => {
794            let Some(unit) = ctx.visibility.inherited_injected_class_owner(
795                &ctx.analyzer,
796                ctx.file,
797                &enclosing_owner,
798                &owner.names[0],
799            ) else {
800                return;
801            };
802            unit
803        }
804        LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => return,
805    };
806    let name = node_text(node, ctx.source);
807    let mut candidates = Vec::new();
808    for candidate in ctx
809        .visibility
810        .visible_members_for_owner_name(ctx.file, &owner_unit, name)
811        .into_iter()
812        .filter(|candidate| {
813            candidate.is_class()
814                || ctx
815                    .analyzer
816                    .type_alias_provider()
817                    .is_some_and(|provider| provider.is_type_alias(candidate))
818        })
819        .filter(|candidate| {
820            ctx.visibility.external_type_candidate_visible_in_context(
821                &ctx.analyzer,
822                ctx.file,
823                candidate,
824                node,
825            )
826        })
827    {
828        if !candidates
829            .iter()
830            .any(|existing| same_logical_symbol(existing, candidate))
831        {
832            candidates.push(candidate.clone());
833        }
834    }
835    if let [candidate] = candidates.as_slice() {
836        ctx.record(candidate.fq_name(), complete);
837    }
838}
839
840fn recovered_sentinel_type_resolution(
841    node: Node<'_>,
842    ctx: &CppScan<'_>,
843) -> Option<LexicalTypeResolution> {
844    let scope = recovered_or_indexed_lexical_scope(node, ctx)?;
845    let components = cpp_type_name_components(node, ctx.source)?;
846    let global = is_globally_qualified_cpp_name(node);
847    Some(ctx.visibility.resolve_type_components_lexically(
848        &ctx.analyzer,
849        ctx.file,
850        &components,
851        global,
852        &scope,
853    ))
854}
855
856/// Forward and inverted C++ scans must agree on the owner scope when
857/// tree-sitter's malformed wrapper hides the structural class/namespace. The
858/// sentinel recovery is the most precise signal; otherwise use the shared
859/// lexical-scope reconstruction, which falls back to the indexed enclosing
860/// code-unit scope for displaced definitions.
861fn recovered_or_indexed_lexical_scope(node: Node<'_>, ctx: &CppScan<'_>) -> Option<Vec<String>> {
862    ctx.recovered_sentinel_scope(node).or_else(|| {
863        match enclosing_lexical_scope_components(
864            node,
865            &ctx.analyzer,
866            ctx.visibility,
867            ctx.file,
868            ctx.source,
869        ) {
870            LexicalScopeResolution::Resolved(scope) => Some(scope),
871            LexicalScopeResolution::Ambiguous | LexicalScopeResolution::Missing => None,
872        }
873    })
874}
875
876/// Tree-sitter can place either side of a missing `::` in a recovered
877/// declaration's qualified scope: a prefix macro may leave the real type in
878/// the scope, while a suffix attribute can leave the macro there instead.
879/// Resolve both structured candidates. Recovery is usable only when exactly
880/// one candidate resolves; even two spellings that happen to resolve to the
881/// same logical symbol are ambiguous because one may be a macro token.
882fn record_recovered_macro_decorated_type_reference(
883    scope_node: Node<'_>,
884    type_node: Node<'_>,
885    ctx: &mut CppScan<'_>,
886) {
887    let mut resolved = Vec::new();
888    for candidate in [scope_node, type_node] {
889        if resolved
890            .iter()
891            .any(|(_, existing): &(CodeUnit, Node<'_>)| same_node(*existing, candidate))
892        {
893            continue;
894        }
895        if let LexicalTypeResolution::Resolved { unit, .. } = resolve_type_node_lexically(
896            candidate,
897            &ctx.analyzer,
898            ctx.visibility,
899            &ctx.ordinary_type_imports,
900            ctx.file,
901            ctx.source,
902        ) {
903            resolved.push((unit, candidate));
904        }
905    }
906    let [(unit, candidate)] = resolved.as_slice() else {
907        return;
908    };
909    ctx.record(unit.fq_name(), type_reference_hit_node(*candidate));
910}
911
912/// Resolve an inverted type edge to the concrete specialization named by a
913/// template-id.  The lexical resolver intentionally resolves the primary
914/// declaration: callers that need to preserve a concrete specialization (the
915/// reference graph included) must apply the parsed arguments afterwards.  If
916/// the primary cannot be specialized, retain the lexical result; that keeps
917/// dependent or incomplete template uses conservative rather than dropping a
918/// proven primary edge.
919fn resolve_inverted_type_node(
920    node: Node<'_>,
921    ctx: &CppScan<'_>,
922    resolution: LexicalTypeResolution,
923) -> LexicalTypeResolution {
924    let Some(arguments) = cpp_template_reference_arguments(node, ctx.source) else {
925        return resolution;
926    };
927    let LexicalTypeResolution::Resolved {
928        unit,
929        components,
930        candidates,
931    } = resolution
932    else {
933        return resolution;
934    };
935
936    let mut specialized = Vec::new();
937    for candidate in candidates.iter().chain(std::iter::once(&unit)) {
938        if let Ok(resolved) =
939            ctx.visibility
940                .resolve_template_arguments(ctx.file, candidate.clone(), &arguments)
941            && !specialized
942                .iter()
943                .any(|existing: &CodeUnit| same_visible_symbol(existing, &resolved))
944        {
945            specialized.push(resolved);
946        }
947    }
948    match specialized.as_slice() {
949        [specialized] => LexicalTypeResolution::Resolved {
950            unit: specialized.clone(),
951            components,
952            candidates,
953        },
954        _ => LexicalTypeResolution::Resolved {
955            unit,
956            components,
957            candidates,
958        },
959    }
960}
961
962fn record_qualified_callable_value(
963    qualified: Node<'_>,
964    global: bool,
965    owner_components: &[Node<'_>],
966    member_node: Node<'_>,
967    ctx: &mut CppScan<'_>,
968) {
969    let member_name = node_text(member_node, ctx.source);
970    if member_name.is_empty() {
971        return;
972    }
973    let owner_components = owner_components
974        .iter()
975        .map(|component| node_text(*component, ctx.source))
976        .map(str::to_string)
977        .collect::<Vec<_>>();
978    let lexical_scope = if global {
979        Vec::new()
980    } else {
981        match enclosing_lexical_scope_components(
982            qualified,
983            &ctx.analyzer,
984            ctx.visibility,
985            ctx.file,
986            ctx.source,
987        ) {
988            LexicalScopeResolution::Resolved(scope) => scope,
989            LexicalScopeResolution::Ambiguous | LexicalScopeResolution::Missing => {
990                ctx.record_unproven(member_name, member_node);
991                return;
992            }
993        }
994    };
995    let owner = match ctx.visibility.resolve_callable_value_components_lexically(
996        &ctx.analyzer,
997        ctx.file,
998        &owner_components,
999        member_name,
1000        global,
1001        &lexical_scope,
1002    ) {
1003        LexicalCallableValueResolution::Type(owner) => owner,
1004        LexicalCallableValueResolution::FreeFunction(function) => {
1005            ctx.record(function.fq_name(), member_node);
1006            return;
1007        }
1008        LexicalCallableValueResolution::Ambiguous => {
1009            ctx.record_unproven(member_name, member_node);
1010            return;
1011        }
1012        LexicalCallableValueResolution::Missing => match resolve_type_components_lexically_at(
1013            qualified,
1014            &owner_components,
1015            global,
1016            &ctx.analyzer,
1017            ctx.visibility,
1018            &ctx.ordinary_type_imports,
1019            ctx.file,
1020            ctx.source,
1021        ) {
1022            LexicalTypeResolution::Resolved { unit, .. } => unit,
1023            LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => {
1024                ctx.record_unproven(member_name, member_node);
1025                return;
1026            }
1027        },
1028    };
1029    match ctx
1030        .visibility
1031        .visible_member_for_owner_name(ctx.file, &owner, member_name)
1032    {
1033        VisibleMemberResolution::Callable(callables) => {
1034            if let Some(callable) = callables.first() {
1035                ctx.record(callable.fq_name(), member_node);
1036            }
1037        }
1038        // Fields are intentionally absent from the workspace usage-graph node
1039        // catalog. A proven non-callable member is therefore a negative for this
1040        // callable edge pass, not an unresolved terminal-name fanout.
1041        VisibleMemberResolution::NonCallable => {}
1042        VisibleMemberResolution::AmbiguousKind | VisibleMemberResolution::Missing => {
1043            ctx.record_unproven(member_name, member_node);
1044        }
1045    }
1046}
1047
1048fn record_call(node: Node<'_>, ctx: &mut CppScan<'_>, bindings: &LocalInferenceEngine<CodeUnit>) {
1049    let Some(function) = node.child_by_field_name("function") else {
1050        return;
1051    };
1052    let Some(call_arity) = ctx
1053        .visibility
1054        .call_arity_evidence(ctx.file, node, ctx.source)
1055        .exact()
1056    else {
1057        let name_node = function
1058            .child_by_field_name("field")
1059            .or_else(|| function.child_by_field_name("name"))
1060            .unwrap_or(function);
1061        let name = node_text(name_node, ctx.source);
1062        if !name.is_empty() {
1063            ctx.record_unproven(name, name_node);
1064        }
1065        return;
1066    };
1067    match function.kind() {
1068        // `obj.m()` / `ptr->m()`: type the receiver, emit `Owner.m`.
1069        "field_expression" => {
1070            let Some(field) = function.child_by_field_name("field") else {
1071                return;
1072            };
1073            let name = node_text(field, ctx.source);
1074            if name.is_empty() {
1075                return;
1076            }
1077            let Some(receiver) = function
1078                .child_by_field_name("argument")
1079                .or_else(|| function.named_child(0))
1080            else {
1081                return;
1082            };
1083            if receiver_is_self_like(receiver, ctx.analyzer.reference_uses_c_semantics(ctx.file)) {
1084                // `this->m()` / `(*this).m()` is a same-owner call (#1138):
1085                // record it as unproven inbound rather than dropping it, so a
1086                // member reachable only through same-owner calls reads
1087                // INCONCLUSIVE, never confidently dead — uniformly with the
1088                // other languages.
1089                ctx.record_unproven(name, field);
1090                return;
1091            }
1092            if let Some(receiver_owner) = receiver_type_unit(receiver, ctx, bindings, 32) {
1093                let ordinary = resolve_declaring_member_owner_cached(ctx, &receiver_owner, name);
1094                match resolve_declaring_callable_owner(
1095                    &ctx.analyzer,
1096                    ctx.visibility,
1097                    ctx.file,
1098                    ordinary,
1099                    name,
1100                    call_arity,
1101                ) {
1102                    EnclosingMemberOwnerResolution::Owner(owner) => {
1103                        match ctx
1104                            .visibility
1105                            .visible_member_for_owner_name(ctx.file, &owner, name)
1106                        {
1107                            VisibleMemberResolution::Callable(callables) => {
1108                                if let Some(callable) = callables.iter().find(|callable| {
1109                                    cpp_callable_arity(&ctx.analyzer, callable).accepts(call_arity)
1110                                }) {
1111                                    ctx.record(callable.fq_name(), field);
1112                                }
1113                            }
1114                            VisibleMemberResolution::AmbiguousKind => {
1115                                ctx.record_unproven(name, field);
1116                            }
1117                            VisibleMemberResolution::NonCallable
1118                            | VisibleMemberResolution::Missing => {}
1119                        }
1120                    }
1121                    EnclosingMemberOwnerResolution::Ambiguous => {
1122                        ctx.record_unproven(name, field);
1123                    }
1124                    EnclosingMemberOwnerResolution::Missing => {}
1125                }
1126            } else {
1127                ctx.record_unproven(name, field);
1128            }
1129        }
1130        // A bare `m(..)` is either a free function or an unqualified member call on
1131        // the enclosing class (`this`). `qualified_identifier` (`X::m`) is handled
1132        // by the type-reference case above.
1133        "identifier" | "template_function" => {
1134            let terminal = super::resolver::function_terminal_node(function);
1135            let name = node_text(terminal, ctx.source);
1136            if name.is_empty() {
1137                return;
1138            }
1139            if bindings.is_shadowed(name) {
1140                return;
1141            }
1142            if let Some(enclosing_owner) = enclosing_callable_owner(function, ctx) {
1143                let ordinary = resolve_declaring_member_owner_cached(ctx, &enclosing_owner, name);
1144                match resolve_declaring_callable_owner(
1145                    &ctx.analyzer,
1146                    ctx.visibility,
1147                    ctx.file,
1148                    ordinary,
1149                    name,
1150                    call_arity,
1151                ) {
1152                    EnclosingMemberOwnerResolution::Owner(owner)
1153                        if !same_visible_symbol(&owner, &enclosing_owner) =>
1154                    {
1155                        match ctx
1156                            .visibility
1157                            .visible_member_for_owner_name(ctx.file, &owner, name)
1158                        {
1159                            VisibleMemberResolution::Callable(callables) => {
1160                                if let Some(callable) = callables.iter().find(|callable| {
1161                                    cpp_callable_arity(&ctx.analyzer, callable).accepts(call_arity)
1162                                }) {
1163                                    ctx.record(callable.fq_name(), function);
1164                                }
1165                            }
1166                            VisibleMemberResolution::AmbiguousKind => {
1167                                ctx.record_unproven(name, function);
1168                            }
1169                            VisibleMemberResolution::NonCallable
1170                            | VisibleMemberResolution::Missing => {}
1171                        }
1172                        return;
1173                    }
1174                    EnclosingMemberOwnerResolution::Owner(_) => {
1175                        // Bare `m(..)` resolving to a method whose owner IS the
1176                        // enclosing class is a same-owner call (#1161, mirroring
1177                        // the `this->m()` fix at #1138): record it as unproven
1178                        // inbound rather than dropping it, so a member reachable
1179                        // only through bare implicit-this calls reads
1180                        // INCONCLUSIVE, never confidently dead — uniformly with
1181                        // the other languages and with the explicit-`this->m()`
1182                        // site above.
1183                        ctx.record_unproven(name, function);
1184                        return;
1185                    }
1186                    EnclosingMemberOwnerResolution::Ambiguous => {
1187                        ctx.record_unproven(name, function);
1188                        return;
1189                    }
1190                    EnclosingMemberOwnerResolution::Missing => {}
1191                }
1192            }
1193            let resolution = resolve_bare_call_target(
1194                node,
1195                function,
1196                &ctx.analyzer,
1197                ctx.visibility,
1198                &ctx.ordinary_type_imports,
1199                ctx.file,
1200                ctx.source,
1201            );
1202            match resolution {
1203                BareCallTargetResolution::FreeFunctions(units) => {
1204                    let mut recorded = HashSet::default();
1205                    for unit in units {
1206                        let fq_name = unit.fq_name();
1207                        if recorded.insert(fq_name.clone()) {
1208                            ctx.record(fq_name, terminal);
1209                        }
1210                    }
1211                }
1212                BareCallTargetResolution::Type(unit) => {
1213                    if let VisibleMemberResolution::Callable(constructors) = ctx
1214                        .visibility
1215                        .visible_member_for_owner_name(ctx.file, &unit, unit.identifier())
1216                        && let Some(constructor) = constructors.iter().find(|constructor| {
1217                            cpp_callable_arity(&ctx.analyzer, constructor).accepts(call_arity)
1218                        })
1219                    {
1220                        ctx.record(constructor.fq_name(), terminal);
1221                    } else {
1222                        ctx.record(unit.fq_name(), function);
1223                    }
1224                }
1225                BareCallTargetResolution::UnprovenFreeFunctions(_)
1226                | BareCallTargetResolution::CallableShadow
1227                | BareCallTargetResolution::Ambiguous => {}
1228                BareCallTargetResolution::Missing => {}
1229            }
1230            // Direct/self member calls are intentionally omitted above; unique inherited
1231            // callable owners are recorded, while an unresolved bare name adds no edge.
1232        }
1233        _ => {}
1234    }
1235}
1236
1237fn resolve_declaring_member_owner_cached(
1238    ctx: &mut CppScan<'_>,
1239    receiver_owner: &CodeUnit,
1240    name: &str,
1241) -> EnclosingMemberOwnerResolution {
1242    if let Some(cached) = ctx
1243        .declaring_member_cache
1244        .get(receiver_owner)
1245        .and_then(|by_name| by_name.get(name))
1246        .cloned()
1247    {
1248        return cached;
1249    }
1250    let resolution = resolve_declaring_member_owner(
1251        &ctx.analyzer,
1252        ctx.visibility,
1253        ctx.file,
1254        receiver_owner,
1255        name,
1256    );
1257    ctx.declaring_member_cache
1258        .entry(receiver_owner.clone())
1259        .or_default()
1260        .insert(name.to_string(), resolution.clone());
1261    resolution
1262}
1263
1264fn enclosing_callable_owner(node: Node<'_>, ctx: &CppScan<'_>) -> Option<CodeUnit> {
1265    let mut current = node.parent();
1266    while let Some(parent) = current {
1267        if parent.kind() == "function_definition" {
1268            let declarator = parent.child_by_field_name("declarator")?;
1269            let function = declarator_name_node(declarator)?;
1270            if let Some(owners) = out_of_line_member_definition_owner(
1271                &ctx.analyzer,
1272                ctx.visibility,
1273                ctx.file,
1274                ctx.source,
1275                function,
1276            ) && let Some((_, owner)) = owners.innermost()
1277            {
1278                return Some(owner.clone());
1279            }
1280            break;
1281        }
1282        current = parent.parent();
1283    }
1284    ctx.enclosing_class(node.start_byte()).and_then(|fqn| {
1285        ctx.analyzer
1286            .definitions(fqn)
1287            .find(|candidate| candidate.is_class())
1288    })
1289}
1290
1291/// Whether `receiver` is the implicit-object receiver of a member call.
1292///
1293/// `reference_is_c` is the compilation language the reference is read in
1294/// (#1970): `this` is the C++ implicit object only where C++ compiles the
1295/// source, and is an ordinary identifier in a `.c` file or a header every
1296/// reaching translation unit compiles as C.
1297fn receiver_is_self_like(receiver: Node<'_>, reference_is_c: bool) -> bool {
1298    match receiver.kind() {
1299        "this" => !reference_is_c,
1300        "parenthesized_expression" | "pointer_expression" => receiver
1301            .child_by_field_name("argument")
1302            .or_else(|| receiver.named_child(0))
1303            .is_some_and(|inner| receiver_is_self_like(inner, reference_is_c)),
1304        _ => false,
1305    }
1306}
1307
1308/// If `node` is the `function` of a namespace-qualified free-function call, its target.
1309fn scoped_free_function(node: Node<'_>, ctx: &CppScan<'_>) -> Option<CodeUnit> {
1310    if node.kind() != "qualified_identifier" {
1311        return None;
1312    }
1313    let parent = node.parent()?;
1314    if parent.kind() != "call_expression" || parent.child_by_field_name("function") != Some(node) {
1315        return None;
1316    }
1317    ctx.visibility.resolve_named(
1318        ctx.file,
1319        node_text(node, ctx.source),
1320        TargetKind::FreeFunction,
1321    )
1322}
1323
1324/// If `node` is the `function` of a `X::m(..)` call, the fqn of `X`'s type.
1325fn scoped_call_owner(node: Node<'_>, ctx: &CppScan<'_>) -> Option<String> {
1326    if node.kind() != "qualified_identifier" {
1327        return None;
1328    }
1329    let parent = node.parent()?;
1330    if parent.kind() != "call_expression" || parent.child_by_field_name("function") != Some(node) {
1331        return None;
1332    }
1333    let scope = node.child_by_field_name("scope")?;
1334    match resolve_type_node_lexically(
1335        scope,
1336        &ctx.analyzer,
1337        ctx.visibility,
1338        &ctx.ordinary_type_imports,
1339        ctx.file,
1340        ctx.source,
1341    ) {
1342        LexicalTypeResolution::Resolved { unit, .. } => Some(unit.fq_name()),
1343        LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => None,
1344    }
1345}
1346
1347/// The trailing member name of a `X::m` qualified identifier.
1348fn scoped_call_member(node: Node<'_>, source: &str) -> String {
1349    node.child_by_field_name("name")
1350        .map(|name| node_text(name, source).to_string())
1351        .unwrap_or_default()
1352}
1353
1354fn receiver_type_unit(
1355    receiver: Node<'_>,
1356    ctx: &CppScan<'_>,
1357    bindings: &LocalInferenceEngine<CodeUnit>,
1358    remaining_call_depth: usize,
1359) -> Option<CodeUnit> {
1360    match receiver.kind() {
1361        "identifier" => {
1362            let name = node_text(receiver, ctx.source);
1363            // A typed local resolves to its type; otherwise the name may itself be a
1364            // type, unless it is a known (shadowed) untyped local — never reinterpret
1365            // a value as a static type.
1366            first_precise(bindings, name).or_else(|| {
1367                if !bindings.is_shadowed(name)
1368                    && let Some(unit) = global_receiver_type_unit(ctx, name)
1369                {
1370                    return Some(unit);
1371                }
1372                (!bindings.is_shadowed(name))
1373                    .then(|| resolve_type_node_with_recovered_scope(receiver, ctx))
1374                    .flatten()
1375                    .or_else(|| {
1376                        (!bindings.is_shadowed(name))
1377                            .then(|| ctx.resolve_type(name))
1378                            .flatten()
1379                    })
1380            })
1381        }
1382        "this" if ctx.analyzer.reference_uses_c_semantics(ctx.file) => {
1383            first_precise(bindings, node_text(receiver, ctx.source))
1384        }
1385        "this" => ctx.enclosing_class(receiver.start_byte()).and_then(|fqn| {
1386            ctx.analyzer
1387                .definitions(fqn)
1388                .find(|candidate| candidate.is_class())
1389        }),
1390        // `(*p).m()` / `(p).m()` unwrap to the inner receiver.
1391        "parenthesized_expression" | "pointer_expression" => receiver
1392            .child_by_field_name("argument")
1393            .or_else(|| receiver.named_child(0))
1394            .and_then(|inner| receiver_type_unit(inner, ctx, bindings, remaining_call_depth)),
1395        "call_expression" if remaining_call_depth > 0 => infer_cpp_initializer_binding(
1396            &ctx.analyzer,
1397            ctx.visibility,
1398            ctx.file,
1399            ctx.source,
1400            receiver,
1401            Some(&|inner, _source| {
1402                receiver_type_unit(inner, ctx, bindings, remaining_call_depth - 1)
1403                    .into_iter()
1404                    .collect()
1405            }),
1406        )
1407        .and_then(|binding| binding.unit),
1408        // A nested C selector carries the declared type of its selected
1409        // field. Resolve that field through the same owner/hierarchy lookup
1410        // used by ordinary field references; this preserves synthetic owners
1411        // for named anonymous aggregates without parsing type text.
1412        "field_expression" => {
1413            let field = receiver.child_by_field_name("field")?;
1414            let name = node_text(field, ctx.source);
1415            let inner = receiver
1416                .child_by_field_name("argument")
1417                .or_else(|| receiver.child_by_field_name("object"))
1418                .or_else(|| receiver.named_child(0))?;
1419            let receiver_owner = receiver_type_unit(inner, ctx, bindings, remaining_call_depth)?;
1420            let owner = match resolve_declaring_member_owner(
1421                &ctx.analyzer,
1422                ctx.visibility,
1423                ctx.file,
1424                &receiver_owner,
1425                name,
1426            ) {
1427                EnclosingMemberOwnerResolution::Owner(owner) => owner,
1428                EnclosingMemberOwnerResolution::Ambiguous
1429                | EnclosingMemberOwnerResolution::Missing => return None,
1430            };
1431            let fields = ctx
1432                .visibility
1433                .visible_members_for_owner_name(ctx.file, &owner, name)
1434                .into_iter()
1435                .filter(|candidate| candidate.is_field())
1436                .collect::<Vec<_>>();
1437            let [field_unit] = fields.as_slice() else {
1438                return None;
1439            };
1440            field_declared_binding(&ctx.analyzer, ctx.visibility, ctx.file, field_unit)
1441                .and_then(|binding| binding.unit)
1442        }
1443        "cast_expression" => cast_expression_type_node(receiver).and_then(|type_node| {
1444            resolve_type_node_with_recovered_scope(type_node, ctx).or_else(|| {
1445                match ctx.resolve_type_node_result(type_node) {
1446                    Ok(Some(unit)) => Some(unit),
1447                    Ok(None) => ctx.resolve_type(node_text(type_node, ctx.source)),
1448                    Err(_) => None,
1449                }
1450            })
1451        }),
1452        _ => None,
1453    }
1454}
1455
1456fn global_receiver_type_unit(ctx: &CppScan<'_>, name: &str) -> Option<CodeUnit> {
1457    let fields = ctx
1458        .visibility
1459        .named_candidates(ctx.file, name, TargetKind::GlobalField)
1460        .into_iter()
1461        .filter(has_persisted_global_field_identity)
1462        .collect::<Vec<_>>();
1463    let first = fields.first()?;
1464    let mut linkage_cache = HashMap::default();
1465    if fields.iter().skip(1).any(|field| {
1466        !same_visible_global_field_symbol(&ctx.analyzer, &mut linkage_cache, first, field)
1467    }) {
1468        return None;
1469    }
1470    let mut units = fields
1471        .into_iter()
1472        .filter_map(|field| {
1473            field_declared_binding(&ctx.analyzer, ctx.visibility, ctx.file, &field)
1474                .and_then(|binding| binding.unit)
1475        })
1476        .collect::<Vec<_>>();
1477    units.dedup_by(|left, right| same_visible_symbol(left, right));
1478    match units.as_slice() {
1479        [unit] => Some(unit.clone()),
1480        _ => None,
1481    }
1482}
1483
1484fn seed_declaration(
1485    node: Node<'_>,
1486    ctx: &mut CppScan<'_>,
1487    bindings: &mut LocalInferenceEngine<CodeUnit>,
1488) {
1489    if recovered_macro_return_type_node(node, ctx.source).is_some()
1490        || crate::declarations::is_direct_recovered_exported_class_field_declaration(
1491            node, ctx.source,
1492        )
1493    {
1494        return;
1495    }
1496    match node.kind() {
1497        "parameter_declaration" | "optional_parameter_declaration" => {
1498            seed_typed_binding(node, ctx, bindings)
1499        }
1500        "declaration" | "field_declaration" => seed_variable_declaration(node, ctx, bindings),
1501        "for_range_loop" => seed_range_binding(node, ctx, bindings),
1502        "expression_statement" => seed_function_macro_local_binding(node, ctx, bindings),
1503        "assignment_expression" => seed_function_macro_container_binding(node, ctx, bindings),
1504        _ => {}
1505    }
1506}
1507
1508fn seed_function_macro_local_binding(
1509    node: Node<'_>,
1510    ctx: &CppScan<'_>,
1511    bindings: &mut LocalInferenceEngine<CodeUnit>,
1512) {
1513    let Some(binding) = ctx
1514        .visibility
1515        .function_macro_local_binding(ctx.file, node, ctx.source)
1516    else {
1517        return;
1518    };
1519    let unit = binding
1520        .type_node
1521        .and_then(|type_node| resolve_type_node_with_recovered_scope(type_node, ctx))
1522        .or_else(|| {
1523            binding
1524                .type_node
1525                .and_then(|type_node| ctx.resolve_type_node_result(type_node).ok().flatten())
1526        })
1527        .or_else(|| ctx.resolve_type(&binding.type_name));
1528    match unit {
1529        Some(unit) => bindings.seed_symbol(binding.name, unit),
1530        None => bindings.declare_shadow(binding.name),
1531    }
1532}
1533
1534fn seed_function_macro_container_binding(
1535    node: Node<'_>,
1536    ctx: &CppScan<'_>,
1537    bindings: &mut LocalInferenceEngine<CodeUnit>,
1538) {
1539    let Some(binding) =
1540        ctx.visibility
1541            .function_macro_container_binding(&ctx.analyzer, ctx.file, node, ctx.source)
1542    else {
1543        return;
1544    };
1545    let unit = binding.proven_unit.or_else(|| {
1546        binding
1547            .type_node
1548            .and_then(|type_node| resolve_type_node_with_recovered_scope(type_node, ctx))
1549            .or_else(|| {
1550                binding
1551                    .type_node
1552                    .and_then(|type_node| ctx.resolve_type_node_result(type_node).ok().flatten())
1553            })
1554            .or_else(|| ctx.resolve_type(&binding.type_name))
1555    });
1556    match unit {
1557        Some(unit) => bindings.seed_symbol(binding.name, unit),
1558        None => bindings.declare_shadow(binding.name),
1559    }
1560}
1561
1562fn seed_typed_binding(
1563    node: Node<'_>,
1564    ctx: &CppScan<'_>,
1565    bindings: &mut LocalInferenceEngine<CodeUnit>,
1566) {
1567    if !parameter_belongs_to_callable_scope(node) {
1568        return;
1569    }
1570    let Some(declarator) = node.child_by_field_name("declarator") else {
1571        return;
1572    };
1573    let Some(name) = extract_variable_name(declarator, ctx.source) else {
1574        return;
1575    };
1576    let type_node = node
1577        .child_by_field_name("type")
1578        .or_else(|| first_type_child(node));
1579    seed_binding(&name, type_node, None, ctx, bindings);
1580}
1581
1582fn seed_range_binding(
1583    node: Node<'_>,
1584    ctx: &CppScan<'_>,
1585    bindings: &mut LocalInferenceEngine<CodeUnit>,
1586) {
1587    let Some(declarator) = node.child_by_field_name("declarator") else {
1588        return;
1589    };
1590    let Some(name) = extract_variable_name(declarator, ctx.source) else {
1591        return;
1592    };
1593    let type_node = node
1594        .child_by_field_name("type")
1595        .or_else(|| first_type_child(node));
1596    seed_binding(&name, type_node, None, ctx, bindings);
1597}
1598
1599fn seed_variable_declaration(
1600    node: Node<'_>,
1601    ctx: &CppScan<'_>,
1602    bindings: &mut LocalInferenceEngine<CodeUnit>,
1603) {
1604    let type_node = node
1605        .child_by_field_name("type")
1606        .or_else(|| first_type_child(node));
1607    let type_text =
1608        type_node.map(|type_node| normalize_type_text(node_text(type_node, ctx.source)));
1609    let mut cursor = node.walk();
1610    for child in node.named_children(&mut cursor) {
1611        let declarator = if child.kind() == "init_declarator" {
1612            child.child_by_field_name("declarator")
1613        } else if is_declarator_node(child) {
1614            Some(child)
1615        } else {
1616            None
1617        };
1618        let Some(declarator) = declarator else {
1619            continue;
1620        };
1621        if declarator.kind() == "function_declarator"
1622            && !constructor_style_local_declaration(
1623                ctx.visibility,
1624                ctx.file,
1625                ctx.source,
1626                declarator,
1627                type_text.as_deref(),
1628                bindings,
1629            )
1630        {
1631            if node.kind() == "declaration"
1632                && has_function_scope_ancestor(node)
1633                && let Some(name) = extract_variable_name(declarator, ctx.source)
1634            {
1635                bindings.declare_shadow(name);
1636            }
1637            continue;
1638        }
1639        let Some(name) = extract_variable_name(declarator, ctx.source) else {
1640            continue;
1641        };
1642        let value = child.child_by_field_name("value");
1643        seed_binding(&name, type_node, value, ctx, bindings);
1644    }
1645}
1646
1647fn has_function_scope_ancestor(mut node: Node<'_>) -> bool {
1648    while let Some(parent) = node.parent() {
1649        if matches!(parent.kind(), "function_definition" | "lambda_expression") {
1650            return true;
1651        }
1652        node = parent;
1653    }
1654    false
1655}
1656
1657fn seed_binding(
1658    name: &str,
1659    type_node: Option<Node<'_>>,
1660    value: Option<Node<'_>>,
1661    ctx: &CppScan<'_>,
1662    bindings: &mut LocalInferenceEngine<CodeUnit>,
1663) {
1664    if name.is_empty() {
1665        return;
1666    }
1667    // A declared type resolves directly; `auto x = new Foo()` infers from the
1668    // initializer. A declared-but-unresolved local is shadowed so a later
1669    // member access never falls back to static type resolution on its name.
1670    let declared_type =
1671        type_node.filter(|node| normalize_type_text(node_text(*node, ctx.source)) != "auto");
1672    let resolved = match declared_type {
1673        Some(node) => resolve_type_node_with_recovered_scope(node, ctx).or_else(|| {
1674            match ctx.resolve_type_node_result(node) {
1675                Ok(Some(unit)) => Some(unit),
1676                Ok(None) => ctx.resolve_type(node_text(node, ctx.source)),
1677                Err(_) => None,
1678            }
1679        }),
1680        None => value.and_then(|value| infer_type_from_value(value, ctx)),
1681    };
1682    match resolved {
1683        Some(unit) => bindings.seed_symbol(name.to_string(), unit),
1684        None => bindings.declare_shadow(name.to_string()),
1685    }
1686}
1687
1688fn resolve_type_node_with_recovered_scope(node: Node<'_>, ctx: &CppScan<'_>) -> Option<CodeUnit> {
1689    if let Some(owner) = anonymous_aggregate_owner(&ctx.analyzer, ctx.file, node) {
1690        return Some(owner);
1691    }
1692    let scope = recovered_or_indexed_lexical_scope(node, ctx)?;
1693    let components = cpp_type_name_components(node, ctx.source)?;
1694    let global = is_globally_qualified_cpp_name(node);
1695    match ctx.visibility.resolve_type_components_lexically(
1696        &ctx.analyzer,
1697        ctx.file,
1698        &components,
1699        global,
1700        &scope,
1701    ) {
1702        LexicalTypeResolution::Resolved { unit, .. } => Some(unit),
1703        LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => None,
1704    }
1705}
1706
1707/// Infer a class type from an initializer expression for `auto`/untyped locals.
1708fn infer_type_from_value(node: Node<'_>, ctx: &CppScan<'_>) -> Option<CodeUnit> {
1709    infer_cpp_initializer_type(&ctx.analyzer, ctx.visibility, ctx.file, ctx.source, node)
1710}