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