Skip to main content

brokk_bifrost_cpp/graph/
resolver.rs

1use crate::call_match::{
2    CppArgType, cpp_signature_param_types, cpp_split_top_level_commas, normalize_cpp_type_name,
3};
4use crate::declarations::{
5    cpp_export_macro_token, cpp_field_declaration_linkage, cpp_template_term, node_text,
6    normalize_cpp_whitespace, recovered_exported_class_has_body,
7};
8use crate::graph::CppGraphSource;
9use crate::graph::extractor::ScanCtx;
10use crate::graph_support::CppSource;
11use crate::imports::{
12    IncludeTargetIndex, include_paths as cpp_include_paths, resolve_include_targets_with_index,
13};
14use brokk_bifrost_core::analyzer::model::{
15    CallableArity, CodeUnitType, CppFieldLinkage, CppTemplateExpression, CppTemplateMetadata,
16    CppTemplateParameterMetadata, CppTemplateTerm,
17};
18use brokk_bifrost_core::analyzer::prepared_syntax::PreparedSyntaxTree;
19use brokk_bifrost_core::analyzer::tree_walk::node_for_exact_range;
20use brokk_bifrost_core::analyzer::usages::common::same_node;
21use brokk_bifrost_core::analyzer::usages::local_inference::LocalInferenceEngine;
22use brokk_bifrost_core::analyzer::{CodeUnit, ProjectFile, Range};
23use brokk_bifrost_core::cancellation::CancellationToken;
24use brokk_bifrost_core::hash::{HashMap, HashSet};
25use std::borrow::Cow;
26#[cfg(any(test, feature = "test-support"))]
27use std::cell::Cell;
28use std::collections::BTreeSet;
29use std::hash::Hash;
30#[cfg(any(test, feature = "test-support"))]
31use std::sync::atomic::{AtomicUsize, Ordering};
32use std::sync::{Arc, Mutex, OnceLock, RwLock};
33use std::thread::ThreadId;
34use tree_sitter::{Node, Parser, Tree};
35
36#[derive(Clone, Copy, PartialEq, Eq)]
37pub enum TargetKind {
38    Type,
39    Constructor,
40    FreeFunction,
41    Method,
42    GlobalField,
43    MemberField,
44}
45
46pub enum LexicalTypeResolution {
47    Resolved {
48        unit: CodeUnit,
49        components: Vec<String>,
50        candidates: Vec<CodeUnit>,
51    },
52    Ambiguous,
53    Missing,
54}
55
56#[derive(Clone, Copy)]
57enum TypeCandidateResolution<'a> {
58    Canonical,
59    PreserveAlias,
60    PreserveTarget(&'a CodeUnit),
61}
62
63pub enum LexicalCallableValueResolution {
64    Type(CodeUnit),
65    FreeFunction(CodeUnit),
66    Ambiguous,
67    Missing,
68}
69
70pub enum UsingEnumMemberResolution {
71    Resolved { owner: CodeUnit, member: CodeUnit },
72    Ambiguous,
73    Missing,
74}
75
76pub enum NamespaceValueResolution {
77    Resolved,
78    Ambiguous,
79    Missing,
80}
81
82pub fn resolve_namespace_value(
83    analyzer: &CppGraphSource<'_>,
84    visibility: &VisibilityIndex<'_>,
85    file: &ProjectFile,
86    namespace: &str,
87    name: &str,
88    before_byte: usize,
89) -> NamespaceValueResolution {
90    let mut matches = Vec::new();
91    for candidate in visibility.visible_identifier_candidates(file, name) {
92        if type_owner_of(analyzer, candidate).is_some()
93            || candidate.package_name() != namespace
94            || (candidate.source() == file
95                && !analyzer
96                    .ranges(candidate)
97                    .iter()
98                    .any(|range| range.start_byte < before_byte))
99            || matches
100                .iter()
101                .any(|existing| same_visible_symbol(existing, candidate))
102        {
103            continue;
104        }
105        matches.push(candidate.clone());
106        if matches.len() > 1 {
107            return NamespaceValueResolution::Ambiguous;
108        }
109    }
110    matches
111        .pop()
112        .map(|_| NamespaceValueResolution::Resolved)
113        .unwrap_or(NamespaceValueResolution::Missing)
114}
115
116pub(crate) struct ScopedUsingEnumOwners {
117    scopes: Vec<Vec<CodeUnit>>,
118}
119
120/// Same-file class and namespace imports collected by the targeted scanner's AST prepass.
121/// Cross-file and inherited class imports are deliberately not inferred without persisted
122/// evidence; a missing imported enumerator therefore remains unproven rather than being
123/// misresolved.
124pub(crate) struct SemanticUsingEnumOwners {
125    class_imports: HashMap<CodeUnit, Vec<CodeUnit>>,
126    namespace_imports: HashMap<Vec<String>, Vec<(usize, CodeUnit)>>,
127}
128
129pub(crate) enum SemanticUsingEnumMemberResolution {
130    Class(UsingEnumMemberResolution),
131    Namespace(UsingEnumMemberResolution),
132    Missing,
133}
134
135impl SemanticUsingEnumOwners {
136    pub(crate) fn new() -> Self {
137        Self {
138            class_imports: HashMap::default(),
139            namespace_imports: HashMap::default(),
140        }
141    }
142
143    pub fn import_class(&mut self, class: CodeUnit, enum_owner: CodeUnit) {
144        let imports = self.class_imports.entry(class).or_default();
145        if !imports
146            .iter()
147            .any(|existing| same_visible_symbol(existing, &enum_owner))
148        {
149            imports.push(enum_owner);
150        }
151    }
152
153    pub fn import_namespace(
154        &mut self,
155        namespace: Vec<String>,
156        declaration_byte: usize,
157        enum_owner: CodeUnit,
158    ) {
159        let imports = self.namespace_imports.entry(namespace).or_default();
160        if !imports
161            .iter()
162            .any(|(_, existing)| same_visible_symbol(existing, &enum_owner))
163        {
164            imports.push((declaration_byte, enum_owner));
165        }
166    }
167
168    pub fn resolve_member(
169        &self,
170        visibility: &VisibilityIndex<'_>,
171        file: &ProjectFile,
172        class: Option<&CodeUnit>,
173        namespace: &[String],
174        before_byte: usize,
175        name: &str,
176    ) -> SemanticUsingEnumMemberResolution {
177        if let Some(class) = class
178            && let Some((_, imports)) = self
179                .class_imports
180                .iter()
181                .find(|(owner, _)| same_visible_symbol(owner, class))
182        {
183            let resolution =
184                resolve_using_enum_member_for_owners(visibility, file, imports.iter(), name);
185            if !matches!(resolution, UsingEnumMemberResolution::Missing) {
186                return SemanticUsingEnumMemberResolution::Class(resolution);
187            }
188        }
189        for prefix_len in (0..=namespace.len()).rev() {
190            let Some(imports) = self.namespace_imports.get(&namespace[..prefix_len]) else {
191                continue;
192            };
193            let owners = imports
194                .iter()
195                .filter(|(declaration_byte, _)| *declaration_byte < before_byte)
196                .map(|(_, owner)| owner);
197            let resolution = resolve_using_enum_member_for_owners(visibility, file, owners, name);
198            if !matches!(resolution, UsingEnumMemberResolution::Missing) {
199                return SemanticUsingEnumMemberResolution::Namespace(resolution);
200            }
201        }
202        SemanticUsingEnumMemberResolution::Missing
203    }
204}
205
206fn resolve_using_enum_member_for_owners<'a>(
207    visibility: &VisibilityIndex<'_>,
208    file: &ProjectFile,
209    owners: impl IntoIterator<Item = &'a CodeUnit>,
210    name: &str,
211) -> UsingEnumMemberResolution {
212    let mut matches: Vec<(CodeUnit, CodeUnit)> = Vec::new();
213    for owner in owners {
214        for member in visibility.visible_members_for_owner_name(file, owner, name) {
215            if !member.is_field()
216                || matches.iter().any(|(existing_owner, existing_member)| {
217                    same_visible_symbol(existing_owner, owner)
218                        && same_visible_symbol(existing_member, member)
219                })
220            {
221                continue;
222            }
223            matches.push((owner.clone(), member.clone()));
224        }
225    }
226    match matches.len() {
227        0 => UsingEnumMemberResolution::Missing,
228        1 => {
229            let (owner, member) = matches.pop().expect("one using-enum match");
230            UsingEnumMemberResolution::Resolved { owner, member }
231        }
232        _ => UsingEnumMemberResolution::Ambiguous,
233    }
234}
235
236impl ScopedUsingEnumOwners {
237    pub(crate) fn new() -> Self {
238        Self {
239            scopes: vec![Vec::new()],
240        }
241    }
242
243    pub fn enter_scope(&mut self) {
244        self.scopes.push(Vec::new());
245    }
246
247    pub fn exit_scope(&mut self) {
248        if self.scopes.len() > 1 {
249            self.scopes.pop();
250        }
251    }
252
253    pub fn import(&mut self, owner: CodeUnit) {
254        let scope = self
255            .scopes
256            .last_mut()
257            .expect("using-enum scope stack is never empty");
258        if !scope
259            .iter()
260            .any(|existing| same_visible_symbol(existing, &owner))
261        {
262            scope.push(owner);
263        }
264    }
265
266    pub fn resolve_member(
267        &self,
268        visibility: &VisibilityIndex<'_>,
269        file: &ProjectFile,
270        name: &str,
271    ) -> UsingEnumMemberResolution {
272        for scope in self.scopes.iter().rev() {
273            let resolution =
274                resolve_using_enum_member_for_owners(visibility, file, scope.iter(), name);
275            if !matches!(resolution, UsingEnumMemberResolution::Missing) {
276                return resolution;
277            }
278        }
279        UsingEnumMemberResolution::Missing
280    }
281}
282
283#[derive(Clone)]
284pub struct TargetSpec {
285    pub target: CodeUnit,
286    pub kind: TargetKind,
287    pub owner: Option<CodeUnit>,
288    pub member_name: String,
289    pub callable_arity: Option<CallableArity>,
290    pub activated_callable_arities: Vec<ActivatedCallableArity>,
291    pub param_types: Option<Vec<String>>,
292    pub enum_owner_kind: EnumOwnerKind,
293    pub owner_is_forward_declaration: bool,
294}
295
296#[derive(Clone, Copy)]
297pub struct ActivatedCallableArity {
298    pub activation_byte: usize,
299    pub arity: CallableArity,
300}
301
302#[derive(Debug, PartialEq, Eq, Hash)]
303pub struct TypeScanKey {
304    target: LogicalSymbolKey,
305    member_name: String,
306}
307
308#[derive(Clone, Debug, PartialEq, Eq, Hash)]
309struct LogicalSymbolKey {
310    kind: CodeUnitType,
311    fq_name: String,
312    signature: Option<String>,
313}
314
315struct ResolvedTypeOwner {
316    unit: CodeUnit,
317    is_forward_declaration: bool,
318}
319
320#[derive(Clone, Copy, PartialEq, Eq)]
321pub enum EnumOwnerKind {
322    Scoped,
323    Unscoped,
324    NonEnum,
325}
326
327impl TargetSpec {
328    pub fn type_scan_key(&self) -> Option<TypeScanKey> {
329        (self.kind == TargetKind::Type).then(|| TypeScanKey {
330            target: logical_symbol_key(&self.target),
331            member_name: self.member_name.clone(),
332        })
333    }
334
335    pub fn from_target(analyzer: &CppGraphSource<'_>, target: &CodeUnit) -> Option<Self> {
336        if target.is_class() {
337            return Some(Self::new(
338                target.clone(),
339                TargetKind::Type,
340                Some(target.clone()),
341                target.identifier().to_string(),
342                None,
343                None,
344            ));
345        }
346
347        if target.is_field() {
348            // A namespace (module) is not a receiver: a namespace-scoped constant such as
349            // `example::DefaultPrefix` is referenced unqualified from inside the namespace and
350            // qualified from outside, exactly like a global. Treating a module owner as a
351            // member-field owner makes the receiver/owner-context match reject every valid
352            // reference, so resolve it as a global field instead.
353            let owner = type_owner_of(analyzer, target);
354            let kind = if owner.is_some() {
355                TargetKind::MemberField
356            } else {
357                TargetKind::GlobalField
358            };
359            let enum_owner_kind = owner
360                .as_ref()
361                .map(|owner| classify_enum_owner(analyzer, owner))
362                .unwrap_or(EnumOwnerKind::NonEnum);
363            let mut spec = Self::new(
364                target.clone(),
365                kind,
366                owner,
367                target.identifier().to_string(),
368                None,
369                None,
370            );
371            spec.enum_owner_kind = enum_owner_kind;
372            return Some(spec);
373        }
374
375        if target.is_function() {
376            // Free functions declared inside a namespace have a module owner; that namespace is
377            // not a call receiver, so resolve them as free functions rather than methods.
378            let owner_resolution = type_owner_resolution(analyzer, target)
379                .or_else(|| target_forward_owner_resolution(analyzer, target));
380            let owner_is_forward_declaration = owner_resolution
381                .as_ref()
382                .is_some_and(|owner| owner.is_forward_declaration);
383            let owner = owner_resolution.map(|owner| owner.unit);
384            let kind = if owner.as_ref().is_some_and(|owner| {
385                target.identifier() == owner.identifier()
386                    || analyzer
387                        .cpp
388                        .and_then(|cpp| cpp.template_metadata(owner))
389                        .is_some_and(|metadata| metadata.primary_name == target.identifier())
390            }) {
391                TargetKind::Constructor
392            } else if owner.is_some() {
393                TargetKind::Method
394            } else {
395                TargetKind::FreeFunction
396            };
397            let mut spec = Self::new(
398                target.clone(),
399                kind,
400                owner,
401                target.identifier().to_string(),
402                Some(cpp_callable_arity(analyzer, target)),
403                target.signature().and_then(cpp_signature_param_types),
404            );
405            spec.owner_is_forward_declaration = owner_is_forward_declaration;
406            return Some(spec);
407        }
408
409        None
410    }
411
412    pub fn with_visible_callable_arities<'a>(
413        &'a self,
414        analyzer: &CppGraphSource<'_>,
415        cpp: &dyn CppSource,
416        visibility: &VisibilityIndex<'_>,
417        file: &ProjectFile,
418        prepared: &PreparedSyntaxTree,
419    ) -> Cow<'a, Self> {
420        let macro_parameter_arity =
421            visibility.callable_parameter_macro_arity(&self.target, self.target.signature());
422        let activated_callable_arities =
423            visibility.callable_arities_for_target(analyzer, cpp, file, prepared, self);
424        if macro_parameter_arity.is_none() && activated_callable_arities.is_empty() {
425            return Cow::Borrowed(self);
426        }
427        let mut effective = self.clone();
428        if let Some(macro_parameter_arity) = macro_parameter_arity {
429            effective.callable_arity = Some(macro_parameter_arity);
430        }
431        effective.activated_callable_arities = activated_callable_arities;
432        Cow::Owned(effective)
433    }
434
435    pub fn callable_arity_at(&self, byte: usize) -> Option<CallableArity> {
436        let base = self.callable_arity?;
437        Some(
438            self.activated_callable_arities
439                .iter()
440                .filter(|candidate| candidate.activation_byte <= byte)
441                .fold(base, |arity, candidate| {
442                    merge_compatible_callable_arities(arity, candidate.arity).unwrap_or(arity)
443                }),
444        )
445    }
446
447    pub fn new(
448        target: CodeUnit,
449        kind: TargetKind,
450        owner: Option<CodeUnit>,
451        member_name: String,
452        callable_arity: Option<CallableArity>,
453        param_types: Option<Vec<String>>,
454    ) -> Self {
455        Self {
456            target,
457            kind,
458            owner,
459            member_name,
460            callable_arity,
461            activated_callable_arities: Vec::new(),
462            param_types,
463            enum_owner_kind: EnumOwnerKind::NonEnum,
464            owner_is_forward_declaration: false,
465        }
466    }
467}
468
469fn logical_symbol_key(unit: &CodeUnit) -> LogicalSymbolKey {
470    LogicalSymbolKey {
471        kind: unit.kind(),
472        fq_name: unit.fq_name(),
473        signature: unit.signature().map(str::to_string),
474    }
475}
476
477fn classify_enum_owner(analyzer: &CppGraphSource<'_>, owner: &CodeUnit) -> EnumOwnerKind {
478    let classify = |source: &str| {
479        let source = source.trim_start();
480        if source.starts_with("enum class ") || source.starts_with("enum struct ") {
481            Some(EnumOwnerKind::Scoped)
482        } else if source.starts_with("enum ") {
483            Some(EnumOwnerKind::Unscoped)
484        } else {
485            None
486        }
487    };
488    owner
489        .signature()
490        .and_then(classify)
491        .or_else(|| {
492            analyzer
493                .get_source(owner, false)
494                .as_deref()
495                .and_then(classify)
496        })
497        .unwrap_or(EnumOwnerKind::NonEnum)
498}
499
500#[derive(Clone, PartialEq, Eq, Hash)]
501pub struct CppScanBinding {
502    pub unit: Option<CodeUnit>,
503    pub type_name: Option<String>,
504    pub indirection: i32,
505}
506
507impl CppScanBinding {
508    pub fn from_unit(unit: CodeUnit, indirection: i32) -> Self {
509        Self {
510            type_name: Some(cpp_name_for(&unit)),
511            unit: Some(unit),
512            indirection,
513        }
514    }
515
516    pub fn from_type_name(type_name: String, unit: Option<CodeUnit>, indirection: i32) -> Self {
517        Self {
518            type_name: Some(type_name),
519            unit,
520            indirection,
521        }
522    }
523
524    pub fn as_arg_type(&self) -> Option<CppArgType> {
525        let name = self
526            .type_name
527            .clone()
528            .or_else(|| self.unit.as_ref().map(cpp_name_for))?;
529        Some(CppArgType {
530            name,
531            unit: self.unit.clone(),
532            indirection: self.indirection,
533            pointee_const: false,
534        })
535    }
536}
537
538type AliasCell = Arc<OnceLock<Box<[CppAlias]>>>;
539type VisibleParserAliasTargetNamesCell = Arc<OnceLock<HashMap<String, HashSet<String>>>>;
540pub type OrdinaryTypeImportCell = Arc<EffectiveUsingIndex>;
541pub type MacroEventCell = Arc<OnceLock<Box<[MacroEvent]>>>;
542type MacroIncludeProtectionCell = Arc<OnceLock<MacroIncludeProtection>>;
543pub type MacroEnvironmentCursorCell = Arc<Mutex<MacroEnvironmentCursor>>;
544type MacroReplacementCache = HashMap<(ProjectFile, usize), Arc<ParsedMacroReplacement>>;
545
546#[derive(Clone, Default)]
547pub struct MacroEnvironment {
548    bindings: HashMap<String, MacroBinding>,
549    unknown_names: bool,
550    applied_pragma_once_files: HashSet<ProjectFile>,
551    maybe_applied_pragma_once_files: HashSet<ProjectFile>,
552}
553
554#[derive(Default)]
555pub struct MacroEnvironmentCursor {
556    frontier: usize,
557    environment: Arc<MacroEnvironment>,
558}
559
560impl MacroEnvironment {
561    fn binding(&self, name: &str) -> Option<&MacroBinding> {
562        self.bindings.get(name)
563    }
564
565    fn may_bind(&self, name: &str) -> bool {
566        self.bindings.contains_key(name) || self.unknown_names
567    }
568
569    fn insert(&mut self, name: String, binding: MacroBinding) {
570        self.bindings.insert(name, binding);
571    }
572
573    fn remove(&mut self, name: &str) {
574        self.bindings.remove(name);
575    }
576
577    fn mark_unknown_names(&mut self, source: &ProjectFile, byte: usize) {
578        for binding in self.bindings.values_mut() {
579            *binding = MacroBinding::ambiguous(source, byte);
580        }
581        self.unknown_names = true;
582    }
583}
584
585#[derive(Clone)]
586pub enum EffectiveUsingTarget {
587    Ordinary {
588        name: String,
589        target_components: Vec<String>,
590        global: bool,
591    },
592    Namespace {
593        namespace_components: Vec<String>,
594        global: bool,
595    },
596}
597
598#[derive(Clone)]
599pub struct OrdinaryTypeImport {
600    pub target: EffectiveUsingTarget,
601    pub source: ProjectFile,
602    pub declaration_byte: usize,
603    pub scope_start: usize,
604    pub scope_end: usize,
605    pub scope_depth: usize,
606    pub lexical_depth: usize,
607    pub declaration_namespace: Vec<String>,
608    pub namespace_scope: Option<Vec<String>>,
609    pub resolved_target_components: Option<Vec<String>>,
610    pub required_guards: HashSet<PreprocessorGuard>,
611}
612
613#[derive(Clone)]
614pub struct ConditionalIncludeProjection {
615    pub activation_byte: usize,
616    pub required_guards: HashSet<PreprocessorGuard>,
617}
618
619#[derive(Default)]
620pub struct SourceUsingIndex {
621    pub ordinary_by_name: HashMap<String, Vec<OrdinaryTypeImport>>,
622    pub directives: Vec<OrdinaryTypeImport>,
623}
624
625#[derive(Default)]
626pub struct ProjectUsingIndex {
627    pub ordinary_by_name: HashMap<String, Vec<OrdinaryTypeImport>>,
628    pub directives: Vec<OrdinaryTypeImport>,
629}
630
631type EffectiveUsingProjectionCell = Arc<OnceLock<Arc<[OrdinaryTypeImport]>>>;
632
633pub struct EffectiveUsingIndex {
634    projected_by_name: Mutex<HashMap<String, EffectiveUsingProjectionCell>>,
635}
636
637impl EffectiveUsingIndex {
638    fn new(_root: ProjectFile) -> Self {
639        Self {
640            projected_by_name: Mutex::new(HashMap::default()),
641        }
642    }
643
644    pub fn projection_cell(&self, name: &str) -> EffectiveUsingProjectionCell {
645        self.projected_by_name
646            .lock()
647            .expect("C++ effective-using projection cache poisoned")
648            .entry(name.to_string())
649            .or_default()
650            .clone()
651    }
652}
653
654pub enum OrdinaryTypeImportResolution {
655    Resolved {
656        target: CodeUnit,
657        target_components: Vec<String>,
658        lexical_depth: usize,
659        is_direct: bool,
660    },
661    Ambiguous {
662        lexical_depth: usize,
663    },
664    Missing,
665}
666
667type CallableReferenceSpecCell = Arc<OnceLock<Option<TargetSpec>>>;
668type ConditionalIncludeProjectionCache =
669    HashMap<(ProjectFile, ProjectFile), Arc<[ConditionalIncludeProjection]>>;
670type VisibleParserAliasNameSetCell = Arc<OnceLock<HashSet<String>>>;
671type IndexedStructuralClassScopeCache = HashMap<(ProjectFile, usize, usize), Option<Vec<String>>>;
672type IndexedEnclosingOwnerScopeCache = HashMap<(ProjectFile, usize, usize), Option<Vec<String>>>;
673
674/// Per-query C++ visibility facts.
675///
676/// The analyzer is *borrowed*, never cloned: `TreeSitterAnalyzer::clone` gives
677/// the clone a fresh, empty `QueryReadCache` on purpose (clones cross
678/// generations and overlays, where another generation's hydrated states would
679/// be wrong). An index that owned a clone would therefore see an inactive read
680/// cache for every `prepared_syntax` call it makes, re-reading and re-parsing
681/// the same source from the store once per candidate instead of once per query
682/// — the #1175 blow-up, where one scan re-parsed a 4.8 MB generated header
683/// tens of thousands of times.
684pub struct VisibilityIndex<'a> {
685    cpp: &'a dyn CppSource,
686    pub visible_by_file: HashMap<ProjectFile, HashSet<CodeUnit>>,
687    visible_by_identifier: HashMap<ProjectFile, HashMap<String, Vec<CodeUnit>>>,
688    global_field_internal_linkage: HashMap<CodeUnit, bool>,
689    visible_source_files_by_root: HashMap<ProjectFile, HashSet<ProjectFile>>,
690    alias_cells: Mutex<HashMap<ProjectFile, AliasCell>>,
691    visible_parser_alias_name_sets: RwLock<HashMap<ProjectFile, VisibleParserAliasNameSetCell>>,
692    visible_parser_alias_target_names:
693        Mutex<HashMap<ProjectFile, VisibleParserAliasTargetNamesCell>>,
694    ordinary_type_import_cells: Mutex<HashMap<ProjectFile, OrdinaryTypeImportCell>>,
695    project_using_index: OnceLock<ProjectUsingIndex>,
696    callable_reference_specs:
697        Mutex<HashMap<(ProjectFile, LogicalSymbolKey), CallableReferenceSpecCell>>,
698    include_activation_cells: Mutex<HashMap<(ProjectFile, ProjectFile), Option<usize>>>,
699    conditional_include_projection_cells: Mutex<ConditionalIncludeProjectionCache>,
700    #[cfg(any(test, feature = "test-support"))]
701    include_activation_build_count: AtomicUsize,
702    #[cfg(any(test, feature = "test-support"))]
703    using_donor_activation_count: AtomicUsize,
704    #[cfg(any(test, feature = "test-support"))]
705    using_namespace_lookup_count: AtomicUsize,
706    #[cfg(any(test, feature = "test-support"))]
707    using_name_candidate_inspection_count: AtomicUsize,
708    #[cfg(any(test, feature = "test-support"))]
709    using_source_index_walk_count: AtomicUsize,
710    #[cfg(any(test, feature = "test-support"))]
711    callable_reference_spec_build_count: AtomicUsize,
712    #[cfg(any(test, feature = "test-support"))]
713    alias_source_parse_counts: Mutex<HashMap<ProjectFile, usize>>,
714    #[cfg(any(test, feature = "test-support"))]
715    visible_parser_alias_name_set_build_count: AtomicUsize,
716    #[cfg(any(test, feature = "test-support"))]
717    visible_parser_alias_target_names_build_count: AtomicUsize,
718    field_type_facts: Mutex<HashMap<CodeUnit, Option<DeclaredFieldTypeFact>>>,
719    structured_alias_targets: Mutex<HashMap<CodeUnit, Option<StructuredAliasTarget>>>,
720    indexed_structural_class_scopes: Mutex<IndexedStructuralClassScopeCache>,
721    indexed_enclosing_owner_scopes: Mutex<IndexedEnclosingOwnerScopeCache>,
722    precise_parent_cache: Mutex<HashMap<CodeUnit, Option<CodeUnit>>>,
723    macro_event_cells: Mutex<HashMap<ProjectFile, MacroEventCell>>,
724    pub macro_include_protection_cells: Mutex<HashMap<ProjectFile, MacroIncludeProtectionCell>>,
725    // A forward cursor is useful only while its caller visits one source in byte order. The
726    // authoritative differential shares this index across target workers, whose frontiers can
727    // interleave arbitrarily, so sharing one cursor per file would serialize the include replay
728    // and repeatedly reset it. Keep one bounded cursor per participating worker instead; the
729    // immutable event and parse caches above remain shared.
730    pub macro_environment_cursors:
731        Mutex<HashMap<(ProjectFile, ThreadId), MacroEnvironmentCursorCell>>,
732    macro_replacements: Mutex<MacroReplacementCache>,
733    callable_parameter_macro_arities: Mutex<HashMap<(ProjectFile, String), Option<CallableArity>>>,
734    #[cfg(any(test, feature = "test-support"))]
735    pub macro_replacement_parse_count: AtomicUsize,
736    #[cfg(any(test, feature = "test-support"))]
737    pub macro_event_application_count: AtomicUsize,
738    #[cfg(any(test, feature = "test-support"))]
739    pub macro_environment_copy_count: AtomicUsize,
740    cpp_template_metadata: HashMap<CodeUnit, CppTemplateMetadata>,
741    cpp_template_families: HashMap<String, Vec<CodeUnit>>,
742    #[cfg(any(test, feature = "test-support"))]
743    qualified_candidate_inspections: AtomicUsize,
744    #[cfg(any(test, feature = "test-support"))]
745    target_preserving_type_resolution_count: AtomicUsize,
746}
747
748#[derive(Clone, Debug, PartialEq, Eq, Hash)]
749pub enum PreprocessorGuard {
750    Defined(String),
751    Undefined(String),
752    Expression(String),
753    NegatedExpression(String),
754    Constant(bool),
755}
756
757impl PreprocessorGuard {
758    fn negated(&self) -> Self {
759        match self {
760            Self::Defined(name) => Self::Undefined(name.clone()),
761            Self::Undefined(name) => Self::Defined(name.clone()),
762            Self::Expression(expression) => Self::NegatedExpression(expression.clone()),
763            Self::NegatedExpression(expression) => Self::Expression(expression.clone()),
764            Self::Constant(value) => Self::Constant(!value),
765        }
766    }
767
768    fn may_depend_on_macro(&self, macro_name: &str) -> bool {
769        match self {
770            Self::Defined(name) | Self::Undefined(name) => name == macro_name,
771            // The expression has already been isolated structurally by
772            // tree-sitter, but its full preprocessor semantics are outside the
773            // analyzer's guard model. Any macro mutation can therefore change
774            // its truth value.
775            Self::Expression(_) | Self::NegatedExpression(_) => true,
776            Self::Constant(_) => false,
777        }
778    }
779}
780
781#[derive(Clone, PartialEq, Eq)]
782pub enum MacroDefinition {
783    Object {
784        replacement: String,
785    },
786    Function {
787        parameters: Vec<String>,
788        replacement: String,
789    },
790    Unsupported,
791}
792
793#[derive(Clone, Debug, PartialEq, Eq)]
794pub enum MacroIncludeProtection {
795    MacroGuard(String),
796    PragmaOnce,
797    None,
798}
799
800enum ParsedMacroReplacement {
801    Parsed { source: String, tree: Tree },
802    Unsupported,
803}
804
805#[derive(Clone, PartialEq, Eq)]
806pub struct MacroBinding {
807    source: ProjectFile,
808    declaration_byte: usize,
809    definition: MacroDefinition,
810    exact: bool,
811}
812
813impl MacroBinding {
814    fn ambiguous(source: &ProjectFile, declaration_byte: usize) -> Self {
815        Self {
816            source: source.clone(),
817            declaration_byte,
818            definition: MacroDefinition::Unsupported,
819            exact: false,
820        }
821    }
822
823    fn is_exact(&self) -> bool {
824        self.exact
825    }
826}
827
828#[derive(Clone)]
829pub enum MacroEvent {
830    Define {
831        name: String,
832        binding: MacroBinding,
833        byte: usize,
834        conditional: bool,
835    },
836    Undef {
837        name: String,
838        byte: usize,
839        conditional: bool,
840    },
841    Include {
842        targets: Vec<ProjectFile>,
843        byte: usize,
844        conditional: bool,
845    },
846    Invalidate {
847        byte: usize,
848    },
849}
850
851impl MacroEvent {
852    pub fn byte(&self) -> usize {
853        match self {
854            Self::Define { byte, .. }
855            | Self::Undef { byte, .. }
856            | Self::Include { byte, .. }
857            | Self::Invalidate { byte } => *byte,
858        }
859    }
860}
861
862#[derive(Clone, Copy, Debug, PartialEq, Eq)]
863pub enum CallArityEvidence {
864    Exact(usize),
865    Unknown,
866}
867
868impl CallArityEvidence {
869    pub fn exact(self) -> Option<usize> {
870        match self {
871            Self::Exact(arity) => Some(arity),
872            Self::Unknown => None,
873        }
874    }
875
876    pub fn accepts(self, expected: CallableArity) -> Option<bool> {
877        self.exact().map(|arity| expected.accepts(arity))
878    }
879}
880
881#[derive(Clone)]
882struct DeclaredFieldTypeFact {
883    type_text: String,
884    indirection: i32,
885    template_arguments: Option<Vec<CppTemplateExpression>>,
886}
887
888#[derive(Clone)]
889enum StructuredAliasTarget {
890    Builtin,
891    Named {
892        components: Vec<String>,
893        global: bool,
894        arguments: Option<Vec<CppTemplateExpression>>,
895    },
896}
897
898struct CppAlias {
899    name: String,
900    target: String,
901    namespace: Option<String>,
902}
903
904type ReceiverResolver<'a> = dyn for<'tree> Fn(Node<'tree>, &str) -> Vec<CodeUnit> + 'a;
905
906/// Why template-argument resolution failed. Definition diagnostics render
907/// each mode differently; graph scans only care that the resolution is
908/// unproven and match `Err(_)`.
909#[derive(Debug, Clone, PartialEq, Eq)]
910pub enum CppTemplateResolutionError {
911    /// A template alias expansion revisited `alias`.
912    AliasCycle { alias: CodeUnit },
913    /// The explicit arguments do not bind to the declared template parameters.
914    ArgumentBinding,
915    /// Bound arguments do not substitute into the alias target's arguments.
916    Substitution,
917    /// No visible primary template declaration could be selected and
918    /// reconciled for the specialization family.
919    PrimarySelection,
920    /// More than one applicable specialization remains and none is strictly
921    /// more specialized than every other candidate.
922    AmbiguousSpecialization { candidates: Vec<CodeUnit> },
923}
924
925/// The ambiguity candidates, deduplicated to one representative per visible
926/// symbol so a diagnostic lists each contender once.
927fn distinct_visible_symbols<'u>(units: impl Iterator<Item = &'u CodeUnit>) -> Vec<CodeUnit> {
928    let mut distinct: Vec<CodeUnit> = Vec::new();
929    for unit in units {
930        if !distinct
931            .iter()
932            .any(|existing| same_visible_symbol(existing, unit))
933        {
934            distinct.push(unit.clone());
935        }
936    }
937    distinct
938}
939
940impl<'a> VisibilityIndex<'a> {
941    pub fn cpp(&self) -> &'a dyn CppSource {
942        self.cpp
943    }
944
945    /// A [`VisibilityIndex`] over a caller-supplied visible-declaration map,
946    /// bypassing the include-closure walk [`Self::build`] performs.
947    ///
948    /// The resolver's own unit tests drive the type-resolution paths against a
949    /// hand-written visibility table; they live in `brokk-bifrost-analysis`
950    /// because they need a real `CppAnalyzer`, so the struct literal they used
951    /// to write inline is here instead of thirty-three public fields.
952    #[cfg(any(test, feature = "test-support"))]
953    pub fn from_visible_files_for_test(
954        cpp: &'a dyn CppSource,
955        visible_by_file: HashMap<ProjectFile, HashSet<CodeUnit>>,
956    ) -> Self {
957        let visible_source_files_by_root = visible_by_file
958            .iter()
959            .map(|(file, visible)| {
960                (
961                    file.clone(),
962                    visible
963                        .iter()
964                        .map(|unit| unit.source().clone())
965                        .chain(std::iter::once(file.clone()))
966                        .collect(),
967                )
968            })
969            .collect();
970        let mut global_field_internal_linkage = HashMap::default();
971        Self {
972            cpp,
973            visible_by_identifier: build_visible_identifier_index(
974                &CppGraphSource::from_source(cpp),
975                &visible_by_file,
976                &visible_source_files_by_root,
977                &mut global_field_internal_linkage,
978            ),
979            global_field_internal_linkage,
980            visible_by_file,
981            visible_source_files_by_root,
982            alias_cells: Mutex::new(HashMap::default()),
983            visible_parser_alias_name_sets: RwLock::new(HashMap::default()),
984            visible_parser_alias_target_names: Mutex::new(HashMap::default()),
985            ordinary_type_import_cells: Mutex::new(HashMap::default()),
986            project_using_index: OnceLock::new(),
987            callable_reference_specs: Mutex::new(HashMap::default()),
988            include_activation_cells: Mutex::new(HashMap::default()),
989            conditional_include_projection_cells: Mutex::new(HashMap::default()),
990            include_activation_build_count: AtomicUsize::new(0),
991            using_donor_activation_count: AtomicUsize::new(0),
992            using_namespace_lookup_count: AtomicUsize::new(0),
993            using_name_candidate_inspection_count: AtomicUsize::new(0),
994            using_source_index_walk_count: AtomicUsize::new(0),
995            callable_reference_spec_build_count: AtomicUsize::new(0),
996            alias_source_parse_counts: Mutex::new(HashMap::default()),
997            visible_parser_alias_name_set_build_count: AtomicUsize::new(0),
998            visible_parser_alias_target_names_build_count: AtomicUsize::new(0),
999            field_type_facts: Mutex::new(HashMap::default()),
1000            structured_alias_targets: Mutex::new(HashMap::default()),
1001            indexed_structural_class_scopes: Mutex::new(HashMap::default()),
1002            indexed_enclosing_owner_scopes: Mutex::new(HashMap::default()),
1003            precise_parent_cache: Mutex::new(HashMap::default()),
1004            macro_event_cells: Mutex::new(HashMap::default()),
1005            macro_include_protection_cells: Mutex::new(HashMap::default()),
1006            macro_environment_cursors: Mutex::new(HashMap::default()),
1007            macro_replacements: Mutex::new(HashMap::default()),
1008            callable_parameter_macro_arities: Mutex::new(HashMap::default()),
1009            macro_replacement_parse_count: AtomicUsize::new(0),
1010            macro_event_application_count: AtomicUsize::new(0),
1011            macro_environment_copy_count: AtomicUsize::new(0),
1012            cpp_template_metadata: HashMap::default(),
1013            cpp_template_families: HashMap::default(),
1014            qualified_candidate_inspections: AtomicUsize::new(0),
1015            target_preserving_type_resolution_count: AtomicUsize::new(0),
1016        }
1017    }
1018
1019    /// The index's own C++ source, in the dispatching-analyzer shape.
1020    ///
1021    /// Four resolution paths reach the workspace through the C++ analyzer they
1022    /// already hold rather than through the analyzer the query was issued
1023    /// against; before the move they passed `&CppAnalyzer` straight into a
1024    /// `&dyn IAnalyzer` parameter. See [`CppGraphSource::from_source`].
1025    fn cpp_source(&self) -> CppGraphSource<'a> {
1026        CppGraphSource::from_source(self.cpp)
1027    }
1028
1029    pub fn build(
1030        cpp: &'a dyn CppSource,
1031        analyzer: &CppGraphSource<'_>,
1032        roots: &HashSet<ProjectFile>,
1033    ) -> Self {
1034        Self::build_with_cancellation(cpp, analyzer, roots, None)
1035    }
1036
1037    pub fn build_with_cancellation(
1038        cpp: &'a dyn CppSource,
1039        analyzer: &CppGraphSource<'_>,
1040        roots: &HashSet<ProjectFile>,
1041        cancellation: Option<&CancellationToken>,
1042    ) -> Self {
1043        let include_targets = cpp.include_target_index();
1044        let VisibilityData {
1045            visible_by_file,
1046            visible_source_files_by_root,
1047        } = build_visibility_data(
1048            roots,
1049            cancellation,
1050            |file| {
1051                let imports = analyzer.import_statements(file);
1052                cpp_include_paths(&imports)
1053                    .into_iter()
1054                    .flat_map(|include| {
1055                        resolve_include_targets_with_index(file, &include, include_targets)
1056                    })
1057                    .collect()
1058            },
1059            |file| analyzer.declarations(file),
1060        );
1061        let mut global_field_internal_linkage = HashMap::default();
1062        let visible_by_identifier = build_visible_identifier_index(
1063            analyzer,
1064            &visible_by_file,
1065            &visible_source_files_by_root,
1066            &mut global_field_internal_linkage,
1067        );
1068        let mut cpp_template_metadata = HashMap::default();
1069        for unit in visible_by_file
1070            .values()
1071            .flatten()
1072            .filter(|unit| unit.is_class())
1073        {
1074            if cpp_template_metadata.contains_key(unit) {
1075                continue;
1076            }
1077            if let Some(metadata) = cpp.template_metadata(unit) {
1078                cpp_template_metadata.insert(unit.clone(), metadata);
1079            }
1080        }
1081        let mut cpp_template_families: HashMap<String, Vec<CodeUnit>> = HashMap::default();
1082        for (unit, metadata) in &cpp_template_metadata {
1083            cpp_template_families
1084                .entry(metadata.primary_fq_name.clone())
1085                .or_default()
1086                .push(unit.clone());
1087        }
1088        Self {
1089            cpp,
1090            visible_by_file,
1091            visible_by_identifier,
1092            global_field_internal_linkage,
1093            visible_source_files_by_root,
1094            alias_cells: Mutex::new(HashMap::default()),
1095            visible_parser_alias_name_sets: RwLock::new(HashMap::default()),
1096            visible_parser_alias_target_names: Mutex::new(HashMap::default()),
1097            ordinary_type_import_cells: Mutex::new(HashMap::default()),
1098            project_using_index: OnceLock::new(),
1099            callable_reference_specs: Mutex::new(HashMap::default()),
1100            include_activation_cells: Mutex::new(HashMap::default()),
1101            conditional_include_projection_cells: Mutex::new(HashMap::default()),
1102            #[cfg(any(test, feature = "test-support"))]
1103            include_activation_build_count: AtomicUsize::new(0),
1104            #[cfg(any(test, feature = "test-support"))]
1105            using_donor_activation_count: AtomicUsize::new(0),
1106            #[cfg(any(test, feature = "test-support"))]
1107            using_namespace_lookup_count: AtomicUsize::new(0),
1108            #[cfg(any(test, feature = "test-support"))]
1109            using_name_candidate_inspection_count: AtomicUsize::new(0),
1110            #[cfg(any(test, feature = "test-support"))]
1111            using_source_index_walk_count: AtomicUsize::new(0),
1112            #[cfg(any(test, feature = "test-support"))]
1113            callable_reference_spec_build_count: AtomicUsize::new(0),
1114            #[cfg(any(test, feature = "test-support"))]
1115            alias_source_parse_counts: Mutex::new(HashMap::default()),
1116            #[cfg(any(test, feature = "test-support"))]
1117            visible_parser_alias_name_set_build_count: AtomicUsize::new(0),
1118            #[cfg(any(test, feature = "test-support"))]
1119            visible_parser_alias_target_names_build_count: AtomicUsize::new(0),
1120            field_type_facts: Mutex::new(HashMap::default()),
1121            structured_alias_targets: Mutex::new(HashMap::default()),
1122            indexed_structural_class_scopes: Mutex::new(HashMap::default()),
1123            indexed_enclosing_owner_scopes: Mutex::new(HashMap::default()),
1124            precise_parent_cache: Mutex::new(HashMap::default()),
1125            macro_event_cells: Mutex::new(HashMap::default()),
1126            macro_include_protection_cells: Mutex::new(HashMap::default()),
1127            macro_environment_cursors: Mutex::new(HashMap::default()),
1128            macro_replacements: Mutex::new(HashMap::default()),
1129            callable_parameter_macro_arities: Mutex::new(HashMap::default()),
1130            #[cfg(any(test, feature = "test-support"))]
1131            macro_replacement_parse_count: AtomicUsize::new(0),
1132            #[cfg(any(test, feature = "test-support"))]
1133            macro_event_application_count: AtomicUsize::new(0),
1134            #[cfg(any(test, feature = "test-support"))]
1135            macro_environment_copy_count: AtomicUsize::new(0),
1136            cpp_template_metadata,
1137            cpp_template_families,
1138            #[cfg(any(test, feature = "test-support"))]
1139            qualified_candidate_inspections: AtomicUsize::new(0),
1140            #[cfg(any(test, feature = "test-support"))]
1141            target_preserving_type_resolution_count: AtomicUsize::new(0),
1142        }
1143    }
1144
1145    pub fn is_visible(&self, file: &ProjectFile, target: &CodeUnit) -> bool {
1146        if file == target.source() {
1147            return true;
1148        }
1149        if self.global_field_has_internal_linkage(target) {
1150            return self
1151                .visible_source_files_by_root
1152                .get(file)
1153                .is_some_and(|sources| sources.contains(target.source()));
1154        }
1155        self.visible_by_file
1156            .get(file)
1157            .is_some_and(|visible| visible.iter().any(|unit| same_visible_symbol(unit, target)))
1158    }
1159
1160    fn global_field_has_internal_linkage(&self, unit: &CodeUnit) -> bool {
1161        self.global_field_internal_linkage
1162            .get(unit)
1163            .copied()
1164            .unwrap_or_else(|| cpp_global_field_has_internal_linkage(&self.cpp_source(), unit))
1165    }
1166
1167    pub fn call_arity_evidence(
1168        &self,
1169        file: &ProjectFile,
1170        call: Node<'_>,
1171        source: &str,
1172    ) -> CallArityEvidence {
1173        let Some(arguments) = call
1174            .child_by_field_name("arguments")
1175            .or_else(|| call.child_by_field_name("parameters"))
1176            .or_else(|| call.child_by_field_name("value"))
1177            .or_else(|| first_named_child_of_kind(call, "argument_list"))
1178            .or_else(|| first_named_child_of_kind(call, "initializer_list"))
1179        else {
1180            return CallArityEvidence::Exact(0);
1181        };
1182        let arguments = argument_children(arguments).collect::<Vec<_>>();
1183        if arguments
1184            .iter()
1185            .all(|argument| !argument_shape_may_change_arity(*argument))
1186        {
1187            return CallArityEvidence::Exact(arguments.len());
1188        }
1189        let environment = self.macro_environment(file, call.start_byte());
1190        let mut stack = Vec::new();
1191        let mut total = 0usize;
1192        for argument in arguments {
1193            if !macro_expansion_shape_is_safe(argument, source, &[], &environment) {
1194                return CallArityEvidence::Unknown;
1195            }
1196            let CallArityEvidence::Exact(spread) =
1197                self.argument_arity_evidence(argument, source, &environment, &mut stack)
1198            else {
1199                return CallArityEvidence::Unknown;
1200            };
1201            total += spread;
1202        }
1203        CallArityEvidence::Exact(total)
1204    }
1205
1206    fn argument_arity_evidence(
1207        &self,
1208        argument: Node<'_>,
1209        source: &str,
1210        environment: &MacroEnvironment,
1211        stack: &mut Vec<(ProjectFile, usize)>,
1212    ) -> CallArityEvidence {
1213        let (name, invocation_arguments, function_like) = match argument.kind() {
1214            "identifier" => (node_text(argument, source), None, false),
1215            "call_expression" => {
1216                let Some(function) = argument.child_by_field_name("function") else {
1217                    return CallArityEvidence::Exact(1);
1218                };
1219                if function.kind() != "identifier" {
1220                    return CallArityEvidence::Exact(1);
1221                }
1222                let Some(arguments) = argument.child_by_field_name("arguments") else {
1223                    return CallArityEvidence::Exact(1);
1224                };
1225                (node_text(function, source), Some(arguments), true)
1226            }
1227            _ => return CallArityEvidence::Exact(1),
1228        };
1229        let Some(binding) = environment.binding(name) else {
1230            return if environment.unknown_names {
1231                CallArityEvidence::Unknown
1232            } else {
1233                CallArityEvidence::Exact(1)
1234            };
1235        };
1236        match (&binding.definition, invocation_arguments, function_like) {
1237            (MacroDefinition::Object { replacement }, None, false) => self
1238                .replacement_arity_evidence(
1239                    replacement,
1240                    &[],
1241                    &[],
1242                    source,
1243                    environment,
1244                    stack,
1245                    binding,
1246                ),
1247            (
1248                MacroDefinition::Function {
1249                    parameters,
1250                    replacement,
1251                },
1252                Some(arguments),
1253                true,
1254            ) => {
1255                let actuals = argument_children(arguments).collect::<Vec<_>>();
1256                if actuals.len() != parameters.len() {
1257                    CallArityEvidence::Unknown
1258                } else {
1259                    self.replacement_arity_evidence(
1260                        replacement,
1261                        parameters,
1262                        &actuals,
1263                        source,
1264                        environment,
1265                        stack,
1266                        binding,
1267                    )
1268                }
1269            }
1270            (MacroDefinition::Function { .. }, None, false) => CallArityEvidence::Exact(1),
1271            _ => CallArityEvidence::Unknown,
1272        }
1273    }
1274
1275    #[allow(clippy::too_many_arguments)]
1276    fn replacement_arity_evidence(
1277        &self,
1278        replacement: &str,
1279        parameters: &[String],
1280        actuals: &[Node<'_>],
1281        actual_source: &str,
1282        environment: &MacroEnvironment,
1283        stack: &mut Vec<(ProjectFile, usize)>,
1284        binding: &MacroBinding,
1285    ) -> CallArityEvidence {
1286        let identity = (binding.source.clone(), binding.declaration_byte);
1287        if stack.contains(&identity) || replacement.trim().is_empty() {
1288            return CallArityEvidence::Unknown;
1289        }
1290        stack.push(identity);
1291        let parsed = self.parsed_macro_replacement(binding, replacement);
1292        let evidence = (|| {
1293            let ParsedMacroReplacement::Parsed {
1294                source: sentinel,
1295                tree,
1296            } = parsed.as_ref()
1297            else {
1298                return None;
1299            };
1300            let call = first_descendant_of_kind(tree.root_node(), "call_expression")?;
1301            let arguments = call.child_by_field_name("arguments")?;
1302            let mut total = 0usize;
1303            for argument in argument_children(arguments) {
1304                if !macro_expansion_shape_is_safe(argument, sentinel, parameters, environment) {
1305                    return None;
1306                }
1307                if argument.kind() == "identifier"
1308                    && let Some(parameter_index) = parameters
1309                        .iter()
1310                        .position(|parameter| parameter == node_text(argument, sentinel))
1311                {
1312                    if !macro_expansion_shape_is_safe(
1313                        actuals[parameter_index],
1314                        actual_source,
1315                        &[],
1316                        environment,
1317                    ) {
1318                        return None;
1319                    }
1320                    let CallArityEvidence::Exact(spread) = self.argument_arity_evidence(
1321                        actuals[parameter_index],
1322                        actual_source,
1323                        environment,
1324                        stack,
1325                    ) else {
1326                        return None;
1327                    };
1328                    total += spread;
1329                    continue;
1330                }
1331                let CallArityEvidence::Exact(spread) =
1332                    self.argument_arity_evidence(argument, sentinel, environment, stack)
1333                else {
1334                    return None;
1335                };
1336                total += spread;
1337            }
1338            Some(CallArityEvidence::Exact(total))
1339        })()
1340        .unwrap_or(CallArityEvidence::Unknown);
1341        stack.pop();
1342        evidence
1343    }
1344
1345    fn parsed_macro_replacement(
1346        &self,
1347        binding: &MacroBinding,
1348        replacement: &str,
1349    ) -> Arc<ParsedMacroReplacement> {
1350        let key = (binding.source.clone(), binding.declaration_byte);
1351        let mut cache = self
1352            .macro_replacements
1353            .lock()
1354            .expect("C++ macro replacement cache poisoned");
1355        if let Some(parsed) = cache.get(&key) {
1356            return Arc::clone(parsed);
1357        }
1358        #[cfg(any(test, feature = "test-support"))]
1359        self.macro_replacement_parse_count
1360            .fetch_add(1, Ordering::Relaxed);
1361        let source =
1362            format!("void __bifrost_macro_arity() {{ __bifrost_macro_call({replacement}); }}");
1363        let mut parser = Parser::new();
1364        let parsed = parser
1365            .set_language(&tree_sitter_cpp::LANGUAGE.into())
1366            .ok()
1367            .and_then(|()| parser.parse(&source, None))
1368            .filter(|tree| !tree.root_node().has_error())
1369            .map_or(ParsedMacroReplacement::Unsupported, |tree| {
1370                ParsedMacroReplacement::Parsed { source, tree }
1371            });
1372        let parsed = Arc::new(parsed);
1373        cache.insert(key, Arc::clone(&parsed));
1374        parsed
1375    }
1376
1377    fn decode_macro_definition(node: Node<'_>, source: &str) -> MacroDefinition {
1378        let Some(value) = node.child_by_field_name("value") else {
1379            return MacroDefinition::Unsupported;
1380        };
1381        let replacement = node_text(value, source).to_string();
1382        if node.kind() == "preproc_def" {
1383            return MacroDefinition::Object { replacement };
1384        }
1385        let Some(parameters) = node.child_by_field_name("parameters") else {
1386            return MacroDefinition::Unsupported;
1387        };
1388        if (0..parameters.child_count()).any(|index| {
1389            parameters
1390                .child(index)
1391                .is_some_and(|child| child.kind() == "...")
1392        }) {
1393            return MacroDefinition::Unsupported;
1394        }
1395        let parameters = (0..parameters.named_child_count())
1396            .filter_map(|index| parameters.named_child(index))
1397            .map(|parameter| node_text(parameter, source).to_string())
1398            .collect();
1399        MacroDefinition::Function {
1400            parameters,
1401            replacement,
1402        }
1403    }
1404
1405    pub fn macro_event_cell(&self, file: &ProjectFile) -> MacroEventCell {
1406        self.macro_event_cells
1407            .lock()
1408            .expect("C++ macro event cache poisoned")
1409            .entry(file.clone())
1410            .or_default()
1411            .clone()
1412    }
1413
1414    pub fn macro_environment_cursor_cell(&self, file: &ProjectFile) -> MacroEnvironmentCursorCell {
1415        let key = (file.clone(), std::thread::current().id());
1416        self.macro_environment_cursors
1417            .lock()
1418            .expect("C++ macro environment cursor cache poisoned")
1419            .entry(key)
1420            .or_default()
1421            .clone()
1422    }
1423
1424    pub fn macro_environment(
1425        &self,
1426        file: &ProjectFile,
1427        before_byte: usize,
1428    ) -> Arc<MacroEnvironment> {
1429        let cell = self.macro_event_cell(file);
1430        let events = cell.get_or_init(|| self.collect_macro_events(file).into_boxed_slice());
1431        let frontier = events.partition_point(|event| event.byte() < before_byte);
1432        let cursor_cell = self.macro_environment_cursor_cell(file);
1433        let mut cursor = cursor_cell
1434            .lock()
1435            .expect("C++ macro environment cursor poisoned");
1436        if frontier < cursor.frontier {
1437            *cursor = MacroEnvironmentCursor::default();
1438        }
1439        if frontier > cursor.frontier {
1440            #[cfg(any(test, feature = "test-support"))]
1441            if Arc::strong_count(&cursor.environment) > 1 {
1442                self.macro_environment_copy_count
1443                    .fetch_add(1, Ordering::Relaxed);
1444            }
1445            let start = cursor.frontier;
1446            let environment = Arc::make_mut(&mut cursor.environment);
1447            let mut include_stack = HashSet::from_iter([file.clone()]);
1448            for event in &events[start..frontier] {
1449                self.apply_macro_event(file, event, environment, &mut include_stack);
1450            }
1451            cursor.frontier = frontier;
1452        }
1453        Arc::clone(&cursor.environment)
1454    }
1455
1456    pub fn object_macro_replacement_at(
1457        &self,
1458        file: &ProjectFile,
1459        name: &str,
1460        before_byte: usize,
1461    ) -> Option<String> {
1462        let environment = self.macro_environment(file, before_byte);
1463        let binding = environment.binding(name)?;
1464        if !binding.exact {
1465            return None;
1466        }
1467        match &binding.definition {
1468            MacroDefinition::Object { replacement } => Some(replacement.clone()),
1469            MacroDefinition::Function { .. } | MacroDefinition::Unsupported => None,
1470        }
1471    }
1472
1473    fn apply_macro_events(
1474        &self,
1475        file: &ProjectFile,
1476        before_byte: Option<usize>,
1477        environment: &mut MacroEnvironment,
1478        include_stack: &mut HashSet<ProjectFile>,
1479    ) {
1480        if !include_stack.insert(file.clone()) {
1481            return;
1482        }
1483        if self.cpp.prepared_syntax(file).is_none() {
1484            environment.mark_unknown_names(file, before_byte.unwrap_or_default());
1485            include_stack.remove(file);
1486            return;
1487        }
1488        match self.macro_include_protection(file) {
1489            MacroIncludeProtection::MacroGuard(guard) => match environment.binding(&guard) {
1490                Some(binding) if binding.is_exact() => {
1491                    include_stack.remove(file);
1492                    return;
1493                }
1494                Some(_) | None if environment.unknown_names => {
1495                    let mut ambiguous_seen = HashSet::default();
1496                    self.mark_macro_events_ambiguous(
1497                        file,
1498                        environment,
1499                        &mut ambiguous_seen,
1500                        file,
1501                        before_byte.unwrap_or_default(),
1502                    );
1503                    include_stack.remove(file);
1504                    return;
1505                }
1506                Some(_) => {
1507                    let mut ambiguous_seen = HashSet::default();
1508                    self.mark_macro_events_ambiguous(
1509                        file,
1510                        environment,
1511                        &mut ambiguous_seen,
1512                        file,
1513                        before_byte.unwrap_or_default(),
1514                    );
1515                    include_stack.remove(file);
1516                    return;
1517                }
1518                None => {}
1519            },
1520            MacroIncludeProtection::PragmaOnce => {
1521                if !environment.applied_pragma_once_files.insert(file.clone()) {
1522                    include_stack.remove(file);
1523                    return;
1524                }
1525                if environment.maybe_applied_pragma_once_files.remove(file) {
1526                    // A prior conditional include may already have consumed the pragma-once
1527                    // header. This unconditional include guarantees it is consumed now, but
1528                    // cannot prove whether its events occur before or after intervening local
1529                    // macro changes, so preserve the union as ambiguous.
1530                    let mut ambiguous_seen = HashSet::default();
1531                    environment.applied_pragma_once_files.remove(file);
1532                    self.mark_macro_events_ambiguous(
1533                        file,
1534                        environment,
1535                        &mut ambiguous_seen,
1536                        file,
1537                        before_byte.unwrap_or_default(),
1538                    );
1539                    environment.maybe_applied_pragma_once_files.remove(file);
1540                    environment.applied_pragma_once_files.insert(file.clone());
1541                    include_stack.remove(file);
1542                    return;
1543                }
1544            }
1545            MacroIncludeProtection::None => {}
1546        }
1547        let cell = self.macro_event_cell(file);
1548        let events = cell.get_or_init(|| self.collect_macro_events(file).into_boxed_slice());
1549        for event in events {
1550            if before_byte.is_some_and(|limit| event.byte() >= limit) {
1551                break;
1552            }
1553            self.apply_macro_event(file, event, environment, include_stack);
1554        }
1555        include_stack.remove(file);
1556    }
1557
1558    fn apply_macro_event(
1559        &self,
1560        file: &ProjectFile,
1561        event: &MacroEvent,
1562        environment: &mut MacroEnvironment,
1563        include_stack: &mut HashSet<ProjectFile>,
1564    ) {
1565        #[cfg(any(test, feature = "test-support"))]
1566        self.macro_event_application_count
1567            .fetch_add(1, Ordering::Relaxed);
1568        match event {
1569            MacroEvent::Define {
1570                name,
1571                binding,
1572                conditional,
1573                byte,
1574            } => {
1575                environment.insert(
1576                    name.clone(),
1577                    if *conditional {
1578                        MacroBinding::ambiguous(file, *byte)
1579                    } else {
1580                        binding.clone()
1581                    },
1582                );
1583            }
1584            MacroEvent::Undef {
1585                name,
1586                conditional,
1587                byte,
1588            } => {
1589                if *conditional {
1590                    if environment.binding(name).is_some() {
1591                        environment.insert(name.clone(), MacroBinding::ambiguous(file, *byte));
1592                    }
1593                } else {
1594                    environment.remove(name);
1595                }
1596            }
1597            MacroEvent::Include {
1598                targets,
1599                conditional,
1600                byte,
1601            } => {
1602                if targets.is_empty() {
1603                    environment.mark_unknown_names(file, *byte);
1604                    return;
1605                }
1606                if *conditional || targets.len() > 1 {
1607                    let mut ambiguous_seen = HashSet::default();
1608                    for target in targets {
1609                        self.mark_macro_events_ambiguous(
1610                            target,
1611                            environment,
1612                            &mut ambiguous_seen,
1613                            file,
1614                            *byte,
1615                        );
1616                    }
1617                } else if let Some(target) = targets.first() {
1618                    self.apply_macro_events(target, None, environment, include_stack);
1619                }
1620            }
1621            MacroEvent::Invalidate { byte } => {
1622                for binding in environment.bindings.values_mut() {
1623                    *binding = MacroBinding::ambiguous(file, *byte);
1624                }
1625            }
1626        }
1627    }
1628
1629    fn mark_macro_events_ambiguous(
1630        &self,
1631        file: &ProjectFile,
1632        environment: &mut MacroEnvironment,
1633        include_stack: &mut HashSet<ProjectFile>,
1634        conditional_file: &ProjectFile,
1635        conditional_byte: usize,
1636    ) {
1637        if !include_stack.insert(file.clone()) {
1638            return;
1639        }
1640        if self.cpp.prepared_syntax(file).is_none() {
1641            environment.mark_unknown_names(conditional_file, conditional_byte);
1642            return;
1643        }
1644        match self.macro_include_protection(file) {
1645            MacroIncludeProtection::MacroGuard(guard) => {
1646                if environment
1647                    .binding(&guard)
1648                    .is_some_and(MacroBinding::is_exact)
1649                {
1650                    return;
1651                }
1652            }
1653            MacroIncludeProtection::PragmaOnce => {
1654                if environment.applied_pragma_once_files.contains(file) {
1655                    return;
1656                }
1657                environment
1658                    .maybe_applied_pragma_once_files
1659                    .insert(file.clone());
1660            }
1661            MacroIncludeProtection::None => {}
1662        }
1663        let cell = self.macro_event_cell(file);
1664        let events = cell.get_or_init(|| self.collect_macro_events(file).into_boxed_slice());
1665        for event in events {
1666            #[cfg(any(test, feature = "test-support"))]
1667            self.macro_event_application_count
1668                .fetch_add(1, Ordering::Relaxed);
1669            match event {
1670                MacroEvent::Define { name, .. } => {
1671                    environment.insert(
1672                        name.clone(),
1673                        MacroBinding::ambiguous(conditional_file, conditional_byte),
1674                    );
1675                }
1676                MacroEvent::Undef { name, .. } => {
1677                    if environment.binding(name).is_some() {
1678                        environment.insert(
1679                            name.clone(),
1680                            MacroBinding::ambiguous(conditional_file, conditional_byte),
1681                        );
1682                    }
1683                }
1684                MacroEvent::Include { targets, .. } => {
1685                    if targets.is_empty() {
1686                        environment.mark_unknown_names(conditional_file, conditional_byte);
1687                        continue;
1688                    }
1689                    for target in targets {
1690                        self.mark_macro_events_ambiguous(
1691                            target,
1692                            environment,
1693                            include_stack,
1694                            conditional_file,
1695                            conditional_byte,
1696                        );
1697                    }
1698                }
1699                MacroEvent::Invalidate { .. } => {
1700                    for binding in environment.bindings.values_mut() {
1701                        *binding = MacroBinding::ambiguous(conditional_file, conditional_byte);
1702                    }
1703                }
1704            }
1705        }
1706    }
1707
1708    pub fn macro_include_protection(&self, file: &ProjectFile) -> MacroIncludeProtection {
1709        let cell = self
1710            .macro_include_protection_cells
1711            .lock()
1712            .expect("C++ include protection cache poisoned")
1713            .entry(file.clone())
1714            .or_default()
1715            .clone();
1716        cell.get_or_init(|| {
1717            self.cpp
1718                .prepared_syntax(file)
1719                .map_or(MacroIncludeProtection::None, |prepared| {
1720                    top_level_macro_include_protection(
1721                        prepared.tree().root_node(),
1722                        prepared.source(),
1723                    )
1724                })
1725        })
1726        .clone()
1727    }
1728
1729    fn collect_macro_events(&self, file: &ProjectFile) -> Vec<MacroEvent> {
1730        let Some(prepared) = self.cpp.prepared_syntax(file) else {
1731            return Vec::new();
1732        };
1733        let source = prepared.source();
1734        let mut events = Vec::new();
1735        let mut stack = vec![prepared.tree().root_node()];
1736        while let Some(node) = stack.pop() {
1737            let conditional = has_preprocessor_conditional_ancestor(node, source);
1738            match node.kind() {
1739                "preproc_def" | "preproc_function_def" => {
1740                    let Some(name) = node.child_by_field_name("name") else {
1741                        continue;
1742                    };
1743                    let name = node_text(name, source).to_string();
1744                    events.push(MacroEvent::Define {
1745                        name,
1746                        binding: MacroBinding {
1747                            source: file.clone(),
1748                            declaration_byte: node.start_byte(),
1749                            definition: Self::decode_macro_definition(node, source),
1750                            exact: true,
1751                        },
1752                        byte: node.start_byte(),
1753                        conditional,
1754                    });
1755                    continue;
1756                }
1757                "preproc_include" => {
1758                    let Some(path) = node.child_by_field_name("path") else {
1759                        events.push(MacroEvent::Include {
1760                            targets: Vec::new(),
1761                            byte: node.start_byte(),
1762                            conditional,
1763                        });
1764                        continue;
1765                    };
1766                    let targets =
1767                        structured_include_path(path, source).map_or_else(Vec::new, |path| {
1768                            resolve_include_targets_with_index(
1769                                file,
1770                                path,
1771                                self.cpp.include_target_index(),
1772                            )
1773                        });
1774                    // An unresolved angle-bracket include crosses into an external system
1775                    // boundary that is absent from the source index. It must not poison all
1776                    // later local macro evidence. Quoted/project-local and computed includes,
1777                    // by contrast, may hide indexed macro state and therefore fail closed.
1778                    if targets.is_empty() && path.kind() == "system_lib_string" {
1779                        continue;
1780                    }
1781                    events.push(MacroEvent::Include {
1782                        targets,
1783                        byte: node.start_byte(),
1784                        conditional,
1785                    });
1786                    continue;
1787                }
1788                "preproc_call" => {
1789                    let Some(directive) = node.child_by_field_name("directive") else {
1790                        continue;
1791                    };
1792                    if node_text(directive, source) != "#undef" {
1793                        continue;
1794                    }
1795                    let name = node
1796                        .child_by_field_name("argument")
1797                        .and_then(|argument| parse_preproc_identifier(node_text(argument, source)));
1798                    if let Some(name) = name {
1799                        events.push(MacroEvent::Undef {
1800                            name,
1801                            byte: node.start_byte(),
1802                            conditional,
1803                        });
1804                    } else {
1805                        events.push(MacroEvent::Invalidate {
1806                            byte: node.start_byte(),
1807                        });
1808                    }
1809                    continue;
1810                }
1811                _ => {}
1812            }
1813            for index in (0..node.named_child_count()).rev() {
1814                if let Some(child) = node.named_child(index) {
1815                    stack.push(child);
1816                }
1817            }
1818        }
1819        events.sort_by_key(MacroEvent::byte);
1820        events
1821    }
1822
1823    pub fn ordinary_type_import_cell(&self, file: &ProjectFile) -> OrdinaryTypeImportCell {
1824        self.ordinary_type_import_cells
1825            .lock()
1826            .expect("C++ ordinary type import cache poisoned")
1827            .entry(file.clone())
1828            .or_insert_with(|| Arc::new(EffectiveUsingIndex::new(file.clone())))
1829            .clone()
1830    }
1831
1832    pub fn project_using_index(
1833        &self,
1834        build: impl FnOnce() -> ProjectUsingIndex,
1835    ) -> &ProjectUsingIndex {
1836        self.project_using_index.get_or_init(build)
1837    }
1838
1839    pub fn all_visible_source_files(&self) -> Vec<ProjectFile> {
1840        let mut files = self
1841            .visible_source_files_by_root
1842            .values()
1843            .flatten()
1844            .cloned()
1845            .collect::<HashSet<_>>()
1846            .into_iter()
1847            .collect::<Vec<_>>();
1848        files.sort_by(|left, right| left.rel_path().cmp(right.rel_path()));
1849        files
1850    }
1851
1852    pub fn source_is_visible(&self, root: &ProjectFile, source: &ProjectFile) -> bool {
1853        self.visible_source_files_by_root
1854            .get(root)
1855            .is_some_and(|files| files.contains(source))
1856    }
1857
1858    fn visible_parser_alias_name_is_visible(&self, file: &ProjectFile, name: &str) -> bool {
1859        let cached = self
1860            .visible_parser_alias_name_sets
1861            .read()
1862            .expect("visible parser alias-name cache poisoned")
1863            .get(file)
1864            .cloned();
1865        let cell = if let Some(cached) = cached {
1866            cached
1867        } else {
1868            let mut cells = self
1869                .visible_parser_alias_name_sets
1870                .write()
1871                .expect("visible parser alias-name cache poisoned");
1872            Arc::clone(
1873                cells
1874                    .entry(file.clone())
1875                    .or_insert_with(|| Arc::new(OnceLock::new())),
1876            )
1877        };
1878        cell.get_or_init(|| {
1879            #[cfg(any(test, feature = "test-support"))]
1880            self.visible_parser_alias_name_set_build_count
1881                .fetch_add(1, Ordering::Relaxed);
1882            let mut names = HashSet::default();
1883            let visible_files = self
1884                .visible_source_files_by_root
1885                .get(file)
1886                .cloned()
1887                .unwrap_or_else(|| HashSet::from_iter([file.clone()]));
1888            for visible_file in visible_files {
1889                let aliases = {
1890                    let mut cells = self.alias_cells.lock().expect("alias cell map lock");
1891                    Arc::clone(
1892                        cells
1893                            .entry(visible_file.clone())
1894                            .or_insert_with(|| Arc::new(OnceLock::new())),
1895                    )
1896                };
1897                for alias in aliases
1898                    .get_or_init(|| {
1899                        #[cfg(any(test, feature = "test-support"))]
1900                        {
1901                            *self
1902                                .alias_source_parse_counts
1903                                .lock()
1904                                .expect("alias source parse count lock")
1905                                .entry(visible_file.clone())
1906                                .or_default() += 1;
1907                        }
1908                        aliases_from_prepared_source(self.cpp, &visible_file).into_boxed_slice()
1909                    })
1910                    .iter()
1911                {
1912                    names.insert(alias.name.clone());
1913                }
1914            }
1915            names
1916        })
1917        .contains(name)
1918    }
1919
1920    fn visible_parser_alias_names_for_target(
1921        &self,
1922        file: &ProjectFile,
1923        target: &CodeUnit,
1924    ) -> HashSet<String> {
1925        let cell = {
1926            let mut cells = self
1927                .visible_parser_alias_target_names
1928                .lock()
1929                .expect("visible parser alias-target cache poisoned");
1930            Arc::clone(
1931                cells
1932                    .entry(file.clone())
1933                    .or_insert_with(|| Arc::new(OnceLock::new())),
1934            )
1935        };
1936        let target_name = cpp_name_for(target);
1937        cell.get_or_init(|| {
1938            #[cfg(any(test, feature = "test-support"))]
1939            self.visible_parser_alias_target_names_build_count
1940                .fetch_add(1, Ordering::Relaxed);
1941            let visible_files = self
1942                .visible_source_files_by_root
1943                .get(file)
1944                .cloned()
1945                .unwrap_or_else(|| HashSet::from_iter([file.clone()]));
1946            let mut names_by_target = HashMap::<String, HashSet<String>>::default();
1947            for visible_file in visible_files {
1948                let aliases = {
1949                    let mut cells = self.alias_cells.lock().expect("alias cell map lock");
1950                    Arc::clone(
1951                        cells
1952                            .entry(visible_file.clone())
1953                            .or_insert_with(|| Arc::new(OnceLock::new())),
1954                    )
1955                };
1956                for alias in aliases
1957                    .get_or_init(|| {
1958                        #[cfg(any(test, feature = "test-support"))]
1959                        {
1960                            *self
1961                                .alias_source_parse_counts
1962                                .lock()
1963                                .expect("alias source parse count lock")
1964                                .entry(visible_file.clone())
1965                                .or_default() += 1;
1966                        }
1967                        aliases_from_prepared_source(self.cpp, &visible_file).into_boxed_slice()
1968                    })
1969                    .iter()
1970                {
1971                    for target_name in parser_alias_target_names(alias) {
1972                        names_by_target
1973                            .entry(target_name)
1974                            .or_default()
1975                            .insert(alias.name.clone());
1976                    }
1977                }
1978            }
1979            names_by_target
1980        })
1981        .get(&target_name)
1982        .cloned()
1983        .unwrap_or_default()
1984    }
1985
1986    fn callable_arities_for_target(
1987        &self,
1988        analyzer: &CppGraphSource<'_>,
1989        cpp: &dyn CppSource,
1990        file: &ProjectFile,
1991        prepared: &PreparedSyntaxTree,
1992        spec: &TargetSpec,
1993    ) -> Vec<ActivatedCallableArity> {
1994        let Some(signature) = spec.target.signature() else {
1995            return Vec::new();
1996        };
1997        let Some(candidates) = self
1998            .visible_by_identifier
1999            .get(file)
2000            .and_then(|by_name| by_name.get(&spec.member_name))
2001        else {
2002            return Vec::new();
2003        };
2004        let differing_candidates = candidates
2005            .iter()
2006            .filter(|candidate| {
2007                candidate.is_function()
2008                    && candidate.fq_name() == spec.target.fq_name()
2009                    && candidate.signature() == Some(signature)
2010            })
2011            .filter_map(|candidate| {
2012                analyzer
2013                    .signature_metadata(candidate)
2014                    .into_iter()
2015                    .find_map(|metadata| metadata.callable_arity())
2016                    .filter(|arity| Some(*arity) != spec.callable_arity)
2017                    .map(|arity| (candidate, arity))
2018            })
2019            .collect::<Vec<_>>();
2020        if differing_candidates.is_empty() {
2021            return Vec::new();
2022        }
2023        let mut arities = Vec::with_capacity(differing_candidates.len());
2024        for (candidate, candidate_arity) in differing_candidates {
2025            let declaration_activation = if candidate.source() == file {
2026                callable_declaration_activation_in_file(analyzer, prepared, candidate, file)
2027            } else {
2028                cpp.prepared_syntax(candidate.source()).and_then(|syntax| {
2029                    callable_declaration_activation_in_file(
2030                        analyzer,
2031                        syntax.as_ref(),
2032                        candidate,
2033                        file,
2034                    )
2035                })
2036            };
2037            let Some(declaration_activation) = declaration_activation else {
2038                continue;
2039            };
2040            let activation_byte = if candidate.source() == file {
2041                Some(declaration_activation)
2042            } else {
2043                self.include_activation_for_source(cpp, file, prepared, candidate.source())
2044            };
2045            if let Some(activation_byte) = activation_byte {
2046                arities.push(ActivatedCallableArity {
2047                    activation_byte,
2048                    arity: candidate_arity,
2049                });
2050            }
2051        }
2052        arities
2053    }
2054
2055    fn callable_parameter_macro_arity(
2056        &self,
2057        target: &CodeUnit,
2058        signature: Option<&str>,
2059    ) -> Option<CallableArity> {
2060        let parameter_types = cpp_signature_param_types(signature?)?;
2061        let [macro_name] = parameter_types.as_slice() else {
2062            return None;
2063        };
2064        if macro_name.is_empty()
2065            || !macro_name
2066                .chars()
2067                .all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
2068        {
2069            return None;
2070        }
2071        let cache_key = (target.source().clone(), macro_name.clone());
2072        if let Some(cached) = self
2073            .callable_parameter_macro_arities
2074            .lock()
2075            .expect("C++ callable parameter-macro arity cache poisoned")
2076            .get(&cache_key)
2077            .copied()
2078        {
2079            return cached;
2080        }
2081        let mut visible_files = HashSet::default();
2082        collect_include_closure(
2083            &self.cpp_source(),
2084            self.cpp.include_target_index(),
2085            target.source(),
2086            &mut visible_files,
2087            None,
2088        );
2089        let mut arities = Vec::new();
2090        for visible_file in visible_files {
2091            let cell = self.macro_event_cell(&visible_file);
2092            for event in
2093                cell.get_or_init(|| self.collect_macro_events(&visible_file).into_boxed_slice())
2094            {
2095                let MacroEvent::Define { name, binding, .. } = event else {
2096                    continue;
2097                };
2098                if name != macro_name {
2099                    continue;
2100                }
2101                let MacroDefinition::Object { replacement } = &binding.definition else {
2102                    continue;
2103                };
2104                let Some(arity) = parse_macro_parameter_list_arity(replacement) else {
2105                    continue;
2106                };
2107                if !arities.contains(&arity) {
2108                    arities.push(arity);
2109                }
2110            }
2111        }
2112        let resolved = (|| {
2113            let required = arities
2114                .iter()
2115                .filter_map(|arity| (0..=arity.total()).find(|count| arity.accepts(*count)))
2116                .min()?;
2117            let total = arities.iter().map(|arity| arity.total()).max()?;
2118            let repeated = arities
2119                .iter()
2120                .any(|arity| arity.accepts(arity.total().saturating_add(1)));
2121            // Preprocessor conditions can leave more than one object-like parameter
2122            // bundle active in the target header's include closure. Preserve their
2123            // conservative callable envelope instead of choosing whichever definition
2124            // happened to be visited first.
2125            Some(CallableArity::new(required, total, repeated))
2126        })();
2127        self.callable_parameter_macro_arities
2128            .lock()
2129            .expect("C++ callable parameter-macro arity cache poisoned")
2130            .insert(cache_key, resolved);
2131        resolved
2132    }
2133
2134    pub fn include_activation_for_source(
2135        &self,
2136        cpp: &dyn CppSource,
2137        file: &ProjectFile,
2138        prepared: &PreparedSyntaxTree,
2139        donor_source: &ProjectFile,
2140    ) -> Option<usize> {
2141        let key = (file.clone(), donor_source.clone());
2142        if let Some(cached) = self
2143            .include_activation_cells
2144            .lock()
2145            .expect("C++ include activation cache poisoned")
2146            .get(&key)
2147            .copied()
2148        {
2149            return cached;
2150        }
2151        #[cfg(any(test, feature = "test-support"))]
2152        self.include_activation_build_count
2153            .fetch_add(1, Ordering::Relaxed);
2154        let activation = find_include_activation(cpp, file, prepared, donor_source);
2155        let mut cells = self
2156            .include_activation_cells
2157            .lock()
2158            .expect("C++ include activation cache poisoned");
2159        *cells.entry(key).or_insert(activation)
2160    }
2161
2162    pub fn conditional_include_projections_for_source(
2163        &self,
2164        file: &ProjectFile,
2165        prepared: &PreparedSyntaxTree,
2166        donor_source: &ProjectFile,
2167    ) -> Arc<[ConditionalIncludeProjection]> {
2168        let key = (file.clone(), donor_source.clone());
2169        if let Some(cached) = self
2170            .conditional_include_projection_cells
2171            .lock()
2172            .expect("C++ conditional include projection cache poisoned")
2173            .get(&key)
2174            .cloned()
2175        {
2176            return cached;
2177        }
2178        let projections: Arc<[ConditionalIncludeProjection]> =
2179            find_conditional_include_projections(self.cpp, file, prepared, donor_source).into();
2180        self.conditional_include_projection_cells
2181            .lock()
2182            .expect("C++ conditional include projection cache poisoned")
2183            .entry(key)
2184            .or_insert_with(|| Arc::clone(&projections))
2185            .clone()
2186    }
2187
2188    #[cfg(any(test, feature = "test-support"))]
2189    pub fn include_activation_build_count_for_test(&self) -> usize {
2190        self.include_activation_build_count.load(Ordering::Relaxed)
2191    }
2192
2193    #[cfg(any(test, feature = "test-support"))]
2194    pub fn note_using_donor_activation_for_test(&self) {
2195        self.using_donor_activation_count
2196            .fetch_add(1, Ordering::Relaxed);
2197    }
2198
2199    #[cfg(not(any(test, feature = "test-support")))]
2200    pub fn note_using_donor_activation_for_test(&self) {}
2201
2202    #[cfg(any(test, feature = "test-support"))]
2203    pub fn note_using_namespace_lookup_for_test(&self) {
2204        self.using_namespace_lookup_count
2205            .fetch_add(1, Ordering::Relaxed);
2206    }
2207
2208    #[cfg(not(any(test, feature = "test-support")))]
2209    pub fn note_using_namespace_lookup_for_test(&self) {}
2210
2211    #[cfg(any(test, feature = "test-support"))]
2212    pub fn note_using_name_candidate_inspection_for_test(&self) {
2213        self.using_name_candidate_inspection_count
2214            .fetch_add(1, Ordering::Relaxed);
2215    }
2216
2217    #[cfg(not(any(test, feature = "test-support")))]
2218    pub fn note_using_name_candidate_inspection_for_test(&self) {}
2219
2220    #[cfg(any(test, feature = "test-support"))]
2221    pub fn note_using_source_index_walk_for_test(&self) {
2222        self.using_source_index_walk_count
2223            .fetch_add(1, Ordering::Relaxed);
2224    }
2225
2226    #[cfg(not(any(test, feature = "test-support")))]
2227    pub fn note_using_source_index_walk_for_test(&self) {}
2228
2229    #[cfg(any(test, feature = "test-support"))]
2230    pub fn using_work_counts_for_test(&self) -> (usize, usize, usize, usize, usize) {
2231        (
2232            self.using_source_index_walk_count.load(Ordering::Relaxed),
2233            self.using_donor_activation_count.load(Ordering::Relaxed),
2234            self.using_namespace_lookup_count.load(Ordering::Relaxed),
2235            self.callable_reference_spec_build_count
2236                .load(Ordering::Relaxed),
2237            self.using_name_candidate_inspection_count
2238                .load(Ordering::Relaxed),
2239        )
2240    }
2241
2242    pub fn is_physically_visible(&self, file: &ProjectFile, target: &CodeUnit) -> bool {
2243        file == target.source()
2244            || self
2245                .visible_by_file
2246                .get(file)
2247                .is_some_and(|visible| visible.contains(target))
2248    }
2249
2250    pub fn declaration_visible_at(
2251        &self,
2252        analyzer: &CppGraphSource<'_>,
2253        file: &ProjectFile,
2254        declaration: &CodeUnit,
2255        reference_byte: usize,
2256    ) -> bool {
2257        self.visible_identifier_candidates(file, declaration.identifier())
2258            .filter(|candidate| {
2259                same_logical_symbol(candidate, declaration)
2260                    || flattened_macro_namespace_declaration_matches(
2261                        analyzer,
2262                        self.cpp,
2263                        file,
2264                        candidate,
2265                        declaration,
2266                        reference_byte,
2267                    )
2268            })
2269            .any(|candidate| {
2270                self.physical_declaration_visible_at(analyzer, file, candidate, reference_byte)
2271            })
2272    }
2273
2274    pub fn callable_arity_at_reference(
2275        &self,
2276        analyzer: &CppGraphSource<'_>,
2277        file: &ProjectFile,
2278        candidate: &CodeUnit,
2279        reference_byte: usize,
2280    ) -> Option<CallableArity> {
2281        let key = (file.clone(), logical_symbol_key(candidate));
2282        let cell = self
2283            .callable_reference_specs
2284            .lock()
2285            .expect("C++ callable reference-spec cache poisoned")
2286            .entry(key)
2287            .or_default()
2288            .clone();
2289        let spec = cell.get_or_init(|| {
2290            let prepared = self.cpp.prepared_syntax(file)?;
2291            let spec = TargetSpec::from_target(analyzer, candidate)?;
2292            let spec = spec
2293                .with_visible_callable_arities(analyzer, self.cpp, self, file, prepared.as_ref())
2294                .into_owned();
2295            #[cfg(any(test, feature = "test-support"))]
2296            self.callable_reference_spec_build_count
2297                .fetch_add(1, Ordering::Relaxed);
2298            Some(spec)
2299        });
2300        spec.as_ref()?.callable_arity_at(reference_byte)
2301    }
2302
2303    fn physical_declaration_visible_at(
2304        &self,
2305        analyzer: &CppGraphSource<'_>,
2306        file: &ProjectFile,
2307        declaration: &CodeUnit,
2308        reference_byte: usize,
2309    ) -> bool {
2310        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2311            return false;
2312        };
2313        if declaration.source() == file {
2314            return callable_declaration_activation_in_file(
2315                analyzer,
2316                prepared.as_ref(),
2317                declaration,
2318                file,
2319            )
2320            .is_some_and(|activation| activation < reference_byte);
2321        }
2322        let Some(donor_syntax) = self.cpp.prepared_syntax(declaration.source()) else {
2323            return false;
2324        };
2325        if callable_declaration_activation_in_file(
2326            analyzer,
2327            donor_syntax.as_ref(),
2328            declaration,
2329            file,
2330        )
2331        .is_none()
2332        {
2333            return false;
2334        }
2335        self.include_activation_for_source(self.cpp, file, prepared.as_ref(), declaration.source())
2336            .is_some_and(|activation| activation < reference_byte)
2337    }
2338
2339    pub fn external_type_candidate_visible_at(
2340        &self,
2341        file: &ProjectFile,
2342        candidate: &CodeUnit,
2343        reference_byte: usize,
2344    ) -> bool {
2345        if candidate.source() == file {
2346            return true;
2347        }
2348        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2349            return false;
2350        };
2351        self.visible_identifier_candidates(file, candidate.identifier())
2352            .filter(|peer| same_logical_symbol(candidate, peer))
2353            .any(|peer| {
2354                peer.source() == file
2355                    || self
2356                        .include_activation_for_source(
2357                            self.cpp,
2358                            file,
2359                            prepared.as_ref(),
2360                            peer.source(),
2361                        )
2362                        .is_some_and(|activation| activation <= reference_byte)
2363            })
2364    }
2365
2366    pub fn external_type_declaration_visible_at(
2367        &self,
2368        file: &ProjectFile,
2369        candidate: &CodeUnit,
2370        reference_byte: usize,
2371    ) -> bool {
2372        if candidate.source() == file {
2373            return true;
2374        }
2375        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2376            return false;
2377        };
2378        self.include_activation_for_source(self.cpp, file, prepared.as_ref(), candidate.source())
2379            .is_some_and(|activation| activation <= reference_byte)
2380    }
2381
2382    pub fn external_type_candidate_visible_in_context(
2383        &self,
2384        analyzer: &CppGraphSource<'_>,
2385        file: &ProjectFile,
2386        candidate: &CodeUnit,
2387        reference: Node<'_>,
2388    ) -> bool {
2389        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2390            return false;
2391        };
2392        let reference_guards = preprocessor_guard_environment(reference, prepared.source());
2393
2394        let directly_visible = self
2395            .visible_identifier_candidates(file, candidate.identifier())
2396            .filter(|peer| same_logical_symbol(candidate, peer))
2397            .any(|peer| {
2398                declaration_guard_requirements(analyzer, self.cpp, peer)
2399                    .into_iter()
2400                    .any(|(declaration_byte, declaration_guards)| {
2401                        if peer.source() == file {
2402                            return declaration_byte < reference.start_byte()
2403                                && guard_requirements_hold_at_reference(
2404                                    &declaration_guards,
2405                                    reference_guards.as_ref(),
2406                                )
2407                                && self.preprocessor_guards_stable_between(
2408                                    file,
2409                                    declaration_byte,
2410                                    reference.start_byte(),
2411                                    &declaration_guards,
2412                                );
2413                        }
2414                        if self
2415                            .include_activation_for_source(
2416                                self.cpp,
2417                                file,
2418                                prepared.as_ref(),
2419                                peer.source(),
2420                            )
2421                            .is_some_and(|activation| {
2422                                activation <= reference.start_byte()
2423                                    && guard_requirements_hold_at_reference(
2424                                        &declaration_guards,
2425                                        reference_guards.as_ref(),
2426                                    )
2427                                    && self.preprocessor_guards_stable_between(
2428                                        file,
2429                                        0,
2430                                        reference.start_byte(),
2431                                        &declaration_guards,
2432                                    )
2433                            })
2434                        {
2435                            return true;
2436                        }
2437                        self.conditional_include_projections_for_source(
2438                            file,
2439                            prepared.as_ref(),
2440                            peer.source(),
2441                        )
2442                        .iter()
2443                        .any(|projection| {
2444                            let Some(required_guards) = merge_preprocessor_guards(
2445                                &projection.required_guards,
2446                                &declaration_guards,
2447                            ) else {
2448                                return false;
2449                            };
2450                            projection.activation_byte <= reference.start_byte()
2451                                && guard_requirements_hold_at_reference(
2452                                    &required_guards,
2453                                    reference_guards.as_ref(),
2454                                )
2455                                && self.preprocessor_guards_stable_between(
2456                                    file,
2457                                    0,
2458                                    reference.start_byte(),
2459                                    &required_guards,
2460                                )
2461                        })
2462                    })
2463            });
2464        let complementary = self
2465            .visible_identifier_candidates(file, candidate.identifier())
2466            .filter(|peer| {
2467                peer.kind() == candidate.kind()
2468                    && peer.fq_name() == candidate.fq_name()
2469                    && peer.source() == candidate.source()
2470            })
2471            .collect::<Vec<_>>();
2472        // A completed #if/#else family declares the shared source-level name
2473        // before this reference. A later macro mutation cannot revoke that
2474        // declaration. The family gate below rejects declarations split across
2475        // separate conditional blocks, where mutation can change coverage.
2476        let candidate_branch_compatible = reference_guards.as_ref().is_some_and(|active| {
2477            declaration_guard_requirements(analyzer, self.cpp, candidate)
2478                .iter()
2479                .any(|(_, required)| merge_preprocessor_guards(required, active).is_some())
2480        });
2481        let complementary_visible = candidate_branch_compatible
2482            && self.complementary_same_fqn_type_declarations(analyzer, &complementary, candidate)
2483            && if candidate.source() == file {
2484                declaration_guard_requirements(analyzer, self.cpp, candidate)
2485                    .iter()
2486                    .any(|(declaration_byte, _)| *declaration_byte < reference.start_byte())
2487            } else {
2488                self.include_activation_for_source(
2489                    self.cpp,
2490                    file,
2491                    prepared.as_ref(),
2492                    candidate.source(),
2493                )
2494                .is_some_and(|activation| activation <= reference.start_byte())
2495            };
2496        directly_visible || complementary_visible
2497    }
2498
2499    pub fn is_exhaustive_same_fqn_type_declaration_family(
2500        &self,
2501        analyzer: &CppGraphSource<'_>,
2502        file: &ProjectFile,
2503        candidate: &CodeUnit,
2504    ) -> bool {
2505        let candidates = self
2506            .visible_identifier_candidates(file, candidate.identifier())
2507            .filter(|peer| {
2508                peer.kind() == candidate.kind()
2509                    && peer.fq_name() == candidate.fq_name()
2510                    && peer.source() == candidate.source()
2511            })
2512            .collect::<Vec<_>>();
2513        self.complementary_same_fqn_type_declarations(analyzer, &candidates, candidate)
2514    }
2515
2516    /// Prove a nested type alias used as a dependent member-pointer owner when
2517    /// its owning class has mutually-exclusive declarations.  A common C++11
2518    /// compatibility shape provides the owning class in one preprocessor
2519    /// branch and aliases it to a standard-library type in the other branch;
2520    /// the nested fallback alias is therefore not itself active in every
2521    /// branch even though the qualified owner API is.
2522    ///
2523    /// This is deliberately narrower than ordinary type visibility.  The
2524    /// caller has already recovered a member-pointer owner path from the CST;
2525    /// this helper additionally requires the target's structured parent to
2526    /// match that path, physical source visibility, and exact preprocessor
2527    /// guard agreement with the parent declaration.  Only then may the
2528    /// parent's direct/complementary same-FQN visibility stand in for the
2529    /// nested terminal's active-branch check.
2530    pub fn dependent_member_pointer_alias_visible_in_context(
2531        &self,
2532        analyzer: &CppGraphSource<'_>,
2533        file: &ProjectFile,
2534        candidate: &CodeUnit,
2535        owner_components: &[String],
2536        reference: Node<'_>,
2537    ) -> bool {
2538        if !analyzer
2539            .type_alias_provider()
2540            .is_some_and(|provider| provider.is_type_alias(candidate))
2541        {
2542            return false;
2543        }
2544        let Some((terminal, owner_prefix)) = owner_components.split_last() else {
2545            return false;
2546        };
2547        if terminal != candidate.identifier()
2548            || canonical_cpp_scope_components(candidate) != owner_components
2549        {
2550            return false;
2551        }
2552        let Some(expected_parent_fq_name) =
2553            brokk_bifrost_core::analyzer::default_parent_fq_name(candidate)
2554        else {
2555            return false;
2556        };
2557        let Some(parent_anchor) = type_owner_of(analyzer, candidate) else {
2558            return false;
2559        };
2560        if parent_anchor.fq_name() != expected_parent_fq_name.as_str()
2561            || parent_anchor.source() != candidate.source()
2562            || canonical_cpp_scope_components(&parent_anchor) != owner_prefix
2563        {
2564            return false;
2565        }
2566
2567        // The ordinary path already handles unguarded aliases (and preserves
2568        // same-file declaration ordering).  This fallback is only for a
2569        // physically visible declaration whose guard is the owning branch's
2570        // guard, so reject a same-file declaration that appears after the
2571        // reference before considering guard compatibility.
2572        if !self.external_type_candidate_visible_at(file, candidate, reference.start_byte())
2573            || candidate.source() == file
2574                && !analyzer
2575                    .ranges(candidate)
2576                    .iter()
2577                    .any(|range| range.start_byte < reference.start_byte())
2578        {
2579            return false;
2580        }
2581
2582        let candidate_guards = declaration_guard_requirements(analyzer, self.cpp, candidate);
2583        if candidate_guards.is_empty() {
2584            return false;
2585        }
2586        let same_guard_sets =
2587            |left: &[(usize, HashSet<PreprocessorGuard>)],
2588             right: &[(usize, HashSet<PreprocessorGuard>)]| {
2589                left.iter().all(|(_, left_guards)| {
2590                    right
2591                        .iter()
2592                        .any(|(_, right_guards)| left_guards == right_guards)
2593                })
2594            };
2595        let parent_candidates = self
2596            .visible_identifier_candidates(file, parent_anchor.identifier())
2597            .filter(|peer| {
2598                peer.kind() == parent_anchor.kind()
2599                    && peer.fq_name() == expected_parent_fq_name.as_str()
2600                    && peer.source() == parent_anchor.source()
2601                    && canonical_cpp_scope_components(peer) == owner_prefix
2602            })
2603            .filter_map(|peer| {
2604                let parent_guards = declaration_guard_requirements(analyzer, self.cpp, peer);
2605                (candidate_guards.len() == parent_guards.len()
2606                    && same_guard_sets(&candidate_guards, &parent_guards)
2607                    && same_guard_sets(&parent_guards, &candidate_guards))
2608                .then(|| (peer.clone(), parent_guards))
2609            })
2610            .collect::<Vec<_>>();
2611        let [(parent, _parent_guards)] = parent_candidates.as_slice() else {
2612            return false;
2613        };
2614
2615        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2616            return false;
2617        };
2618        let Some(reference_guards) = preprocessor_guard_environment(reference, prepared.source())
2619        else {
2620            return false;
2621        };
2622        // An external header selects its declaration branch before the
2623        // reference file is parsed. Require compatible reference guards, but
2624        // do not test the header's guard expression for stability in the
2625        // reference file. Same-file aliases still require that stability.
2626        if !candidate_guards.iter().any(|(_, target_guards)| {
2627            merge_preprocessor_guards(target_guards, &reference_guards).is_some()
2628                && (candidate.source() != file
2629                    || self.preprocessor_guards_stable_between(
2630                        file,
2631                        0,
2632                        reference.start_byte(),
2633                        target_guards,
2634                    ))
2635        }) {
2636            return false;
2637        }
2638
2639        self.external_type_candidate_visible_in_context(analyzer, file, parent, reference)
2640    }
2641
2642    /// Check a type candidate's preprocessor/import context without imposing
2643    /// ordinary declaration-before-reference ordering for same-file peers.
2644    ///
2645    /// C++ class scope makes member names visible throughout the complete
2646    /// class, including a trailing return type that appears before the member
2647    /// alias declaration in source order. Callers must first prove that the
2648    /// reference is inside the candidate's indexed class owner; this helper
2649    /// only relaxes the byte-order predicate while retaining guard and include
2650    /// activation checks.
2651    pub fn external_type_candidate_guard_compatible_in_context(
2652        &self,
2653        analyzer: &CppGraphSource<'_>,
2654        file: &ProjectFile,
2655        candidate: &CodeUnit,
2656        reference: Node<'_>,
2657    ) -> bool {
2658        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2659            return false;
2660        };
2661        let reference_guards = preprocessor_guard_environment(reference, prepared.source());
2662
2663        self.visible_identifier_candidates(file, candidate.identifier())
2664            .filter(|peer| same_logical_symbol(candidate, peer))
2665            .any(|peer| {
2666                declaration_guard_requirements(analyzer, self.cpp, peer)
2667                    .into_iter()
2668                    .any(|(declaration_byte, declaration_guards)| {
2669                        if peer.source() == file {
2670                            let (start, end) = if declaration_byte <= reference.start_byte() {
2671                                (declaration_byte, reference.start_byte())
2672                            } else {
2673                                (reference.start_byte(), declaration_byte)
2674                            };
2675                            return guard_requirements_hold_at_reference(
2676                                &declaration_guards,
2677                                reference_guards.as_ref(),
2678                            ) && self.preprocessor_guards_stable_between(
2679                                file,
2680                                start,
2681                                end,
2682                                &declaration_guards,
2683                            );
2684                        }
2685                        if self
2686                            .include_activation_for_source(
2687                                self.cpp,
2688                                file,
2689                                prepared.as_ref(),
2690                                peer.source(),
2691                            )
2692                            .is_some_and(|activation| {
2693                                activation <= reference.start_byte()
2694                                    && guard_requirements_hold_at_reference(
2695                                        &declaration_guards,
2696                                        reference_guards.as_ref(),
2697                                    )
2698                                    && self.preprocessor_guards_stable_between(
2699                                        file,
2700                                        0,
2701                                        reference.start_byte(),
2702                                        &declaration_guards,
2703                                    )
2704                            })
2705                        {
2706                            return true;
2707                        }
2708                        self.conditional_include_projections_for_source(
2709                            file,
2710                            prepared.as_ref(),
2711                            peer.source(),
2712                        )
2713                        .iter()
2714                        .any(|projection| {
2715                            let Some(required_guards) = merge_preprocessor_guards(
2716                                &projection.required_guards,
2717                                &declaration_guards,
2718                            ) else {
2719                                return false;
2720                            };
2721                            projection.activation_byte <= reference.start_byte()
2722                                && guard_requirements_hold_at_reference(
2723                                    &required_guards,
2724                                    reference_guards.as_ref(),
2725                                )
2726                                && self.preprocessor_guards_stable_between(
2727                                    file,
2728                                    0,
2729                                    reference.start_byte(),
2730                                    &required_guards,
2731                                )
2732                        })
2733                    })
2734            })
2735    }
2736
2737    pub fn type_candidate_may_be_visible_before_reference(
2738        &self,
2739        analyzer: &CppGraphSource<'_>,
2740        file: &ProjectFile,
2741        candidate: &CodeUnit,
2742        reference_byte: usize,
2743    ) -> bool {
2744        let Some(prepared) = self.cpp.prepared_syntax(file) else {
2745            return false;
2746        };
2747        let root = prepared.tree().root_node();
2748        let end_byte = reference_byte
2749            .saturating_add(1)
2750            .min(prepared.source().len());
2751        let Some(reference) = root.descendant_for_byte_range(reference_byte, end_byte) else {
2752            return false;
2753        };
2754        self.external_type_candidate_visible_in_context(analyzer, file, candidate, reference)
2755    }
2756
2757    pub fn preprocessor_guards_stable_between(
2758        &self,
2759        file: &ProjectFile,
2760        start_byte: usize,
2761        end_byte: usize,
2762        guards: &HashSet<PreprocessorGuard>,
2763    ) -> bool {
2764        if guards.is_empty() || start_byte >= end_byte {
2765            return true;
2766        }
2767        let cell = self.macro_event_cell(file);
2768        let events = cell.get_or_init(|| self.collect_macro_events(file).into_boxed_slice());
2769        let mut visited = HashSet::from_iter([file.clone()]);
2770        !events.iter().any(|event| {
2771            event.byte() >= start_byte
2772                && event.byte() < end_byte
2773                && self.macro_event_may_mutate_guards(event, guards, &mut visited)
2774        })
2775    }
2776
2777    fn macro_event_may_mutate_guards(
2778        &self,
2779        event: &MacroEvent,
2780        guards: &HashSet<PreprocessorGuard>,
2781        visited: &mut HashSet<ProjectFile>,
2782    ) -> bool {
2783        match event {
2784            MacroEvent::Define { name, .. } | MacroEvent::Undef { name, .. } => {
2785                guards.iter().any(|guard| guard.may_depend_on_macro(name))
2786            }
2787            MacroEvent::Include { targets, .. } => {
2788                targets.is_empty()
2789                    || targets
2790                        .iter()
2791                        .any(|target| self.source_may_mutate_guards(target, guards, visited))
2792            }
2793            MacroEvent::Invalidate { .. } => true,
2794        }
2795    }
2796
2797    fn source_may_mutate_guards(
2798        &self,
2799        file: &ProjectFile,
2800        guards: &HashSet<PreprocessorGuard>,
2801        visited: &mut HashSet<ProjectFile>,
2802    ) -> bool {
2803        if !visited.insert(file.clone()) {
2804            return false;
2805        }
2806        let cell = self.macro_event_cell(file);
2807        let events = cell.get_or_init(|| self.collect_macro_events(file).into_boxed_slice());
2808        events
2809            .iter()
2810            .any(|event| self.macro_event_may_mutate_guards(event, guards, visited))
2811    }
2812
2813    pub fn resolve_type(&self, file: &ProjectFile, raw_name: &str) -> Option<CodeUnit> {
2814        let normalized = normalize_reference_name(raw_name)?;
2815        self.type_candidates(file, &normalized)
2816            .into_iter()
2817            .next()
2818            .cloned()
2819    }
2820
2821    pub fn resolve_type_node_result(
2822        &self,
2823        file: &ProjectFile,
2824        node: Node<'_>,
2825        source: &str,
2826    ) -> std::result::Result<Option<CodeUnit>, CppTemplateResolutionError> {
2827        let Some(primary) = self.resolve_type_node_primary(file, node, source) else {
2828            return Ok(None);
2829        };
2830        let Some(arguments) = cpp_template_reference_arguments(node, source) else {
2831            return Ok(Some(primary));
2832        };
2833        self.resolve_template_arguments(file, primary, &arguments)
2834            .map(Some)
2835    }
2836
2837    pub fn resolve_type_node_primary(
2838        &self,
2839        file: &ProjectFile,
2840        node: Node<'_>,
2841        source: &str,
2842    ) -> Option<CodeUnit> {
2843        let components = cpp_type_name_components(node, source)?;
2844        self.resolve_type(file, &components.join("::"))
2845    }
2846
2847    pub fn resolve_template_arguments(
2848        &self,
2849        file: &ProjectFile,
2850        primary: CodeUnit,
2851        arguments: &[CppTemplateExpression],
2852    ) -> std::result::Result<CodeUnit, CppTemplateResolutionError> {
2853        self.resolve_template_arguments_inner(file, primary, arguments, &mut HashSet::default())
2854    }
2855
2856    fn resolve_template_arguments_inner(
2857        &self,
2858        file: &ProjectFile,
2859        primary: CodeUnit,
2860        arguments: &[CppTemplateExpression],
2861        seen_aliases: &mut HashSet<CodeUnit>,
2862    ) -> std::result::Result<CodeUnit, CppTemplateResolutionError> {
2863        if let Some(metadata) = self.cpp_template_metadata.get(&primary)
2864            && let Some(alias_target) = &metadata.alias_target
2865        {
2866            if !seen_aliases.insert(primary.clone()) {
2867                return Err(CppTemplateResolutionError::AliasCycle { alias: primary });
2868            }
2869            let (_, bindings) = cpp_bind_template_arguments(&metadata.parameters, arguments)
2870                .ok_or(CppTemplateResolutionError::ArgumentBinding)?;
2871            let target_name = alias_target.components.join("::");
2872            let target_primary = if alias_target.global {
2873                unique_logical_type_candidate(self.type_candidates(file, &target_name))
2874            } else {
2875                self.resolve_unique_type_for_declaration(file, &primary, &target_name)
2876            };
2877            let Some(target_primary) = target_primary else {
2878                // A dependent or external RHS cannot be canonicalized from the
2879                // indexed graph. Preserve the alias's direct identity instead
2880                // of inventing a target from its source spelling.
2881                return Ok(primary);
2882            };
2883            let Some(target_arguments) = &alias_target.arguments else {
2884                return Ok(target_primary);
2885            };
2886            let target_arguments = cpp_substitute_template_arguments(target_arguments, &bindings)
2887                .ok_or(CppTemplateResolutionError::Substitution)?;
2888            return self.resolve_template_arguments_inner(
2889                file,
2890                target_primary,
2891                &target_arguments,
2892                seen_aliases,
2893            );
2894        }
2895
2896        let primary_fq_name = self
2897            .cpp_template_metadata
2898            .get(&primary)
2899            .map(|metadata| metadata.primary_fq_name.clone())
2900            .unwrap_or_else(|| primary.fq_name());
2901        let has_specialization_metadata = self
2902            .cpp_template_families
2903            .get(&primary_fq_name)
2904            .is_some_and(|family| family.iter().any(|unit| self.is_visible(file, unit)));
2905        if !has_specialization_metadata {
2906            return Ok(primary);
2907        }
2908        self.select_template_specialization(file, &primary, arguments)
2909    }
2910
2911    fn select_template_specialization(
2912        &self,
2913        file: &ProjectFile,
2914        resolved: &CodeUnit,
2915        explicit_arguments: &[CppTemplateExpression],
2916    ) -> std::result::Result<CodeUnit, CppTemplateResolutionError> {
2917        let primary_fq_name = self
2918            .cpp_template_metadata
2919            .get(resolved)
2920            .map(|metadata| metadata.primary_fq_name.clone())
2921            .unwrap_or_else(|| resolved.fq_name());
2922        let family = self
2923            .cpp_template_families
2924            .get(&primary_fq_name)
2925            .ok_or(CppTemplateResolutionError::PrimarySelection)?;
2926        let primary_candidates = family
2927            .iter()
2928            .filter_map(|unit| {
2929                let metadata = self.cpp_template_metadata.get(unit)?;
2930                (metadata.specialization_arguments.is_empty() && self.is_visible(file, unit))
2931                    .then_some((unit, metadata))
2932            })
2933            .collect::<Vec<_>>();
2934        let primary_unit = primary_candidates
2935            .iter()
2936            .find_map(|(unit, _)| (*unit == resolved).then_some(*unit))
2937            .or_else(|| {
2938                primary_candidates
2939                    .iter()
2940                    .map(|(unit, _)| *unit)
2941                    .min_by_key(|unit| {
2942                        (
2943                            unit.source().to_string(),
2944                            unit.signature().unwrap_or_default(),
2945                        )
2946                    })
2947            })
2948            .ok_or(CppTemplateResolutionError::PrimarySelection)?;
2949        let primary_parameters =
2950            cpp_reconcile_primary_template_parameters(&primary_candidates, primary_unit)
2951                .ok_or(CppTemplateResolutionError::PrimarySelection)?;
2952        let (expanded, _) = cpp_bind_template_arguments(&primary_parameters, explicit_arguments)
2953            .ok_or(CppTemplateResolutionError::ArgumentBinding)?;
2954
2955        let mut applicable = Vec::new();
2956        for unit in family {
2957            let Some(metadata) = self.cpp_template_metadata.get(unit) else {
2958                continue;
2959            };
2960            if metadata.specialization_arguments.is_empty() || !self.is_visible(file, unit) {
2961                continue;
2962            }
2963            if !cpp_specialization_matches(metadata, &expanded) {
2964                continue;
2965            }
2966            applicable.push((unit, metadata));
2967        }
2968        if applicable.is_empty() {
2969            return Ok(primary_unit.clone());
2970        }
2971
2972        // A scalar constraint count cannot represent C++ partial ordering:
2973        // e.g. `<T*, U>` and `<T, int>` are incomparable for `<int*, int>`.
2974        // Select only a logical candidate whose structural pattern is strictly
2975        // more specialized than every other distinct applicable candidate.
2976        let winners = applicable
2977            .iter()
2978            .filter(|(candidate, candidate_metadata)| {
2979                applicable.iter().all(|(other, other_metadata)| {
2980                    same_visible_symbol(candidate, other)
2981                        || cpp_specialization_more_specialized(candidate_metadata, other_metadata)
2982                })
2983            })
2984            .copied()
2985            .collect::<Vec<_>>();
2986        let Some((selected, _)) = winners.first() else {
2987            // Mutually incomparable applicable candidates: every one of them
2988            // is a live contender.
2989            return Err(CppTemplateResolutionError::AmbiguousSpecialization {
2990                candidates: distinct_visible_symbols(applicable.iter().map(|(unit, _)| *unit)),
2991            });
2992        };
2993        if winners
2994            .iter()
2995            .any(|(unit, _)| !same_visible_symbol(unit, selected))
2996        {
2997            return Err(CppTemplateResolutionError::AmbiguousSpecialization {
2998                candidates: distinct_visible_symbols(winners.iter().map(|(unit, _)| *unit)),
2999            });
3000        }
3001        Ok((*selected).clone())
3002    }
3003
3004    pub fn resolve_type_components_lexically(
3005        &self,
3006        analyzer: &CppGraphSource<'_>,
3007        file: &ProjectFile,
3008        components: &[String],
3009        global: bool,
3010        lexical_scope: &[String],
3011    ) -> LexicalTypeResolution {
3012        self.resolve_type_components_lexically_inner(
3013            analyzer,
3014            file,
3015            components,
3016            global,
3017            lexical_scope,
3018            TypeCandidateResolution::Canonical,
3019        )
3020    }
3021
3022    pub fn resolve_type_components_lexically_for_forward(
3023        &self,
3024        analyzer: &CppGraphSource<'_>,
3025        file: &ProjectFile,
3026        components: &[String],
3027        global: bool,
3028        lexical_scope: &[String],
3029    ) -> LexicalTypeResolution {
3030        self.resolve_type_components_lexically_inner(
3031            analyzer,
3032            file,
3033            components,
3034            global,
3035            lexical_scope,
3036            TypeCandidateResolution::PreserveAlias,
3037        )
3038    }
3039
3040    pub fn resolve_type_components_lexically_for_target(
3041        &self,
3042        analyzer: &CppGraphSource<'_>,
3043        file: &ProjectFile,
3044        components: &[String],
3045        global: bool,
3046        lexical_scope: &[String],
3047        target: &CodeUnit,
3048    ) -> LexicalTypeResolution {
3049        #[cfg(any(test, feature = "test-support"))]
3050        self.target_preserving_type_resolution_count
3051            .fetch_add(1, Ordering::Relaxed);
3052        self.resolve_type_components_lexically_inner(
3053            analyzer,
3054            file,
3055            components,
3056            global,
3057            lexical_scope,
3058            TypeCandidateResolution::PreserveTarget(target),
3059        )
3060    }
3061
3062    pub fn coarse_unqualified_type_reference_may_resolve(
3063        &self,
3064        file: &ProjectFile,
3065        name: &str,
3066    ) -> bool {
3067        if name.is_empty() {
3068            return true;
3069        }
3070        self.visible_identifier_candidates(file, name)
3071            .any(|candidate| candidate.kind() == CodeUnitType::Class || is_type_alias(candidate))
3072            || self.visible_parser_alias_name_is_visible(file, name)
3073    }
3074
3075    #[allow(clippy::too_many_arguments)]
3076    pub fn structured_type_reference_may_resolve_to_target(
3077        &self,
3078        analyzer: &CppGraphSource<'_>,
3079        file: &ProjectFile,
3080        components: &[String],
3081        global: bool,
3082        lexical_scope: &[String],
3083        target: &CodeUnit,
3084    ) -> bool {
3085        if components.is_empty() {
3086            return true;
3087        }
3088        let Some(terminal) = components.last() else {
3089            return true;
3090        };
3091        let parser_alias_visible = self.visible_parser_alias_name_is_visible(file, terminal);
3092        if parser_alias_visible
3093            && self.parser_alias_resolves_to_type(analyzer, file, terminal, target)
3094        {
3095            return true;
3096        }
3097        let qualified_tiers = lexical_component_tiers(components, global, lexical_scope)
3098            .map(|qualified| qualified.join("::"))
3099            .collect::<Vec<_>>();
3100        let target_name = cpp_name_for(target);
3101        if qualified_tiers
3102            .iter()
3103            .any(|qualified| qualified == &target_name)
3104        {
3105            return true;
3106        }
3107
3108        let mut saw_shape_candidate = parser_alias_visible;
3109        for candidate in self.visible_identifier_candidates(file, terminal) {
3110            if candidate.kind() != CodeUnitType::Class && !declared_type_alias(analyzer, candidate)
3111            {
3112                continue;
3113            }
3114            let candidate_name = cpp_name_for(candidate);
3115            let shape_matches = if global || components.len() > 1 {
3116                qualified_tiers
3117                    .iter()
3118                    .any(|qualified| qualified == &candidate_name)
3119            } else {
3120                true
3121            };
3122            if !shape_matches {
3123                continue;
3124            }
3125            saw_shape_candidate = true;
3126            if same_visible_symbol(candidate, target)
3127                || self.compatible_primary_template_redeclarations(candidate, target)
3128                || (declared_type_alias(analyzer, candidate)
3129                    && self.alias_candidate_may_preserve_target(analyzer, file, candidate, target))
3130            {
3131                return true;
3132            }
3133        }
3134
3135        !saw_shape_candidate
3136    }
3137
3138    pub fn target_preserving_reference_namespace(
3139        &self,
3140        analyzer: &CppGraphSource<'_>,
3141        file: &ProjectFile,
3142        identifier: &str,
3143        target: &CodeUnit,
3144    ) -> Option<Vec<String>> {
3145        let mut namespace = None;
3146        for candidate in self.visible_identifier_candidates(file, identifier) {
3147            if candidate.kind() != CodeUnitType::Class && !declared_type_alias(analyzer, candidate)
3148            {
3149                continue;
3150            }
3151            if !(same_visible_symbol(candidate, target)
3152                || self.compatible_primary_template_redeclarations(candidate, target)
3153                || declared_type_alias(analyzer, candidate)
3154                    && self.structured_alias_primary_preserves_target(
3155                        analyzer, file, candidate, target,
3156                    ))
3157            {
3158                continue;
3159            }
3160            if namespace
3161                .as_ref()
3162                .is_some_and(|existing| existing != candidate.package_name())
3163            {
3164                return None;
3165            }
3166            namespace = Some(candidate.package_name().to_string());
3167        }
3168        let namespace = namespace?;
3169        Some(
3170            brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
3171                brokk_bifrost_core::analyzer::Language::Cpp,
3172                &namespace,
3173            ),
3174        )
3175    }
3176
3177    pub fn resolve_imported_type_candidate(
3178        &self,
3179        analyzer: &CppGraphSource<'_>,
3180        file: &ProjectFile,
3181        target: &CodeUnit,
3182        target_components: &[String],
3183        direct_target: Option<&CodeUnit>,
3184        preserve_alias: bool,
3185    ) -> LexicalTypeResolution {
3186        let candidates = [target];
3187        let resolution = if preserve_alias {
3188            TypeCandidateResolution::PreserveAlias
3189        } else {
3190            direct_target.map_or(
3191                TypeCandidateResolution::Canonical,
3192                TypeCandidateResolution::PreserveTarget,
3193            )
3194        };
3195        let Some(unit) = self.resolve_type_candidates(analyzer, file, &candidates, resolution)
3196        else {
3197            return LexicalTypeResolution::Ambiguous;
3198        };
3199        LexicalTypeResolution::Resolved {
3200            unit,
3201            components: target_components.to_vec(),
3202            candidates: vec![target.clone()],
3203        }
3204    }
3205
3206    fn resolve_type_components_lexically_inner(
3207        &self,
3208        analyzer: &CppGraphSource<'_>,
3209        file: &ProjectFile,
3210        components: &[String],
3211        global: bool,
3212        lexical_scope: &[String],
3213        resolution: TypeCandidateResolution<'_>,
3214    ) -> LexicalTypeResolution {
3215        if components.is_empty() {
3216            return LexicalTypeResolution::Missing;
3217        }
3218        // A C++ class injects its own name into the class scope.  The indexed
3219        // FqName for that declaration is the class path itself (for example,
3220        // `n::raw_hash_set`), not a synthetic child named
3221        // `n::raw_hash_set::raw_hash_set`.  Ordinary lexical tiers append the
3222        // requested identifier to every scope component, so they cannot
3223        // represent that injected binding when the enclosing class is the
3224        // closest scope.  Recover the binding from the structured class path
3225        // before allowing lookup to fall through to an outer same-spelled
3226        // declaration.
3227        let mut injected = self.resolve_injected_class_name(
3228            analyzer,
3229            file,
3230            components,
3231            global,
3232            lexical_scope,
3233            resolution,
3234        );
3235        for (tier_index, qualified) in
3236            lexical_component_tiers(components, global, lexical_scope).enumerate()
3237        {
3238            let prefix_len = qualified.len().saturating_sub(components.len());
3239            if injected
3240                .as_ref()
3241                .is_some_and(|(owner_len, _)| prefix_len < *owner_len)
3242            {
3243                return injected
3244                    .take()
3245                    .expect("injected class resolution was just present")
3246                    .1;
3247            }
3248            let qualified_name = qualified.join("::");
3249            let candidates = self
3250                .type_candidates(file, &qualified_name)
3251                .into_iter()
3252                .filter(|candidate| canonical_cpp_name_matches(candidate, &qualified_name))
3253                .collect::<Vec<_>>();
3254            if candidates.is_empty() {
3255                if tier_index == 0 && !global && components.len() == 1 {
3256                    match self.resolve_inherited_type_for_lexical_scope(
3257                        analyzer,
3258                        file,
3259                        lexical_scope,
3260                        &components[0],
3261                        resolution,
3262                    ) {
3263                        LexicalTypeResolution::Missing => {}
3264                        inherited => return inherited,
3265                    }
3266                }
3267                continue;
3268            }
3269            let unit = self.resolve_type_candidates(analyzer, file, &candidates, resolution);
3270            let Some(unit) = unit else {
3271                return LexicalTypeResolution::Ambiguous;
3272            };
3273            return LexicalTypeResolution::Resolved {
3274                unit,
3275                components: qualified,
3276                candidates: candidates.into_iter().cloned().collect(),
3277            };
3278        }
3279        LexicalTypeResolution::Missing
3280    }
3281
3282    fn resolve_injected_class_name(
3283        &self,
3284        analyzer: &CppGraphSource<'_>,
3285        file: &ProjectFile,
3286        components: &[String],
3287        global: bool,
3288        lexical_scope: &[String],
3289        resolution: TypeCandidateResolution<'_>,
3290    ) -> Option<(usize, LexicalTypeResolution)> {
3291        if global
3292            || components.len() != 1
3293            || file.rel_path().extension().is_some_and(|ext| ext == "c")
3294            || matches!(resolution, TypeCandidateResolution::PreserveTarget(target) if !target.is_class())
3295        {
3296            return None;
3297        }
3298        let name = components.first()?;
3299        let mut matches: Vec<&CodeUnit> = Vec::new();
3300        let mut owner_len = 0;
3301        for candidate in self.visible_identifier_candidates(file, name) {
3302            if !candidate.is_class()
3303                || declared_type_alias(analyzer, candidate)
3304                || candidate.identifier() != name
3305            {
3306                continue;
3307            }
3308            let candidate_scope = canonical_cpp_scope_components(candidate);
3309            if candidate_scope.len() > lexical_scope.len()
3310                || !lexical_scope.starts_with(&candidate_scope)
3311                || candidate_scope.last().is_none_or(|last| last != name)
3312            {
3313                continue;
3314            }
3315            if candidate_scope.len() > owner_len {
3316                owner_len = candidate_scope.len();
3317                matches.clear();
3318            }
3319            if candidate_scope.len() == owner_len
3320                && !matches
3321                    .iter()
3322                    .any(|existing| same_logical_symbol(existing, candidate))
3323            {
3324                matches.push(candidate);
3325            }
3326        }
3327        if matches.is_empty() {
3328            return None;
3329        }
3330        // A same-named class at the current lexical boundary is already
3331        // represented by the ordinary namespace/class tier.  The injected
3332        // recovery is only needed when lookup is occurring inside a nested
3333        // class, where the enclosing class name is injected across that
3334        // additional class boundary.  Keeping this boundary strict avoids
3335        // treating qualified receiver/static-qualifier context as an
3336        // injected-name reference.
3337        if owner_len >= lexical_scope.len() {
3338            return None;
3339        }
3340        let owner_components = lexical_scope[..owner_len].to_vec();
3341        let resolved = self.resolve_type_candidates(analyzer, file, &matches, resolution);
3342        let resolution = resolved.map_or(LexicalTypeResolution::Ambiguous, |unit| {
3343            LexicalTypeResolution::Resolved {
3344                unit,
3345                components: owner_components,
3346                candidates: matches.into_iter().cloned().collect(),
3347            }
3348        });
3349        Some((owner_len, resolution))
3350    }
3351
3352    fn resolve_inherited_type_for_lexical_scope(
3353        &self,
3354        analyzer: &CppGraphSource<'_>,
3355        file: &ProjectFile,
3356        lexical_scope: &[String],
3357        name: &str,
3358        resolution: TypeCandidateResolution<'_>,
3359    ) -> LexicalTypeResolution {
3360        let Some(hierarchy) = analyzer.type_hierarchy_provider() else {
3361            return LexicalTypeResolution::Missing;
3362        };
3363        let lexical_owner_name = lexical_scope.join("::");
3364        if lexical_owner_name.is_empty() {
3365            return LexicalTypeResolution::Missing;
3366        }
3367        let owner_candidates = self
3368            .type_candidates(file, &lexical_owner_name)
3369            .into_iter()
3370            .filter(|candidate| {
3371                canonical_cpp_name_matches(candidate, &lexical_owner_name)
3372                    && !declared_type_alias(analyzer, candidate)
3373            })
3374            .collect::<Vec<_>>();
3375        if owner_candidates.is_empty() {
3376            return LexicalTypeResolution::Missing;
3377        }
3378        let Some(lexical_owner) = unique_logical_type_candidate(owner_candidates) else {
3379            return LexicalTypeResolution::Ambiguous;
3380        };
3381
3382        let mut frontier = hierarchy.get_direct_ancestors(&lexical_owner);
3383        let mut visited_owners = HashSet::default();
3384        while !frontier.is_empty() {
3385            let mut level_matches: Vec<(CodeUnit, Vec<CodeUnit>)> = Vec::new();
3386            let mut next_frontier = Vec::new();
3387            for owner in frontier {
3388                if !visited_owners.insert(owner.fq_name()) {
3389                    continue;
3390                }
3391                let qualified_name = format!("{}::{name}", cpp_name_for(&owner));
3392                let candidates = self
3393                    .type_candidates(file, &qualified_name)
3394                    .into_iter()
3395                    .filter(|candidate| canonical_cpp_name_matches(candidate, &qualified_name))
3396                    .collect::<Vec<_>>();
3397                if candidates.is_empty() {
3398                    for ancestor in hierarchy.get_direct_ancestors(&owner) {
3399                        if !next_frontier
3400                            .iter()
3401                            .any(|existing: &CodeUnit| existing.fq_name() == ancestor.fq_name())
3402                        {
3403                            next_frontier.push(ancestor);
3404                        }
3405                    }
3406                    continue;
3407                }
3408                let Some(unit) =
3409                    self.resolve_type_candidates(analyzer, file, &candidates, resolution)
3410                else {
3411                    return LexicalTypeResolution::Ambiguous;
3412                };
3413                level_matches.push((unit, candidates.into_iter().cloned().collect::<Vec<_>>()));
3414            }
3415            if let Some((unit, candidates)) = level_matches.first().cloned() {
3416                let Some(first_declaration) = candidates.first() else {
3417                    return LexicalTypeResolution::Ambiguous;
3418                };
3419                if !level_matches.iter().all(|(_, declarations)| {
3420                    declarations
3421                        .iter()
3422                        .all(|declaration| same_logical_symbol(first_declaration, declaration))
3423                }) {
3424                    return LexicalTypeResolution::Ambiguous;
3425                }
3426                let mut components = lexical_scope.to_vec();
3427                components.push(name.to_string());
3428                return LexicalTypeResolution::Resolved {
3429                    unit,
3430                    components,
3431                    candidates,
3432                };
3433            }
3434            frontier = next_frontier;
3435        }
3436        LexicalTypeResolution::Missing
3437    }
3438
3439    fn resolve_type_candidates(
3440        &self,
3441        analyzer: &CppGraphSource<'_>,
3442        file: &ProjectFile,
3443        candidates: &[&CodeUnit],
3444        resolution: TypeCandidateResolution<'_>,
3445    ) -> Option<CodeUnit> {
3446        match resolution {
3447            TypeCandidateResolution::Canonical => {
3448                self.unique_canonical_type_candidate(analyzer, file, candidates)
3449            }
3450            TypeCandidateResolution::PreserveAlias => {
3451                unique_type_candidate_preserving_alias(analyzer, candidates)
3452            }
3453            TypeCandidateResolution::PreserveTarget(target) => {
3454                self.unique_type_candidate_preserving_target(analyzer, file, candidates, target)
3455            }
3456        }
3457    }
3458
3459    pub fn resolve_callable_value_components_lexically(
3460        &self,
3461        analyzer: &CppGraphSource<'_>,
3462        file: &ProjectFile,
3463        owner_components: &[String],
3464        member_name: &str,
3465        global: bool,
3466        lexical_scope: &[String],
3467    ) -> LexicalCallableValueResolution {
3468        if owner_components.is_empty() || member_name.is_empty() {
3469            return LexicalCallableValueResolution::Missing;
3470        }
3471        for qualified_owner in lexical_component_tiers(owner_components, global, lexical_scope) {
3472            let owner_name = qualified_owner.join("::");
3473            let type_candidates = self
3474                .type_candidates(file, &owner_name)
3475                .into_iter()
3476                .filter(|candidate| canonical_cpp_name_matches(candidate, &owner_name))
3477                .collect::<Vec<_>>();
3478            let resolved_type = if type_candidates.is_empty() {
3479                None
3480            } else {
3481                let Some(unit) =
3482                    self.unique_canonical_type_candidate(analyzer, file, &type_candidates)
3483                else {
3484                    return LexicalCallableValueResolution::Ambiguous;
3485                };
3486                Some(unit)
3487            };
3488
3489            let mut qualified_callable = qualified_owner;
3490            qualified_callable.push(member_name.to_string());
3491            let callable_name = qualified_callable.join("::");
3492            let free_function = self
3493                .named_candidates_for_normalized(file, &callable_name, TargetKind::FreeFunction)
3494                .into_iter()
3495                .find(|candidate| {
3496                    canonical_cpp_name_matches(candidate, &callable_name)
3497                        && type_owner_of(analyzer, candidate).is_none()
3498                })
3499                .cloned();
3500
3501            match (resolved_type, free_function) {
3502                (Some(_), Some(_)) => return LexicalCallableValueResolution::Ambiguous,
3503                (Some(owner), None) => return LexicalCallableValueResolution::Type(owner),
3504                (None, Some(function)) => {
3505                    return LexicalCallableValueResolution::FreeFunction(function);
3506                }
3507                (None, None) => {}
3508            }
3509        }
3510        LexicalCallableValueResolution::Missing
3511    }
3512
3513    fn resolve_type_for_declaration(
3514        &self,
3515        visible_from: &ProjectFile,
3516        declaration: &CodeUnit,
3517        raw_name: &str,
3518    ) -> Option<CodeUnit> {
3519        let normalized = normalize_reference_name(raw_name)?;
3520        if !normalized.contains("::")
3521            && let Some(namespace) = cpp_namespace_for(declaration)
3522        {
3523            for prefix in namespace_prefixes(&namespace) {
3524                let qualified = format!("{prefix}::{normalized}");
3525                if let Some(unit) = self
3526                    .type_candidates(visible_from, &qualified)
3527                    .into_iter()
3528                    .next()
3529                {
3530                    return Some(unit.clone());
3531                }
3532            }
3533        }
3534        self.resolve_type(visible_from, raw_name)
3535    }
3536
3537    fn resolve_unique_canonical_type_for_declaration(
3538        &self,
3539        analyzer: &CppGraphSource<'_>,
3540        visible_from: &ProjectFile,
3541        declaration: &CodeUnit,
3542        raw_name: &str,
3543    ) -> Option<CodeUnit> {
3544        let mut current =
3545            self.resolve_unique_type_for_declaration(visible_from, declaration, raw_name)?;
3546        let mut seen_aliases = HashSet::default();
3547        loop {
3548            let Some(target) = self.structured_alias_target(analyzer, &current) else {
3549                return current.is_class().then_some(current);
3550            };
3551            if matches!(target, StructuredAliasTarget::Builtin) {
3552                return current.is_class().then_some(current);
3553            }
3554            if !seen_aliases.insert(current.clone()) {
3555                return None;
3556            }
3557            current = self.resolve_structured_alias_target(visible_from, &current, &target)?;
3558        }
3559    }
3560
3561    pub fn canonical_type_unit(
3562        &self,
3563        analyzer: &CppGraphSource<'_>,
3564        visible_from: &ProjectFile,
3565        unit: &CodeUnit,
3566    ) -> Option<CodeUnit> {
3567        let mut current = unit.clone();
3568        let mut seen_aliases = HashSet::default();
3569        loop {
3570            let Some(target) = self.structured_alias_target(analyzer, &current) else {
3571                return current.is_class().then_some(current);
3572            };
3573            if matches!(target, StructuredAliasTarget::Builtin) {
3574                return current.is_class().then_some(current);
3575            }
3576            if !seen_aliases.insert(current.clone()) {
3577                return None;
3578            }
3579            current = self.resolve_structured_alias_target(visible_from, &current, &target)?;
3580        }
3581    }
3582
3583    pub fn canonical_visible_full_type_unit(
3584        &self,
3585        analyzer: &CppGraphSource<'_>,
3586        visible_from: &ProjectFile,
3587        unit: &CodeUnit,
3588    ) -> Option<CodeUnit> {
3589        let canonical = self.canonical_type_unit(analyzer, visible_from, unit)?;
3590        if cpp_class_declaration_strength(analyzer, &canonical)
3591            != CppClassDeclarationStrength::Forward
3592        {
3593            return Some(canonical);
3594        }
3595        let mut full = Vec::new();
3596        for candidate in self
3597            .visible_identifier_candidates(visible_from, canonical.identifier())
3598            .filter(|candidate| {
3599                candidate.is_class()
3600                    && candidate.fq_name() == canonical.fq_name()
3601                    && cpp_class_declaration_strength(analyzer, candidate)
3602                        == CppClassDeclarationStrength::Full
3603            })
3604        {
3605            if !full.iter().any(|existing| same_symbol(existing, candidate)) {
3606                full.push(candidate.clone());
3607            }
3608        }
3609        match full.len() {
3610            0 => Some(canonical),
3611            1 => full.pop(),
3612            _ => None,
3613        }
3614    }
3615
3616    fn resolve_structured_alias_target(
3617        &self,
3618        visible_from: &ProjectFile,
3619        declaration: &CodeUnit,
3620        target: &StructuredAliasTarget,
3621    ) -> Option<CodeUnit> {
3622        let primary = self.resolve_structured_alias_primary(visible_from, declaration, target)?;
3623        let StructuredAliasTarget::Named { arguments, .. } = target else {
3624            return None;
3625        };
3626        match arguments {
3627            Some(arguments) => self
3628                .resolve_template_arguments(visible_from, primary, arguments)
3629                .ok(),
3630            None => Some(primary),
3631        }
3632    }
3633
3634    fn resolve_structured_alias_primary(
3635        &self,
3636        visible_from: &ProjectFile,
3637        declaration: &CodeUnit,
3638        target: &StructuredAliasTarget,
3639    ) -> Option<CodeUnit> {
3640        let StructuredAliasTarget::Named {
3641            components, global, ..
3642        } = target
3643        else {
3644            return None;
3645        };
3646        let qualified = components.join("::");
3647        if *global {
3648            unique_logical_type_candidate(self.type_candidates(visible_from, &qualified))
3649        } else {
3650            self.resolve_unique_type_for_declaration(visible_from, declaration, &qualified)
3651        }
3652    }
3653
3654    pub fn structured_alias_primary_preserves_target(
3655        &self,
3656        analyzer: &CppGraphSource<'_>,
3657        visible_from: &ProjectFile,
3658        candidate: &CodeUnit,
3659        target: &CodeUnit,
3660    ) -> bool {
3661        let mut current = candidate.clone();
3662        let mut seen = HashSet::default();
3663        let mut matched_target = false;
3664        loop {
3665            if same_visible_symbol(&current, target)
3666                || self.compatible_primary_template_redeclarations(&current, target)
3667            {
3668                matched_target = true;
3669            }
3670            if !seen.insert(current.clone()) {
3671                return false;
3672            }
3673            let Some(alias_target) = self.structured_alias_target(analyzer, &current) else {
3674                return matched_target;
3675            };
3676            if matches!(alias_target, StructuredAliasTarget::Builtin) {
3677                return matched_target;
3678            };
3679            let Some(primary) =
3680                self.resolve_structured_alias_primary(visible_from, &current, &alias_target)
3681            else {
3682                // A dependent member target such as `Detector<T>::type`
3683                // cannot be reduced to an indexed primary, but a preceding
3684                // structured alias hop may already have proven the requested
3685                // alias identity. Cycles still resolve a primary and are
3686                // rejected by `seen` above.
3687                return matched_target;
3688            };
3689            current = primary;
3690        }
3691    }
3692
3693    pub fn structured_class_alias_resolves_to_target(
3694        &self,
3695        analyzer: &CppGraphSource<'_>,
3696        visible_from: &ProjectFile,
3697        alias: &CodeUnit,
3698        target: &CodeUnit,
3699    ) -> bool {
3700        let Some(owner) = type_owner_of(analyzer, alias).filter(CodeUnit::is_class) else {
3701            return false;
3702        };
3703        let Some(alias_target) = self.structured_alias_target(analyzer, alias) else {
3704            return false;
3705        };
3706        let StructuredAliasTarget::Named {
3707            components, global, ..
3708        } = &alias_target
3709        else {
3710            return false;
3711        };
3712        let lexical_scope = canonical_cpp_scope_components(&owner);
3713        match self.resolve_type_components_lexically_for_target(
3714            analyzer,
3715            visible_from,
3716            components,
3717            *global,
3718            &lexical_scope,
3719            target,
3720        ) {
3721            LexicalTypeResolution::Resolved {
3722                unit, candidates, ..
3723            } => {
3724                same_visible_symbol(&unit, target)
3725                    || self.same_template_member_identity(analyzer, &unit, target)
3726                    || candidates.iter().any(|candidate| {
3727                        same_visible_symbol(candidate, target)
3728                            || self.same_template_member_identity(analyzer, candidate, target)
3729                    })
3730            }
3731            LexicalTypeResolution::Ambiguous | LexicalTypeResolution::Missing => {
3732                self.structured_alias_primary_preserves_target(
3733                    analyzer,
3734                    visible_from,
3735                    alias,
3736                    target,
3737                ) || self.flattened_macro_namespace_alias_target_matches(
3738                    analyzer,
3739                    visible_from,
3740                    alias,
3741                    &alias_target,
3742                    target,
3743                )
3744            }
3745        }
3746    }
3747
3748    fn flattened_macro_namespace_alias_target_matches(
3749        &self,
3750        analyzer: &CppGraphSource<'_>,
3751        visible_from: &ProjectFile,
3752        alias: &CodeUnit,
3753        alias_target: &StructuredAliasTarget,
3754        target: &CodeUnit,
3755    ) -> bool {
3756        let StructuredAliasTarget::Named {
3757            components,
3758            global: false,
3759            arguments: None,
3760        } = alias_target
3761        else {
3762            return false;
3763        };
3764        let Some((target_name, namespace_components)) = components.split_last() else {
3765            return false;
3766        };
3767        if namespace_components.is_empty()
3768            || target_name != target.identifier()
3769            || alias.source() != target.source()
3770            || alias.source() != visible_from
3771            || !target.is_class()
3772            || declared_type_alias(analyzer, target)
3773        {
3774            return false;
3775        }
3776        if self
3777            .resolve_structured_alias_target(visible_from, alias, alias_target)
3778            .is_some()
3779        {
3780            return false;
3781        }
3782
3783        let alias_ranges = analyzer.ranges(alias);
3784        let target_ranges = analyzer.ranges(target);
3785        if alias_ranges.is_empty() || target_ranges.is_empty() {
3786            return false;
3787        }
3788        let alias_start = alias_ranges
3789            .iter()
3790            .map(|range| range.start_byte)
3791            .min()
3792            .expect("non-empty alias ranges have a minimum");
3793        let Some(prepared) = self.cpp.prepared_syntax(target.source()) else {
3794            return false;
3795        };
3796        let root = prepared.tree().root_node();
3797        let has_matching_declaration = target_ranges
3798            .iter()
3799            .filter(|range| range.end_byte <= alias_start)
3800            .filter_map(|range| node_for_exact_range(root, range))
3801            .any(|node| {
3802                flattened_macro_namespace_components(node, prepared.source())
3803                    .is_some_and(|recovered| recovered == namespace_components)
3804            });
3805        if !has_matching_declaration {
3806            return false;
3807        }
3808
3809        let alias_guards = declaration_guard_requirements(analyzer, self.cpp, alias);
3810        let target_guards = declaration_guard_requirements(analyzer, self.cpp, target);
3811        guard_requirement_sets_match(&alias_guards, &target_guards)
3812    }
3813
3814    pub fn template_alias_arguments_preserve_target(
3815        &self,
3816        analyzer: &CppGraphSource<'_>,
3817        visible_from: &ProjectFile,
3818        alias: &CodeUnit,
3819        arguments: &[CppTemplateExpression],
3820        target: &CodeUnit,
3821    ) -> bool {
3822        let Some(metadata) = self.cpp_template_metadata.get(alias) else {
3823            return false;
3824        };
3825        if metadata.alias_target.is_none()
3826            || cpp_bind_template_arguments(&metadata.parameters, arguments).is_none()
3827        {
3828            return false;
3829        }
3830        self.structured_alias_primary_preserves_target(analyzer, visible_from, alias, target)
3831    }
3832
3833    pub fn is_primary_template(&self, unit: &CodeUnit) -> bool {
3834        self.cpp_template_metadata
3835            .get(unit)
3836            .is_some_and(|metadata| metadata.specialization_arguments.is_empty())
3837    }
3838
3839    pub fn is_template_specialization(&self, unit: &CodeUnit) -> bool {
3840        self.cpp_template_metadata
3841            .get(unit)
3842            .is_some_and(|metadata| !metadata.specialization_arguments.is_empty())
3843    }
3844
3845    pub fn same_template_owner_identity(&self, left: &CodeUnit, right: &CodeUnit) -> bool {
3846        same_visible_symbol(left, right)
3847            || self.compatible_primary_template_redeclarations(left, right)
3848    }
3849
3850    pub fn same_template_member_identity(
3851        &self,
3852        analyzer: &CppGraphSource<'_>,
3853        left: &CodeUnit,
3854        right: &CodeUnit,
3855    ) -> bool {
3856        if same_visible_symbol(left, right) {
3857            return true;
3858        }
3859        if left.kind() != right.kind()
3860            || left.identifier() != right.identifier()
3861            || left.signature() != right.signature()
3862        {
3863            return false;
3864        }
3865        let (Some(left_owner), Some(right_owner)) =
3866            (analyzer.parent_of(left), analyzer.parent_of(right))
3867        else {
3868            return false;
3869        };
3870        left_owner.is_class()
3871            && right_owner.is_class()
3872            && self.same_template_owner_identity(&left_owner, &right_owner)
3873    }
3874
3875    fn unique_canonical_type_candidate(
3876        &self,
3877        analyzer: &CppGraphSource<'_>,
3878        visible_from: &ProjectFile,
3879        candidates: &[&CodeUnit],
3880    ) -> Option<CodeUnit> {
3881        let mut canonical = Vec::new();
3882        for candidate in candidates {
3883            let resolved = self.canonical_type_unit(analyzer, visible_from, candidate)?;
3884            if canonical
3885                .iter()
3886                .any(|existing| same_visible_symbol(existing, &resolved))
3887            {
3888                continue;
3889            }
3890            canonical.push(resolved);
3891            if canonical.len() > 1 {
3892                return None;
3893            }
3894        }
3895        canonical.pop()
3896    }
3897
3898    pub fn unique_type_candidate_preserving_target(
3899        &self,
3900        analyzer: &CppGraphSource<'_>,
3901        visible_from: &ProjectFile,
3902        candidates: &[&CodeUnit],
3903        target: &CodeUnit,
3904    ) -> Option<CodeUnit> {
3905        // C++ headers often expose one logical type through mutually exclusive
3906        // physical declarations, for example a class in the fallback branch
3907        // and a `using` alias to the standard-library type in the configured
3908        // branch. The index intentionally retains both declarations so forward
3909        // lookup can report each target. Preserve the requested target when
3910        // that is the only ambiguity: every candidate has the same type kind,
3911        // exact canonical FQN, and source file, and the requested declaration
3912        // itself is one of the physical candidates. Do not merge same-named
3913        // declarations from different files or namespaces; those remain
3914        // ambiguous and fail closed below.
3915        if self.alternate_same_fqn_type_declarations(analyzer, candidates, target) {
3916            return Some(target.clone());
3917        }
3918        let mut resolved_candidates = Vec::new();
3919        for candidate in candidates {
3920            let resolved =
3921                self.type_candidate_preserving_target(analyzer, visible_from, candidate, target)?;
3922            if resolved_candidates
3923                .iter()
3924                .any(|existing| same_visible_symbol(existing, &resolved))
3925            {
3926                continue;
3927            }
3928            resolved_candidates.push(resolved);
3929            if resolved_candidates.len() > 1 {
3930                return None;
3931            }
3932        }
3933        resolved_candidates.pop()
3934    }
3935
3936    pub fn alternate_same_fqn_type_declarations(
3937        &self,
3938        analyzer: &CppGraphSource<'_>,
3939        candidates: &[&CodeUnit],
3940        target: &CodeUnit,
3941    ) -> bool {
3942        let Some(first) = candidates.first() else {
3943            return false;
3944        };
3945        let same_api = first.kind() == target.kind()
3946            && first.fq_name() == target.fq_name()
3947            && first.source() == target.source()
3948            && candidates.iter().all(|candidate| {
3949                candidate.kind() == target.kind()
3950                    && candidate.fq_name() == target.fq_name()
3951                    && candidate.source() == target.source()
3952            })
3953            && candidates
3954                .iter()
3955                .any(|candidate| same_symbol(candidate, target))
3956            && candidates
3957                .iter()
3958                .any(|candidate| !same_logical_symbol(candidate, target));
3959        if !same_api {
3960            return false;
3961        }
3962
3963        let requirements = candidates
3964            .iter()
3965            .map(|candidate| declaration_guard_requirements(analyzer, self.cpp, candidate))
3966            .collect::<Vec<_>>();
3967        requirements.len() > 1
3968            && requirements
3969                .iter()
3970                .all(|requirement| !requirement.is_empty())
3971            && requirements.iter().enumerate().all(|(index, left)| {
3972                requirements[index + 1..].iter().all(|right| {
3973                    left.iter().all(|(_, left_guards)| {
3974                        right.iter().all(|(_, right_guards)| {
3975                            merge_preprocessor_guards(left_guards, right_guards).is_none()
3976                        })
3977                    })
3978                })
3979            })
3980    }
3981
3982    fn preprocessor_guard_terms_cover_all_paths(terms: &[HashSet<PreprocessorGuard>]) -> bool {
3983        let mut pending = vec![terms.to_vec()];
3984        while let Some(branch_terms) = pending.pop() {
3985            let mut normalized = Vec::new();
3986            let mut covers_branch = false;
3987            for term in branch_terms {
3988                if term.iter().any(|guard| term.contains(&guard.negated())) {
3989                    continue;
3990                }
3991                if term.is_empty() {
3992                    covers_branch = true;
3993                    break;
3994                }
3995                if !normalized.iter().any(|existing| existing == &term) {
3996                    normalized.push(term);
3997                }
3998            }
3999            if covers_branch {
4000                continue;
4001            }
4002            let Some(split_guard) = normalized
4003                .iter()
4004                .flat_map(|term| term.iter())
4005                .next()
4006                .cloned()
4007            else {
4008                return false;
4009            };
4010            let negated_guard = split_guard.negated();
4011            let mut when_defined = Vec::new();
4012            let mut when_undefined = Vec::new();
4013            for term in normalized {
4014                if term.contains(&negated_guard) {
4015                    // This term cannot hold when `split_guard` is true.
4016                } else if term.contains(&split_guard) {
4017                    let mut reduced = term.clone();
4018                    reduced.remove(&split_guard);
4019                    when_defined.push(reduced);
4020                } else {
4021                    when_defined.push(term.clone());
4022                }
4023                if term.contains(&split_guard) {
4024                    // This term cannot hold when `split_guard` is false.
4025                } else if term.contains(&negated_guard) {
4026                    let mut reduced = term;
4027                    reduced.remove(&negated_guard);
4028                    when_undefined.push(reduced);
4029                } else {
4030                    when_undefined.push(term);
4031                }
4032            }
4033            pending.push(when_defined);
4034            pending.push(when_undefined);
4035        }
4036        true
4037    }
4038
4039    fn declarations_share_exhaustive_conditional_family(
4040        &self,
4041        analyzer: &CppGraphSource<'_>,
4042        candidates: &[&CodeUnit],
4043    ) -> bool {
4044        // Guard terms alone cannot distinguish one #if family from separate
4045        // blocks whose macros changed between declarations. Require every
4046        // physical range to belong to one syntax-tree family with a terminal
4047        // #else before the terms can prove branch coverage.
4048        let mut family_range = None;
4049        for candidate in candidates {
4050            let Some(prepared) = self.cpp.prepared_syntax(candidate.source()) else {
4051                return false;
4052            };
4053            let root = prepared.tree().root_node();
4054            let mut candidate_family = None;
4055            for range in analyzer.ranges(candidate) {
4056                let Some(node) = root.descendant_for_byte_range(range.start_byte, range.end_byte)
4057                else {
4058                    return false;
4059                };
4060                let Some(family) = preprocessor_conditional_family_for_declaration(node) else {
4061                    return false;
4062                };
4063                let key = (family.start_byte(), family.end_byte());
4064                if candidate_family.is_some_and(|existing| existing != key) {
4065                    return false;
4066                }
4067                candidate_family = Some(key);
4068            }
4069            let Some(candidate_family) = candidate_family else {
4070                return false;
4071            };
4072            if family_range.is_some_and(|existing| existing != candidate_family) {
4073                return false;
4074            }
4075            family_range = Some(candidate_family);
4076        }
4077        family_range.is_some()
4078    }
4079
4080    pub fn complementary_same_fqn_type_declarations(
4081        &self,
4082        analyzer: &CppGraphSource<'_>,
4083        candidates: &[&CodeUnit],
4084        target: &CodeUnit,
4085    ) -> bool {
4086        if candidates.len() < 2
4087            || !self.alternate_same_fqn_type_declarations(analyzer, candidates, target)
4088            || !self.declarations_share_exhaustive_conditional_family(analyzer, candidates)
4089        {
4090            return false;
4091        }
4092        let requirements = candidates
4093            .iter()
4094            .map(|candidate| declaration_guard_requirements(analyzer, self.cpp, candidate))
4095            .collect::<Vec<_>>();
4096        let terms = requirements
4097            .into_iter()
4098            .flat_map(|ranges| ranges.into_iter().map(|(_, guards)| guards))
4099            .collect::<Vec<_>>();
4100        Self::preprocessor_guard_terms_cover_all_paths(&terms)
4101    }
4102
4103    fn type_candidate_preserving_target(
4104        &self,
4105        analyzer: &CppGraphSource<'_>,
4106        visible_from: &ProjectFile,
4107        candidate: &CodeUnit,
4108        target: &CodeUnit,
4109    ) -> Option<CodeUnit> {
4110        let mut current = candidate.clone();
4111        let mut matched_target = same_visible_symbol(&current, target)
4112            || self.compatible_primary_template_redeclarations(&current, target);
4113        let mut seen = HashSet::default();
4114        loop {
4115            if !seen.insert(current.clone()) {
4116                return None;
4117            }
4118            let Some(alias_target) = self.structured_alias_target(analyzer, &current) else {
4119                return matched_target
4120                    .then(|| target.clone())
4121                    .or_else(|| current.is_class().then_some(current));
4122            };
4123            if self.flattened_macro_namespace_alias_target_matches(
4124                analyzer,
4125                visible_from,
4126                &current,
4127                &alias_target,
4128                target,
4129            ) {
4130                return Some(target.clone());
4131            }
4132            if matches!(alias_target, StructuredAliasTarget::Builtin) {
4133                return matched_target
4134                    .then(|| target.clone())
4135                    .or_else(|| current.is_class().then_some(current));
4136            }
4137            // A non-template alias can name a template alias with explicit
4138            // arguments (for example, `using Result = Expected<int>`).  When
4139            // the requested target is that alias's primary declaration, keep
4140            // the primary identity before expanding the RHS arguments.  The
4141            // expansion would otherwise canonicalize through the underlying
4142            // implementation type and lose the target spelling used by the
4143            // forward resolver.
4144            if !self.cpp_template_metadata.contains_key(&current)
4145                && let Some(primary) =
4146                    self.resolve_structured_alias_primary(visible_from, &current, &alias_target)
4147                && (same_visible_symbol(&primary, target)
4148                    || self.compatible_primary_template_redeclarations(&primary, target))
4149            {
4150                return Some(target.clone());
4151            }
4152            if same_visible_symbol(&current, target) {
4153                return Some(target.clone());
4154            }
4155            if self.cpp_template_metadata.contains_key(&current) {
4156                return None;
4157            }
4158            let Some(next) =
4159                self.resolve_structured_alias_target(visible_from, &current, &alias_target)
4160            else {
4161                return matched_target.then(|| target.clone());
4162            };
4163            current = next;
4164            matched_target |= same_visible_symbol(&current, target)
4165                || self.compatible_primary_template_redeclarations(&current, target);
4166        }
4167    }
4168
4169    fn compatible_primary_template_redeclarations(
4170        &self,
4171        left: &CodeUnit,
4172        right: &CodeUnit,
4173    ) -> bool {
4174        let (Some(left_metadata), Some(right_metadata)) = (
4175            self.cpp_template_metadata.get(left),
4176            self.cpp_template_metadata.get(right),
4177        ) else {
4178            return false;
4179        };
4180        left_metadata.primary_fq_name == right_metadata.primary_fq_name
4181            && left_metadata.specialization_arguments.is_empty()
4182            && right_metadata.specialization_arguments.is_empty()
4183            && cpp_reconcile_primary_template_parameters(
4184                &[(left, left_metadata), (right, right_metadata)],
4185                right,
4186            )
4187            .is_some()
4188    }
4189
4190    fn alias_candidate_may_preserve_target(
4191        &self,
4192        analyzer: &CppGraphSource<'_>,
4193        visible_from: &ProjectFile,
4194        candidate: &CodeUnit,
4195        target: &CodeUnit,
4196    ) -> bool {
4197        let mut current = candidate.clone();
4198        let mut seen = HashSet::default();
4199        loop {
4200            if same_visible_symbol(&current, target)
4201                || self.compatible_primary_template_redeclarations(&current, target)
4202            {
4203                return true;
4204            }
4205            if self.cpp_template_metadata.contains_key(&current) {
4206                return true;
4207            }
4208            let Some(alias_target) = self.structured_alias_target(analyzer, &current) else {
4209                return false;
4210            };
4211            let StructuredAliasTarget::Named {
4212                components,
4213                global,
4214                arguments,
4215            } = alias_target
4216            else {
4217                return false;
4218            };
4219            if arguments.is_some() || !seen.insert(current.clone()) {
4220                return true;
4221            }
4222            let qualified = components.join("::");
4223            let next = if global {
4224                unique_logical_type_candidate(self.type_candidates(visible_from, &qualified))
4225            } else {
4226                self.resolve_unique_type_for_declaration(visible_from, &current, &qualified)
4227            };
4228            let Some(next) = next else {
4229                return true;
4230            };
4231            current = next;
4232        }
4233    }
4234
4235    fn resolve_unique_type_for_declaration(
4236        &self,
4237        visible_from: &ProjectFile,
4238        declaration: &CodeUnit,
4239        raw_name: &str,
4240    ) -> Option<CodeUnit> {
4241        let normalized = normalize_reference_name(raw_name)?;
4242        if let Some(namespace) = cpp_namespace_for(declaration) {
4243            for prefix in namespace_prefixes(&namespace) {
4244                let qualified = format!("{prefix}::{normalized}");
4245                let candidates = self.type_candidates(visible_from, &qualified);
4246                if !candidates.is_empty() {
4247                    return unique_logical_type_candidate(candidates);
4248                }
4249            }
4250        }
4251        unique_logical_type_candidate(self.type_candidates(visible_from, &normalized))
4252    }
4253
4254    pub fn resolves_to_type(
4255        &self,
4256        analyzer: &CppGraphSource<'_>,
4257        file: &ProjectFile,
4258        raw_name: &str,
4259        target: &CodeUnit,
4260    ) -> bool {
4261        let Some(normalized) = normalize_reference_name(raw_name) else {
4262            return false;
4263        };
4264        let candidates = self.type_candidates(file, &normalized);
4265        if candidates.is_empty() {
4266            return self.parser_alias_resolves_to_type(analyzer, file, raw_name, target);
4267        }
4268        let Some(resolved) =
4269            self.unique_type_candidate_preserving_target(analyzer, file, &candidates, target)
4270        else {
4271            return false;
4272        };
4273        same_symbol(&resolved, target) || same_visible_symbol(&resolved, target)
4274    }
4275
4276    pub fn alias_target(&self, alias: &CodeUnit) -> Option<CodeUnit> {
4277        let raw_target = type_alias_target_text(alias)?;
4278        let resolved = self.resolve_type_for_declaration(alias.source(), alias, raw_target)?;
4279        match resolved.kind() {
4280            CodeUnitType::Class => Some(resolved),
4281            _ if is_type_alias(&resolved) => self.alias_target(&resolved),
4282            _ => None,
4283        }
4284    }
4285
4286    pub fn canonical_type_for_reference(
4287        &self,
4288        file: &ProjectFile,
4289        raw_name: &str,
4290    ) -> Option<CodeUnit> {
4291        let resolved = self.resolve_type(file, raw_name)?;
4292        self.alias_target(&resolved).or(Some(resolved))
4293    }
4294
4295    pub fn parser_alias_resolves_to_type(
4296        &self,
4297        analyzer: &CppGraphSource<'_>,
4298        file: &ProjectFile,
4299        raw_name: &str,
4300        target: &CodeUnit,
4301    ) -> bool {
4302        let Some(alias_name) = normalize_reference_name(raw_name) else {
4303            return false;
4304        };
4305        let Some(cpp) = analyzer.cpp else {
4306            return false;
4307        };
4308        let matches_file = |source_file: &ProjectFile| {
4309            self.file_alias_matches(cpp, source_file, &alias_name, target)
4310        };
4311        self.visible_source_files_by_root.get(file).map_or_else(
4312            || matches_file(file),
4313            |files| files.iter().any(matches_file),
4314        )
4315    }
4316
4317    fn file_alias_matches(
4318        &self,
4319        cpp: &dyn CppSource,
4320        file: &ProjectFile,
4321        alias_name: &str,
4322        target: &CodeUnit,
4323    ) -> bool {
4324        let cell = {
4325            let mut cells = self.alias_cells.lock().expect("alias cell map lock");
4326            Arc::clone(
4327                cells
4328                    .entry(file.clone())
4329                    .or_insert_with(|| Arc::new(OnceLock::new())),
4330            )
4331        };
4332        cell.get_or_init(|| {
4333            #[cfg(any(test, feature = "test-support"))]
4334            {
4335                *self
4336                    .alias_source_parse_counts
4337                    .lock()
4338                    .expect("alias source parse count lock")
4339                    .entry(file.clone())
4340                    .or_default() += 1;
4341            }
4342            aliases_from_prepared_source(cpp, file).into_boxed_slice()
4343        })
4344        .iter()
4345        .any(|alias| alias.name == alias_name && alias_target_matches_target(alias, target))
4346    }
4347
4348    #[cfg(any(test, feature = "test-support"))]
4349    pub fn visible_source_files_for_test(&self, file: &ProjectFile) -> HashSet<ProjectFile> {
4350        self.visible_source_files_by_root
4351            .get(file)
4352            .cloned()
4353            .unwrap_or_else(|| HashSet::from_iter([file.clone()]))
4354    }
4355
4356    #[cfg(any(test, feature = "test-support"))]
4357    pub fn alias_source_parse_count_for_test(&self, file: &ProjectFile) -> usize {
4358        self.alias_source_parse_counts
4359            .lock()
4360            .expect("alias source parse count lock")
4361            .get(file)
4362            .copied()
4363            .unwrap_or(0)
4364    }
4365
4366    pub fn resolve_named(
4367        &self,
4368        file: &ProjectFile,
4369        raw_name: &str,
4370        kind: TargetKind,
4371    ) -> Option<CodeUnit> {
4372        let normalized = normalize_reference_name(raw_name)?;
4373        self.named_candidates_for_normalized(file, &normalized, kind)
4374            .into_iter()
4375            .next()
4376            .cloned()
4377    }
4378
4379    pub fn contains_named_symbol(
4380        &self,
4381        file: &ProjectFile,
4382        raw_name: &str,
4383        kind: TargetKind,
4384        target: &CodeUnit,
4385    ) -> bool {
4386        let Some(normalized) = normalize_reference_name(raw_name) else {
4387            return false;
4388        };
4389        self.named_candidates_for_normalized(file, &normalized, kind)
4390            .into_iter()
4391            .any(|unit| {
4392                matches_kind_for_lookup(unit, kind)
4393                    && reference_matches_unit(&normalized, unit)
4394                    && same_visible_symbol(unit, target)
4395            })
4396    }
4397
4398    pub fn named_candidates(
4399        &self,
4400        file: &ProjectFile,
4401        raw_name: &str,
4402        kind: TargetKind,
4403    ) -> Vec<CodeUnit> {
4404        let Some(normalized) = normalize_reference_name(raw_name) else {
4405            return Vec::new();
4406        };
4407        self.named_candidates_for_normalized(file, &normalized, kind)
4408            .into_iter()
4409            .cloned()
4410            .collect()
4411    }
4412
4413    pub fn resolve_known_non_target(
4414        &self,
4415        file: &ProjectFile,
4416        raw_name: &str,
4417        kind: TargetKind,
4418        target: &CodeUnit,
4419    ) -> bool {
4420        let Some(normalized) = normalize_reference_name(raw_name) else {
4421            return false;
4422        };
4423        normalized.contains("::")
4424            && self
4425                .named_candidates_for_normalized(file, &normalized, kind)
4426                .into_iter()
4427                .any(|unit| {
4428                    matches_kind_for_lookup(unit, kind)
4429                        && reference_matches_unit(&normalized, unit)
4430                        && !same_visible_symbol(unit, target)
4431                })
4432    }
4433
4434    pub fn resolve_call_return_binding(
4435        &self,
4436        analyzer: &CppGraphSource<'_>,
4437        file: &ProjectFile,
4438        raw_name: &str,
4439        arity: usize,
4440        lexical_namespace: Option<&str>,
4441        direct_type: Option<&CodeUnit>,
4442    ) -> Option<CppScanBinding> {
4443        let normalized = normalize_reference_name(raw_name)?;
4444        let mut candidates = Vec::new();
4445        for function in
4446            self.named_candidates_for_normalized(file, &normalized, TargetKind::FreeFunction)
4447        {
4448            if cpp_callable_arity(analyzer, function).accepts(arity)
4449                && !direct_type.is_some_and(|direct_type| {
4450                    self.callable_is_constructor_declaration(analyzer, function)
4451                        && type_owner_of(analyzer, function)
4452                            .is_some_and(|owner| same_visible_symbol(&owner, direct_type))
4453                })
4454            {
4455                candidates.push(function.clone());
4456            }
4457        }
4458        candidates = nearest_namespace_candidates(candidates, &normalized, lexical_namespace);
4459        unanimous_return_binding(analyzer, self, file, &candidates)
4460    }
4461
4462    pub fn resolve_call_return_binding_without_arity(
4463        &self,
4464        analyzer: &CppGraphSource<'_>,
4465        file: &ProjectFile,
4466        raw_name: &str,
4467        lexical_namespace: Option<&str>,
4468        direct_type: Option<&CodeUnit>,
4469    ) -> (bool, Option<CppScanBinding>) {
4470        let Some(normalized) = normalize_reference_name(raw_name) else {
4471            return (false, None);
4472        };
4473        let mut candidates = self
4474            .named_candidates_for_normalized(file, &normalized, TargetKind::FreeFunction)
4475            .into_iter()
4476            .filter(|function| {
4477                function.is_function()
4478                    && !direct_type.is_some_and(|direct_type| {
4479                        self.callable_is_constructor_declaration(analyzer, function)
4480                            && type_owner_of(analyzer, function)
4481                                .is_some_and(|owner| same_visible_symbol(&owner, direct_type))
4482                    })
4483            })
4484            .cloned()
4485            .collect::<Vec<_>>();
4486        candidates = nearest_namespace_candidates(candidates, &normalized, lexical_namespace);
4487        let has_candidates = !candidates.is_empty();
4488        (
4489            has_candidates,
4490            unanimous_return_binding(analyzer, self, file, &candidates),
4491        )
4492    }
4493
4494    pub fn visible_identifier_candidates<'b>(
4495        &'b self,
4496        file: &ProjectFile,
4497        identifier: &str,
4498    ) -> impl Iterator<Item = &'b CodeUnit> + 'b {
4499        self.visible_by_identifier
4500            .get(file)
4501            .and_then(|by_name| by_name.get(identifier))
4502            .into_iter()
4503            .flatten()
4504    }
4505
4506    /// Return terminal reference names that can denote `target` from `file`.
4507    ///
4508    /// The indexed candidate table covers ordinary declarations and aliases;
4509    /// parser-only aliases are read through their per-file cells so this path
4510    /// never reparses a source that has already been inspected by the visibility
4511    /// index.
4512    pub fn visible_type_reference_component_names_for_target(
4513        &self,
4514        analyzer: &CppGraphSource<'_>,
4515        file: &ProjectFile,
4516        target: &CodeUnit,
4517    ) -> HashSet<String> {
4518        let mut names = HashSet::from_iter([target.identifier().to_string()]);
4519        if let Some(metadata) = self.cpp_template_metadata.get(target) {
4520            names.insert(metadata.primary_name.clone());
4521        }
4522
4523        if let Some(by_identifier) = self.visible_by_identifier.get(file) {
4524            for (identifier, candidates) in by_identifier {
4525                if candidates.iter().any(|candidate| {
4526                    (candidate.is_class()
4527                        && (same_visible_symbol(candidate, target)
4528                            || self.compatible_primary_template_redeclarations(candidate, target)))
4529                        || (declared_type_alias(analyzer, candidate)
4530                            && self.alias_candidate_may_preserve_target(
4531                                analyzer, file, candidate, target,
4532                            ))
4533                }) {
4534                    names.insert(identifier.clone());
4535                }
4536            }
4537        }
4538
4539        names.extend(self.visible_parser_alias_names_for_target(file, target));
4540
4541        names
4542    }
4543
4544    pub fn indexed_structural_class_scope(
4545        &self,
4546        file: &ProjectFile,
4547        class: Node<'_>,
4548        source: &str,
4549    ) -> Option<Vec<String>> {
4550        let key = (file.clone(), class.start_byte(), class.end_byte());
4551        if let Some(cached) = self
4552            .indexed_structural_class_scopes
4553            .lock()
4554            .expect("C++ indexed structural-class scope cache poisoned")
4555            .get(&key)
4556            .cloned()
4557        {
4558            return cached;
4559        }
4560        let resolved = (|| {
4561            let name = class.child_by_field_name("name")?;
4562            let identifier = if name.kind() == "template_type" {
4563                node_text(name.child_by_field_name("name")?, source).to_string()
4564            } else {
4565                let mut components = Vec::new();
4566                append_cpp_name_components(name, source, &mut components)?;
4567                components.last()?.clone()
4568            };
4569            let visible = self
4570                .visible_identifier_candidates(file, &identifier)
4571                .cloned()
4572                .collect::<Vec<_>>();
4573            let mut visible = visible;
4574            for candidate in
4575                self.visible_by_file
4576                    .get(file)
4577                    .into_iter()
4578                    .flatten()
4579                    .filter(|candidate| {
4580                        self.cpp_template_metadata
4581                            .get(candidate)
4582                            .is_some_and(|metadata| metadata.primary_name == identifier)
4583                    })
4584            {
4585                if !visible
4586                    .iter()
4587                    .any(|existing| same_logical_symbol(existing, candidate))
4588                {
4589                    visible.push(candidate.clone());
4590                }
4591            }
4592            // Built once per call rather than per candidate; `cpp_source` rebuilds
4593            // the five-field source from the same `self.cpp` on every call.
4594            let cpp_source = self.cpp_source();
4595            let candidates = visible
4596                .iter()
4597                .filter(|candidate| {
4598                    candidate.source() == file
4599                        && candidate.is_class()
4600                        && !declared_type_alias(&cpp_source, candidate)
4601                        && self.cpp.ranges(candidate).iter().any(|range| {
4602                            range.start_byte <= class.start_byte()
4603                                && class.end_byte() <= range.end_byte
4604                        })
4605                })
4606                .collect::<Vec<_>>();
4607            let owner = if name.kind() == "template_type" {
4608                let expected = normalize_cpp_whitespace(node_text(name, source));
4609                let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
4610                let exact = candidates
4611                    .iter()
4612                    .copied()
4613                    .filter(|candidate| {
4614                        candidate
4615                            .fq()
4616                            .segments()
4617                            .iter()
4618                            .rev()
4619                            .find_map(|&segment| {
4620                                let (text, kind) = interner.resolve(segment);
4621                                matches!(
4622                                    kind,
4623                                    brokk_bifrost_core::analyzer::fq_name::SegmentKind::Type
4624                                        | brokk_bifrost_core::analyzer::fq_name::SegmentKind::Nested
4625                                )
4626                                .then_some(text)
4627                            })
4628                            .is_some_and(|text| text == expected)
4629                    })
4630                    .collect::<Vec<_>>();
4631                unique_logical_type_candidate(exact)
4632                    .or_else(|| unique_logical_type_candidate(candidates.clone()))?
4633            } else {
4634                unique_logical_type_candidate(candidates)?
4635            };
4636            Some(canonical_cpp_scope_components(&owner))
4637        })();
4638        self.indexed_structural_class_scopes
4639            .lock()
4640            .expect("C++ indexed structural-class scope cache poisoned")
4641            .insert(key, resolved.clone());
4642        resolved
4643    }
4644
4645    pub fn indexed_enclosing_owner_scope(
4646        &self,
4647        analyzer: &CppGraphSource<'_>,
4648        file: &ProjectFile,
4649        node: Node<'_>,
4650    ) -> Option<Vec<String>> {
4651        let anchor = std::iter::successors(Some(node), |current| current.parent())
4652            .find(|current| {
4653                matches!(
4654                    current.kind(),
4655                    "function_definition"
4656                        | "class_specifier"
4657                        | "struct_specifier"
4658                        | "union_specifier"
4659                )
4660            })
4661            .unwrap_or(node);
4662        let key = (file.clone(), anchor.start_byte(), anchor.end_byte());
4663        if let Some(cached) = self
4664            .indexed_enclosing_owner_scopes
4665            .lock()
4666            .expect("C++ indexed enclosing-owner scope cache poisoned")
4667            .get(&key)
4668            .cloned()
4669        {
4670            return cached;
4671        }
4672        let resolved = (|| {
4673            let range = Range {
4674                start_byte: node.start_byte(),
4675                end_byte: node.end_byte(),
4676                start_line: node.start_position().row,
4677                end_line: node.end_position().row,
4678            };
4679            let start = analyzer.enclosing_code_unit(file, &range)?;
4680            let owner = brokk_bifrost_core::analyzer::usages::common::enclosing_owner_chain(
4681                start,
4682                |unit| self.cached_precise_parent_of(analyzer, unit),
4683            )
4684            .find(|unit| {
4685                unit.is_class()
4686                    && !analyzer
4687                        .type_alias_provider()
4688                        .is_some_and(|provider| provider.is_type_alias(unit))
4689            })?;
4690            Some(canonical_cpp_scope_components(&owner))
4691        })();
4692        self.indexed_enclosing_owner_scopes
4693            .lock()
4694            .expect("C++ indexed enclosing-owner scope cache poisoned")
4695            .insert(key, resolved.clone());
4696        resolved
4697    }
4698
4699    fn cached_precise_parent_of(
4700        &self,
4701        analyzer: &CppGraphSource<'_>,
4702        code_unit: &CodeUnit,
4703    ) -> Option<CodeUnit> {
4704        if let Some(cached) = self
4705            .precise_parent_cache
4706            .lock()
4707            .expect("C++ precise-parent cache poisoned")
4708            .get(code_unit)
4709            .cloned()
4710        {
4711            return cached;
4712        }
4713        let resolved = precise_parent_resolution(analyzer, code_unit).map(|owner| owner.unit);
4714        self.precise_parent_cache
4715            .lock()
4716            .expect("C++ precise-parent cache poisoned")
4717            .insert(code_unit.clone(), resolved.clone());
4718        resolved
4719    }
4720
4721    pub fn callable_is_constructor_declaration(
4722        &self,
4723        analyzer: &CppGraphSource<'_>,
4724        candidate: &CodeUnit,
4725    ) -> bool {
4726        if !candidate.is_function() {
4727            return false;
4728        }
4729        let Some(prepared) = self.cpp.prepared_syntax(candidate.source()) else {
4730            return false;
4731        };
4732        let root = prepared.tree().root_node();
4733        let candidate_ranges = analyzer.ranges(candidate);
4734        let enclosed_by_matching_type = candidate_ranges.iter().any(|range| {
4735            let mut current = root
4736                .descendant_for_byte_range(range.start_byte, range.end_byte)
4737                .and_then(|node| node.parent());
4738            while let Some(node) = current {
4739                if matches!(
4740                    node.kind(),
4741                    "class_specifier" | "struct_specifier" | "union_specifier"
4742                ) {
4743                    return node
4744                        .child_by_field_name("name")
4745                        .map(|name| terminal_name(node_text(name, prepared.source())))
4746                        .is_some_and(|name| name == candidate.identifier());
4747                }
4748                current = node.parent();
4749            }
4750            false
4751        });
4752        if enclosed_by_matching_type {
4753            return true;
4754        }
4755        let indexed_containment = analyzer
4756            .declarations(candidate.source())
4757            .into_iter()
4758            .filter(|unit| unit.is_class() && unit.identifier() == candidate.identifier())
4759            .any(|owner| {
4760                analyzer.ranges(&owner).iter().any(|owner_range| {
4761                    candidate_ranges.iter().any(|candidate_range| {
4762                        owner_range.start_byte <= candidate_range.start_byte
4763                            && candidate_range.end_byte <= owner_range.end_byte
4764                    })
4765                })
4766            });
4767        if indexed_containment {
4768            return true;
4769        }
4770        let metadata = analyzer.signature_metadata(candidate);
4771        !metadata.is_empty()
4772            && metadata
4773                .iter()
4774                .all(|signature| signature.return_type_text().is_none())
4775    }
4776
4777    pub fn type_name_candidates<'b>(
4778        &'b self,
4779        file: &ProjectFile,
4780        normalized: &str,
4781    ) -> Vec<&'b CodeUnit> {
4782        self.candidate_units(file, normalized, TargetKind::Type)
4783    }
4784
4785    pub fn visible_members_for_owner_name<'b>(
4786        &'b self,
4787        file: &ProjectFile,
4788        owner: &CodeUnit,
4789        name: &str,
4790    ) -> Vec<&'b CodeUnit> {
4791        self.visible_identifier_candidates(file, name)
4792            .filter(|unit| {
4793                // Structured owner pop on the unit's own `fq()` (shared with
4794                // `CodeUnitIndex::parent_of`), not a re-split of its rendered fqn
4795                // string.
4796                brokk_bifrost_core::analyzer::default_parent_fq_name(unit)
4797                    .is_some_and(|parent| parent == owner.fq_name())
4798            })
4799            .collect()
4800    }
4801
4802    pub fn visible_member_for_owner_name(
4803        &self,
4804        file: &ProjectFile,
4805        owner: &CodeUnit,
4806        name: &str,
4807    ) -> VisibleMemberResolution {
4808        let candidates = self.visible_members_for_owner_name(file, owner, name);
4809        let mut callables = Vec::new();
4810        let mut non_callable = None;
4811        for candidate in candidates {
4812            if candidate.is_function() {
4813                callables.push(candidate.clone());
4814            } else if non_callable.is_none() {
4815                non_callable = Some(candidate.clone());
4816            }
4817        }
4818        match (callables.is_empty(), non_callable) {
4819            (false, None) => VisibleMemberResolution::Callable(callables),
4820            (true, Some(_)) => VisibleMemberResolution::NonCallable,
4821            (false, Some(_)) => VisibleMemberResolution::AmbiguousKind,
4822            (true, None) => VisibleMemberResolution::Missing,
4823        }
4824    }
4825
4826    fn field_declared_type_fact(
4827        &self,
4828        analyzer: &CppGraphSource<'_>,
4829        field: &CodeUnit,
4830    ) -> Option<DeclaredFieldTypeFact> {
4831        if let Some(cached) = self
4832            .field_type_facts
4833            .lock()
4834            .expect("C++ field type fact cache poisoned")
4835            .get(field)
4836            .cloned()
4837        {
4838            return cached;
4839        }
4840        let decoded = decode_field_declared_type_fact(analyzer, field);
4841        self.field_type_facts
4842            .lock()
4843            .expect("C++ field type fact cache poisoned")
4844            .insert(field.clone(), decoded.clone());
4845        decoded
4846    }
4847
4848    fn structured_alias_target(
4849        &self,
4850        analyzer: &CppGraphSource<'_>,
4851        unit: &CodeUnit,
4852    ) -> Option<StructuredAliasTarget> {
4853        if let Some(cached) = self
4854            .structured_alias_targets
4855            .lock()
4856            .expect("C++ structured alias target cache poisoned")
4857            .get(unit)
4858            .cloned()
4859        {
4860            return cached;
4861        }
4862        let decoded = decode_structured_alias_target(analyzer, unit);
4863        self.structured_alias_targets
4864            .lock()
4865            .expect("C++ structured alias target cache poisoned")
4866            .insert(unit.clone(), decoded.clone());
4867        decoded
4868    }
4869
4870    pub fn type_candidates<'b>(
4871        &'b self,
4872        file: &ProjectFile,
4873        normalized: &str,
4874    ) -> Vec<&'b CodeUnit> {
4875        let mut candidates = self
4876            .candidate_units(file, normalized, TargetKind::Type)
4877            .into_iter()
4878            .filter(|unit| unit.kind() == CodeUnitType::Class || is_type_alias(unit))
4879            .collect::<Vec<_>>();
4880        dedup_unit_refs(&mut candidates);
4881        candidates
4882    }
4883
4884    pub fn named_candidates_for_normalized<'b>(
4885        &'b self,
4886        file: &ProjectFile,
4887        normalized: &str,
4888        kind: TargetKind,
4889    ) -> Vec<&'b CodeUnit> {
4890        let mut candidates = self
4891            .candidate_units(file, normalized, kind)
4892            .into_iter()
4893            .filter(|unit| {
4894                matches_kind_for_lookup(unit, kind) && reference_matches_unit(normalized, unit)
4895            })
4896            .collect::<Vec<_>>();
4897        dedup_unit_refs(&mut candidates);
4898        candidates
4899    }
4900
4901    pub fn candidate_units<'b>(
4902        &'b self,
4903        file: &ProjectFile,
4904        normalized: &str,
4905        kind: TargetKind,
4906    ) -> Vec<&'b CodeUnit> {
4907        if normalized.contains("::") {
4908            // `normalized` comes from `normalize_cpp_reference_text`, which
4909            // truncates at the first `(`/`{`/`<`, leaving a plain `::`-joined
4910            // qualified-id with no embedded `.`/`/`/`\` and operator tokens
4911            // kept intact by the shared splitter's operator merge — the same
4912            // domain `cpp_reference_fqn_candidates` below already parses with
4913            // the shared splitter. Re-tokenizing and taking the last segment
4914            // reproduces `rsplit("::").find(non-empty)`'s terminal-component
4915            // scan exactly.
4916            let Some(identifier) = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
4917                brokk_bifrost_core::analyzer::Language::Cpp,
4918                normalized,
4919            )
4920            .pop() else {
4921                return Vec::new();
4922            };
4923            let fqns = cpp_reference_fqn_candidates(normalized, kind);
4924            return self
4925                .visible_identifier_candidates(file, &identifier)
4926                .filter(|unit| {
4927                    #[cfg(any(test, feature = "test-support"))]
4928                    self.qualified_candidate_inspections
4929                        .fetch_add(1, Ordering::Relaxed);
4930                    fqns.iter().any(|fqn| unit.fq_name() == *fqn)
4931                        || canonical_cpp_name_matches(unit, normalized)
4932                })
4933                .collect();
4934        }
4935        self.visible_identifier_candidates(file, normalized)
4936            .collect()
4937    }
4938
4939    #[cfg(any(test, feature = "test-support"))]
4940    pub fn reset_qualified_candidate_inspections(&self) {
4941        self.qualified_candidate_inspections
4942            .store(0, Ordering::Relaxed);
4943    }
4944
4945    #[cfg(any(test, feature = "test-support"))]
4946    pub fn qualified_candidate_inspections(&self) -> usize {
4947        self.qualified_candidate_inspections.load(Ordering::Relaxed)
4948    }
4949
4950    #[cfg(any(test, feature = "test-support"))]
4951    pub fn reset_target_preserving_type_resolution_count(&self) {
4952        self.target_preserving_type_resolution_count
4953            .store(0, Ordering::Relaxed);
4954    }
4955
4956    #[cfg(any(test, feature = "test-support"))]
4957    pub fn target_preserving_type_resolution_count(&self) -> usize {
4958        self.target_preserving_type_resolution_count
4959            .load(Ordering::Relaxed)
4960    }
4961
4962    #[cfg(any(test, feature = "test-support"))]
4963    pub fn visible_parser_alias_name_set_build_count(&self) -> usize {
4964        self.visible_parser_alias_name_set_build_count
4965            .load(Ordering::Relaxed)
4966    }
4967
4968    #[cfg(any(test, feature = "test-support"))]
4969    pub fn visible_parser_alias_target_names_build_count(&self) -> usize {
4970        self.visible_parser_alias_target_names_build_count
4971            .load(Ordering::Relaxed)
4972    }
4973}
4974
4975#[derive(Default)]
4976struct IncludeGraph {
4977    targets_by_file: HashMap<ProjectFile, Vec<ProjectFile>>,
4978}
4979
4980impl IncludeGraph {
4981    fn extend_with<F>(
4982        &mut self,
4983        root: &ProjectFile,
4984        cancellation: Option<&CancellationToken>,
4985        targets_for: &mut F,
4986    ) where
4987        F: FnMut(&ProjectFile) -> Vec<ProjectFile>,
4988    {
4989        let mut stack = vec![root.clone()];
4990        while let Some(file) = stack.pop() {
4991            if cancellation.is_some_and(CancellationToken::is_cancelled) {
4992                break;
4993            }
4994            if self.targets_by_file.contains_key(&file) {
4995                continue;
4996            }
4997            let targets = targets_for(&file);
4998            stack.extend(targets.iter().cloned());
4999            self.targets_by_file.insert(file, targets);
5000        }
5001    }
5002
5003    fn files(&self) -> impl Iterator<Item = &ProjectFile> {
5004        self.targets_by_file.keys()
5005    }
5006
5007    fn targets(&self, file: &ProjectFile) -> &[ProjectFile] {
5008        self.targets_by_file
5009            .get(file)
5010            .map(Vec::as_slice)
5011            .unwrap_or_default()
5012    }
5013}
5014
5015pub struct VisibilityData {
5016    pub visible_by_file: HashMap<ProjectFile, HashSet<CodeUnit>>,
5017    pub visible_source_files_by_root: HashMap<ProjectFile, HashSet<ProjectFile>>,
5018}
5019
5020pub fn build_visibility_data<F, D>(
5021    roots: &HashSet<ProjectFile>,
5022    cancellation: Option<&CancellationToken>,
5023    mut targets_for: F,
5024    mut declarations_for: D,
5025) -> VisibilityData
5026where
5027    F: FnMut(&ProjectFile) -> Vec<ProjectFile>,
5028    D: FnMut(&ProjectFile) -> BTreeSet<CodeUnit>,
5029{
5030    let mut include_graph = IncludeGraph::default();
5031    for file in roots {
5032        if cancellation.is_some_and(CancellationToken::is_cancelled) {
5033            break;
5034        }
5035        include_graph.extend_with(file, cancellation, &mut targets_for);
5036    }
5037    let declarations_by_file: HashMap<ProjectFile, BTreeSet<CodeUnit>> = include_graph
5038        .files()
5039        .take_while(|_| !cancellation.is_some_and(CancellationToken::is_cancelled))
5040        .map(|file| (file.clone(), declarations_for(file)))
5041        .collect();
5042    let mut visible_by_file = HashMap::default();
5043    let mut visible_source_files_by_root = HashMap::default();
5044    for file in roots {
5045        if cancellation.is_some_and(CancellationToken::is_cancelled) {
5046            break;
5047        }
5048        let mut visited = HashSet::default();
5049        let mut visible = HashSet::default();
5050        collect_visible_declarations(
5051            &include_graph,
5052            &declarations_by_file,
5053            file,
5054            &mut visited,
5055            &mut visible,
5056            cancellation,
5057        );
5058        visible_by_file.insert(file.clone(), visible);
5059        visible_source_files_by_root.insert(file.clone(), visited);
5060    }
5061    VisibilityData {
5062        visible_by_file,
5063        visible_source_files_by_root,
5064    }
5065}
5066
5067pub enum VisibleMemberResolution {
5068    Callable(Vec<CodeUnit>),
5069    NonCallable,
5070    AmbiguousKind,
5071    Missing,
5072}
5073
5074#[derive(Clone)]
5075pub enum EnclosingMemberOwnerResolution {
5076    Owner(CodeUnit),
5077    Ambiguous,
5078    Missing,
5079}
5080
5081pub fn resolve_declaring_member_owner(
5082    analyzer: &CppGraphSource<'_>,
5083    visibility: &VisibilityIndex<'_>,
5084    file: &ProjectFile,
5085    receiver_owner: &CodeUnit,
5086    member_name: &str,
5087) -> EnclosingMemberOwnerResolution {
5088    let Some(hierarchy) = analyzer.type_hierarchy_provider() else {
5089        return EnclosingMemberOwnerResolution::Missing;
5090    };
5091    let Some(receiver_owner) =
5092        visibility.canonical_visible_full_type_unit(analyzer, file, receiver_owner)
5093    else {
5094        return EnclosingMemberOwnerResolution::Ambiguous;
5095    };
5096    let resolve_level = |frontier: &[CodeUnit]| {
5097        let mut member_owners = Vec::new();
5098        for raw_owner in frontier {
5099            let Some(owner) =
5100                visibility.canonical_visible_full_type_unit(analyzer, file, raw_owner)
5101            else {
5102                return EnclosingMemberOwnerResolution::Ambiguous;
5103            };
5104            for member in visibility.visible_members_for_owner_name(file, &owner, member_name) {
5105                let Some(member_owner) = type_owner_of(analyzer, member) else {
5106                    return EnclosingMemberOwnerResolution::Ambiguous;
5107                };
5108                if !member_owners
5109                    .iter()
5110                    .any(|existing| same_visible_symbol(existing, &member_owner))
5111                {
5112                    member_owners.push(member_owner);
5113                }
5114            }
5115        }
5116        match member_owners.len() {
5117            0 => EnclosingMemberOwnerResolution::Missing,
5118            1 => EnclosingMemberOwnerResolution::Owner(member_owners.pop().unwrap()),
5119            _ => EnclosingMemberOwnerResolution::Ambiguous,
5120        }
5121    };
5122    // The first declaration on each structured base path hides deeper names,
5123    // regardless of whether its callable overload is applicable at a particular
5124    // call site. Applicability is checked only after this owner is established.
5125    let direct = resolve_level(std::slice::from_ref(&receiver_owner));
5126    if !matches!(direct, EnclosingMemberOwnerResolution::Missing) {
5127        return direct;
5128    }
5129    let mut stack = hierarchy.get_direct_ancestors(&receiver_owner);
5130    let mut propagated_counts: HashMap<CodeUnit, u8> = HashMap::default();
5131    let mut path_matches = Vec::new();
5132    while let Some(raw_owner) = stack.pop() {
5133        let Some(owner) = visibility.canonical_visible_full_type_unit(analyzer, file, &raw_owner)
5134        else {
5135            return EnclosingMemberOwnerResolution::Ambiguous;
5136        };
5137        // Persisted hierarchy edges do not encode virtual-base or base-subobject paths.
5138        // Propagate at most two occurrences of each owner: that preserves the distinction
5139        // between one and multiple resolving base paths without exponential diamond walks.
5140        let propagated = propagated_counts.entry(owner.clone()).or_default();
5141        if *propagated == 2 {
5142            continue;
5143        }
5144        *propagated += 1;
5145        match resolve_level(std::slice::from_ref(&owner)) {
5146            EnclosingMemberOwnerResolution::Owner(owner) => {
5147                path_matches.push(owner);
5148                if path_matches.len() == 2 {
5149                    return EnclosingMemberOwnerResolution::Ambiguous;
5150                }
5151            }
5152            EnclosingMemberOwnerResolution::Ambiguous => {
5153                return EnclosingMemberOwnerResolution::Ambiguous;
5154            }
5155            EnclosingMemberOwnerResolution::Missing => {
5156                stack.extend(hierarchy.get_direct_ancestors(&owner));
5157            }
5158        }
5159    }
5160    match path_matches.len() {
5161        0 => EnclosingMemberOwnerResolution::Missing,
5162        1 => EnclosingMemberOwnerResolution::Owner(path_matches.pop().unwrap()),
5163        _ => unreachable!("base-path matches are capped at one before returning"),
5164    }
5165}
5166
5167pub fn lexical_component_tiers<'a>(
5168    components: &'a [String],
5169    global: bool,
5170    lexical_scope: &'a [String],
5171) -> impl Iterator<Item = Vec<String>> + 'a {
5172    let first_prefix_len = if global { 0 } else { lexical_scope.len() };
5173    (0..=first_prefix_len).rev().map(move |prefix_len| {
5174        let mut qualified = Vec::with_capacity(prefix_len + components.len());
5175        qualified.extend_from_slice(&lexical_scope[..prefix_len]);
5176        qualified.extend_from_slice(components);
5177        qualified
5178    })
5179}
5180
5181pub fn build_visible_identifier_index(
5182    analyzer: &CppGraphSource<'_>,
5183    visible_by_file: &HashMap<ProjectFile, HashSet<CodeUnit>>,
5184    visible_source_files_by_root: &HashMap<ProjectFile, HashSet<ProjectFile>>,
5185    global_field_internal_linkage: &mut HashMap<CodeUnit, bool>,
5186) -> HashMap<ProjectFile, HashMap<String, Vec<CodeUnit>>> {
5187    let mut out = HashMap::default();
5188    for (file, visible) in visible_by_file {
5189        let mut by_identifier: HashMap<String, Vec<CodeUnit>> = HashMap::default();
5190        for unit in visible {
5191            if unit.is_field()
5192                && !visible_source_files_by_root
5193                    .get(file)
5194                    .is_some_and(|sources| sources.contains(unit.source()))
5195                && cpp_global_field_has_internal_linkage_cached(
5196                    analyzer,
5197                    global_field_internal_linkage,
5198                    unit,
5199                )
5200            {
5201                continue;
5202            }
5203            by_identifier
5204                .entry(unit.identifier().to_string())
5205                .or_default()
5206                .push(unit.clone());
5207        }
5208        for units in by_identifier.values_mut() {
5209            sort_lookup_units(units);
5210            units.dedup();
5211        }
5212        out.insert(file.clone(), by_identifier);
5213    }
5214    out
5215}
5216
5217fn sort_lookup_units(units: &mut [CodeUnit]) {
5218    units.sort_by(|left, right| {
5219        left.fq_name()
5220            .cmp(&right.fq_name())
5221            .then_with(|| left.signature().cmp(&right.signature()))
5222            .then_with(|| left.source().cmp(right.source()))
5223    });
5224}
5225
5226fn dedup_unit_refs(units: &mut Vec<&CodeUnit>) {
5227    let mut deduped = Vec::with_capacity(units.len());
5228    for unit in units.drain(..) {
5229        if !deduped.contains(&unit) {
5230            deduped.push(unit);
5231        }
5232    }
5233    *units = deduped;
5234}
5235
5236pub fn cpp_reference_fqn_candidates(reference: &str, kind: TargetKind) -> Vec<String> {
5237    // Same domain as `candidate_units` above: `reference` is a plain
5238    // `::`-joined qualified-id with operator tokens kept intact by the shared
5239    // splitter's operator merge.
5240    let parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
5241        brokk_bifrost_core::analyzer::Language::Cpp,
5242        reference,
5243    );
5244    if parts.is_empty() {
5245        return Vec::new();
5246    }
5247
5248    let mut candidates = Vec::new();
5249    for package_len in 0..parts.len() {
5250        let package = parts[..package_len].join("::");
5251        let rest = &parts[package_len..];
5252        if rest.is_empty() {
5253            continue;
5254        }
5255        match kind {
5256            TargetKind::Type | TargetKind::Constructor => {
5257                push_cpp_fqn_candidate(&mut candidates, &package, &rest.join("$"));
5258                push_cpp_fqn_candidate(&mut candidates, &package, &rest.join("."));
5259            }
5260            TargetKind::FreeFunction
5261            | TargetKind::Method
5262            | TargetKind::GlobalField
5263            | TargetKind::MemberField => {
5264                push_cpp_fqn_candidate(&mut candidates, &package, &rest.join("."));
5265                if rest.len() > 1 {
5266                    let owner = rest[..rest.len() - 1].join("$");
5267                    let short = format!("{}.{}", owner, rest[rest.len() - 1]);
5268                    push_cpp_fqn_candidate(&mut candidates, &package, &short);
5269                }
5270            }
5271        }
5272    }
5273    candidates
5274}
5275
5276fn push_cpp_fqn_candidate(out: &mut Vec<String>, package: &str, short: &str) {
5277    let fqn = if package.is_empty() {
5278        short.to_string()
5279    } else {
5280        format!("{package}.{short}")
5281    };
5282    if !out.contains(&fqn) {
5283        out.push(fqn);
5284    }
5285}
5286
5287pub fn infer_cpp_initializer_type(
5288    analyzer: &CppGraphSource<'_>,
5289    visibility: &VisibilityIndex<'_>,
5290    file: &ProjectFile,
5291    source: &str,
5292    node: Node<'_>,
5293) -> Option<CodeUnit> {
5294    infer_cpp_initializer_binding(analyzer, visibility, file, source, node, None)
5295        .and_then(|binding| binding.unit)
5296}
5297
5298pub fn infer_cpp_initializer_binding(
5299    analyzer: &CppGraphSource<'_>,
5300    visibility: &VisibilityIndex<'_>,
5301    file: &ProjectFile,
5302    source: &str,
5303    node: Node<'_>,
5304    receiver_resolver: Option<&ReceiverResolver<'_>>,
5305) -> Option<CppScanBinding> {
5306    match node.kind() {
5307        "new_expression" => {
5308            let text = normalize_cpp_whitespace(node_text(node, source));
5309            let rest = text.strip_prefix("new ").unwrap_or(text.as_str());
5310            let type_text = rest.split(['(', '{']).next().unwrap_or(rest);
5311            let name = normalize_cpp_type_name(type_text);
5312            Some(CppScanBinding::from_type_name(
5313                name.clone(),
5314                visibility.resolve_type(file, &name),
5315                1,
5316            ))
5317        }
5318        "call_expression" => node.child_by_field_name("function").and_then(|function| {
5319            let function_text = node_text(function, source);
5320            let direct_type_binding = visibility
5321                .resolve_type(file, function_text)
5322                .map(|unit| CppScanBinding::from_unit(unit, 0));
5323            if function.kind() == "template_function" && direct_type_binding.is_some() {
5324                let lexical_namespace = enclosing_namespace_context(node, source);
5325                let arity = visibility.call_arity_evidence(file, node, source).exact();
5326                if let Some(arity) = arity
5327                    && let Some(binding) = visibility.resolve_call_return_binding(
5328                        analyzer,
5329                        file,
5330                        function_text,
5331                        arity,
5332                        lexical_namespace.as_deref(),
5333                        direct_type_binding
5334                            .as_ref()
5335                            .and_then(|binding| binding.unit.as_ref()),
5336                    )
5337                {
5338                    return Some(binding);
5339                }
5340                let (has_callable, callable_binding) = visibility
5341                    .resolve_call_return_binding_without_arity(
5342                        analyzer,
5343                        file,
5344                        function_text,
5345                        lexical_namespace.as_deref(),
5346                        direct_type_binding
5347                            .as_ref()
5348                            .and_then(|binding| binding.unit.as_ref()),
5349                    );
5350                if let Some(binding) = callable_binding {
5351                    return Some(binding);
5352                }
5353                if has_callable {
5354                    return None;
5355                }
5356                return direct_type_binding;
5357            }
5358            let arity = visibility.call_arity_evidence(file, node, source).exact()?;
5359            let direct_type_binding_for_call = direct_type_binding.clone();
5360            resolve_static_method_call_return_binding(
5361                analyzer, visibility, file, source, function, arity,
5362            )
5363            .or(direct_type_binding)
5364            .or_else(|| {
5365                visibility.resolve_call_return_binding(
5366                    analyzer,
5367                    file,
5368                    function_text,
5369                    arity,
5370                    enclosing_namespace_context(node, source).as_deref(),
5371                    direct_type_binding_for_call
5372                        .as_ref()
5373                        .and_then(|binding| binding.unit.as_ref()),
5374                )
5375            })
5376            .or_else(|| {
5377                resolve_field_method_call_return_binding(
5378                    analyzer,
5379                    visibility,
5380                    file,
5381                    source,
5382                    function,
5383                    arity,
5384                    receiver_resolver,
5385                )
5386            })
5387        }),
5388        _ => None,
5389    }
5390}
5391
5392fn resolve_static_method_call_return_binding(
5393    analyzer: &CppGraphSource<'_>,
5394    visibility: &VisibilityIndex<'_>,
5395    file: &ProjectFile,
5396    source: &str,
5397    function: Node<'_>,
5398    arity: usize,
5399) -> Option<CppScanBinding> {
5400    if function.kind() != "qualified_identifier" {
5401        return None;
5402    }
5403    let qualified = normalize_cpp_reference_text(node_text(function, source));
5404    // A C++ qualified-id is `::`-joined with no embedded delimiters in any
5405    // single component (the shared splitter's operator-token merge keeps
5406    // `operator+`-style names intact), so re-tokenizing with the shared
5407    // structured splitter and peeling the terminal segment reproduces
5408    // `rsplit_once("::")`'s (owner, member) split exactly — same shape as
5409    // `cpp_out_of_line_function_owner`'s `qualified` split above.
5410    let parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
5411        brokk_bifrost_core::analyzer::Language::Cpp,
5412        &qualified,
5413    );
5414    let (owner_text, member_name) = match parts.split_last() {
5415        Some((member, owner_parts)) if !owner_parts.is_empty() => {
5416            (owner_parts.join("::"), member.clone())
5417        }
5418        _ => {
5419            let scope = function.child_by_field_name("scope")?;
5420            let name = function.child_by_field_name("name")?;
5421            (
5422                node_text(scope, source).to_string(),
5423                node_text(name, source).to_string(),
5424            )
5425        }
5426    };
5427    let owner = visibility.resolve_type(file, &owner_text)?;
5428    let candidates = visibility
5429        .visible_members_for_owner_name(file, &owner, &member_name)
5430        .into_iter()
5431        .filter(|unit| unit.is_function() && cpp_callable_arity(analyzer, unit).accepts(arity))
5432        .cloned()
5433        .collect::<Vec<_>>();
5434    unanimous_return_binding(analyzer, visibility, file, &candidates)
5435}
5436
5437fn resolve_field_method_call_return_binding(
5438    analyzer: &CppGraphSource<'_>,
5439    visibility: &VisibilityIndex<'_>,
5440    file: &ProjectFile,
5441    source: &str,
5442    function: Node<'_>,
5443    arity: usize,
5444    receiver_resolver: Option<&ReceiverResolver<'_>>,
5445) -> Option<CppScanBinding> {
5446    if function.kind() != "field_expression" {
5447        return None;
5448    }
5449    let receiver_resolver = receiver_resolver?;
5450    let field = function.child_by_field_name("field")?;
5451    let member_name = node_text(field, source);
5452    let receiver = function
5453        .child_by_field_name("argument")
5454        .or_else(|| function.named_child(0))?;
5455    let owners = receiver_resolver(receiver, source);
5456    let mut candidates = Vec::new();
5457    for owner in owners {
5458        candidates.extend(
5459            visibility
5460                .visible_members_for_owner_name(file, &owner, member_name)
5461                .into_iter()
5462                .filter(|unit| {
5463                    unit.is_function() && cpp_callable_arity(analyzer, unit).accepts(arity)
5464                })
5465                .cloned(),
5466        );
5467    }
5468    unanimous_return_binding(analyzer, visibility, file, &candidates)
5469}
5470
5471fn unanimous_return_binding(
5472    analyzer: &CppGraphSource<'_>,
5473    visibility: &VisibilityIndex<'_>,
5474    file: &ProjectFile,
5475    candidates: &[CodeUnit],
5476) -> Option<CppScanBinding> {
5477    let mut resolved_return: Option<CppScanBinding> = None;
5478    for function in candidates {
5479        let metadata = analyzer.signature_metadata(function);
5480        let return_types = if metadata.is_empty() {
5481            vec![cpp_function_return_type_text(analyzer, function)?]
5482        } else {
5483            metadata
5484                .iter()
5485                .map(|metadata| metadata.return_type_text().map(str::to_string))
5486                .collect::<Option<Vec<_>>>()?
5487        };
5488        for return_text in return_types {
5489            let indirection = crate::call_match::cpp_type_text_pointer_depth(&return_text);
5490            let name = normalize_cpp_type_name(&return_text);
5491            let binding = CppScanBinding::from_type_name(
5492                name.clone(),
5493                visibility
5494                    .resolve_unique_canonical_type_for_declaration(analyzer, file, function, &name),
5495                indirection,
5496            );
5497            if let Some(existing) = resolved_return.as_ref()
5498                && (existing.indirection != binding.indirection
5499                    || match (&existing.unit, &binding.unit) {
5500                        (Some(left), Some(right)) => !same_visible_symbol(left, right),
5501                        (None, None) => existing.type_name != binding.type_name,
5502                        (Some(_), None) | (None, Some(_)) => true,
5503                    })
5504            {
5505                return None;
5506            }
5507            resolved_return = Some(binding);
5508        }
5509    }
5510    resolved_return
5511}
5512
5513fn aliases_from_prepared_source(cpp: &dyn CppSource, file: &ProjectFile) -> Vec<CppAlias> {
5514    let Some(prepared) = cpp.prepared_syntax(file) else {
5515        return Vec::new();
5516    };
5517    let mut aliases = Vec::new();
5518    collect_cpp_aliases(prepared.tree().root_node(), prepared.source(), &mut aliases);
5519    aliases
5520}
5521
5522fn collect_cpp_aliases(root: Node<'_>, source: &str, out: &mut Vec<CppAlias>) {
5523    let mut stack = vec![root];
5524    while let Some(node) = stack.pop() {
5525        match node.kind() {
5526            "alias_declaration" if alias_has_visible_file_scope(node) => {
5527                if let Some(alias) = cpp_alias_from_alias_declaration(node, source) {
5528                    out.push(alias);
5529                }
5530            }
5531            "type_definition" if alias_has_visible_file_scope(node) => {
5532                collect_typedef_aliases(node, source, out)
5533            }
5534            _ => {}
5535        }
5536
5537        for index in (0..node.named_child_count()).rev() {
5538            if let Some(child) = node.named_child(index) {
5539                stack.push(child);
5540            }
5541        }
5542    }
5543}
5544
5545fn alias_has_visible_file_scope(node: Node<'_>) -> bool {
5546    let mut current = node.parent();
5547    while let Some(parent) = current {
5548        match parent.kind() {
5549            "translation_unit"
5550            | "namespace_definition"
5551            | "declaration_list"
5552            | "linkage_specification" => current = parent.parent(),
5553            "template_declaration" => current = parent.parent(),
5554            _ => return false,
5555        }
5556    }
5557    true
5558}
5559
5560fn cpp_alias_from_alias_declaration(node: Node<'_>, source: &str) -> Option<CppAlias> {
5561    let name = node
5562        .child_by_field_name("name")
5563        .and_then(|node| normalize_reference_name(node_text(node, source)))?;
5564    let target = node
5565        .child_by_field_name("type")
5566        .and_then(|node| normalize_reference_name(node_text(node, source)))?;
5567    Some(CppAlias {
5568        name,
5569        target,
5570        namespace: enclosing_namespace_context(node, source),
5571    })
5572}
5573
5574fn collect_typedef_aliases(node: Node<'_>, source: &str, out: &mut Vec<CppAlias>) {
5575    let Some(type_node) = node.child_by_field_name("type") else {
5576        return;
5577    };
5578    let Some(target) = normalize_reference_name(node_text(type_node, source)) else {
5579        return;
5580    };
5581
5582    let mut cursor = node.walk();
5583    for child in node.named_children(&mut cursor) {
5584        if same_node(child, type_node) {
5585            continue;
5586        }
5587        if let Some(name) = extract_typedef_declarator_name(child, source) {
5588            out.push(CppAlias {
5589                name,
5590                target: target.clone(),
5591                namespace: enclosing_namespace_context(node, source),
5592            });
5593        }
5594    }
5595}
5596
5597fn extract_typedef_declarator_name(node: Node<'_>, source: &str) -> Option<String> {
5598    match node.kind() {
5599        "identifier" | "field_identifier" | "type_identifier" | "qualified_identifier" => {
5600            normalize_reference_name(node_text(node, source))
5601        }
5602        _ => node
5603            .child_by_field_name("declarator")
5604            .or_else(|| node.child_by_field_name("name"))
5605            .or_else(|| last_named_child(node))
5606            .and_then(|child| extract_typedef_declarator_name(child, source)),
5607    }
5608}
5609
5610fn last_named_child(node: Node<'_>) -> Option<Node<'_>> {
5611    let count = node.named_child_count();
5612    if count == 0 {
5613        None
5614    } else {
5615        node.named_child(count - 1)
5616    }
5617}
5618
5619pub fn collect_include_closure(
5620    analyzer: &CppGraphSource<'_>,
5621    include_targets: &IncludeTargetIndex,
5622    file: &ProjectFile,
5623    out: &mut HashSet<ProjectFile>,
5624    cancellation: Option<&CancellationToken>,
5625) {
5626    let mut stack = vec![file.clone()];
5627    while let Some(file) = stack.pop() {
5628        if cancellation.is_some_and(CancellationToken::is_cancelled) {
5629            break;
5630        }
5631        if !out.insert(file.clone()) {
5632            continue;
5633        }
5634        let imports = analyzer.import_statements(&file);
5635        for include in cpp_include_paths(&imports) {
5636            for target in resolve_include_targets_with_index(&file, &include, include_targets) {
5637                stack.push(target);
5638            }
5639        }
5640    }
5641}
5642
5643fn collect_visible_declarations(
5644    include_graph: &IncludeGraph,
5645    declarations_by_file: &HashMap<ProjectFile, BTreeSet<CodeUnit>>,
5646    file: &ProjectFile,
5647    visited: &mut HashSet<ProjectFile>,
5648    out: &mut HashSet<CodeUnit>,
5649    cancellation: Option<&CancellationToken>,
5650) {
5651    let mut stack = vec![file.clone()];
5652    while let Some(file) = stack.pop() {
5653        if cancellation.is_some_and(CancellationToken::is_cancelled) {
5654            break;
5655        }
5656        if !visited.insert(file.clone()) {
5657            continue;
5658        }
5659        if let Some(declarations) = declarations_by_file.get(&file) {
5660            out.extend(declarations.iter().cloned());
5661        }
5662        stack.extend(include_graph.targets(&file).iter().cloned());
5663    }
5664}
5665
5666pub fn signature_arity(signature: Option<&str>) -> usize {
5667    let Some(signature) = signature else {
5668        return 0;
5669    };
5670    let inner = signature
5671        .find('(')
5672        .and_then(|open| {
5673            signature[open + 1..]
5674                .find(')')
5675                .map(|close| &signature[open + 1..open + 1 + close])
5676        })
5677        .unwrap_or(signature)
5678        .trim();
5679    if inner.is_empty() || inner == "void" {
5680        return 0;
5681    }
5682    cpp_split_top_level_commas(inner).count()
5683}
5684
5685fn parse_macro_parameter_list_arity(replacement: &str) -> Option<CallableArity> {
5686    let source = format!("void __bifrost_macro_parameters({replacement});");
5687    let mut parser = Parser::new();
5688    parser
5689        .set_language(&tree_sitter_cpp::LANGUAGE.into())
5690        .ok()?;
5691    let tree = parser.parse(&source, None)?;
5692    let root = tree.root_node();
5693    if root.has_error() {
5694        return None;
5695    }
5696    let declaration = root.named_child(0)?;
5697    let declarator = declaration.child_by_field_name("declarator")?;
5698    let parameters = declarator.child_by_field_name("parameters")?;
5699    let mut required = 0;
5700    let mut total = 0;
5701    let mut repeated = false;
5702    let mut cursor = parameters.walk();
5703    for parameter in parameters.children(&mut cursor) {
5704        match parameter.kind() {
5705            "parameter_declaration" => {
5706                if parameter.child_by_field_name("declarator").is_none()
5707                    && parameter
5708                        .child_by_field_name("type")
5709                        .is_some_and(|type_node| node_text(type_node, &source).trim() == "void")
5710                {
5711                    continue;
5712                }
5713                required += 1;
5714                total += 1;
5715            }
5716            "optional_parameter_declaration" => total += 1,
5717            "variadic_parameter" | "variadic_parameter_declaration" | "..." => {
5718                repeated = true;
5719            }
5720            _ => {}
5721        }
5722    }
5723    Some(CallableArity::new(required, total, repeated))
5724}
5725
5726pub fn cpp_callable_arity(analyzer: &CppGraphSource<'_>, unit: &CodeUnit) -> CallableArity {
5727    analyzer
5728        .signature_metadata(unit)
5729        .into_iter()
5730        .find_map(|metadata| metadata.callable_arity())
5731        .unwrap_or_else(|| CallableArity::exact(signature_arity(unit.signature())))
5732}
5733
5734fn merge_compatible_callable_arities(
5735    left: CallableArity,
5736    right: CallableArity,
5737) -> Option<CallableArity> {
5738    let total = left.total();
5739    let left_repeated = left.accepts(total.saturating_add(1));
5740    let right_repeated = right.accepts(right.total().saturating_add(1));
5741    if total != right.total() || left_repeated != right_repeated {
5742        return None;
5743    }
5744    let required = (0..=total).find(|arity| left.accepts(*arity) || right.accepts(*arity))?;
5745    Some(CallableArity::new(required, total, left_repeated))
5746}
5747
5748fn find_include_activation(
5749    cpp: &dyn CppSource,
5750    file: &ProjectFile,
5751    prepared: &PreparedSyntaxTree,
5752    donor_source: &ProjectFile,
5753) -> Option<usize> {
5754    let include_targets = cpp.include_target_index();
5755    let mut direct_includes = Vec::new();
5756    let mut nodes = vec![prepared.tree().root_node()];
5757    while let Some(node) = nodes.pop() {
5758        if node.kind() == "preproc_include" {
5759            if callable_preprocessor_context_is_visible_for_reference(node, prepared.source(), file)
5760            {
5761                let raw = normalize_cpp_whitespace(node_text(node, prepared.source()));
5762                for include in cpp_include_paths(std::slice::from_ref(&raw)) {
5763                    if let Some(target) = unique_include_target(resolve_include_targets_with_index(
5764                        file,
5765                        &include,
5766                        include_targets,
5767                    )) {
5768                        direct_includes.push((node.end_byte(), target));
5769                    }
5770                }
5771            }
5772            continue;
5773        }
5774        for index in (0..node.named_child_count()).rev() {
5775            if let Some(child) = node.named_child(index) {
5776                nodes.push(child);
5777            }
5778        }
5779    }
5780    direct_includes.sort_by_key(|(activation, _)| *activation);
5781    let mut known_missing = HashSet::default();
5782    direct_includes
5783        .into_iter()
5784        .find(|(_, direct)| {
5785            unconditional_include_reaches(
5786                cpp,
5787                include_targets,
5788                direct,
5789                donor_source,
5790                file,
5791                &mut known_missing,
5792            )
5793        })
5794        .map(|(activation, _)| activation)
5795}
5796
5797fn find_conditional_include_projections(
5798    cpp: &dyn CppSource,
5799    file: &ProjectFile,
5800    prepared: &PreparedSyntaxTree,
5801    donor_source: &ProjectFile,
5802) -> Vec<ConditionalIncludeProjection> {
5803    let include_targets = cpp.include_target_index();
5804    let mut projections = Vec::new();
5805    let mut nodes = vec![prepared.tree().root_node()];
5806    while let Some(node) = nodes.pop() {
5807        if node.kind() == "preproc_include" {
5808            let Some(required_guards) = preprocessor_guard_environment(node, prepared.source())
5809            else {
5810                continue;
5811            };
5812            let raw = normalize_cpp_whitespace(node_text(node, prepared.source()));
5813            for include in cpp_include_paths(std::slice::from_ref(&raw)) {
5814                let Some(target) = unique_include_target(resolve_include_targets_with_index(
5815                    file,
5816                    &include,
5817                    include_targets,
5818                )) else {
5819                    continue;
5820                };
5821                let paths = conditional_include_requirement_paths(
5822                    cpp,
5823                    &target,
5824                    donor_source,
5825                    required_guards.clone(),
5826                );
5827                for required_guards in paths {
5828                    if !projections
5829                        .iter()
5830                        .any(|projection: &ConditionalIncludeProjection| {
5831                            projection.activation_byte == node.end_byte()
5832                                && projection.required_guards == required_guards
5833                        })
5834                    {
5835                        projections.push(ConditionalIncludeProjection {
5836                            activation_byte: node.end_byte(),
5837                            required_guards,
5838                        });
5839                    }
5840                }
5841            }
5842            continue;
5843        }
5844        for index in (0..node.named_child_count()).rev() {
5845            if let Some(child) = node.named_child(index) {
5846                nodes.push(child);
5847            }
5848        }
5849    }
5850    projections.sort_by_key(|projection| projection.activation_byte);
5851    projections
5852}
5853
5854fn conditional_include_requirement_paths(
5855    cpp: &dyn CppSource,
5856    first: &ProjectFile,
5857    donor_source: &ProjectFile,
5858    required_guards: HashSet<PreprocessorGuard>,
5859) -> Vec<HashSet<PreprocessorGuard>> {
5860    let include_targets = cpp.include_target_index();
5861    let mut paths = Vec::new();
5862    let mut stack = vec![(
5863        first.clone(),
5864        required_guards,
5865        HashSet::from_iter([first.clone()]),
5866    )];
5867    while let Some((file, required_guards, visited)) = stack.pop() {
5868        if file == *donor_source {
5869            if !paths.contains(&required_guards) {
5870                paths.push(required_guards);
5871            }
5872            continue;
5873        }
5874        let Some(prepared) = cpp.prepared_syntax(&file) else {
5875            continue;
5876        };
5877        let mut nodes = vec![prepared.tree().root_node()];
5878        while let Some(node) = nodes.pop() {
5879            if node.kind() == "preproc_include" {
5880                let Some(include_guards) = preprocessor_guard_environment(node, prepared.source())
5881                else {
5882                    continue;
5883                };
5884                let Some(path_guards) =
5885                    merge_preprocessor_guards(&required_guards, &include_guards)
5886                else {
5887                    continue;
5888                };
5889                let raw = normalize_cpp_whitespace(node_text(node, prepared.source()));
5890                for include in cpp_include_paths(std::slice::from_ref(&raw)) {
5891                    let Some(target) = unique_include_target(resolve_include_targets_with_index(
5892                        &file,
5893                        &include,
5894                        include_targets,
5895                    )) else {
5896                        continue;
5897                    };
5898                    if visited.contains(&target) {
5899                        continue;
5900                    }
5901                    let mut next_visited = visited.clone();
5902                    next_visited.insert(target.clone());
5903                    stack.push((target, path_guards.clone(), next_visited));
5904                }
5905                continue;
5906            }
5907            for index in (0..node.named_child_count()).rev() {
5908                if let Some(child) = node.named_child(index) {
5909                    nodes.push(child);
5910                }
5911            }
5912        }
5913    }
5914    paths
5915}
5916
5917fn unconditional_include_reaches(
5918    cpp: &dyn CppSource,
5919    include_targets: &IncludeTargetIndex,
5920    first: &ProjectFile,
5921    donor_source: &ProjectFile,
5922    reference_file: &ProjectFile,
5923    known_missing: &mut HashSet<ProjectFile>,
5924) -> bool {
5925    if first == donor_source {
5926        return true;
5927    }
5928    if known_missing.contains(first) {
5929        return false;
5930    }
5931    let reference_is_c = reference_file
5932        .rel_path()
5933        .extension()
5934        .and_then(|extension| extension.to_str())
5935        == Some("c");
5936    if let Some(reaches) =
5937        cpp.cached_unconditional_include_reachability(first, donor_source, reference_is_c)
5938    {
5939        return reaches;
5940    }
5941    let mut visited = HashSet::default();
5942    let mut files = vec![first.clone()];
5943    while let Some(file) = files.pop() {
5944        if file == *donor_source {
5945            cpp.cache_unconditional_include_reachability(first, donor_source, reference_is_c, true);
5946            return true;
5947        }
5948        if known_missing.contains(&file) || !visited.insert(file.clone()) {
5949            continue;
5950        }
5951        let Some(prepared) = cpp.prepared_syntax(&file) else {
5952            continue;
5953        };
5954        let mut nodes = vec![prepared.tree().root_node()];
5955        while let Some(node) = nodes.pop() {
5956            if node.kind() == "preproc_include" {
5957                if callable_preprocessor_context_is_visible_for_reference(
5958                    node,
5959                    prepared.source(),
5960                    reference_file,
5961                ) {
5962                    let raw = normalize_cpp_whitespace(node_text(node, prepared.source()));
5963                    for include in cpp_include_paths(std::slice::from_ref(&raw)) {
5964                        if let Some(target) = unique_include_target(
5965                            resolve_include_targets_with_index(&file, &include, include_targets),
5966                        ) {
5967                            files.push(target);
5968                        }
5969                    }
5970                }
5971                continue;
5972            }
5973            for index in (0..node.named_child_count()).rev() {
5974                if let Some(child) = node.named_child(index) {
5975                    nodes.push(child);
5976                }
5977            }
5978        }
5979    }
5980    known_missing.extend(visited);
5981    cpp.cache_unconditional_include_reachability(first, donor_source, reference_is_c, false);
5982    false
5983}
5984
5985fn declaration_guard_requirements(
5986    analyzer: &CppGraphSource<'_>,
5987    cpp: &dyn CppSource,
5988    candidate: &CodeUnit,
5989) -> Vec<(usize, HashSet<PreprocessorGuard>)> {
5990    let Some(prepared) = cpp.prepared_syntax(candidate.source()) else {
5991        return Vec::new();
5992    };
5993    let root = prepared.tree().root_node();
5994    analyzer
5995        .ranges(candidate)
5996        .into_iter()
5997        .filter_map(|range| {
5998            root.descendant_for_byte_range(range.start_byte, range.end_byte)
5999                .and_then(|node| preprocessor_guard_environment(node, prepared.source()))
6000                // A class name is injected into its own body at the declaration's
6001                // introduction point, not after the complete class range. Using
6002                // the start also preserves normal before/after ordering for aliases.
6003                .map(|required| (range.start_byte, required))
6004        })
6005        .collect()
6006}
6007
6008fn guard_requirements_hold_at_reference(
6009    required: &HashSet<PreprocessorGuard>,
6010    reference: Option<&HashSet<PreprocessorGuard>>,
6011) -> bool {
6012    reference.is_some_and(|active| required.is_subset(active))
6013}
6014
6015fn preprocessor_conditional_family_for_declaration(node: Node<'_>) -> Option<Node<'_>> {
6016    let mut ancestor = node.parent();
6017    while let Some(current) = ancestor {
6018        if is_preprocessor_conditional(current) {
6019            let family = preprocessor_conditional_family_root(current);
6020            if preprocessor_conditional_family_has_terminal_else(family) {
6021                return Some(family);
6022            }
6023        }
6024        ancestor = current.parent();
6025    }
6026    None
6027}
6028
6029fn preprocessor_conditional_family_root(mut conditional: Node<'_>) -> Node<'_> {
6030    while let Some(parent) = conditional.parent() {
6031        let is_alternative = parent
6032            .child_by_field_name("alternative")
6033            .is_some_and(|alternative| {
6034                alternative.start_byte() == conditional.start_byte()
6035                    && alternative.end_byte() == conditional.end_byte()
6036            });
6037        if !is_alternative {
6038            break;
6039        }
6040        conditional = parent;
6041    }
6042    conditional
6043}
6044
6045fn preprocessor_conditional_family_has_terminal_else(mut conditional: Node<'_>) -> bool {
6046    loop {
6047        let Some(alternative) = conditional.child_by_field_name("alternative") else {
6048            return false;
6049        };
6050        match alternative.kind() {
6051            "preproc_else" => return true,
6052            "preproc_elif" => conditional = alternative,
6053            _ => return false,
6054        }
6055    }
6056}
6057
6058pub fn preprocessor_guard_environment(
6059    node: Node<'_>,
6060    source: &str,
6061) -> Option<HashSet<PreprocessorGuard>> {
6062    let mut guards = HashSet::default();
6063    let mut ancestor = node.parent();
6064    while let Some(conditional) = ancestor {
6065        if matches!(
6066            conditional.kind(),
6067            "preproc_if" | "preproc_ifdef" | "preproc_elif"
6068        ) && !is_file_covering_include_guard(conditional, source)
6069        {
6070            let guard = preprocessor_guard_for_descendant(conditional, node, source)?;
6071            match guard {
6072                PreprocessorGuard::Constant(true) => {
6073                    ancestor = conditional.parent();
6074                    continue;
6075                }
6076                PreprocessorGuard::Constant(false) => return None,
6077                _ => {}
6078            }
6079            if guards.contains(&guard.negated()) {
6080                return None;
6081            }
6082            guards.insert(guard);
6083        }
6084        ancestor = conditional.parent();
6085    }
6086    Some(guards)
6087}
6088
6089fn preprocessor_guard_for_descendant(
6090    conditional: Node<'_>,
6091    descendant: Node<'_>,
6092    source: &str,
6093) -> Option<PreprocessorGuard> {
6094    let mut guard = simple_preprocessor_guard(conditional, source)?;
6095    if conditional
6096        .child_by_field_name("alternative")
6097        .is_some_and(|alternative| {
6098            alternative.start_byte() <= descendant.start_byte()
6099                && descendant.end_byte() <= alternative.end_byte()
6100        })
6101    {
6102        let alternative = conditional.child_by_field_name("alternative")?;
6103        // Tree-sitter nests an `#elif` chain in each `alternative` field. A
6104        // descendant in any later branch must first exclude the parent branch,
6105        // then collect the nested `preproc_elif` guard from its own ancestor.
6106        if !matches!(alternative.kind(), "preproc_else" | "preproc_elif") {
6107            return None;
6108        }
6109        guard = guard.negated();
6110    }
6111    Some(guard)
6112}
6113
6114pub fn merge_preprocessor_guards(
6115    left: &HashSet<PreprocessorGuard>,
6116    right: &HashSet<PreprocessorGuard>,
6117) -> Option<HashSet<PreprocessorGuard>> {
6118    let mut merged = left.clone();
6119    for guard in right {
6120        if merged.contains(&guard.negated()) {
6121            return None;
6122        }
6123        merged.insert(guard.clone());
6124    }
6125    Some(merged)
6126}
6127
6128fn simple_preprocessor_guard(conditional: Node<'_>, source: &str) -> Option<PreprocessorGuard> {
6129    if conditional.kind() == "preproc_ifdef" {
6130        let name = conditional.child_by_field_name("name")?;
6131        let name = node_text(name, source).to_string();
6132        return match conditional.child(0)?.kind() {
6133            "#ifdef" => Some(PreprocessorGuard::Defined(name)),
6134            "#ifndef" => Some(PreprocessorGuard::Undefined(name)),
6135            _ => None,
6136        };
6137    }
6138    let condition = conditional.child_by_field_name("condition")?;
6139    simple_preprocessor_expression_guard(condition, source).or_else(|| {
6140        Some(PreprocessorGuard::Expression(normalize_cpp_whitespace(
6141            node_text(condition, source),
6142        )))
6143    })
6144}
6145
6146fn simple_preprocessor_expression_guard(
6147    expression: Node<'_>,
6148    source: &str,
6149) -> Option<PreprocessorGuard> {
6150    match expression.kind() {
6151        "number_literal" => match node_text(expression, source).trim() {
6152            "0" => Some(PreprocessorGuard::Constant(false)),
6153            "1" => Some(PreprocessorGuard::Constant(true)),
6154            _ => None,
6155        },
6156        "preproc_defined" => {
6157            let identifier = (0..expression.named_child_count())
6158                .filter_map(|index| expression.named_child(index))
6159                .find(|child| child.kind() == "identifier")?;
6160            Some(PreprocessorGuard::Defined(
6161                node_text(identifier, source).to_string(),
6162            ))
6163        }
6164        "unary_expression"
6165            if expression
6166                .child_by_field_name("operator")
6167                .is_some_and(|operator| operator.kind() == "!") =>
6168        {
6169            simple_preprocessor_expression_guard(
6170                expression.child_by_field_name("argument")?,
6171                source,
6172            )
6173            .map(|guard| guard.negated())
6174        }
6175        "parenthesized_expression" => (0..expression.named_child_count())
6176            .filter_map(|index| expression.named_child(index))
6177            .next()
6178            .and_then(|child| simple_preprocessor_expression_guard(child, source)),
6179        _ => None,
6180    }
6181}
6182
6183fn unique_include_target(mut targets: Vec<ProjectFile>) -> Option<ProjectFile> {
6184    if targets.len() == 1 {
6185        targets.pop()
6186    } else {
6187        None
6188    }
6189}
6190
6191fn callable_declaration_activation_in_file(
6192    analyzer: &CppGraphSource<'_>,
6193    prepared: &PreparedSyntaxTree,
6194    candidate: &CodeUnit,
6195    reference_file: &ProjectFile,
6196) -> Option<usize> {
6197    let root = prepared.tree().root_node();
6198    analyzer
6199        .ranges(candidate)
6200        .into_iter()
6201        .filter_map(|range| {
6202            let mut declaration =
6203                root.descendant_for_byte_range(range.start_byte, range.end_byte)?;
6204            while !matches!(
6205                declaration.kind(),
6206                "declaration" | "field_declaration" | "function_definition"
6207            ) {
6208                declaration = declaration.parent()?;
6209            }
6210            let mut ancestor = declaration.parent();
6211            while let Some(node) = ancestor {
6212                // An export macro between `class` and its name can make
6213                // tree-sitter recover the class body as the compound body of
6214                // a synthetic function_definition.  A member declaration in
6215                // that body is still namespace/class scoped; do not discard
6216                // its declaration activation merely because the malformed
6217                // wrapper looks callable.
6218                if node.kind() == "function_definition"
6219                    && crate::declarations::is_recovered_exported_class_container(
6220                        node,
6221                        prepared.source(),
6222                    )
6223                {
6224                    ancestor = node.parent();
6225                    continue;
6226                }
6227                if node.kind() == "compound_statement"
6228                    && node.parent().is_some_and(|parent| {
6229                        crate::declarations::is_recovered_exported_class_container(
6230                            parent,
6231                            prepared.source(),
6232                        )
6233                    })
6234                {
6235                    ancestor = node.parent().and_then(|parent| parent.parent());
6236                    continue;
6237                }
6238                if matches!(
6239                    node.kind(),
6240                    "compound_statement" | "function_definition" | "lambda_expression"
6241                ) {
6242                    return None;
6243                }
6244                ancestor = node.parent();
6245            }
6246            callable_preprocessor_context_is_visible_for_reference(
6247                declaration,
6248                prepared.source(),
6249                reference_file,
6250            )
6251            .then_some(declaration.end_byte())
6252        })
6253        .min()
6254}
6255
6256fn callable_preprocessor_context_is_visible_for_reference(
6257    node: Node<'_>,
6258    source: &str,
6259    reference_file: &ProjectFile,
6260) -> bool {
6261    let reference_is_c = reference_file
6262        .rel_path()
6263        .extension()
6264        .and_then(|extension| extension.to_str())
6265        == Some("c");
6266    let mut ancestor = node.parent();
6267    while let Some(conditional) = ancestor {
6268        if matches!(conditional.kind(), "preproc_if" | "preproc_ifdef")
6269            && !is_file_covering_include_guard(conditional, source)
6270            && !is_split_cpp_language_linkage_wrapper(conditional, node, source)
6271        {
6272            let Some(guard) = preprocessor_guard_for_descendant(conditional, node, source) else {
6273                return false;
6274            };
6275            match guard {
6276                PreprocessorGuard::Constant(true) => {}
6277                PreprocessorGuard::Constant(false) => return false,
6278                PreprocessorGuard::Defined(name) if name == "__cplusplus" => {
6279                    if reference_is_c {
6280                        return false;
6281                    }
6282                }
6283                PreprocessorGuard::Undefined(name) if name == "__cplusplus" => {
6284                    if !reference_is_c {
6285                        return false;
6286                    }
6287                }
6288                _ => return false,
6289            }
6290        }
6291        ancestor = conditional.parent();
6292    }
6293    true
6294}
6295
6296fn flattened_macro_namespace_declaration_matches(
6297    analyzer: &CppGraphSource<'_>,
6298    cpp: &dyn CppSource,
6299    reference_file: &ProjectFile,
6300    visible_declaration: &CodeUnit,
6301    qualified_candidate: &CodeUnit,
6302    reference_byte: usize,
6303) -> bool {
6304    // Namespace-opening macros can leave tree-sitter unable to retain the
6305    // namespace owner after a later recovery point. In that shape the forward
6306    // declaration is indexed at translation-unit scope, while the definition
6307    // still has its qualified owner. Require all surviving structural evidence
6308    // before treating the declaration as activation for that definition.
6309    if visible_declaration.kind() != qualified_candidate.kind()
6310        || visible_declaration.identifier() != qualified_candidate.identifier()
6311        || visible_declaration.signature() != qualified_candidate.signature()
6312        || !visible_declaration.package_name().is_empty()
6313        || qualified_candidate.package_name().is_empty()
6314    {
6315        return false;
6316    }
6317
6318    let Some(prepared) = cpp.prepared_syntax(visible_declaration.source()) else {
6319        return false;
6320    };
6321    let root = prepared.tree().root_node();
6322    let closing_brace_limit = if visible_declaration.source() == reference_file {
6323        reference_byte
6324    } else {
6325        usize::MAX
6326    };
6327
6328    analyzer
6329        .ranges(visible_declaration)
6330        .into_iter()
6331        .any(|range| {
6332            let Some(mut declaration) =
6333                root.descendant_for_byte_range(range.start_byte, range.end_byte)
6334            else {
6335                return false;
6336            };
6337            while !matches!(
6338                declaration.kind(),
6339                "declaration" | "field_declaration" | "function_definition"
6340            ) {
6341                let Some(parent) = declaration.parent() else {
6342                    return false;
6343                };
6344                declaration = parent;
6345            }
6346            if declaration
6347                .parent()
6348                .is_none_or(|parent| parent.kind() != "translation_unit")
6349                || !macro_displaced_cpp_return_type(declaration, prepared.source())
6350            {
6351                return false;
6352            }
6353
6354            let mut cursor = root.walk();
6355            root.named_children(&mut cursor).any(|sibling| {
6356                sibling.start_byte() >= declaration.end_byte()
6357                    && sibling.start_byte() < closing_brace_limit
6358                    && direct_unmatched_closing_brace(sibling)
6359            })
6360        })
6361}
6362
6363fn flattened_macro_namespace_components(
6364    declaration: Node<'_>,
6365    source: &str,
6366) -> Option<Vec<String>> {
6367    flattened_macro_function_namespace_components(declaration, source)
6368        .or_else(|| flattened_macro_error_namespace_components(declaration, source))
6369}
6370
6371fn flattened_macro_function_namespace_components(
6372    declaration: Node<'_>,
6373    source: &str,
6374) -> Option<Vec<String>> {
6375    let body = declaration
6376        .parent()
6377        .filter(|parent| parent.kind() == "compound_statement")?;
6378    let function = body
6379        .parent()
6380        .filter(|parent| parent.kind() == "function_definition" && parent.has_error())?;
6381    if function.child_by_field_name("body") != Some(body) {
6382        return None;
6383    }
6384    let mut cursor = function.walk();
6385    let prefix = function
6386        .named_children(&mut cursor)
6387        .take_while(|child| child.start_byte() < body.start_byte())
6388        .filter(|child| child.kind() != "comment")
6389        .collect::<Vec<_>>();
6390    let begin_index = prefix.iter().rposition(|child| {
6391        flattened_macro_sentinel_name(*child, source)
6392            .is_some_and(|name| name.ends_with("NAMESPACE_BEGIN"))
6393    })?;
6394    let mut identifiers = Vec::new();
6395    let mut stack = prefix[begin_index + 1..]
6396        .iter()
6397        .rev()
6398        .copied()
6399        .collect::<Vec<_>>();
6400    while let Some(current) = stack.pop() {
6401        if let Some(identifier) = direct_cpp_identifier_name(current, source) {
6402            identifiers.push(identifier);
6403            continue;
6404        }
6405        let mut cursor = current.walk();
6406        let children = current.named_children(&mut cursor).collect::<Vec<_>>();
6407        stack.extend(children.into_iter().rev());
6408    }
6409    let [keyword, namespace_name] = identifiers.as_slice() else {
6410        return None;
6411    };
6412    if keyword != "namespace" || namespace_name.is_empty() || cpp_export_macro_token(namespace_name)
6413    {
6414        return None;
6415    }
6416    let mut next = function.next_named_sibling();
6417    let next = loop {
6418        let candidate = next?;
6419        next = candidate.next_named_sibling();
6420        if candidate.kind() != "comment" {
6421            break candidate;
6422        }
6423    };
6424    if !flattened_macro_sentinel_name(next, source)
6425        .is_some_and(|name| name.ends_with("NAMESPACE_END"))
6426    {
6427        return None;
6428    }
6429    let mut components = enclosing_namespace_components(declaration, source)?;
6430    components.push(namespace_name.clone());
6431    Some(components)
6432}
6433
6434fn flattened_macro_error_namespace_components(
6435    declaration: Node<'_>,
6436    source: &str,
6437) -> Option<Vec<String>> {
6438    let parent = declaration
6439        .parent()
6440        .filter(|parent| parent.kind() == "ERROR" && parent.has_error())?;
6441    let mut cursor = parent.walk();
6442    let siblings = parent.named_children(&mut cursor).collect::<Vec<_>>();
6443    let declaration_index = siblings
6444        .iter()
6445        .position(|candidate| same_node(*candidate, declaration))?;
6446    let begin_index = (0..declaration_index).rev().find(|index| {
6447        flattened_macro_sentinel_name(siblings[*index], source)
6448            .is_some_and(|name| name.ends_with("NAMESPACE_BEGIN"))
6449    })?;
6450
6451    let significant = siblings[begin_index + 1..declaration_index]
6452        .iter()
6453        .copied()
6454        .filter(|node| node.kind() != "comment")
6455        .collect::<Vec<_>>();
6456    let [namespace_keyword, namespace_name, ..] = significant.as_slice() else {
6457        return None;
6458    };
6459    if direct_cpp_identifier_name(*namespace_keyword, source).as_deref() != Some("namespace") {
6460        return None;
6461    }
6462    let namespace_name = flattened_macro_namespace_name(*namespace_name, source)?;
6463    if significant[2..].iter().any(|node| {
6464        flattened_macro_sentinel_name(*node, source).is_some_and(|name| {
6465            name.ends_with("NAMESPACE_BEGIN") || name.ends_with("NAMESPACE_END")
6466        })
6467    }) {
6468        return None;
6469    }
6470
6471    let mut saw_namespace_close = false;
6472    for sibling in siblings.iter().skip(declaration_index + 1).copied() {
6473        if sibling.kind() == "comment" {
6474            continue;
6475        }
6476        if !saw_namespace_close {
6477            if direct_unmatched_closing_brace(sibling) {
6478                saw_namespace_close = true;
6479                continue;
6480            }
6481            if flattened_macro_sentinel_name(sibling, source).is_some() {
6482                return None;
6483            }
6484            continue;
6485        }
6486        if !flattened_macro_sentinel_name(sibling, source)
6487            .is_some_and(|name| name.ends_with("NAMESPACE_END"))
6488        {
6489            return None;
6490        }
6491        let mut components = enclosing_namespace_components(declaration, source)?;
6492        components.push(namespace_name);
6493        return Some(components);
6494    }
6495    None
6496}
6497
6498fn flattened_macro_sentinel_name(node: Node<'_>, source: &str) -> Option<String> {
6499    let candidate = direct_cpp_identifier_name(node, source).or_else(|| {
6500        node.child_by_field_name("type")
6501            .and_then(|type_node| direct_cpp_identifier_name(type_node, source))
6502    })?;
6503    (cpp_export_macro_token(&candidate)
6504        && (candidate.ends_with("NAMESPACE_BEGIN") || candidate.ends_with("NAMESPACE_END")))
6505    .then_some(candidate)
6506}
6507
6508fn flattened_macro_namespace_name(node: Node<'_>, source: &str) -> Option<String> {
6509    if node.kind() != "ERROR" || node.named_child_count() != 1 {
6510        return None;
6511    }
6512    let name = direct_cpp_identifier_name(node.named_child(0)?, source)?;
6513    (!cpp_export_macro_token(&name)).then_some(name)
6514}
6515
6516fn direct_cpp_identifier_name(node: Node<'_>, source: &str) -> Option<String> {
6517    if !matches!(
6518        node.kind(),
6519        "identifier" | "namespace_identifier" | "type_identifier"
6520    ) {
6521        return None;
6522    }
6523    let name = normalize_cpp_whitespace(node_text(node, source));
6524    (!name.is_empty()).then_some(name)
6525}
6526
6527fn guard_requirement_sets_match(
6528    left: &[(usize, HashSet<PreprocessorGuard>)],
6529    right: &[(usize, HashSet<PreprocessorGuard>)],
6530) -> bool {
6531    left.len() == right.len()
6532        && left.iter().all(|(_, left_guards)| {
6533            right
6534                .iter()
6535                .any(|(_, right_guards)| left_guards == right_guards)
6536        })
6537        && right.iter().all(|(_, right_guards)| {
6538            left.iter()
6539                .any(|(_, left_guards)| right_guards == left_guards)
6540        })
6541}
6542
6543fn macro_displaced_cpp_return_type(declaration: Node<'_>, source: &str) -> bool {
6544    let Some(type_node) = declaration.child_by_field_name("type") else {
6545        return false;
6546    };
6547    let type_name = normalize_cpp_whitespace(node_text(type_node, source));
6548    !type_name.is_empty()
6549        && type_name
6550            .chars()
6551            .all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_')
6552        && (0..declaration.named_child_count()).any(|index| {
6553            declaration
6554                .named_child(index)
6555                .is_some_and(|child| child.kind() == "ERROR")
6556        })
6557}
6558
6559fn direct_unmatched_closing_brace(node: Node<'_>) -> bool {
6560    node.kind() == "ERROR"
6561        && (0..node.child_count())
6562            .any(|index| node.child(index).is_some_and(|child| child.kind() == "}"))
6563}
6564
6565pub fn callable_preprocessor_context_is_visible(node: Node<'_>, source: &str) -> bool {
6566    let mut ancestor = node.parent();
6567    while let Some(parent) = ancestor {
6568        if is_preprocessor_conditional(parent)
6569            && !is_file_covering_include_guard(parent, source)
6570            && !is_split_cpp_language_linkage_wrapper(parent, node, source)
6571        {
6572            return false;
6573        }
6574        ancestor = parent.parent();
6575    }
6576    true
6577}
6578
6579fn is_split_cpp_language_linkage_wrapper(
6580    conditional: Node<'_>,
6581    descendant: Node<'_>,
6582    source: &str,
6583) -> bool {
6584    if conditional.kind() != "preproc_ifdef"
6585        || conditional.child_by_field_name("alternative").is_some()
6586        || conditional
6587            .child_by_field_name("name")
6588            .is_none_or(|name| node_text(name, source) != "__cplusplus")
6589    {
6590        return false;
6591    }
6592    let mut current = descendant.parent();
6593    let linkage = loop {
6594        let Some(node) = current else {
6595            return false;
6596        };
6597        if node == conditional {
6598            return false;
6599        }
6600        if node.kind() == "linkage_specification" {
6601            break node;
6602        }
6603        current = node.parent();
6604    };
6605    if linkage
6606        .child_by_field_name("value")
6607        .is_none_or(|value| node_text(value, source) != "\"C\"")
6608    {
6609        return false;
6610    }
6611    let Some(body) = linkage.child_by_field_name("body") else {
6612        return false;
6613    };
6614    let closes_opening_branch = (0..body.named_child_count())
6615        .filter_map(|index| body.named_child(index))
6616        .take_while(|child| child.end_byte() <= descendant.start_byte())
6617        .any(|child| {
6618            child.kind() == "preproc_call"
6619                && child
6620                    .child_by_field_name("directive")
6621                    .is_some_and(|directive| node_text(directive, source) == "#endif")
6622        });
6623    let reopens_for_closing_brace = (0..body.named_child_count())
6624        .filter_map(|index| body.named_child(index))
6625        .skip_while(|child| child.start_byte() < descendant.end_byte())
6626        .any(|child| {
6627            child.kind() == "preproc_ifdef"
6628                && child
6629                    .child_by_field_name("name")
6630                    .is_some_and(|name| node_text(name, source) == "__cplusplus")
6631                && (0..child.child_count()).any(|index| {
6632                    child
6633                        .child(index)
6634                        .is_some_and(|token| token.kind() == "#endif" && token.is_missing())
6635                })
6636        });
6637    closes_opening_branch && reopens_for_closing_brace
6638}
6639
6640pub fn call_arity(node: Node<'_>) -> usize {
6641    node.child_by_field_name("arguments")
6642        .or_else(|| node.child_by_field_name("parameters"))
6643        .or_else(|| node.child_by_field_name("value"))
6644        .or_else(|| first_named_child_of_kind(node, "argument_list"))
6645        .or_else(|| first_named_child_of_kind(node, "initializer_list"))
6646        .map(|args| argument_children(args).count())
6647        .unwrap_or(0)
6648}
6649
6650pub fn argument_children<'tree>(node: Node<'tree>) -> impl Iterator<Item = Node<'tree>> {
6651    let recovered_block_arguments = recovered_block_literal_arguments(node);
6652    (0..node.child_count())
6653        .filter_map(move |index| node.child(index))
6654        .filter(|child| child.is_named() && !child.is_extra())
6655        .flat_map(move |child| {
6656            if let Some((raw, left, right)) = recovered_block_arguments
6657                && child == raw
6658            {
6659                [Some(left), Some(right)]
6660            } else {
6661                [Some(child), None]
6662            }
6663        })
6664        .flatten()
6665}
6666
6667fn recovered_block_literal_arguments<'tree>(
6668    arguments: Node<'tree>,
6669) -> Option<(Node<'tree>, Node<'tree>, Node<'tree>)> {
6670    if arguments.kind() != "argument_list" {
6671        return None;
6672    }
6673    let mut raw_arguments = (0..arguments.child_count())
6674        .filter_map(|index| arguments.child(index))
6675        .filter(|child| child.is_named() && !child.is_extra());
6676    let raw = raw_arguments.next()?;
6677    if raw_arguments.next().is_some() || raw.kind() != "binary_expression" {
6678        return None;
6679    }
6680
6681    let left = raw.child_by_field_name("left")?;
6682    if left.is_missing() || left.start_byte() == left.end_byte() {
6683        return None;
6684    }
6685    let right = raw.child_by_field_name("right")?;
6686    if right.kind() != "compound_literal_expression"
6687        || right.is_missing()
6688        || right
6689            .child_by_field_name("type")
6690            .is_none_or(|node| node.kind() != "type_descriptor" || node.is_missing())
6691        || right
6692            .child_by_field_name("value")
6693            .is_none_or(|node| node.kind() != "initializer_list" || node.is_missing())
6694    {
6695        return None;
6696    }
6697    let has_intervening_error = (0..raw.child_count())
6698        .filter_map(|index| raw.child(index))
6699        .any(|child| {
6700            child.kind() == "ERROR"
6701                && !child.is_missing()
6702                && child.start_byte() >= left.end_byte()
6703                && child.end_byte() <= right.start_byte()
6704        });
6705    has_intervening_error.then_some((raw, left, right))
6706}
6707
6708pub fn constructor_type_node(node: Node<'_>) -> Option<Node<'_>> {
6709    match node.kind() {
6710        "new_expression" => node
6711            .child_by_field_name("type")
6712            .or_else(|| node.named_child(0)),
6713        "compound_literal_expression" => node.child_by_field_name("type"),
6714        "call_expression" => node.child_by_field_name("function"),
6715        _ => None,
6716    }
6717}
6718
6719pub fn field_initializer_constructs_target(
6720    node: Node<'_>,
6721    ctx: &ScanCtx<'_>,
6722    owner: &CodeUnit,
6723) -> bool {
6724    // A qualified name in a constructor initializer denotes a base
6725    // subobject constructor (`namespace::Base(args)`), not a member field.  The
6726    // field-initializer grammar exposes the qualified name as one structured
6727    // `qualified_identifier`; resolve its owner through the same lexical type
6728    // machinery used for ordinary C++ type references before considering the
6729    // initializer a hit.  This keeps an unrelated `namespace::Other(...)`, a
6730    // qualified non-constructor member, and an unresolved owner out of the
6731    // target constructor's inverse usage set.
6732    if first_named_child_of_kind(node, "qualified_identifier").is_some() {
6733        return qualified_base_initializer_constructs_target(node, ctx, owner);
6734    }
6735    let Some(name) = node
6736        .child_by_field_name("name")
6737        .or_else(|| first_named_child_of_kind(node, "field_identifier"))
6738        .or_else(|| first_named_child_of_kind(node, "qualified_identifier"))
6739    else {
6740        return false;
6741    };
6742    let field_name = node_text(name, ctx.source);
6743    ctx.visibility
6744        .visible_identifier_candidates(ctx.file, field_name)
6745        .filter(|unit| unit.is_field() && unit.identifier() == field_name)
6746        .any(|unit| field_declares_type(unit, ctx, owner))
6747}
6748
6749fn qualified_base_initializer_constructs_target(
6750    node: Node<'_>,
6751    ctx: &ScanCtx<'_>,
6752    owner: &CodeUnit,
6753) -> bool {
6754    let Some(qualified) = first_named_child_of_kind(node, "qualified_identifier") else {
6755        return false;
6756    };
6757    let Some(components) = cpp_type_name_components(qualified, ctx.source) else {
6758        return false;
6759    };
6760    let Some(lexical_scope) = enclosing_namespace_components(node, ctx.source) else {
6761        return false;
6762    };
6763    let resolves_target = |components: &[String]| {
6764        matches!(
6765            ctx.visibility.resolve_type_components_lexically_for_target(
6766                &ctx.analyzer,
6767                ctx.file,
6768                components,
6769                is_globally_qualified_cpp_name(qualified),
6770                &lexical_scope,
6771                owner,
6772            ),
6773            LexicalTypeResolution::Resolved { unit, .. }
6774                if same_visible_symbol(&unit, owner)
6775        )
6776    };
6777    if resolves_target(&components) {
6778        return true;
6779    }
6780
6781    // Some real-world code spells a base mem-initializer as
6782    // `Base::Base(args)`. In that structured path the final component repeats
6783    // the constructor name; resolve the preceding type path. The terminal
6784    // identity check prevents an arbitrary qualified member from taking this
6785    // route.
6786    components
6787        .last()
6788        .is_some_and(|terminal| terminal == owner.identifier())
6789        && resolves_target(&components[..components.len() - 1])
6790}
6791
6792fn field_declares_type(unit: &CodeUnit, ctx: &ScanCtx<'_>, owner: &CodeUnit) -> bool {
6793    unit.signature()
6794        .is_some_and(|declaration| field_declaration_type_matches(declaration, unit, ctx, owner))
6795        || ctx
6796            .analyzer
6797            .get_source(unit, false)
6798            .is_some_and(|declaration| {
6799                field_declaration_type_matches(&declaration, unit, ctx, owner)
6800            })
6801}
6802
6803pub fn field_declared_binding(
6804    analyzer: &CppGraphSource<'_>,
6805    visibility: &VisibilityIndex<'_>,
6806    visible_from: &ProjectFile,
6807    field: &CodeUnit,
6808) -> Option<CppScanBinding> {
6809    let fact = visibility.field_declared_type_fact(analyzer, field)?;
6810    let normalized = normalize_field_type_text(&fact.type_text);
6811    let resolved = visibility.resolve_unique_canonical_type_for_declaration(
6812        analyzer,
6813        visible_from,
6814        field,
6815        &normalized,
6816    );
6817    let resolved = match (resolved, fact.template_arguments.as_deref()) {
6818        (Some(primary), Some(arguments)) => visibility
6819            .resolve_template_arguments(visible_from, primary, arguments)
6820            .ok(),
6821        (resolved, None) => resolved,
6822        (None, Some(_)) => None,
6823    };
6824    Some(CppScanBinding::from_type_name(
6825        normalized,
6826        resolved,
6827        fact.indirection,
6828    ))
6829}
6830
6831fn type_alias_target_text(alias: &CodeUnit) -> Option<&str> {
6832    alias
6833        .signature()?
6834        .strip_prefix("using ")
6835        .and_then(|rest| rest.split_once('=').map(|(_, rhs)| rhs))
6836        .or_else(|| {
6837            alias
6838                .signature()?
6839                .strip_prefix("typedef ")
6840                .and_then(|rest| rest.rsplit_once(' ').map(|(lhs, _)| lhs))
6841        })
6842        .map(str::trim)
6843        .map(|target| target.trim_end_matches(';').trim())
6844}
6845
6846fn unique_logical_type_candidate(candidates: Vec<&CodeUnit>) -> Option<CodeUnit> {
6847    let first = candidates.first()?;
6848    candidates
6849        .iter()
6850        .all(|candidate| candidate.kind() == first.kind() && candidate.fq_name() == first.fq_name())
6851        .then(|| (*first).clone())
6852}
6853
6854fn unique_type_candidate_preserving_alias(
6855    analyzer: &CppGraphSource<'_>,
6856    candidates: &[&CodeUnit],
6857) -> Option<CodeUnit> {
6858    let first = *candidates.first()?;
6859    if declared_type_alias(analyzer, first) {
6860        return candidates
6861            .iter()
6862            .all(|candidate| {
6863                declared_type_alias(analyzer, candidate) && same_logical_symbol(first, candidate)
6864            })
6865            .then(|| first.clone());
6866    }
6867    candidates
6868        .iter()
6869        .all(|candidate| {
6870            !declared_type_alias(analyzer, candidate)
6871                && candidate.kind() == first.kind()
6872                && candidate.fq_name() == first.fq_name()
6873        })
6874        .then(|| first.clone())
6875}
6876
6877fn declared_type_alias(analyzer: &CppGraphSource<'_>, unit: &CodeUnit) -> bool {
6878    is_type_alias(unit)
6879        || analyzer
6880            .type_alias_provider()
6881            .is_some_and(|provider| provider.is_type_alias(unit))
6882}
6883
6884pub fn field_declared_type_binding(
6885    analyzer: &CppGraphSource<'_>,
6886    visibility: &VisibilityIndex<'_>,
6887    visible_from: &ProjectFile,
6888    field: &CodeUnit,
6889) -> Option<(String, Option<CodeUnit>, i32)> {
6890    let fact = visibility.field_declared_type_fact(analyzer, field)?;
6891    let normalized = normalize_field_type_text(&fact.type_text);
6892    let primary = visibility.resolve_unique_canonical_type_for_declaration(
6893        analyzer,
6894        visible_from,
6895        field,
6896        &normalized,
6897    );
6898    let resolved = match (primary, fact.template_arguments.as_deref()) {
6899        (Some(primary), Some(arguments)) => visibility
6900            .resolve_template_arguments(visible_from, primary, arguments)
6901            .ok(),
6902        (resolved, None) => resolved,
6903        (None, Some(_)) => None,
6904    };
6905    Some((normalized, resolved, fact.indirection))
6906}
6907
6908fn decode_field_declared_type_fact(
6909    analyzer: &CppGraphSource<'_>,
6910    field: &CodeUnit,
6911) -> Option<DeclaredFieldTypeFact> {
6912    let declaration = analyzer.get_source(field, false)?;
6913    let mut parser = Parser::new();
6914    parser
6915        .set_language(&tree_sitter_cpp::LANGUAGE.into())
6916        .ok()?;
6917    let tree = parser.parse(&declaration, None)?;
6918    let mut stack = vec![tree.root_node()];
6919    while let Some(node) = stack.pop() {
6920        if matches!(node.kind(), "declaration" | "field_declaration")
6921            && let Some(type_node) = node
6922                .child_by_field_name("type")
6923                .or_else(|| first_type_child(node))
6924            && let Some(indirection) =
6925                declared_name_indirection(node, type_node, field.identifier(), &declaration)
6926        {
6927            return Some(DeclaredFieldTypeFact {
6928                type_text: node_text(type_node, &declaration).to_string(),
6929                indirection,
6930                template_arguments: cpp_template_reference_arguments(type_node, &declaration),
6931            });
6932        }
6933        let mut cursor = node.walk();
6934        stack.extend(node.named_children(&mut cursor));
6935    }
6936    None
6937}
6938
6939fn decode_structured_alias_target(
6940    analyzer: &CppGraphSource<'_>,
6941    unit: &CodeUnit,
6942) -> Option<StructuredAliasTarget> {
6943    analyzer
6944        .get_source(unit, false)
6945        .and_then(|declaration| decode_structured_alias_target_source(unit, &declaration, true))
6946        .or_else(|| {
6947            let signature = unit.signature()?;
6948            decode_structured_alias_target_source(unit, signature, false)
6949        })
6950}
6951
6952fn decode_structured_alias_target_source(
6953    unit: &CodeUnit,
6954    declaration: &str,
6955    require_top_level: bool,
6956) -> Option<StructuredAliasTarget> {
6957    let mut parser = Parser::new();
6958    parser
6959        .set_language(&tree_sitter_cpp::LANGUAGE.into())
6960        .ok()?;
6961    let tree = parser.parse(declaration, None)?;
6962    let mut stack = vec![tree.root_node()];
6963    while let Some(node) = stack.pop() {
6964        let type_node = match node.kind() {
6965            "type_definition" => {
6966                if require_top_level
6967                    && node
6968                        .parent()
6969                        .is_none_or(|parent| parent.kind() != "translation_unit")
6970                {
6971                    let mut cursor = node.walk();
6972                    stack.extend(node.named_children(&mut cursor));
6973                    continue;
6974                }
6975                let mut declarator_cursor = node.walk();
6976                let declares_unit = node
6977                    .children_by_field_name("declarator", &mut declarator_cursor)
6978                    .any(|declarator| {
6979                        extract_typedef_declarator_name(declarator, declaration)
6980                            .is_some_and(|name| name == unit.identifier())
6981                    });
6982                declares_unit.then(|| node.child_by_field_name("type"))??
6983            }
6984            "alias_declaration" => {
6985                if require_top_level
6986                    && node
6987                        .parent()
6988                        .is_none_or(|parent| parent.kind() != "translation_unit")
6989                {
6990                    let mut cursor = node.walk();
6991                    stack.extend(node.named_children(&mut cursor));
6992                    continue;
6993                }
6994                let name = node.child_by_field_name("name")?;
6995                (node_text(name, declaration) == unit.identifier())
6996                    .then(|| node.child_by_field_name("type"))??
6997            }
6998            _ => {
6999                let mut cursor = node.walk();
7000                stack.extend(node.named_children(&mut cursor));
7001                continue;
7002            }
7003        };
7004        return structured_alias_type_target(type_node, declaration);
7005    }
7006    None
7007}
7008
7009fn structured_alias_type_target(
7010    mut type_node: Node<'_>,
7011    source: &str,
7012) -> Option<StructuredAliasTarget> {
7013    while type_node.kind() == "type_descriptor" {
7014        type_node = type_node.child_by_field_name("type")?;
7015    }
7016    if type_node.kind() == "primitive_type" {
7017        return Some(StructuredAliasTarget::Builtin);
7018    }
7019    if matches!(
7020        type_node.kind(),
7021        "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
7022    ) {
7023        type_node = type_node.child_by_field_name("name")?;
7024    }
7025    let global = type_node.child_by_field_name("scope").is_none()
7026        && type_node.child(0).is_some_and(|child| child.kind() == "::");
7027    let mut components = Vec::new();
7028    append_structured_type_components(type_node, source, &mut components)?;
7029    let arguments = cpp_template_reference_arguments(type_node, source);
7030    (!components.is_empty()).then_some(StructuredAliasTarget::Named {
7031        components,
7032        global,
7033        arguments,
7034    })
7035}
7036
7037fn append_structured_type_components(
7038    node: Node<'_>,
7039    source: &str,
7040    out: &mut Vec<String>,
7041) -> Option<()> {
7042    match node.kind() {
7043        "identifier" | "namespace_identifier" | "type_identifier" => {
7044            out.push(node_text(node, source).to_string());
7045            Some(())
7046        }
7047        "template_type" => {
7048            append_structured_type_components(node.child_by_field_name("name")?, source, out)
7049        }
7050        "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
7051            if let Some(scope) = node.child_by_field_name("scope") {
7052                append_structured_type_components(scope, source, out)?;
7053            }
7054            append_structured_type_components(node.child_by_field_name("name")?, source, out)
7055        }
7056        _ => None,
7057    }
7058}
7059
7060fn declared_name_indirection(
7061    declaration: Node<'_>,
7062    type_node: Node<'_>,
7063    field_name: &str,
7064    source: &str,
7065) -> Option<i32> {
7066    let mut stack = Vec::new();
7067    let mut cursor = declaration.walk();
7068    stack.extend(
7069        declaration
7070            .named_children(&mut cursor)
7071            .filter(|child| !same_node(*child, type_node)),
7072    );
7073    while let Some(node) = stack.pop() {
7074        if matches!(node.kind(), "identifier" | "field_identifier")
7075            && node_text(node, source) == field_name
7076        {
7077            let mut indirection = 0;
7078            let mut current = node.parent();
7079            while let Some(parent) = current {
7080                if same_node(parent, declaration) {
7081                    return Some(indirection);
7082                }
7083                if parent.kind() == "pointer_declarator" {
7084                    indirection += 1;
7085                }
7086                current = parent.parent();
7087            }
7088            return None;
7089        }
7090        let mut cursor = node.walk();
7091        stack.extend(node.named_children(&mut cursor));
7092    }
7093    None
7094}
7095
7096fn field_declaration_type_matches(
7097    declaration: &str,
7098    unit: &CodeUnit,
7099    ctx: &ScanCtx<'_>,
7100    owner: &CodeUnit,
7101) -> bool {
7102    ctx.visibility
7103        .resolves_to_type(&ctx.analyzer, ctx.file, declaration, owner)
7104        || field_type_prefix(declaration, unit.identifier()).is_some_and(|type_text| {
7105            let normalized = normalize_field_type_text(type_text);
7106            ctx.visibility
7107                .resolves_to_type(&ctx.analyzer, ctx.file, type_text, owner)
7108                || ctx.visibility.resolves_to_type(
7109                    &ctx.analyzer,
7110                    ctx.file,
7111                    normalized.as_str(),
7112                    owner,
7113                )
7114        })
7115}
7116
7117fn field_type_prefix<'a>(declaration: &'a str, field_name: &str) -> Option<&'a str> {
7118    let declaration = declaration
7119        .split(['=', ';'])
7120        .next()
7121        .unwrap_or(declaration)
7122        .trim();
7123    let index = declaration.rfind(field_name)?;
7124    let before = &declaration[..index];
7125    let after = &declaration[index + field_name.len()..];
7126    if before.chars().next_back().is_some_and(is_identifier_char)
7127        || after.chars().next().is_some_and(is_identifier_char)
7128    {
7129        return None;
7130    }
7131    Some(before.trim())
7132}
7133
7134fn normalize_field_type_text(type_text: &str) -> String {
7135    const FIELD_SPECIFIERS: [&str; 8] = [
7136        "extern ",
7137        "static ",
7138        "mutable ",
7139        "constexpr ",
7140        "constinit ",
7141        "inline ",
7142        "volatile ",
7143        "const ",
7144    ];
7145
7146    let mut normalized = normalize_type_text(type_text);
7147    loop {
7148        let Some(stripped) = FIELD_SPECIFIERS
7149            .iter()
7150            .find_map(|specifier| normalized.strip_prefix(specifier))
7151        else {
7152            return normalized;
7153        };
7154        normalized = normalize_type_text(stripped);
7155    }
7156}
7157
7158fn is_identifier_char(ch: char) -> bool {
7159    ch == '_' || ch.is_ascii_alphanumeric()
7160}
7161
7162pub fn declaration_mentions_type(node: Node<'_>, ctx: &ScanCtx<'_>, owner: &CodeUnit) -> bool {
7163    let Some(type_node) = node.child_by_field_name("type") else {
7164        return false;
7165    };
7166    ctx.visibility.resolves_to_type(
7167        &ctx.analyzer,
7168        ctx.file,
7169        node_text(type_node, ctx.source),
7170        owner,
7171    )
7172}
7173
7174pub fn declaration_is_object_construction_candidate(node: Node<'_>, ctx: &ScanCtx<'_>) -> bool {
7175    !ctx.analyzer
7176        .declarations(ctx.file)
7177        .into_iter()
7178        .filter(|unit| unit.is_function())
7179        .any(|unit| {
7180            ctx.analyzer.ranges(&unit).iter().any(|range| {
7181                node.start_byte() <= range.start_byte && range.end_byte <= node.end_byte()
7182            })
7183        })
7184}
7185
7186pub fn declaration_constructor_arity(node: Node<'_>, _ctx: &ScanCtx<'_>) -> usize {
7187    let mut cursor = node.walk();
7188    for child in node.named_children(&mut cursor) {
7189        if child.kind() == "init_declarator" {
7190            return child
7191                .child_by_field_name("value")
7192                .or_else(|| first_named_child_of_kind(child, "initializer_list"))
7193                .or_else(|| first_named_child_of_kind(child, "compound_literal_expression"))
7194                .map(declaration_init_value_arity)
7195                .unwrap_or(0);
7196        }
7197        if is_declarator_node(child) {
7198            return declaration_declarator_arity(child);
7199        }
7200    }
7201    0
7202}
7203
7204fn declaration_init_value_arity(value: Node<'_>) -> usize {
7205    match value.kind() {
7206        "argument_list" | "initializer_list" => argument_children(value).count(),
7207        "compound_literal_expression" => call_arity(value),
7208        _ => 1,
7209    }
7210}
7211
7212fn declaration_declarator_arity(node: Node<'_>) -> usize {
7213    if let Some(parameters) = node.child_by_field_name("parameters") {
7214        return argument_children(parameters).count();
7215    }
7216    node.child_by_field_name("declarator")
7217        .map(declaration_declarator_arity)
7218        .unwrap_or(0)
7219}
7220
7221fn first_named_child_of_kind<'tree>(node: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
7222    let mut cursor = node.walk();
7223    node.named_children(&mut cursor)
7224        .find(|child| child.kind() == kind)
7225}
7226
7227fn first_descendant_of_kind<'tree>(root: Node<'tree>, kind: &str) -> Option<Node<'tree>> {
7228    let mut stack = vec![root];
7229    while let Some(node) = stack.pop() {
7230        if node.kind() == kind {
7231            return Some(node);
7232        }
7233        for index in (0..node.named_child_count()).rev() {
7234            if let Some(child) = node.named_child(index) {
7235                stack.push(child);
7236            }
7237        }
7238    }
7239    None
7240}
7241
7242fn argument_shape_may_change_arity(node: Node<'_>) -> bool {
7243    if node.kind() == "identifier" {
7244        return true;
7245    }
7246    if node.kind() == "parenthesized_expression" {
7247        return false;
7248    }
7249    if node.kind() == "call_expression" {
7250        return node
7251            .child_by_field_name("function")
7252            .is_some_and(|function| function.kind() == "identifier");
7253    }
7254    let mut stack = vec![node];
7255    while let Some(descendant) = stack.pop() {
7256        if descendant != node && descendant.kind() == "parenthesized_expression" {
7257            continue;
7258        }
7259        if descendant.kind() == "identifier" {
7260            return true;
7261        }
7262        if descendant.kind() == "call_expression" {
7263            if descendant
7264                .child_by_field_name("function")
7265                .is_some_and(|function| function.kind() == "identifier")
7266            {
7267                return true;
7268            }
7269            continue;
7270        }
7271        for index in (0..descendant.named_child_count()).rev() {
7272            if let Some(child) = descendant.named_child(index) {
7273                stack.push(child);
7274            }
7275        }
7276    }
7277    false
7278}
7279
7280fn macro_expansion_shape_is_safe(
7281    node: Node<'_>,
7282    source: &str,
7283    parameters: &[String],
7284    environment: &MacroEnvironment,
7285) -> bool {
7286    if matches!(node.kind(), "identifier" | "parenthesized_expression") {
7287        return true;
7288    }
7289    if node.kind() == "call_expression" {
7290        let Some(function) = node.child_by_field_name("function") else {
7291            return true;
7292        };
7293        if function.kind() != "identifier" {
7294            return true;
7295        }
7296        let function_name = node_text(function, source);
7297        if parameters
7298            .iter()
7299            .any(|parameter| parameter == function_name)
7300        {
7301            return false;
7302        }
7303        if !environment.may_bind(function_name) {
7304            return true;
7305        }
7306        let Some(arguments) = node.child_by_field_name("arguments") else {
7307            return false;
7308        };
7309        return argument_children(arguments).all(|argument| {
7310            if argument.kind() == "identifier"
7311                && parameters
7312                    .iter()
7313                    .any(|parameter| parameter == node_text(argument, source))
7314            {
7315                return false;
7316            }
7317            macro_expansion_shape_is_safe(argument, source, parameters, environment)
7318        });
7319    }
7320    let mut stack = vec![node];
7321    while let Some(descendant) = stack.pop() {
7322        if descendant != node {
7323            if descendant.kind() == "parenthesized_expression" {
7324                continue;
7325            }
7326            if descendant.kind() == "call_expression" {
7327                let expands = descendant
7328                    .child_by_field_name("function")
7329                    .filter(|function| function.kind() == "identifier")
7330                    .is_some_and(|function| environment.may_bind(node_text(function, source)));
7331                if expands {
7332                    return false;
7333                }
7334                continue;
7335            }
7336        }
7337        if descendant.kind() == "identifier" {
7338            let identifier = node_text(descendant, source);
7339            if parameters.iter().any(|parameter| parameter == identifier)
7340                || environment.may_bind(identifier)
7341            {
7342                return false;
7343            }
7344        }
7345        for index in (0..descendant.named_child_count()).rev() {
7346            if let Some(child) = descendant.named_child(index) {
7347                stack.push(child);
7348            }
7349        }
7350    }
7351    true
7352}
7353
7354fn structured_include_path<'a>(path: Node<'_>, source: &'a str) -> Option<&'a str> {
7355    let text = node_text(path, source);
7356    match path.kind() {
7357        "string_literal" => text.strip_prefix('"')?.strip_suffix('"'),
7358        "system_lib_string" => text.strip_prefix('<')?.strip_suffix('>'),
7359        _ => None,
7360    }
7361}
7362
7363fn has_preprocessor_conditional_ancestor(mut node: Node<'_>, source: &str) -> bool {
7364    while let Some(parent) = node.parent() {
7365        if is_preprocessor_conditional(parent) && !is_file_covering_include_guard(parent, source) {
7366            return true;
7367        }
7368        node = parent;
7369    }
7370    false
7371}
7372
7373fn is_preprocessor_conditional(node: Node<'_>) -> bool {
7374    matches!(
7375        node.kind(),
7376        "preproc_if"
7377            | "preproc_ifdef"
7378            | "preproc_ifndef"
7379            | "preproc_elif"
7380            | "preproc_elifdef"
7381            | "preproc_else"
7382    )
7383}
7384
7385fn is_file_covering_include_guard(node: Node<'_>, source: &str) -> bool {
7386    node.parent()
7387        .filter(|parent| parent.kind() == "translation_unit")
7388        .is_some_and(|root| top_level_canonical_include_guard_name(root, source).is_some())
7389        && is_canonical_include_guard(node, source)
7390}
7391
7392fn is_canonical_include_guard(node: Node<'_>, source: &str) -> bool {
7393    if node.kind() != "preproc_ifdef"
7394        || node
7395            .child(0)
7396            .is_none_or(|directive| directive.kind() != "#ifndef")
7397        || node.child_by_field_name("alternative").is_some()
7398    {
7399        return false;
7400    }
7401    let Some(guard_name) = node.child_by_field_name("name") else {
7402        return false;
7403    };
7404    let mut cursor = node.walk();
7405    node.named_children(&mut cursor)
7406        .find(|child| *child != guard_name && child.kind() != "comment")
7407        .filter(|child| child.kind() == "preproc_def")
7408        .and_then(|definition| definition.child_by_field_name("name"))
7409        .is_some_and(|defined_name| {
7410            node_text(defined_name, source) == node_text(guard_name, source)
7411        })
7412}
7413
7414fn top_level_canonical_include_guard_name(root: Node<'_>, source: &str) -> Option<String> {
7415    let mut guard = None;
7416    for index in 0..root.named_child_count() {
7417        let Some(child) = root.named_child(index) else {
7418            continue;
7419        };
7420        if child.kind() == "comment" || is_pragma_once(child, source) {
7421            continue;
7422        }
7423        if guard.is_none() && is_canonical_include_guard(child, source) {
7424            guard = Some(child);
7425        } else {
7426            return None;
7427        }
7428    }
7429    guard
7430        .and_then(|guard: Node<'_>| guard.child_by_field_name("name"))
7431        .map(|name| node_text(name, source).to_string())
7432}
7433
7434fn top_level_macro_include_protection(root: Node<'_>, source: &str) -> MacroIncludeProtection {
7435    if (0..root.named_child_count())
7436        .filter_map(|index| root.named_child(index))
7437        .any(|child| is_pragma_once(child, source))
7438    {
7439        return MacroIncludeProtection::PragmaOnce;
7440    }
7441    top_level_canonical_include_guard_name(root, source)
7442        .map(MacroIncludeProtection::MacroGuard)
7443        .unwrap_or(MacroIncludeProtection::None)
7444}
7445
7446fn is_pragma_once(node: Node<'_>, source: &str) -> bool {
7447    node.kind() == "preproc_call"
7448        && node
7449            .child_by_field_name("directive")
7450            .is_some_and(|directive| node_text(directive, source) == "#pragma")
7451        && node
7452            .child_by_field_name("argument")
7453            .is_some_and(|argument| node_text(argument, source).trim() == "once")
7454}
7455
7456fn parse_preproc_identifier(argument: &str) -> Option<String> {
7457    let sentinel = format!("void __bifrost_undef() {{ {argument}; }}");
7458    let mut parser = Parser::new();
7459    parser
7460        .set_language(&tree_sitter_cpp::LANGUAGE.into())
7461        .ok()?;
7462    let tree = parser.parse(&sentinel, None)?;
7463    if tree.root_node().has_error() {
7464        return None;
7465    }
7466    let statement = first_descendant_of_kind(tree.root_node(), "expression_statement")?;
7467    let identifier = statement.named_child(0)?;
7468    (identifier.kind() == "identifier" && statement.named_child_count() == 1)
7469        .then(|| node_text(identifier, &sentinel).to_string())
7470}
7471
7472pub fn extract_variable_name(node: Node<'_>, source: &str) -> Option<String> {
7473    match node.kind() {
7474        "identifier" | "field_identifier" => {
7475            let name = node_text(node, source).trim();
7476            (!name.is_empty()).then(|| name.to_string())
7477        }
7478        "abstract_array_declarator"
7479        | "abstract_function_declarator"
7480        | "abstract_parenthesized_declarator"
7481        | "abstract_pointer_declarator"
7482        | "abstract_reference_declarator" => None,
7483        "function_declarator" => node
7484            .child_by_field_name("declarator")
7485            .or_else(|| node.child_by_field_name("name"))
7486            .and_then(|child| extract_variable_name(child, source)),
7487        _ => node
7488            .child_by_field_name("declarator")
7489            .or_else(|| node.child_by_field_name("name"))
7490            .or_else(|| node.named_child(node.named_child_count().saturating_sub(1)))
7491            .and_then(|child| extract_variable_name(child, source)),
7492    }
7493}
7494
7495pub fn is_declarator_node(node: Node<'_>) -> bool {
7496    matches!(
7497        node.kind(),
7498        "identifier"
7499            | "field_identifier"
7500            | "pointer_declarator"
7501            | "reference_declarator"
7502            | "array_declarator"
7503            | "parenthesized_declarator"
7504            | "function_declarator"
7505    )
7506}
7507
7508#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7509pub enum RecoveredDeclaratorTypeContext {
7510    Declaration,
7511    FunctionDefinition,
7512}
7513
7514/// Recognize a real type displaced into a qualified declarator by parser
7515/// recovery.
7516///
7517/// Tree-sitter parses `API Result *make(Arg);` as if `API` were the declared
7518/// type and `Result` were the scope of a qualified declarator with a missing
7519/// `::`. The same recovery occurs for macro-prefixed definitions and extern
7520/// variables. Keep this intentionally structural: the recovered scope must
7521/// have the grammar's missing separator, the qualified node must occupy the
7522/// declaration's declarator chain, a separate nonempty type must occupy the
7523/// normal type field, and the recovered name must unwrap to a real declarator
7524/// name.
7525pub fn recovered_macro_decorated_declarator_type(
7526    node: Node<'_>,
7527) -> Option<RecoveredDeclaratorTypeContext> {
7528    recovered_macro_decorated_type_node(node).map(|(_, context)| context)
7529}
7530
7531/// Return the declaration/function `type` displaced by a macro-shaped
7532/// qualified declarator, together with the enclosing declaration context.
7533/// Callers use the macro scope only as structural admission evidence; the
7534/// returned node is the real type reference to resolve and record.
7535pub fn recovered_macro_decorated_type_node(
7536    node: Node<'_>,
7537) -> Option<(Node<'_>, RecoveredDeclaratorTypeContext)> {
7538    if node.kind() != "namespace_identifier" || node.is_missing() {
7539        return None;
7540    }
7541    let qualified = node.parent()?;
7542    if qualified.kind() != "qualified_identifier"
7543        || qualified.child_by_field_name("scope") != Some(node)
7544        || !(0..qualified.child_count())
7545            .filter_map(|index| qualified.child(index))
7546            .any(|child| child.kind() == "::" && child.is_missing())
7547    {
7548        return None;
7549    }
7550    if !concrete_recovered_declarator_name(qualified.child_by_field_name("name")?) {
7551        return None;
7552    }
7553
7554    let (declaration, context) = recovered_declarator_container(qualified)?;
7555    let type_node = declaration
7556        .child_by_field_name("type")
7557        .filter(|type_node| {
7558            *type_node != qualified
7559                && !type_node.is_missing()
7560                && type_node.start_byte() != type_node.end_byte()
7561        })?;
7562    Some((type_node, context))
7563}
7564
7565fn recovered_declarator_container(
7566    mut declarator: Node<'_>,
7567) -> Option<(Node<'_>, RecoveredDeclaratorTypeContext)> {
7568    loop {
7569        let parent = declarator.parent()?;
7570        if parent.kind() == "init_declarator" && has_field_child(parent, "declarator", declarator) {
7571            return Some((
7572                parent
7573                    .parent()
7574                    .filter(|declaration| declaration.kind() == "declaration")?,
7575                RecoveredDeclaratorTypeContext::Declaration,
7576            ));
7577        }
7578        if parent.kind() == "declaration" && has_field_child(parent, "declarator", declarator) {
7579            return Some((parent, RecoveredDeclaratorTypeContext::Declaration));
7580        }
7581        if parent.kind() == "function_definition"
7582            && has_field_child(parent, "declarator", declarator)
7583        {
7584            return Some((parent, RecoveredDeclaratorTypeContext::FunctionDefinition));
7585        }
7586        if !matches!(
7587            parent.kind(),
7588            "array_declarator"
7589                | "function_declarator"
7590                | "parenthesized_declarator"
7591                | "pointer_declarator"
7592                | "pointer_type_declarator"
7593                | "reference_declarator"
7594        ) || !has_field_child(parent, "declarator", declarator)
7595        {
7596            return None;
7597        }
7598        declarator = parent;
7599    }
7600}
7601
7602fn has_field_child(parent: Node<'_>, field: &str, target: Node<'_>) -> bool {
7603    let mut cursor = parent.walk();
7604    parent
7605        .children_by_field_name(field, &mut cursor)
7606        .any(|child| child == target)
7607}
7608
7609fn concrete_recovered_declarator_name(mut node: Node<'_>) -> bool {
7610    loop {
7611        if node.is_missing() || node.start_byte() == node.end_byte() {
7612            return false;
7613        }
7614        match node.kind() {
7615            "identifier" | "field_identifier" | "type_identifier" => return true,
7616            "array_declarator"
7617            | "function_declarator"
7618            | "parenthesized_declarator"
7619            | "pointer_declarator"
7620            | "pointer_type_declarator"
7621            | "reference_declarator" => {
7622                let Some(declarator) = node.child_by_field_name("declarator") else {
7623                    return false;
7624                };
7625                node = declarator;
7626            }
7627            _ => return false,
7628        }
7629    }
7630}
7631
7632/// Aggregate-owner proof for a structurally recognized designated initializer.
7633pub enum DesignatedInitializerOwner {
7634    Resolved(CodeUnit),
7635    Unresolved,
7636}
7637
7638/// Recognize a designated-initializer field and, when possible, resolve its
7639/// aggregate owner.
7640///
7641/// Covers both the grammar's ordinary `field_designator` shape and the exact
7642/// recovery used for `.field = value` after a preprocessor-split array
7643/// initializer. Nested aggregate levels are deliberately left unresolved unless
7644/// the single outer level is the containing array initializer: resolving those
7645/// would require following the enclosing field's declared type. `None` means the
7646/// node is not a designator at all; an unresolved designator remains classified so
7647/// callers cannot fall through to unrelated global/member heuristics.
7648pub fn designated_initializer_owner(
7649    visibility: &VisibilityIndex<'_>,
7650    file: &ProjectFile,
7651    source: &str,
7652    node: Node<'_>,
7653) -> Option<DesignatedInitializerOwner> {
7654    if let Some(designator) = node
7655        .parent()
7656        .filter(|parent| parent.kind() == "field_designator")
7657    {
7658        let pair = designator.parent()?;
7659        if pair.kind() != "initializer_pair"
7660            || pair.child_by_field_name("designator") != Some(designator)
7661        {
7662            return None;
7663        }
7664        let initializer = pair.parent()?;
7665        if initializer.kind() != "initializer_list" {
7666            return None;
7667        }
7668        return Some(classified_designated_owner(initializer_list_owner(
7669            visibility,
7670            file,
7671            source,
7672            initializer,
7673        )));
7674    }
7675
7676    let init_declarator = node.parent()?;
7677    if init_declarator.child_by_field_name("declarator") != Some(node)
7678        || !crate::structural::is_recovered_designator_init_declarator(init_declarator)
7679    {
7680        return None;
7681    }
7682    Some(classified_designated_owner(declaration_owner(
7683        visibility,
7684        file,
7685        source,
7686        init_declarator.parent()?,
7687    )))
7688}
7689
7690fn classified_designated_owner(owner: Option<CodeUnit>) -> DesignatedInitializerOwner {
7691    owner.map_or(
7692        DesignatedInitializerOwner::Unresolved,
7693        DesignatedInitializerOwner::Resolved,
7694    )
7695}
7696
7697fn initializer_list_owner(
7698    visibility: &VisibilityIndex<'_>,
7699    file: &ProjectFile,
7700    source: &str,
7701    initializer: Node<'_>,
7702) -> Option<CodeUnit> {
7703    let mut current = initializer;
7704    let mut outer_initializer_lists = 0usize;
7705    loop {
7706        let parent = current.parent()?;
7707        match parent.kind() {
7708            "initializer_pair" => return None,
7709            "initializer_list" => {
7710                outer_initializer_lists += 1;
7711                if outer_initializer_lists > 1 {
7712                    return None;
7713                }
7714                current = parent;
7715            }
7716            "init_declarator" if parent.child_by_field_name("value") == Some(current) => {
7717                let declaration = parent.parent()?;
7718                if outer_initializer_lists == 1
7719                    && !parent
7720                        .child_by_field_name("declarator")
7721                        .is_some_and(contains_array_declarator)
7722                {
7723                    return None;
7724                }
7725                return declaration_owner(visibility, file, source, declaration);
7726            }
7727            "compound_literal_expression"
7728                if parent.child_by_field_name("value") == Some(current)
7729                    && outer_initializer_lists == 0 =>
7730            {
7731                let type_node = parent.child_by_field_name("type")?;
7732                return resolve_designated_owner_type(visibility, file, source, type_node);
7733            }
7734            "ERROR" => current = parent,
7735            _ => return None,
7736        }
7737    }
7738}
7739
7740fn declaration_owner(
7741    visibility: &VisibilityIndex<'_>,
7742    file: &ProjectFile,
7743    source: &str,
7744    declaration: Node<'_>,
7745) -> Option<CodeUnit> {
7746    if !matches!(declaration.kind(), "declaration" | "field_declaration") {
7747        return None;
7748    }
7749    let type_node = declaration
7750        .child_by_field_name("type")
7751        .or_else(|| first_type_child(declaration))?;
7752    resolve_designated_owner_type(visibility, file, source, type_node)
7753}
7754
7755fn resolve_designated_owner_type(
7756    visibility: &VisibilityIndex<'_>,
7757    file: &ProjectFile,
7758    source: &str,
7759    type_node: Node<'_>,
7760) -> Option<CodeUnit> {
7761    let type_name = normalize_type_text(node_text(type_node, source));
7762    visibility
7763        .resolve_type(file, &type_name)
7764        .filter(CodeUnit::is_class)
7765}
7766
7767fn contains_array_declarator(declarator: Node<'_>) -> bool {
7768    let mut stack = vec![declarator];
7769    while let Some(node) = stack.pop() {
7770        if node.kind() == "array_declarator" {
7771            return true;
7772        }
7773        if matches!(node.kind(), "initializer_list" | "compound_statement") {
7774            continue;
7775        }
7776        let mut cursor = node.walk();
7777        stack.extend(node.named_children(&mut cursor));
7778    }
7779    false
7780}
7781
7782pub fn first_type_child(node: Node<'_>) -> Option<Node<'_>> {
7783    let mut cursor = node.walk();
7784    node.named_children(&mut cursor).find(|child| {
7785        matches!(
7786            child.kind(),
7787            "type_identifier"
7788                | "primitive_type"
7789                | "qualified_identifier"
7790                | "scoped_type_identifier"
7791                | "struct_specifier"
7792                | "union_specifier"
7793                | "enum_specifier"
7794        )
7795    })
7796}
7797
7798pub fn constructor_style_local_declaration<T: Clone + Eq + Hash>(
7799    visibility: &VisibilityIndex<'_>,
7800    file: &ProjectFile,
7801    source: &str,
7802    declarator: Node<'_>,
7803    type_text: Option<&str>,
7804    bindings: &LocalInferenceEngine<T>,
7805) -> bool {
7806    if !has_ancestor_kind(declarator, "compound_statement") {
7807        return false;
7808    }
7809    if declarator
7810        .child_by_field_name("declarator")
7811        .is_none_or(|declarator| declarator.kind() != "identifier")
7812    {
7813        return false;
7814    }
7815    if !type_text
7816        .and_then(|text| visibility.resolve_type(file, text))
7817        .is_some_and(|unit| unit.is_class())
7818    {
7819        return false;
7820    }
7821    declarator
7822        .child_by_field_name("parameters")
7823        .is_some_and(|parameters| {
7824            constructor_parameters_look_like_expressions(parameters, source, bindings)
7825        })
7826}
7827
7828fn constructor_parameters_look_like_expressions<T: Clone + Eq + Hash>(
7829    parameters: Node<'_>,
7830    source: &str,
7831    bindings: &LocalInferenceEngine<T>,
7832) -> bool {
7833    let mut cursor = parameters.walk();
7834    parameters.named_children(&mut cursor).any(|parameter| {
7835        !matches!(
7836            parameter.kind(),
7837            "parameter_declaration" | "optional_parameter_declaration"
7838        ) || parameter_declaration_is_local_expression(parameter, source, bindings)
7839    })
7840}
7841
7842fn parameter_declaration_is_local_expression<T: Clone + Eq + Hash>(
7843    parameter: Node<'_>,
7844    source: &str,
7845    bindings: &LocalInferenceEngine<T>,
7846) -> bool {
7847    let text = node_text(parameter, source).trim();
7848    text.chars()
7849        .all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
7850        && bindings.is_shadowed(text)
7851}
7852
7853pub fn is_declaration_name(node: Node<'_>) -> bool {
7854    let Some(parent) = node.parent() else {
7855        return false;
7856    };
7857    if parent
7858        .child_by_field_name("name")
7859        .is_some_and(|name| same_node(name, node))
7860    {
7861        if matches!(
7862            parent.kind(),
7863            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
7864        ) {
7865            return cpp_tag_specifier_declares_name(parent);
7866        }
7867        if matches!(
7868            parent.kind(),
7869            "namespace_definition"
7870                | "namespace_alias_definition"
7871                | "alias_declaration"
7872                | "enumerator"
7873        ) {
7874            return true;
7875        }
7876    }
7877
7878    let mut current = Some(parent);
7879    while let Some(ancestor) = current {
7880        let type_definition = ancestor.kind() == "type_definition";
7881        let mut declarator_cursor = ancestor.walk();
7882        if ancestor
7883            .children_by_field_name("declarator", &mut declarator_cursor)
7884            .any(|declarator| declarator_name_path_contains(declarator, node, type_definition))
7885        {
7886            return true;
7887        }
7888        if matches!(
7889            ancestor.kind(),
7890            "declaration"
7891                | "field_declaration"
7892                | "parameter_declaration"
7893                | "optional_parameter_declaration"
7894                | "function_definition"
7895                | "type_definition"
7896                | "alias_declaration"
7897                | "class_specifier"
7898                | "struct_specifier"
7899                | "union_specifier"
7900                | "enum_specifier"
7901        ) {
7902            return false;
7903        }
7904        current = ancestor.parent();
7905    }
7906    false
7907}
7908
7909/// Whether a parameter declaration belongs to the callable scope whose body can
7910/// contain references to it.
7911///
7912/// Error recovery can wrap a macro-decorated class body in a synthetic outer
7913/// `function_definition`. Merely finding any callable ancestor would then leak
7914/// parameters from member prototypes into later member bodies. Require the
7915/// parameter to be inside that definition's own declarator instead.
7916pub fn parameter_belongs_to_callable_scope(parameter: Node<'_>) -> bool {
7917    let mut current = parameter.parent();
7918    while let Some(ancestor) = current {
7919        if ancestor.kind() == "lambda_expression" {
7920            return ancestor
7921                .child_by_field_name("declarator")
7922                .is_some_and(|declarator| {
7923                    declarator.start_byte() <= parameter.start_byte()
7924                        && parameter.end_byte() <= declarator.end_byte()
7925                });
7926        }
7927        if ancestor.kind() == "function_definition" {
7928            return ancestor
7929                .child_by_field_name("declarator")
7930                .is_some_and(|declarator| {
7931                    declarator.start_byte() <= parameter.start_byte()
7932                        && parameter.end_byte() <= declarator.end_byte()
7933                });
7934        }
7935        current = ancestor.parent();
7936    }
7937    false
7938}
7939
7940fn cpp_tag_specifier_declares_name(specifier: Node<'_>) -> bool {
7941    if specifier.child_by_field_name("body").is_some() {
7942        return true;
7943    }
7944    let mut current = specifier.parent();
7945    while let Some(ancestor) = current {
7946        match ancestor.kind() {
7947            "type_descriptor"
7948            | "parameter_declaration"
7949            | "optional_parameter_declaration"
7950            | "template_argument_list"
7951            | "cast_expression" => return false,
7952            "declaration" | "field_declaration" => {
7953                let mut cursor = ancestor.walk();
7954                return ancestor
7955                    .children_by_field_name("declarator", &mut cursor)
7956                    .next()
7957                    .is_none();
7958            }
7959            "translation_unit" => return true,
7960            _ => current = ancestor.parent(),
7961        }
7962    }
7963    false
7964}
7965
7966pub fn declarator_name_node(node: Node<'_>) -> Option<Node<'_>> {
7967    match node.kind() {
7968        "identifier"
7969        | "field_identifier"
7970        | "qualified_identifier"
7971        | "scoped_identifier"
7972        | "operator_name"
7973        | "destructor_name"
7974        | "literal_operator_name" => Some(node),
7975        _ => node
7976            .child_by_field_name("declarator")
7977            .or_else(|| node.child_by_field_name("name"))
7978            .or_else(|| node.child_by_field_name("field"))
7979            .and_then(declarator_name_node),
7980    }
7981}
7982
7983fn declarator_name_path_contains(
7984    declarator: Node<'_>,
7985    candidate: Node<'_>,
7986    allow_type_identifier: bool,
7987) -> bool {
7988    let Some(name) = declarator_name_leaf(declarator, allow_type_identifier) else {
7989        return false;
7990    };
7991    let mut current = Some(declarator);
7992    while let Some(node) = current {
7993        if same_node(node, candidate) {
7994            return true;
7995        }
7996        if same_node(node, name) {
7997            return false;
7998        }
7999        current = node
8000            .child_by_field_name("declarator")
8001            .or_else(|| node.child_by_field_name("name"))
8002            .or_else(|| node.child_by_field_name("field"));
8003    }
8004    false
8005}
8006
8007fn declarator_name_leaf(node: Node<'_>, allow_type_identifier: bool) -> Option<Node<'_>> {
8008    match node.kind() {
8009        "identifier"
8010        | "field_identifier"
8011        | "operator_name"
8012        | "destructor_name"
8013        | "literal_operator_name" => Some(node),
8014        "type_identifier" if allow_type_identifier => Some(node),
8015        _ => node
8016            .child_by_field_name("declarator")
8017            .or_else(|| node.child_by_field_name("name"))
8018            .or_else(|| node.child_by_field_name("field"))
8019            .and_then(|child| declarator_name_leaf(child, allow_type_identifier)),
8020    }
8021}
8022
8023/// True when `node` is a component of a larger structured type node whose outer
8024/// range is the single reference surfaced to callers.
8025pub fn is_nested_type_node(node: Node<'_>) -> bool {
8026    node.parent().is_some_and(|parent| {
8027        matches!(
8028            parent.kind(),
8029            "qualified_identifier" | "scoped_type_identifier" | "template_type"
8030        )
8031    })
8032}
8033
8034pub struct OutOfLineMemberDefinitionOwners<'tree> {
8035    pub owners: Vec<(Node<'tree>, CodeUnit)>,
8036    innermost: Option<(Node<'tree>, CodeUnit)>,
8037}
8038
8039impl OutOfLineMemberDefinitionOwners<'_> {
8040    pub fn innermost(&self) -> Option<(Node<'_>, &CodeUnit)> {
8041        self.innermost.as_ref().map(|(node, owner)| (*node, owner))
8042    }
8043}
8044
8045pub struct QualifiedOwnerComponents<'tree> {
8046    pub nodes: Vec<Node<'tree>>,
8047    pub names: Vec<String>,
8048    pub global: bool,
8049}
8050
8051pub fn qualified_owner_components<'tree>(
8052    node: Node<'tree>,
8053    source: &str,
8054) -> Option<QualifiedOwnerComponents<'tree>> {
8055    let mut nodes = cpp_name_component_nodes(node)?;
8056    nodes.pop()?;
8057    if nodes.is_empty() {
8058        return None;
8059    }
8060    let names = nodes
8061        .iter()
8062        .map(|component| node_text(*component, source).to_string())
8063        .collect();
8064    Some(QualifiedOwnerComponents {
8065        nodes,
8066        names,
8067        global: is_globally_qualified_cpp_name(node),
8068    })
8069}
8070
8071/// Return the terminal type-name occurrence in an out-of-line destructor
8072/// declarator such as `endpoint::~endpoint`.  Unlike an ordinary terminal
8073/// method name, this identifier is a second reference to the owner type.
8074pub fn out_of_line_destructor_type_reference(node: Node<'_>) -> Option<Node<'_>> {
8075    if node.kind() != "qualified_identifier" {
8076        return None;
8077    }
8078    let destructor = node.child_by_field_name("name")?;
8079    if destructor.kind() != "destructor_name" {
8080        return None;
8081    }
8082    (0..destructor.named_child_count())
8083        .filter_map(|index| destructor.named_child(index))
8084        .find(|child| matches!(child.kind(), "identifier" | "type_identifier"))
8085}
8086
8087pub fn out_of_line_member_definition_owner<'tree>(
8088    analyzer: &CppGraphSource<'_>,
8089    visibility: &VisibilityIndex<'_>,
8090    file: &ProjectFile,
8091    source: &str,
8092    node: Node<'tree>,
8093) -> Option<OutOfLineMemberDefinitionOwners<'tree>> {
8094    if !matches!(node.kind(), "qualified_identifier" | "scoped_identifier")
8095        || !has_ancestor_kind(node, "function_definition")
8096        || !is_function_declarator_name_root(node)
8097    {
8098        return None;
8099    }
8100    let qualified = qualified_owner_components(node, source)?;
8101    let lexical_scope = enclosing_namespace_components(node, source)?;
8102    let mut owners = Vec::new();
8103    let mut innermost = None;
8104
8105    for component_count in 1..=qualified.names.len() {
8106        if let LexicalTypeResolution::Resolved { unit, .. } = visibility
8107            .resolve_type_components_lexically(
8108                analyzer,
8109                file,
8110                &qualified.names[..component_count],
8111                qualified.global,
8112                &lexical_scope,
8113            )
8114            && !owners
8115                .iter()
8116                .any(|(_, existing)| same_visible_symbol(existing, &unit))
8117        {
8118            if component_count == qualified.names.len() {
8119                innermost = Some((qualified.nodes[component_count - 1], unit.clone()));
8120            }
8121            owners.push((qualified.nodes[component_count - 1], unit));
8122        }
8123    }
8124
8125    // The C++ analyzer has already reconciled an indexed out-of-line callable
8126    // against the include-visible class table. Consult that canonical owner
8127    // chain only when ordinary lexical lookup could not recover the innermost
8128    // owner.  A one-segment qualifier is safe here only when the enclosing
8129    // indexed callable has an authoritative class owner and the parser's
8130    // namespace path is a (possibly sparse) subsequence of that owner path.
8131    // The latter is what lets macro-wrapped namespace sentinels recover a
8132    // missing `time_internal`/`cord_internal` component without guessing an
8133    // unrelated short name.
8134    if innermost.is_none() {
8135        let indexed_owner_components = visibility
8136            .indexed_enclosing_owner_scope(analyzer, file, node)
8137            .or_else(|| {
8138                // Retain the legacy rendered-name fallback for the existing
8139                // multi-segment path when an enclosing owner chain is not
8140                // available (for example, cache-loaded units without parent
8141                // links).  One-segment recovery must stay canonical-only.
8142                if qualified.names.len() <= 1 {
8143                    return None;
8144                }
8145                let range = Range {
8146                    start_byte: node.start_byte(),
8147                    end_byte: node.end_byte(),
8148                    start_line: node.start_position().row,
8149                    end_line: node.end_position().row,
8150                };
8151                let start = analyzer.enclosing_code_unit(file, &range)?;
8152                let mut components = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
8153                    brokk_bifrost_core::analyzer::Language::Cpp,
8154                    &cpp_name_for(&start),
8155                );
8156                components.pop();
8157                Some(components)
8158            });
8159        if let Some(indexed_owner_components) = indexed_owner_components
8160            && indexed_owner_components.len() > qualified.names.len()
8161            && indexed_owner_components.ends_with(&qualified.names)
8162            && indexed_namespace_path_is_recoverable(
8163                &lexical_scope,
8164                &indexed_owner_components,
8165            )
8166            // A globally-qualified one-segment owner is an explicit request
8167            // for the top-level binding; do not reinterpret it as a missing
8168            // namespace component.  Existing multi-segment global lookups
8169            // retain their historical indexed recovery.
8170            && (qualified.names.len() > 1 || !qualified.global)
8171        {
8172            let namespace_count = indexed_owner_components.len() - qualified.names.len();
8173            for component_count in 1..=qualified.names.len() {
8174                let expected = &indexed_owner_components[..namespace_count + component_count];
8175                let owner_node = qualified.nodes[component_count - 1];
8176                for owner in visibility
8177                    .visible_identifier_candidates(file, &qualified.names[component_count - 1])
8178                    .filter(|candidate| candidate.is_class())
8179                    .filter(|candidate| {
8180                        canonical_cpp_scope_components(candidate) == expected
8181                            && visibility.external_type_candidate_visible_in_context(
8182                                analyzer, file, candidate, node,
8183                            )
8184                    })
8185                {
8186                    if component_count == qualified.names.len() && innermost.is_none() {
8187                        innermost = Some((owner_node, owner.clone()));
8188                    }
8189                    if !owners
8190                        .iter()
8191                        .any(|(_, existing)| same_symbol(existing, owner))
8192                    {
8193                        owners.push((owner_node, owner.clone()));
8194                    }
8195                }
8196            }
8197        }
8198    }
8199    (!owners.is_empty()).then_some(OutOfLineMemberDefinitionOwners { owners, innermost })
8200}
8201
8202fn is_function_declarator_name_root(node: Node<'_>) -> bool {
8203    let mut current = node;
8204    while let Some(parent) = current.parent() {
8205        if parent.kind() == "function_declarator" {
8206            return parent.child_by_field_name("declarator") == Some(current);
8207        }
8208        if matches!(
8209            parent.kind(),
8210            "pointer_declarator" | "reference_declarator" | "parenthesized_declarator"
8211        ) && parent.child_by_field_name("declarator") == Some(current)
8212        {
8213            current = parent;
8214            continue;
8215        }
8216        return false;
8217    }
8218    false
8219}
8220
8221pub fn append_cpp_name_components(
8222    node: Node<'_>,
8223    source: &str,
8224    out: &mut Vec<String>,
8225) -> Option<()> {
8226    out.extend(
8227        cpp_name_component_nodes(node)?
8228            .into_iter()
8229            .map(|component| node_text(component, source).to_string()),
8230    );
8231    Some(())
8232}
8233
8234pub fn cpp_type_name_components(node: Node<'_>, source: &str) -> Option<Vec<String>> {
8235    let mut components = Vec::new();
8236    append_cpp_name_components(node, source, &mut components)?;
8237    Some(components)
8238}
8239
8240pub fn cpp_template_reference_arguments(
8241    mut node: Node<'_>,
8242    source: &str,
8243) -> Option<Vec<CppTemplateExpression>> {
8244    loop {
8245        match node.kind() {
8246            "template_type" | "template_function" => {
8247                let arguments = node.child_by_field_name("arguments")?;
8248                let mut cursor = arguments.walk();
8249                return Some(
8250                    arguments
8251                        .named_children(&mut cursor)
8252                        .filter(|argument| !argument.is_extra() && argument.kind() != "comment")
8253                        .map(|argument| CppTemplateExpression {
8254                            text: normalize_cpp_whitespace(node_text(argument, source)),
8255                            term: cpp_template_term(argument, source, &[]),
8256                        })
8257                        .collect(),
8258                );
8259            }
8260            "qualified_identifier" | "scoped_type_identifier" | "type_descriptor" => {
8261                node = node
8262                    .child_by_field_name("name")
8263                    .or_else(|| node.child_by_field_name("type"))?;
8264            }
8265            _ => return None,
8266        }
8267    }
8268}
8269
8270fn cpp_reconcile_primary_template_parameters(
8271    candidates: &[(&CodeUnit, &CppTemplateMetadata)],
8272    preferred: &CodeUnit,
8273) -> Option<Vec<CppTemplateParameterMetadata>> {
8274    let canonical = candidates
8275        .iter()
8276        .find_map(|(unit, metadata)| (*unit == preferred).then_some(*metadata))?;
8277    let mut merged = canonical
8278        .parameters
8279        .iter()
8280        .map(|parameter| CppTemplateParameterMetadata {
8281            name: parameter.name.clone(),
8282            kind: parameter.kind,
8283            variadic: parameter.variadic,
8284            default: None,
8285        })
8286        .collect::<Vec<_>>();
8287
8288    for (_, metadata) in candidates {
8289        if metadata.parameters.len() != merged.len() {
8290            return None;
8291        }
8292        let rename_bindings = metadata
8293            .parameters
8294            .iter()
8295            .zip(&merged)
8296            .map(|(parameter, canonical)| {
8297                (
8298                    parameter.name.clone(),
8299                    CppTemplateTerm::Parameter(canonical.name.clone()),
8300                )
8301            })
8302            .collect::<HashMap<_, _>>();
8303        for ((parameter, canonical), merged_parameter) in metadata
8304            .parameters
8305            .iter()
8306            .zip(&canonical.parameters)
8307            .zip(&mut merged)
8308        {
8309            if parameter.kind != canonical.kind || parameter.variadic != canonical.variadic {
8310                return None;
8311            }
8312            let Some(default) = &parameter.default else {
8313                continue;
8314            };
8315            let normalized_term = cpp_substitute_template_term(&default.term, &rename_bindings)?;
8316            if let Some(existing) = &merged_parameter.default {
8317                if !cpp_template_terms_equal(&existing.term, &normalized_term) {
8318                    return None;
8319                }
8320            } else {
8321                merged_parameter.default = Some(CppTemplateExpression {
8322                    text: default.text.clone(),
8323                    term: normalized_term,
8324                });
8325            }
8326        }
8327    }
8328    Some(merged)
8329}
8330
8331pub fn cpp_bind_template_arguments(
8332    parameters: &[CppTemplateParameterMetadata],
8333    explicit_arguments: &[CppTemplateExpression],
8334) -> Option<(Vec<CppTemplateExpression>, HashMap<String, CppTemplateTerm>)> {
8335    let variadic_index = parameters.iter().position(|parameter| parameter.variadic);
8336    if variadic_index.is_some_and(|index| {
8337        index + 1 != parameters.len()
8338            || parameters[index + 1..]
8339                .iter()
8340                .any(|parameter| parameter.variadic)
8341    }) {
8342        return None;
8343    }
8344    let fixed_count = variadic_index.unwrap_or(parameters.len());
8345    if variadic_index.is_none() && explicit_arguments.len() > fixed_count {
8346        return None;
8347    }
8348    let explicit_fixed_count = explicit_arguments.len().min(fixed_count);
8349    let mut expanded = explicit_arguments[..explicit_fixed_count]
8350        .iter()
8351        .map(cpp_clone_template_expression_iterative)
8352        .collect::<Vec<_>>();
8353    let mut bindings = HashMap::default();
8354    for (parameter, argument) in parameters[..explicit_fixed_count].iter().zip(&expanded) {
8355        bindings.insert(
8356            parameter.name.clone(),
8357            cpp_clone_template_term_iterative(&argument.term),
8358        );
8359    }
8360    for parameter in &parameters[explicit_fixed_count..fixed_count] {
8361        let default = parameter.default.as_ref()?;
8362        let term = cpp_substitute_template_term(&default.term, &bindings)?;
8363        bindings.insert(parameter.name.clone(), term.clone());
8364        expanded.push(CppTemplateExpression {
8365            text: default.text.clone(),
8366            term,
8367        });
8368    }
8369    if let Some(index) = variadic_index {
8370        let packed_arguments = &explicit_arguments[explicit_fixed_count..];
8371        expanded.extend(
8372            packed_arguments
8373                .iter()
8374                .map(cpp_clone_template_expression_iterative),
8375        );
8376        bindings.insert(
8377            parameters[index].name.clone(),
8378            CppTemplateTerm::Node {
8379                kind: "parameter_pack".to_string(),
8380                children: packed_arguments
8381                    .iter()
8382                    .map(|argument| cpp_clone_template_term_iterative(&argument.term))
8383                    .collect(),
8384            },
8385        );
8386    }
8387    Some((expanded, bindings))
8388}
8389
8390fn cpp_specialization_matches(
8391    metadata: &CppTemplateMetadata,
8392    arguments: &[CppTemplateExpression],
8393) -> bool {
8394    if metadata.specialization_arguments.len() != arguments.len() {
8395        return false;
8396    }
8397    let parameter_names = metadata
8398        .parameters
8399        .iter()
8400        .map(|parameter| parameter.name.as_str())
8401        .collect::<HashSet<_>>();
8402    let mut bindings: HashMap<String, CppTemplateTerm> = HashMap::default();
8403    for (pattern, argument) in metadata.specialization_arguments.iter().zip(arguments) {
8404        if !cpp_unify_template_term(
8405            &pattern.term,
8406            &argument.term,
8407            &parameter_names,
8408            &mut bindings,
8409        ) {
8410            return false;
8411        }
8412    }
8413    true
8414}
8415
8416fn cpp_specialization_more_specialized(
8417    candidate: &CppTemplateMetadata,
8418    other: &CppTemplateMetadata,
8419) -> bool {
8420    cpp_specialization_pattern_accepts(other, candidate)
8421        && !cpp_specialization_pattern_accepts(candidate, other)
8422}
8423
8424fn cpp_specialization_pattern_accepts(
8425    broader: &CppTemplateMetadata,
8426    narrower: &CppTemplateMetadata,
8427) -> bool {
8428    if broader.specialization_arguments.len() != narrower.specialization_arguments.len() {
8429        return false;
8430    }
8431    let parameter_names = broader
8432        .parameters
8433        .iter()
8434        .map(|parameter| parameter.name.as_str())
8435        .collect::<HashSet<_>>();
8436    let mut bindings: HashMap<String, CppTemplateTerm> = HashMap::default();
8437    broader
8438        .specialization_arguments
8439        .iter()
8440        .zip(&narrower.specialization_arguments)
8441        .all(|(pattern, argument)| {
8442            cpp_unify_template_term(
8443                &pattern.term,
8444                &argument.term,
8445                &parameter_names,
8446                &mut bindings,
8447            )
8448        })
8449}
8450
8451pub fn cpp_substitute_template_term(
8452    term: &CppTemplateTerm,
8453    bindings: &HashMap<String, CppTemplateTerm>,
8454) -> Option<CppTemplateTerm> {
8455    enum Work<'a> {
8456        Visit(&'a CppTemplateTerm),
8457        Build { kind: String, child_count: usize },
8458    }
8459
8460    let mut work = vec![Work::Visit(term)];
8461    let mut substituted = Vec::new();
8462    while let Some(next) = work.pop() {
8463        match next {
8464            Work::Visit(CppTemplateTerm::Parameter(name)) => {
8465                substituted.push(cpp_clone_template_term_iterative(bindings.get(name)?));
8466            }
8467            Work::Visit(CppTemplateTerm::Atom { kind, text }) => {
8468                substituted.push(CppTemplateTerm::Atom {
8469                    kind: kind.clone(),
8470                    text: text.clone(),
8471                });
8472            }
8473            Work::Visit(CppTemplateTerm::Node { kind, children }) => {
8474                work.push(Work::Build {
8475                    kind: kind.clone(),
8476                    child_count: children.len(),
8477                });
8478                work.extend(children.iter().rev().map(Work::Visit));
8479            }
8480            Work::Build { kind, child_count } => {
8481                let children = substituted.split_off(substituted.len() - child_count);
8482                substituted.push(CppTemplateTerm::Node { kind, children });
8483            }
8484        }
8485    }
8486    substituted.pop()
8487}
8488
8489pub fn cpp_substitute_template_arguments(
8490    arguments: &[CppTemplateExpression],
8491    bindings: &HashMap<String, CppTemplateTerm>,
8492) -> Option<Vec<CppTemplateExpression>> {
8493    let mut substituted = Vec::new();
8494    for argument in arguments {
8495        let CppTemplateTerm::Node { kind, children } = &argument.term else {
8496            substituted.push(CppTemplateExpression {
8497                text: argument.text.clone(),
8498                term: cpp_substitute_template_term(&argument.term, bindings)?,
8499            });
8500            continue;
8501        };
8502        if kind != "parameter_pack_expansion" {
8503            substituted.push(CppTemplateExpression {
8504                text: argument.text.clone(),
8505                term: cpp_substitute_template_term(&argument.term, bindings)?,
8506            });
8507            continue;
8508        }
8509        let [pattern, CppTemplateTerm::Atom { text: ellipsis, .. }] = children.as_slice() else {
8510            return None;
8511        };
8512        if ellipsis != "..." {
8513            return None;
8514        }
8515
8516        let mut pack_names = Vec::new();
8517        let mut work = vec![pattern];
8518        while let Some(term) = work.pop() {
8519            match term {
8520                CppTemplateTerm::Parameter(name)
8521                    if matches!(
8522                        bindings.get(name),
8523                        Some(CppTemplateTerm::Node { kind, .. }) if kind == "parameter_pack"
8524                    ) =>
8525                {
8526                    if !pack_names.contains(name) {
8527                        pack_names.push(name.clone());
8528                    }
8529                }
8530                CppTemplateTerm::Node { children, .. } => work.extend(children),
8531                CppTemplateTerm::Parameter(_) | CppTemplateTerm::Atom { .. } => {}
8532            }
8533        }
8534        let first_pack = pack_names.first()?;
8535        let CppTemplateTerm::Node {
8536            children: first_elements,
8537            ..
8538        } = bindings.get(first_pack)?
8539        else {
8540            return None;
8541        };
8542        let pack_len = first_elements.len();
8543        for pack_name in &pack_names {
8544            let CppTemplateTerm::Node { children, .. } = bindings.get(pack_name)? else {
8545                return None;
8546            };
8547            if children.len() != pack_len {
8548                return None;
8549            }
8550        }
8551        for index in 0..pack_len {
8552            let mut element_bindings = bindings.clone();
8553            for pack_name in &pack_names {
8554                let CppTemplateTerm::Node { children, .. } = bindings.get(pack_name)? else {
8555                    return None;
8556                };
8557                element_bindings.insert(
8558                    pack_name.clone(),
8559                    cpp_clone_template_term_iterative(&children[index]),
8560                );
8561            }
8562            substituted.push(CppTemplateExpression {
8563                text: argument.text.clone(),
8564                term: cpp_substitute_template_term(pattern, &element_bindings)?,
8565            });
8566        }
8567    }
8568    Some(substituted)
8569}
8570
8571fn cpp_clone_template_term_iterative(term: &CppTemplateTerm) -> CppTemplateTerm {
8572    enum Work<'a> {
8573        Visit(&'a CppTemplateTerm),
8574        Build { kind: String, child_count: usize },
8575    }
8576
8577    let mut work = vec![Work::Visit(term)];
8578    let mut cloned = Vec::new();
8579    while let Some(next) = work.pop() {
8580        match next {
8581            Work::Visit(CppTemplateTerm::Parameter(name)) => {
8582                cloned.push(CppTemplateTerm::Parameter(name.clone()));
8583            }
8584            Work::Visit(CppTemplateTerm::Atom { kind, text }) => {
8585                cloned.push(CppTemplateTerm::Atom {
8586                    kind: kind.clone(),
8587                    text: text.clone(),
8588                });
8589            }
8590            Work::Visit(CppTemplateTerm::Node { kind, children }) => {
8591                work.push(Work::Build {
8592                    kind: kind.clone(),
8593                    child_count: children.len(),
8594                });
8595                work.extend(children.iter().rev().map(Work::Visit));
8596            }
8597            Work::Build { kind, child_count } => {
8598                let children = cloned.split_off(cloned.len() - child_count);
8599                cloned.push(CppTemplateTerm::Node { kind, children });
8600            }
8601        }
8602    }
8603    cloned
8604        .pop()
8605        .expect("template term traversal emits one root")
8606}
8607
8608fn cpp_clone_template_expression_iterative(
8609    expression: &CppTemplateExpression,
8610) -> CppTemplateExpression {
8611    CppTemplateExpression {
8612        text: expression.text.clone(),
8613        term: cpp_clone_template_term_iterative(&expression.term),
8614    }
8615}
8616
8617pub fn cpp_unify_template_term(
8618    pattern: &CppTemplateTerm,
8619    argument: &CppTemplateTerm,
8620    parameters: &HashSet<&str>,
8621    bindings: &mut HashMap<String, CppTemplateTerm>,
8622) -> bool {
8623    let mut work = vec![(pattern, argument)];
8624    while let Some((pattern, argument)) = work.pop() {
8625        match pattern {
8626            CppTemplateTerm::Parameter(name) if parameters.contains(name.as_str()) => {
8627                if let Some(bound) = bindings.get(name) {
8628                    if !cpp_template_terms_equal(bound, argument) {
8629                        return false;
8630                    }
8631                } else {
8632                    bindings.insert(name.clone(), cpp_clone_template_term_iterative(argument));
8633                }
8634            }
8635            CppTemplateTerm::Atom {
8636                kind: pattern_kind,
8637                text: pattern_text,
8638            } => {
8639                if !matches!(
8640                    argument,
8641                    CppTemplateTerm::Atom { kind, text }
8642                        if kind == pattern_kind && text == pattern_text
8643                ) {
8644                    return false;
8645                }
8646            }
8647            CppTemplateTerm::Node {
8648                kind: pattern_kind,
8649                children: pattern_children,
8650            } => {
8651                let CppTemplateTerm::Node { kind, children } = argument else {
8652                    return false;
8653                };
8654                if kind != pattern_kind || children.len() != pattern_children.len() {
8655                    return false;
8656                }
8657                work.extend(pattern_children.iter().zip(children).rev());
8658            }
8659            CppTemplateTerm::Parameter(_) => return false,
8660        }
8661    }
8662    true
8663}
8664
8665fn cpp_template_terms_equal(left: &CppTemplateTerm, right: &CppTemplateTerm) -> bool {
8666    let mut work = vec![(left, right)];
8667    while let Some((left, right)) = work.pop() {
8668        match (left, right) {
8669            (CppTemplateTerm::Parameter(left), CppTemplateTerm::Parameter(right)) => {
8670                if left != right {
8671                    return false;
8672                }
8673            }
8674            (
8675                CppTemplateTerm::Atom {
8676                    kind: left_kind,
8677                    text: left_text,
8678                },
8679                CppTemplateTerm::Atom {
8680                    kind: right_kind,
8681                    text: right_text,
8682                },
8683            ) => {
8684                if left_kind != right_kind || left_text != right_text {
8685                    return false;
8686                }
8687            }
8688            (
8689                CppTemplateTerm::Node {
8690                    kind: left_kind,
8691                    children: left_children,
8692                },
8693                CppTemplateTerm::Node {
8694                    kind: right_kind,
8695                    children: right_children,
8696                },
8697            ) => {
8698                if left_kind != right_kind || left_children.len() != right_children.len() {
8699                    return false;
8700                }
8701                work.extend(left_children.iter().zip(right_children).rev());
8702            }
8703            _ => return false,
8704        }
8705    }
8706    true
8707}
8708
8709pub fn cpp_name_component_nodes(node: Node<'_>) -> Option<Vec<Node<'_>>> {
8710    let mut components = Vec::new();
8711    let mut stack = vec![node];
8712    while let Some(current) = stack.pop() {
8713        match current.kind() {
8714            "identifier"
8715            | "field_identifier"
8716            | "namespace_identifier"
8717            | "type_identifier"
8718            | "operator_name"
8719            | "destructor_name" => components.push(current),
8720            "template_type" | "template_function" => {
8721                stack.push(current.child_by_field_name("name")?);
8722            }
8723            "qualified_identifier" | "scoped_identifier" | "scoped_type_identifier" => {
8724                stack.push(current.child_by_field_name("name")?);
8725                if let Some(scope) = current.child_by_field_name("scope") {
8726                    stack.push(scope);
8727                }
8728            }
8729            "nested_namespace_specifier" => {
8730                for index in (0..current.named_child_count()).rev() {
8731                    stack.push(current.named_child(index)?);
8732                }
8733            }
8734            _ => return None,
8735        }
8736    }
8737    Some(components)
8738}
8739
8740pub fn is_globally_qualified_cpp_name(node: Node<'_>) -> bool {
8741    node.child_by_field_name("scope").is_none()
8742        && node.child(0).is_some_and(|child| child.kind() == "::")
8743}
8744
8745fn enclosing_namespace_components(node: Node<'_>, source: &str) -> Option<Vec<String>> {
8746    let mut namespaces = Vec::new();
8747    let mut current = node.parent();
8748    while let Some(parent) = current {
8749        if parent.kind() == "namespace_definition"
8750            && let Some(name) = parent.child_by_field_name("name")
8751        {
8752            let mut components = Vec::new();
8753            append_cpp_name_components(name, source, &mut components)?;
8754            namespaces.push(components);
8755        }
8756        current = parent.parent();
8757    }
8758    namespaces.reverse();
8759    Some(namespaces.into_iter().flatten().collect())
8760}
8761
8762/// Whether a parser-derived namespace path can be reconciled with an indexed
8763/// owner scope without inventing an unrelated short-name binding.
8764///
8765/// Macro namespace sentinels can make tree-sitter omit one or more namespace
8766/// definitions from the ancestor chain.  Preserve the order of every
8767/// namespace that did survive parsing, but allow indexed components between
8768/// them.  An empty path is deliberately rejected: a one-segment owner at the
8769/// translation-unit root is not evidence of a malformed namespace.
8770fn indexed_namespace_path_is_recoverable(
8771    lexical_scope: &[String],
8772    indexed_owner_scope: &[String],
8773) -> bool {
8774    if lexical_scope.is_empty() || lexical_scope.len() >= indexed_owner_scope.len() {
8775        return false;
8776    }
8777    let mut indexed = indexed_owner_scope.iter();
8778    lexical_scope
8779        .iter()
8780        .all(|component| indexed.any(|candidate| candidate == component))
8781}
8782
8783pub fn has_ancestor_kind(node: Node<'_>, kind: &str) -> bool {
8784    let mut current = node.parent();
8785    while let Some(parent) = current {
8786        if parent.kind() == kind {
8787            return true;
8788        }
8789        current = parent.parent();
8790    }
8791    false
8792}
8793
8794/// Return the terminal identifier represented by a callable or type callee.
8795///
8796/// Qualified, scoped, template, and field wrappers are traversed through their
8797/// grammar fields so both function calls and type constructions emit the token
8798/// that names the referenced declaration.
8799pub fn function_terminal_node(mut node: Node<'_>) -> Node<'_> {
8800    loop {
8801        let next = match node.kind() {
8802            "qualified_identifier"
8803            | "scoped_identifier"
8804            | "template_function"
8805            | "template_type" => node.child_by_field_name("name"),
8806            "field_expression" => node.child_by_field_name("field"),
8807            _ => None,
8808        };
8809        let Some(next) = next else {
8810            return node;
8811        };
8812        node = next;
8813    }
8814}
8815
8816/// Whether `node` is part of a call's callee expression, walking only through
8817/// the grammar wrappers that can structurally contain that callee.
8818pub fn is_call_callee_node(mut node: Node<'_>) -> bool {
8819    while let Some(parent) = node.parent() {
8820        match parent.kind() {
8821            "call_expression" => {
8822                return parent
8823                    .child_by_field_name("function")
8824                    .or_else(|| parent.named_child(0))
8825                    == Some(node);
8826            }
8827            "qualified_identifier"
8828            | "scoped_identifier"
8829            | "template_function"
8830            | "template_type"
8831            | "field_expression" => node = parent,
8832            _ => return false,
8833        }
8834    }
8835    false
8836}
8837
8838pub fn type_reference_hit_node<'tree, T: Clone + Eq + Hash>(
8839    node: Node<'tree>,
8840    file: &ProjectFile,
8841    source: &str,
8842    bindings: &LocalInferenceEngine<T>,
8843) -> Node<'tree> {
8844    if is_call_callee_node(node) {
8845        return function_terminal_node(node);
8846    }
8847    if file.rel_path().extension().is_some_and(|ext| ext == "c") {
8848        return node;
8849    }
8850    let mut current = node;
8851    let declaration = loop {
8852        let Some(parent) = current.parent() else {
8853            return node;
8854        };
8855        if parent.kind() == "declaration" {
8856            break parent;
8857        }
8858        if matches!(
8859            parent.kind(),
8860            "compound_statement" | "function_definition" | "lambda_expression"
8861        ) {
8862            return node;
8863        }
8864        current = parent;
8865    };
8866    let Some(_type_node) = declaration.child_by_field_name("type").filter(|type_node| {
8867        type_node.start_byte() <= node.start_byte() && node.end_byte() <= type_node.end_byte()
8868    }) else {
8869        return node;
8870    };
8871    let mut cursor = declaration.walk();
8872    let constructs_object = declaration.named_children(&mut cursor).any(|child| {
8873        if child.kind() == "init_declarator" {
8874            return child.child_by_field_name("value").is_some()
8875                || first_named_child_of_kind(child, "initializer_list").is_some()
8876                || first_named_child_of_kind(child, "compound_literal_expression").is_some();
8877        }
8878        let declarator = if is_declarator_node(child) {
8879            Some(child)
8880        } else {
8881            None
8882        };
8883        declarator.is_some_and(|declarator| {
8884            declarator.kind() == "function_declarator"
8885                && has_ancestor_kind(declarator, "compound_statement")
8886                && declarator
8887                    .child_by_field_name("declarator")
8888                    .is_some_and(|name| name.kind() == "identifier")
8889                && declarator
8890                    .child_by_field_name("parameters")
8891                    .is_some_and(|parameters| {
8892                        constructor_parameters_look_like_expressions(parameters, source, bindings)
8893                    })
8894        })
8895    });
8896    if constructs_object {
8897        function_terminal_node(node)
8898    } else {
8899        node
8900    }
8901}
8902
8903pub fn normalize_type_text(value: &str) -> String {
8904    strip_tag_type_prefix(
8905        normalize_cpp_whitespace(value)
8906            .trim_start_matches("const ")
8907            .trim_end_matches('*')
8908            .trim_end_matches('&')
8909            .trim(),
8910    )
8911    .to_string()
8912}
8913
8914fn strip_tag_type_prefix(value: &str) -> &str {
8915    let value = value.trim_start_matches("const ");
8916    value
8917        .strip_prefix("struct ")
8918        .or_else(|| value.strip_prefix("class "))
8919        .or_else(|| value.strip_prefix("enum "))
8920        .unwrap_or(value)
8921        .trim()
8922}
8923
8924pub fn normalize_reference_name(value: &str) -> Option<String> {
8925    let normalized = normalize_cpp_reference_text(value);
8926    (!normalized.is_empty()).then_some(normalized)
8927}
8928
8929pub fn normalize_cpp_reference_text(value: &str) -> String {
8930    let mut text = normalize_cpp_whitespace(value)
8931        .trim_start_matches("new ")
8932        .trim()
8933        .to_string();
8934    if let Some(index) = text.find(['(', '{']) {
8935        text.truncate(index);
8936    }
8937    if let Some(index) = text.find('<') {
8938        text.truncate(index);
8939    }
8940    let normalized = text
8941        .trim()
8942        .trim_start_matches("const ")
8943        .trim_end_matches(|ch: char| ch == '*' || ch == '&' || ch.is_whitespace())
8944        .trim_matches(':')
8945        .trim();
8946    strip_tag_type_prefix(normalized).to_string()
8947}
8948
8949pub fn cpp_name_for(unit: &CodeUnit) -> String {
8950    let short = unit.short_name().replace(['.', '$'], "::");
8951    if unit.package_name().is_empty() {
8952        short
8953    } else {
8954        format!("{}::{}", unit.package_name(), short)
8955    }
8956}
8957
8958/// Render an indexed C++ qualified name from its authoritative FqName
8959/// segments. Unlike the legacy `cpp_name_for` renderer, this preserves dots
8960/// that belong to a template argument (for example `Args...`).
8961fn canonical_cpp_name_from_fq(unit: &CodeUnit) -> Option<String> {
8962    let fq = unit.fq();
8963    if fq.is_empty() {
8964        return None;
8965    }
8966    let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
8967    Some(
8968        fq.segments()
8969            .iter()
8970            .map(|&segment| interner.resolve(segment).0)
8971            .collect::<Vec<_>>()
8972            .join("::"),
8973    )
8974}
8975
8976fn canonical_cpp_name_matches(unit: &CodeUnit, expected: &str) -> bool {
8977    canonical_cpp_name_from_fq(unit).as_deref() == Some(expected)
8978        || unit.fq().is_empty() && cpp_name_for(unit) == expected
8979}
8980
8981/// Return the indexed C++ owner scope without reparsing its rendered name.
8982///
8983/// Template spellings are opaque within an indexed `FqName` segment.  In
8984/// particular, the ellipsis in a parameter pack (`Args...`) is part of the
8985/// `AtomicHook<...>` type segment; feeding the legacy all-`::` rendering back
8986/// through `parse_symbol_path` would mistake those dots for component
8987/// separators.  Cache-loaded/legacy units may still have an empty structured
8988/// name, so retain the parser only as that explicit fallback.
8989pub fn canonical_cpp_scope_components(unit: &CodeUnit) -> Vec<String> {
8990    let fq = unit.fq();
8991    if !fq.is_empty() {
8992        let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
8993        let scope = fq
8994            .segments()
8995            .iter()
8996            .filter_map(|&segment| {
8997                let (text, kind) = interner.resolve(segment);
8998                matches!(
8999                    kind,
9000                    brokk_bifrost_core::analyzer::fq_name::SegmentKind::Package
9001                        | brokk_bifrost_core::analyzer::fq_name::SegmentKind::Type
9002                        | brokk_bifrost_core::analyzer::fq_name::SegmentKind::Nested
9003                )
9004                .then(|| text.to_string())
9005            })
9006            .collect();
9007        return scope;
9008    }
9009    brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
9010        brokk_bifrost_core::analyzer::Language::Cpp,
9011        &cpp_name_for(unit),
9012    )
9013}
9014
9015// fqname-M4: the second stage splits on the individual chars '.', '-', '>'
9016// (not the substring "->"), which deliberately reduces an `operator->`-style
9017// terminal segment to an empty tail rather than keeping it intact; the shared
9018// structured splitter's cpp operator-token merge would keep `operator->`
9019// whole instead, changing this function's result — `name_matches_callable`'s
9020// `expected.starts_with("operator")` fallback exists specifically to
9021// compensate for that reduction, and a pinned regression test
9022// (`operator-> must not be reduced with terminal_name-style punctuation
9023// splitting`) asserts today's char-class behavior. Not equivalence-provable;
9024// revisit alongside that pinned test if it is ever relaxed.
9025pub fn terminal_name(value: &str) -> &str {
9026    value
9027        .rsplit("::")
9028        .next()
9029        .unwrap_or(value)
9030        .rsplit(['.', '-', '>'])
9031        .next()
9032        .unwrap_or(value)
9033        .trim()
9034}
9035
9036pub fn name_matches_terminal(value: &str, expected: &str) -> bool {
9037    terminal_name(&normalize_cpp_reference_text(value)) == expected
9038}
9039
9040pub fn name_matches_callable(value: &str, expected: &str) -> bool {
9041    name_matches_terminal(value, expected)
9042        || expected.starts_with("operator")
9043            && terminal_name(&normalize_cpp_reference_text(value)) == "operator"
9044}
9045
9046pub fn name_mentions(value: &str, expected: &str) -> bool {
9047    normalize_cpp_reference_text(value)
9048        .split("::")
9049        .any(|part| part == expected)
9050}
9051
9052pub fn reference_matches_unit(reference: &str, unit: &CodeUnit) -> bool {
9053    let cpp_name = cpp_name_for(unit);
9054    if reference.contains("::") {
9055        return reference == cpp_name;
9056    }
9057    reference == cpp_name
9058        || terminal_name(reference) == unit.identifier()
9059            && (unit.package_name().is_empty() || reference == unit.identifier())
9060}
9061
9062pub fn matches_kind_for_lookup(unit: &CodeUnit, kind: TargetKind) -> bool {
9063    match kind {
9064        TargetKind::Type
9065        | TargetKind::Constructor
9066        | TargetKind::Method
9067        | TargetKind::MemberField => true,
9068        TargetKind::FreeFunction => unit.is_function(),
9069        TargetKind::GlobalField => unit.is_field(),
9070    }
9071}
9072
9073pub fn is_type_alias(unit: &CodeUnit) -> bool {
9074    unit.kind() == CodeUnitType::Field
9075        && unit.signature().is_some_and(|signature| {
9076            signature.starts_with("typedef ") || signature.starts_with("using ")
9077        })
9078}
9079
9080fn alias_target_matches_target(alias: &CppAlias, target: &CodeUnit) -> bool {
9081    let normalized = normalize_cpp_reference_text(alias.target.trim().trim_end_matches(';'));
9082    let target_name = cpp_name_for(target);
9083    if normalized.contains("::") {
9084        return normalized == target_name;
9085    }
9086    if let Some(namespace) = alias.namespace.as_deref() {
9087        return namespace_prefixes(namespace)
9088            .into_iter()
9089            .any(|prefix| format!("{prefix}::{normalized}") == target_name);
9090    }
9091    target.package_name().is_empty() && normalized == target.identifier()
9092}
9093
9094fn parser_alias_target_names(alias: &CppAlias) -> Vec<String> {
9095    let normalized = normalize_cpp_reference_text(alias.target.trim().trim_end_matches(';'));
9096    if normalized.contains("::") {
9097        return vec![normalized];
9098    }
9099    alias
9100        .namespace
9101        .as_deref()
9102        .map(namespace_prefixes)
9103        .map(|prefixes| {
9104            prefixes
9105                .into_iter()
9106                .map(|prefix| format!("{prefix}::{normalized}"))
9107                .collect()
9108        })
9109        .unwrap_or_else(|| vec![normalized])
9110}
9111
9112/// The declared return type text of a C++ function unit, with leading declaration specifiers
9113/// stripped, e.g. `T*` for `T* operator->()`.
9114pub fn cpp_function_return_type_text(
9115    analyzer: &CppGraphSource<'_>,
9116    function: &CodeUnit,
9117) -> Option<String> {
9118    let metadata = analyzer.signature_metadata(function);
9119    if !metadata.is_empty() {
9120        let first = metadata.first()?.return_type_text()?;
9121        return metadata
9122            .iter()
9123            .all(|metadata| metadata.return_type_text() == Some(first))
9124            .then(|| first.to_string());
9125    }
9126    let signature = cpp_function_signature_text(analyzer, function)?;
9127    cpp_function_return_type_text_from_signature(&signature)
9128}
9129
9130fn cpp_function_signature_text(
9131    analyzer: &CppGraphSource<'_>,
9132    function: &CodeUnit,
9133) -> Option<String> {
9134    function
9135        .signature()
9136        .filter(|signature| signature.contains(function.identifier()))
9137        .map(str::to_string)
9138        .or_else(|| analyzer.signatures(function).first().cloned())
9139        .or_else(|| analyzer.get_source(function, false))
9140}
9141
9142fn cpp_function_return_type_text_from_signature(signature: &str) -> Option<String> {
9143    let open = signature.find('(')?;
9144    let name_at = cpp_function_name_start(signature, open)?;
9145    if let Some(return_type) = cpp_trailing_return_type(&signature[name_at..]) {
9146        return Some(return_type);
9147    }
9148    let type_text = cpp_strip_leading_template_clause(&signature[..name_at])
9149        .split_whitespace()
9150        .filter(|token| {
9151            !matches!(
9152                *token,
9153                "static" | "virtual" | "inline" | "constexpr" | "explicit" | "friend"
9154            )
9155        })
9156        .collect::<Vec<_>>()
9157        .join(" ");
9158    let type_text = type_text.trim();
9159    (!type_text.is_empty()).then(|| type_text.to_string())
9160}
9161
9162fn cpp_function_name_start(signature: &str, open: usize) -> Option<usize> {
9163    let before_parameters = &signature[..open];
9164    if let Some(operator_at) = before_parameters.rfind("operator") {
9165        let boundary = operator_at == 0
9166            || before_parameters[..operator_at]
9167                .chars()
9168                .next_back()
9169                .is_some_and(|ch| !(ch == '_' || ch.is_ascii_alphanumeric()));
9170        if boundary {
9171            return Some(operator_at);
9172        }
9173    }
9174    before_parameters
9175        .rfind(|ch: char| !(ch == '_' || ch.is_ascii_alphanumeric()))
9176        .map(|index| index + 1)
9177}
9178
9179fn cpp_trailing_return_type(signature_from_name: &str) -> Option<String> {
9180    let open = signature_from_name.find('(')?;
9181    let mut depth = 0i32;
9182    for (offset, ch) in signature_from_name[open..].char_indices() {
9183        match ch {
9184            '(' => depth += 1,
9185            ')' => {
9186                depth -= 1;
9187                if depth == 0 {
9188                    let rest = signature_from_name[open + offset + ch.len_utf8()..].trim_start();
9189                    let arrow = rest.find("->")?;
9190                    let return_type = rest[arrow + 2..].trim_start();
9191                    let return_type = return_type
9192                        .split(['{', ';'])
9193                        .next()
9194                        .unwrap_or(return_type)
9195                        .trim();
9196                    return (!return_type.is_empty()).then(|| return_type.to_string());
9197                }
9198            }
9199            _ => {}
9200        }
9201    }
9202    None
9203}
9204
9205/// Strip a leading `template <...>` parameter clause, leaving the declaration that follows.
9206/// Returns the input unchanged when there is no such clause.
9207fn cpp_strip_leading_template_clause(text: &str) -> &str {
9208    let trimmed = text.trim_start();
9209    let Some(rest) = trimmed.strip_prefix("template") else {
9210        return text;
9211    };
9212    let rest = rest.trim_start();
9213    if !rest.starts_with('<') {
9214        return text;
9215    }
9216    let mut depth = 0i32;
9217    for (offset, ch) in rest.char_indices() {
9218        match ch {
9219            '<' => depth += 1,
9220            '>' => {
9221                depth -= 1;
9222                if depth == 0 {
9223                    return rest[offset + ch.len_utf8()..].trim_start();
9224                }
9225            }
9226            _ => {}
9227        }
9228    }
9229    text
9230}
9231
9232pub fn cpp_namespace_for(unit: &CodeUnit) -> Option<String> {
9233    // fqname-M4: `cpp_name_for` is a bespoke all-`::` rendering of the unit's
9234    // name (it replaces every `.`/`$` in `short_name` with `::`), which is NOT
9235    // the same string `default_parent_fq_name`/`fq().parent()` would render:
9236    // the structured `FqName`'s native cpp display deliberately keeps `.` (not
9237    // `::`) between a trailing `Package` segment and a following `Type`
9238    // segment (see `separator` in `fq_name.rs`, landed for issue #1163), so
9239    // popping the unit's own `fq()` segment would NOT reproduce this
9240    // fully-`::`-joined string. Left as a split on the locally-built
9241    // all-colon string rather than the unit's structured name.
9242    cpp_name_for(unit).rsplit_once("::").map(|(namespace, _)| {
9243        namespace
9244            .strip_prefix("anonymous_namespace::")
9245            .unwrap_or(namespace)
9246            .to_string()
9247    })
9248}
9249
9250fn namespace_prefixes(namespace: &str) -> Vec<String> {
9251    // `namespace` is built by `cpp_name_for`/`cpp_namespace_for` with every
9252    // non-`::` separator already converted to `::`, so re-tokenizing it with
9253    // the shared structured splitter and progressively popping the last
9254    // component reproduces the `rsplit_once("::")` outward walk exactly (same
9255    // shape as `cpp_qualifier_lookup_tiers`'s namespace-chain walk).
9256    let mut parts = brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path(
9257        brokk_bifrost_core::analyzer::Language::Cpp,
9258        namespace,
9259    );
9260    let mut prefixes = Vec::new();
9261    while !parts.is_empty() {
9262        prefixes.push(parts.join("::"));
9263        parts.pop();
9264    }
9265    prefixes
9266}
9267
9268fn nearest_namespace_candidates(
9269    candidates: Vec<CodeUnit>,
9270    normalized: &str,
9271    lexical_namespace: Option<&str>,
9272) -> Vec<CodeUnit> {
9273    if normalized.contains("::") {
9274        return candidates;
9275    }
9276    if let Some(namespace) = lexical_namespace {
9277        for prefix in namespace_prefixes(namespace) {
9278            let scoped = candidates
9279                .iter()
9280                .filter(|function| cpp_namespace_for(function).as_deref() == Some(prefix.as_str()))
9281                .cloned()
9282                .collect::<Vec<_>>();
9283            if !scoped.is_empty() {
9284                return scoped;
9285            }
9286        }
9287    }
9288    candidates
9289        .into_iter()
9290        .filter(|function| cpp_namespace_for(function).is_none_or(|namespace| namespace.is_empty()))
9291        .collect()
9292}
9293
9294pub fn enclosing_namespace_context(node: Node<'_>, source: &str) -> Option<String> {
9295    let mut namespaces = Vec::new();
9296    let mut current = node.parent();
9297    while let Some(parent) = current {
9298        if parent.kind() == "namespace_definition"
9299            && let Some(name) = parent.child_by_field_name("name")
9300        {
9301            let namespace = normalize_cpp_reference_text(node_text(name, source));
9302            if !namespace.is_empty() {
9303                namespaces.push(namespace);
9304            }
9305        }
9306        current = parent.parent();
9307    }
9308    if namespaces.is_empty() {
9309        None
9310    } else {
9311        namespaces.reverse();
9312        Some(namespaces.join("::"))
9313    }
9314}
9315
9316/// Like [`precise_parent_of`], but drops module (namespace) parents. A namespace is a scope, not a
9317/// type or receiver, so namespace-scoped functions and constants resolve as free functions and
9318/// globals rather than members.
9319pub fn type_owner_of(analyzer: &CppGraphSource<'_>, code_unit: &CodeUnit) -> Option<CodeUnit> {
9320    type_owner_resolution(analyzer, code_unit).map(|owner| owner.unit)
9321}
9322
9323fn type_owner_resolution(
9324    analyzer: &CppGraphSource<'_>,
9325    code_unit: &CodeUnit,
9326) -> Option<ResolvedTypeOwner> {
9327    precise_parent_resolution(analyzer, code_unit).filter(|owner| !owner.unit.is_module())
9328}
9329
9330/// Recover method identity for an indexed out-of-line definition when the
9331/// analyzer has retained only its unique include-visible class forward
9332/// declaration. This is deliberately target-only: canonical declaration
9333/// resolution must continue to prefer the callable definition rather than
9334/// replacing it with the forward owner.
9335fn target_forward_owner_resolution(
9336    analyzer: &CppGraphSource<'_>,
9337    code_unit: &CodeUnit,
9338) -> Option<ResolvedTypeOwner> {
9339    if !code_unit.is_function() {
9340        return None;
9341    }
9342    let interner = brokk_bifrost_core::analyzer::fq_name::segment_interner();
9343    let owner_fqn = code_unit.fq().parent()?.display(interner);
9344    let cpp = analyzer.cpp?;
9345    let mut visible_files = HashSet::default();
9346    collect_include_closure(
9347        analyzer,
9348        cpp.include_target_index(),
9349        code_unit.source(),
9350        &mut visible_files,
9351        None,
9352    );
9353    let mut forward = None;
9354    for candidate in analyzer
9355        .global_usage_definition_index()
9356        .fqn(&owner_fqn)
9357        .into_iter()
9358        .filter(|candidate| candidate.is_class() && visible_files.contains(candidate.source()))
9359    {
9360        match cpp_class_declaration_strength(analyzer, candidate) {
9361            CppClassDeclarationStrength::Forward if forward.is_none() => {
9362                forward = Some(candidate.clone());
9363            }
9364            CppClassDeclarationStrength::Forward
9365            | CppClassDeclarationStrength::Full
9366            | CppClassDeclarationStrength::Unknown => return None,
9367        }
9368    }
9369    forward.map(|unit| ResolvedTypeOwner {
9370        unit,
9371        is_forward_declaration: true,
9372    })
9373}
9374
9375pub fn precise_parent_of(
9376    analyzer: &CppGraphSource<'_>,
9377    visibility: &VisibilityIndex<'_>,
9378    code_unit: &CodeUnit,
9379) -> Option<CodeUnit> {
9380    visibility.cached_precise_parent_of(analyzer, code_unit)
9381}
9382
9383fn precise_parent_resolution(
9384    analyzer: &CppGraphSource<'_>,
9385    code_unit: &CodeUnit,
9386) -> Option<ResolvedTypeOwner> {
9387    #[cfg(any(test, feature = "test-support"))]
9388    if let Some(cpp) = analyzer.cpp {
9389        cpp.record_cpp_parent_resolution_for_test();
9390    }
9391    if let Some(unit) = exact_structural_type_parent(analyzer, code_unit) {
9392        return Some(ResolvedTypeOwner {
9393            unit,
9394            is_forward_declaration: false,
9395        });
9396    }
9397    let fallback = analyzer.parent_of(code_unit);
9398    // fqname-M4: `owner_name` is used both bare (passed standalone to the
9399    // owner-resolution calls below) and manually recombined with
9400    // `package_name()` a few lines down, so this needs the package-less
9401    // `short_name` owner specifically; `default_parent_fq_name`/`fq.parent()`
9402    // would render the package-qualified owner instead, changing both uses.
9403    let Some(owner_name) = code_unit
9404        .short_name()
9405        .rsplit_once('.')
9406        .map(|(owner, _)| owner)
9407    else {
9408        return fallback.map(|unit| ResolvedTypeOwner {
9409            unit,
9410            is_forward_declaration: false,
9411        });
9412    };
9413    let owner_fqn = if code_unit.package_name().is_empty() {
9414        owner_name.to_string()
9415    } else {
9416        format!("{}.{}", code_unit.package_name(), owner_name)
9417    };
9418    match same_source_owner(analyzer, code_unit, &owner_fqn, owner_name) {
9419        DirectOwnerResolution::UniqueFull(owner) => {
9420            return Some(ResolvedTypeOwner {
9421                unit: owner,
9422                is_forward_declaration: false,
9423            });
9424        }
9425        DirectOwnerResolution::Ambiguous => return None,
9426        DirectOwnerResolution::ForwardsOnly(_) | DirectOwnerResolution::None => {}
9427    }
9428    match directly_included_owner(analyzer, code_unit, &owner_fqn, owner_name) {
9429        DirectOwnerResolution::UniqueFull(owner) => Some(ResolvedTypeOwner {
9430            unit: owner,
9431            is_forward_declaration: false,
9432        }),
9433        DirectOwnerResolution::Ambiguous => None,
9434        DirectOwnerResolution::ForwardsOnly(forwards) => {
9435            match visible_full_cpp_owner(analyzer, code_unit, &owner_fqn, owner_name) {
9436                FullOwnerResolution::Unique(owner) => Some(ResolvedTypeOwner {
9437                    unit: owner,
9438                    is_forward_declaration: false,
9439                }),
9440                FullOwnerResolution::None if forwards.len() == 1 => {
9441                    forwards.into_iter().next().map(|unit| ResolvedTypeOwner {
9442                        unit,
9443                        is_forward_declaration: true,
9444                    })
9445                }
9446                FullOwnerResolution::None | FullOwnerResolution::Ambiguous => None,
9447            }
9448        }
9449        DirectOwnerResolution::None => {
9450            match visible_full_cpp_owner(analyzer, code_unit, &owner_fqn, owner_name) {
9451                FullOwnerResolution::Unique(owner) => Some(ResolvedTypeOwner {
9452                    unit: owner,
9453                    is_forward_declaration: false,
9454                }),
9455                FullOwnerResolution::Ambiguous => None,
9456                FullOwnerResolution::None => fallback
9457                    .filter(|parent| {
9458                        parent.source() == code_unit.source()
9459                            && parent.short_name() == owner_name
9460                            && parent.package_name() == code_unit.package_name()
9461                            && (!parent.is_class()
9462                                || cpp_class_declaration_strength(analyzer, parent)
9463                                    == CppClassDeclarationStrength::Full)
9464                    })
9465                    .map(|unit| ResolvedTypeOwner {
9466                        unit,
9467                        is_forward_declaration: false,
9468                    }),
9469            }
9470        }
9471    }
9472}
9473
9474fn exact_structural_type_parent(
9475    analyzer: &CppGraphSource<'_>,
9476    code_unit: &CodeUnit,
9477) -> Option<CodeUnit> {
9478    if !code_unit.is_function() && !code_unit.is_field() {
9479        return None;
9480    }
9481    let encoded_owner = code_unit.short_name().rsplit_once('.')?.0; // fqname-M4: package-less short_name owner used as an encoded key; fq.parent() would render the `::`-headed package-qualified owner
9482    let cpp = analyzer.cpp?;
9483    let parent = cpp.structural_parent_of(code_unit)?;
9484    (!parent.is_module()
9485        && parent.source() == code_unit.source()
9486        && parent.package_name() == code_unit.package_name()
9487        && parent.short_name() == encoded_owner)
9488        .then_some(parent)
9489}
9490
9491fn same_source_owner(
9492    analyzer: &CppGraphSource<'_>,
9493    code_unit: &CodeUnit,
9494    owner_fqn: &str,
9495    owner_name: &str,
9496) -> DirectOwnerResolution {
9497    let candidates = analyzer
9498        .global_usage_definition_index()
9499        .fqn(owner_fqn)
9500        .into_iter()
9501        .filter(|candidate| {
9502            candidate.is_class()
9503                && candidate.source() == code_unit.source()
9504                && candidate.short_name() == owner_name
9505                && candidate.package_name() == code_unit.package_name()
9506        })
9507        .collect::<Vec<_>>();
9508    let candidates = prefer_member_declaring_owners(analyzer, code_unit, candidates);
9509    classify_direct_owner_candidates(analyzer, candidates.into_iter())
9510}
9511
9512fn visible_full_cpp_owner(
9513    analyzer: &CppGraphSource<'_>,
9514    code_unit: &CodeUnit,
9515    owner_fqn: &str,
9516    owner_name: &str,
9517) -> FullOwnerResolution {
9518    let Some(cpp) = analyzer.cpp else {
9519        return FullOwnerResolution::None;
9520    };
9521    let mut visible_files = HashSet::default();
9522    collect_include_closure(
9523        analyzer,
9524        cpp.include_target_index(),
9525        code_unit.source(),
9526        &mut visible_files,
9527        None,
9528    );
9529    let candidates = analyzer
9530        .global_usage_definition_index()
9531        .fqn(owner_fqn)
9532        .into_iter()
9533        .filter(|candidate| {
9534            candidate.is_class()
9535                && candidate.short_name() == owner_name
9536                && candidate.package_name() == code_unit.package_name()
9537                && visible_files.contains(candidate.source())
9538        })
9539        .collect::<Vec<_>>();
9540    let candidates = prefer_member_declaring_owners(analyzer, code_unit, candidates);
9541    let mut full_definition = None;
9542    for candidate in candidates {
9543        match cpp_class_declaration_strength(analyzer, candidate) {
9544            CppClassDeclarationStrength::Full if full_definition.is_some() => {
9545                return FullOwnerResolution::Ambiguous;
9546            }
9547            CppClassDeclarationStrength::Full => full_definition = Some(candidate.clone()),
9548            CppClassDeclarationStrength::Forward => {}
9549            CppClassDeclarationStrength::Unknown => return FullOwnerResolution::Ambiguous,
9550        }
9551    }
9552    full_definition.map_or(FullOwnerResolution::None, FullOwnerResolution::Unique)
9553}
9554
9555pub enum DirectOwnerResolution {
9556    None,
9557    ForwardsOnly(Vec<CodeUnit>),
9558    UniqueFull(CodeUnit),
9559    Ambiguous,
9560}
9561
9562enum FullOwnerResolution {
9563    None,
9564    Unique(CodeUnit),
9565    Ambiguous,
9566}
9567
9568#[derive(Clone, Copy, PartialEq, Eq)]
9569pub enum CppClassDeclarationStrength {
9570    Full,
9571    Forward,
9572    Unknown,
9573}
9574
9575fn directly_included_owner(
9576    analyzer: &CppGraphSource<'_>,
9577    code_unit: &CodeUnit,
9578    owner_fqn: &str,
9579    owner_name: &str,
9580) -> DirectOwnerResolution {
9581    let Some(cpp) = analyzer.cpp else {
9582        return DirectOwnerResolution::None;
9583    };
9584    let imports = analyzer.import_statements(code_unit.source());
9585    let direct_includes: HashSet<ProjectFile> = cpp_include_paths(&imports)
9586        .into_iter()
9587        .flat_map(|include| {
9588            resolve_include_targets_with_index(
9589                code_unit.source(),
9590                &include,
9591                cpp.include_target_index(),
9592            )
9593        })
9594        .collect();
9595    let candidates = analyzer
9596        .global_usage_definition_index()
9597        .fqn(owner_fqn)
9598        .into_iter()
9599        .filter(|candidate| {
9600            candidate.is_class()
9601                && candidate.short_name() == owner_name
9602                && candidate.package_name() == code_unit.package_name()
9603                && direct_includes.contains(candidate.source())
9604        })
9605        .collect::<Vec<_>>();
9606    let candidates = prefer_member_declaring_owners(analyzer, code_unit, candidates);
9607    classify_direct_owner_candidates(analyzer, candidates.into_iter())
9608}
9609
9610fn prefer_member_declaring_owners<'a>(
9611    analyzer: &CppGraphSource<'_>,
9612    member: &CodeUnit,
9613    candidates: Vec<&'a CodeUnit>,
9614) -> Vec<&'a CodeUnit> {
9615    let matching = candidates
9616        .iter()
9617        .copied()
9618        .filter(|owner| owner_declares_member(analyzer, owner, member))
9619        .collect::<Vec<_>>();
9620    if matching.is_empty() {
9621        candidates
9622    } else {
9623        matching
9624    }
9625}
9626
9627fn owner_declares_member(
9628    analyzer: &CppGraphSource<'_>,
9629    owner: &CodeUnit,
9630    member: &CodeUnit,
9631) -> bool {
9632    analyzer.direct_children(owner).into_iter().any(|child| {
9633        child.kind() == member.kind()
9634            && child.identifier() == member.identifier()
9635            && child.signature() == member.signature()
9636    })
9637}
9638
9639fn classify_direct_owner_candidates<'a>(
9640    analyzer: &CppGraphSource<'_>,
9641    candidates: impl Iterator<Item = &'a CodeUnit>,
9642) -> DirectOwnerResolution {
9643    collapse_owner_candidates(candidates.map(|candidate| {
9644        (
9645            candidate.clone(),
9646            cpp_class_declaration_strength(analyzer, candidate),
9647        )
9648    }))
9649}
9650
9651pub fn collapse_owner_candidates(
9652    candidates: impl Iterator<Item = (CodeUnit, CppClassDeclarationStrength)>,
9653) -> DirectOwnerResolution {
9654    let mut full_definition = None;
9655    let mut forwards = Vec::new();
9656    for (candidate, strength) in candidates {
9657        match strength {
9658            CppClassDeclarationStrength::Full if full_definition.is_some() => {
9659                return DirectOwnerResolution::Ambiguous;
9660            }
9661            CppClassDeclarationStrength::Full => full_definition = Some(candidate),
9662            CppClassDeclarationStrength::Forward => forwards.push(candidate),
9663            CppClassDeclarationStrength::Unknown => return DirectOwnerResolution::Ambiguous,
9664        }
9665    }
9666    if let Some(owner) = full_definition {
9667        DirectOwnerResolution::UniqueFull(owner)
9668    } else if !forwards.is_empty() {
9669        DirectOwnerResolution::ForwardsOnly(forwards)
9670    } else {
9671        DirectOwnerResolution::None
9672    }
9673}
9674
9675pub fn cpp_class_declaration_strength(
9676    analyzer: &CppGraphSource<'_>,
9677    candidate: &CodeUnit,
9678) -> CppClassDeclarationStrength {
9679    if let Some(prepared) = analyzer
9680        .cpp
9681        .and_then(|cpp| cpp.prepared_syntax(candidate.source()))
9682    {
9683        return cpp_class_declaration_strength_in_tree(
9684            analyzer,
9685            candidate,
9686            prepared.source(),
9687            prepared.tree().root_node(),
9688        );
9689    }
9690    let Some(source) = analyzer.indexed_source(candidate.source()) else {
9691        return CppClassDeclarationStrength::Unknown;
9692    };
9693    #[cfg(any(test, feature = "test-support"))]
9694    if let Some(cpp) = analyzer.cpp {
9695        cpp.record_cpp_class_strength_parse_for_test();
9696    }
9697    let mut parser = Parser::new();
9698    if parser
9699        .set_language(&tree_sitter_cpp::LANGUAGE.into())
9700        .is_err()
9701    {
9702        return CppClassDeclarationStrength::Unknown;
9703    }
9704    let Some(tree) = parser.parse(&source, None) else {
9705        return CppClassDeclarationStrength::Unknown;
9706    };
9707    cpp_class_declaration_strength_in_tree(analyzer, candidate, &source, tree.root_node())
9708}
9709
9710fn cpp_class_declaration_strength_in_tree(
9711    analyzer: &CppGraphSource<'_>,
9712    candidate: &CodeUnit,
9713    source: &str,
9714    root: Node<'_>,
9715) -> CppClassDeclarationStrength {
9716    let ranges = analyzer.ranges(candidate);
9717    let mut saw_forward = false;
9718    for range in ranges {
9719        let mut stack = vec![root];
9720        while let Some(node) = stack.pop() {
9721            if node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
9722                continue;
9723            }
9724            if node.start_byte() == range.start_byte && node.end_byte() == range.end_byte {
9725                if matches!(
9726                    node.kind(),
9727                    "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
9728                ) {
9729                    if cpp_class_node_has_body(node) {
9730                        return CppClassDeclarationStrength::Full;
9731                    }
9732                    saw_forward = true;
9733                } else if let Some(has_body) =
9734                    recovered_exported_class_has_body(node, source, candidate.identifier())
9735                {
9736                    if has_body {
9737                        return CppClassDeclarationStrength::Full;
9738                    }
9739                    saw_forward = true;
9740                }
9741            }
9742            let mut cursor = node.walk();
9743            stack.extend(node.named_children(&mut cursor));
9744        }
9745    }
9746    if saw_forward {
9747        CppClassDeclarationStrength::Forward
9748    } else {
9749        CppClassDeclarationStrength::Unknown
9750    }
9751}
9752
9753fn cpp_class_node_has_body(node: Node<'_>) -> bool {
9754    node.child_by_field_name("body").is_some() || {
9755        let mut cursor = node.walk();
9756        node.named_children(&mut cursor).any(|child| {
9757            matches!(
9758                child.kind(),
9759                "declaration_list" | "field_declaration_list" | "enumerator_list"
9760            )
9761        })
9762    }
9763}
9764
9765pub fn visible_owner_from_member_name(ctx: &ScanCtx<'_>, code_unit: &CodeUnit) -> Option<CodeUnit> {
9766    // fqname-M4: `owner_name` is used both bare and manually recombined with
9767    // `package_name()` below (same package-less short_name owner shape as
9768    // `precise_parent_resolution` above); `default_parent_fq_name` would
9769    // render the package-qualified owner instead, changing both uses.
9770    let owner_name = code_unit
9771        .short_name()
9772        .rsplit_once('.')
9773        .map(|(owner, _)| owner)?;
9774    let owner_fqn = if code_unit.package_name().is_empty() {
9775        owner_name.to_string()
9776    } else {
9777        format!("{}.{}", code_unit.package_name(), owner_name)
9778    };
9779    ctx.analyzer
9780        .global_usage_definition_index()
9781        .fqn(&owner_fqn)
9782        .into_iter()
9783        .find(|candidate| {
9784            candidate.is_class()
9785                && ctx.visibility.is_visible(ctx.file, candidate)
9786                && candidate.short_name() == owner_name
9787                && candidate.package_name() == code_unit.package_name()
9788        })
9789        .cloned()
9790}
9791
9792pub fn same_symbol(left: &CodeUnit, right: &CodeUnit) -> bool {
9793    left.kind() == right.kind()
9794        && left.fq_name() == right.fq_name()
9795        && left.signature() == right.signature()
9796        && left.source() == right.source()
9797}
9798
9799pub fn same_visible_symbol(left: &CodeUnit, right: &CodeUnit) -> bool {
9800    same_symbol(left, right) || same_logical_symbol(left, right)
9801}
9802
9803pub fn same_visible_global_field_symbol(
9804    analyzer: &CppGraphSource<'_>,
9805    internal_linkage_cache: &mut HashMap<CodeUnit, bool>,
9806    left: &CodeUnit,
9807    right: &CodeUnit,
9808) -> bool {
9809    if same_symbol(left, right) {
9810        return true;
9811    }
9812    if !same_logical_symbol(left, right) {
9813        return false;
9814    }
9815    if cpp_global_field_has_internal_linkage_cached(analyzer, internal_linkage_cache, left)
9816        || cpp_global_field_has_internal_linkage_cached(analyzer, internal_linkage_cache, right)
9817    {
9818        left.source() == right.source()
9819    } else {
9820        true
9821    }
9822}
9823
9824fn cpp_global_field_has_internal_linkage_cached(
9825    analyzer: &CppGraphSource<'_>,
9826    cache: &mut HashMap<CodeUnit, bool>,
9827    candidate: &CodeUnit,
9828) -> bool {
9829    if let Some(internal) = cache.get(candidate) {
9830        return *internal;
9831    }
9832    #[cfg(any(test, feature = "test-support"))]
9833    note_cpp_global_field_internal_linkage_classification_for_test();
9834    let internal = cpp_global_field_has_internal_linkage(analyzer, candidate);
9835    cache.insert(candidate.clone(), internal);
9836    internal
9837}
9838
9839#[cfg(any(test, feature = "test-support"))]
9840thread_local! {
9841    static CPP_GLOBAL_FIELD_INTERNAL_LINKAGE_CLASSIFICATIONS_FOR_TEST: Cell<usize> = const { Cell::new(0) };
9842}
9843
9844#[cfg(any(test, feature = "test-support"))]
9845fn note_cpp_global_field_internal_linkage_classification_for_test() {
9846    CPP_GLOBAL_FIELD_INTERNAL_LINKAGE_CLASSIFICATIONS_FOR_TEST.with(|count| {
9847        count.set(count.get() + 1);
9848    });
9849}
9850
9851#[cfg(any(test, feature = "test-support"))]
9852pub fn with_cpp_global_field_internal_linkage_classification_counter_for_test<T>(
9853    body: impl FnOnce() -> T,
9854) -> (T, usize) {
9855    CPP_GLOBAL_FIELD_INTERNAL_LINKAGE_CLASSIFICATIONS_FOR_TEST.with(|count| {
9856        count.set(0);
9857        let result = body();
9858        let observed = count.get();
9859        count.set(0);
9860        (result, observed)
9861    })
9862}
9863
9864pub fn same_logical_symbol(left: &CodeUnit, right: &CodeUnit) -> bool {
9865    left.kind() == right.kind()
9866        && left.fq_name() == right.fq_name()
9867        && left.signature() == right.signature()
9868}
9869
9870pub fn cpp_global_field_has_internal_linkage(
9871    analyzer: &CppGraphSource<'_>,
9872    candidate: &CodeUnit,
9873) -> bool {
9874    if !candidate.is_field() || candidate.short_name().contains('.') {
9875        return false;
9876    }
9877    let Some(local_linkage) = cpp_global_field_declaration_linkage(analyzer, candidate) else {
9878        return false;
9879    };
9880    match local_linkage {
9881        CppFieldLinkage::Internal => true,
9882        CppFieldLinkage::External => false,
9883        CppFieldLinkage::InternalUnlessExternalPeer => {
9884            !cpp_global_field_linkage_peers(analyzer, candidate)
9885                .filter_map(|peer| cpp_global_field_declaration_linkage(analyzer, peer))
9886                .any(|linkage| matches!(linkage, CppFieldLinkage::External))
9887        }
9888    }
9889}
9890
9891fn cpp_global_field_linkage_peers<'a>(
9892    analyzer: &CppGraphSource<'a>,
9893    candidate: &'a CodeUnit,
9894) -> impl Iterator<Item = &'a CodeUnit> + 'a {
9895    // These peers are returned to the caller, so they must borrow the analyzer
9896    // for `'a` rather than a handle that dies with this call. `fqn` reads the
9897    // shards directly for exactly that reason.
9898    let fq_name = candidate.fq_name();
9899    analyzer
9900        .global_usage_definition_index()
9901        .fqn(&fq_name)
9902        .into_iter()
9903        .filter(move |peer| {
9904            if *peer == candidate {
9905                return false;
9906            }
9907            #[cfg(any(test, feature = "test-support"))]
9908            note_cpp_global_field_linkage_peer_inspection_for_test();
9909            same_logical_symbol(peer, candidate)
9910        })
9911}
9912
9913#[cfg(any(test, feature = "test-support"))]
9914thread_local! {
9915    static CPP_GLOBAL_FIELD_LINKAGE_PEER_INSPECTIONS_FOR_TEST: Cell<usize> = const { Cell::new(0) };
9916}
9917
9918#[cfg(any(test, feature = "test-support"))]
9919fn note_cpp_global_field_linkage_peer_inspection_for_test() {
9920    CPP_GLOBAL_FIELD_LINKAGE_PEER_INSPECTIONS_FOR_TEST.with(|count| {
9921        count.set(count.get() + 1);
9922    });
9923}
9924
9925#[cfg(any(test, feature = "test-support"))]
9926pub fn with_cpp_global_field_linkage_peer_inspection_counter_for_test<T>(
9927    body: impl FnOnce() -> T,
9928) -> (T, usize) {
9929    CPP_GLOBAL_FIELD_LINKAGE_PEER_INSPECTIONS_FOR_TEST.with(|count| {
9930        count.set(0);
9931        let result = body();
9932        let observed = count.get();
9933        count.set(0);
9934        (result, observed)
9935    })
9936}
9937
9938fn cpp_global_field_declaration_linkage(
9939    analyzer: &CppGraphSource<'_>,
9940    candidate: &CodeUnit,
9941) -> Option<CppFieldLinkage> {
9942    if let Some(linkage) = analyzer.cpp_field_linkage(candidate) {
9943        return Some(linkage);
9944    }
9945    let cpp = analyzer.cpp?;
9946    if let Some(prepared) = cpp.prepared_syntax(candidate.source()) {
9947        return cpp_global_field_declaration_linkage_in_tree(
9948            analyzer,
9949            candidate,
9950            prepared.source(),
9951            prepared.tree().root_node(),
9952        );
9953    }
9954    let source = analyzer.indexed_source(candidate.source())?;
9955    let mut parser = Parser::new();
9956    if parser
9957        .set_language(&tree_sitter_cpp::LANGUAGE.into())
9958        .is_err()
9959    {
9960        return None;
9961    }
9962    let tree = parser.parse(&source, None)?;
9963    cpp_global_field_declaration_linkage_in_tree(analyzer, candidate, &source, tree.root_node())
9964}
9965
9966fn cpp_global_field_declaration_linkage_in_tree(
9967    analyzer: &CppGraphSource<'_>,
9968    candidate: &CodeUnit,
9969    source: &str,
9970    root: Node<'_>,
9971) -> Option<CppFieldLinkage> {
9972    analyzer.ranges(candidate).iter().find_map(|range| {
9973        node_for_exact_range(root, range)
9974            .and_then(enclosing_cpp_field_declaration)
9975            .map(|declaration| cpp_field_declaration_linkage(declaration, source))
9976    })
9977}
9978
9979fn enclosing_cpp_field_declaration(mut node: Node<'_>) -> Option<Node<'_>> {
9980    loop {
9981        if matches!(node.kind(), "declaration" | "field_declaration") {
9982            return Some(node);
9983        }
9984        node = node.parent()?;
9985    }
9986}
9987
9988#[cfg(test)]
9989mod tests {
9990    use super::*;
9991
9992    fn first_enum_flattened_namespace(source: &str) -> Option<Vec<String>> {
9993        let mut parser = Parser::new();
9994        parser
9995            .set_language(&tree_sitter_cpp::LANGUAGE.into())
9996            .expect("C++ grammar");
9997        let tree = parser.parse(source, None).expect("C++ fixture tree");
9998        let mut stack = vec![tree.root_node()];
9999        while let Some(node) = stack.pop() {
10000            if node.kind() == "enum_specifier" {
10001                return flattened_macro_namespace_components(node, source);
10002            }
10003            let mut cursor = node.walk();
10004            let children = node.named_children(&mut cursor).collect::<Vec<_>>();
10005            stack.extend(children.into_iter().rev());
10006        }
10007        None
10008    }
10009
10010    #[test]
10011    fn flattened_namespace_scope_requires_a_complete_sentinel_envelope() {
10012        let complete = r#"NLOHMANN_JSON_NAMESPACE_BEGIN
10013namespace detail
10014{
10015enum class value_t { null };
10016}
10017NLOHMANN_JSON_NAMESPACE_END
10018NLOHMANN_JSON_NAMESPACE_BEGIN
10019namespace next
10020{
10021struct next_type {};
10022}
10023NLOHMANN_JSON_NAMESPACE_END
10024"#;
10025        assert_eq!(
10026            first_enum_flattened_namespace(complete),
10027            Some(vec!["detail".to_string()])
10028        );
10029
10030        let stale_end = format!("NLOHMANN_JSON_NAMESPACE_END\n{complete}");
10031        assert_eq!(
10032            first_enum_flattened_namespace(&stale_end),
10033            Some(vec!["detail".to_string()]),
10034            "a stale end marker before the begin marker must not replace the intended namespace"
10035        );
10036
10037        let incomplete = r#"NLOHMANN_JSON_NAMESPACE_BEGIN
10038namespace detail
10039{
10040enum class value_t { null };
10041}
10042struct next_type {};
10043"#;
10044        assert_eq!(first_enum_flattened_namespace(incomplete), None);
10045    }
10046}