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