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