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