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