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