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