Skip to main content

brokk_bifrost_cpp/graph/
inverted.rs

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