Skip to main content

brokk_bifrost_cpp/graph/
inverted.rs

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