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