Skip to main content

brokk_bifrost_cpp/
identity.rs

1//! Which C++ callable is which: declaration/definition roles, the linkage
2//! evidence that unifies a header declaration with its `.cpp` definition, and
3//! the #1134 resolution-time identity reconciliation built on top of both.
4//!
5//! Two things stay in `analyzer/cpp/identity.rs` on purpose:
6//!
7//! * [`cpp_header_body_files_are_related`] here reads the include closure off a
8//!   [`CppSource`]. The analysis wrapper of the same name owns the
9//!   `resolve_analyzer::<CppAnalyzer>` downcast that produces one, because the
10//!   searchtools identity block reaches this predicate through `&dyn IAnalyzer`
11//!   and no capability carries the include graph.
12//! * The moka cells that memoize [`cpp_reconcile_candidates`] per member
13//!   identifier and [`cpp_reconcile_group`] per [`CppReconcileGroupKey`] stay
14//!   on the analyzer, as does every other cache, so `IAnalyzer::update` keeps
15//!   rebuilding them wholesale.
16
17use crate::declarations::{
18    CppRecoveredExportClassIndex, cpp_member_fq, extract_function_declarator, node_text,
19    recovered_callable_body_at, recovered_class_body_at,
20};
21use crate::graph::CppGraphSource;
22use crate::graph::resolver::{
23    VisibilityIndex, cpp_include_closure_reaches, cpp_name_component_nodes, declarator_name_node,
24    lexical_component_tiers, qualified_owner_components,
25};
26use crate::graph_support::CppSource;
27use crate::reconcile::{ReconciledIdentity, VisibleClass, reconcile_out_of_line_member_identity};
28use brokk_bifrost_core::analyzer::fq_name::{SegmentKind, segment_interner};
29use brokk_bifrost_core::analyzer::model::{CallableLinkage, Range, SignatureMetadata};
30use brokk_bifrost_core::analyzer::query_token::QueryToken;
31use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path_fq;
32use brokk_bifrost_core::analyzer::tree_walk::{node_for_exact_range, subtree_contains};
33pub use brokk_bifrost_core::analyzer::usages::common::same_node as cpp_same_node;
34use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
35use brokk_bifrost_core::hash::HashMap;
36use brokk_bifrost_core::path_utils::rel_path_string;
37use brokk_bifrost_core::profiling;
38use std::sync::Arc;
39use tree_sitter::Node;
40use tree_sitter::{Parser, Tree};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum CppCallableUnitRole {
44    DeclarationOnly,
45    Definition,
46    Both,
47    Unknown,
48}
49
50pub use brokk_bifrost_core::analyzer::cpp_facts::CppOccurrenceRole;
51pub struct CppOccurrenceClassifier {
52    tree: Tree,
53    /// The parsed text. A class role reads the recovered export-macro shapes,
54    /// and those are named from source text the tree alone does not carry.
55    source: String,
56    /// Resolved once for the whole tree rather than per classified occurrence
57    /// (#1496).
58    recovered_export_classes: CppRecoveredExportClassIndex,
59}
60
61impl CppOccurrenceClassifier {
62    pub fn new(source: &str) -> Option<Self> {
63        let mut parser = Parser::new();
64        parser
65            .set_language(&tree_sitter_cpp::LANGUAGE.into())
66            .ok()?;
67        parser.parse(source, None).map(|tree| {
68            let recovered_export_classes =
69                CppRecoveredExportClassIndex::build(tree.root_node(), source);
70            Self {
71                tree,
72                source: source.to_owned(),
73                recovered_export_classes,
74            }
75        })
76    }
77
78    pub fn classify(&self, candidate: &CodeUnit, range: &Range) -> CppOccurrenceRole {
79        cpp_occurrence_role_for_range(
80            &self.recovered_export_classes,
81            self.tree.root_node(),
82            &self.source,
83            candidate,
84            range,
85        )
86    }
87}
88
89pub fn cpp_callable_unit_role(
90    index: &dyn CodeUnitIndex,
91    callable: &CodeUnit,
92) -> CppCallableUnitRole {
93    cpp_callable_unit_role_from_metadata(callable, index.signature_metadata(callable))
94}
95
96fn cpp_callable_unit_role_from_metadata(
97    callable: &CodeUnit,
98    metadata: impl IntoIterator<Item = SignatureMetadata>,
99) -> CppCallableUnitRole {
100    if !callable.is_callable() {
101        return CppCallableUnitRole::Unknown;
102    }
103    let mut declaration = false;
104    let mut definition = false;
105    for metadata in metadata {
106        if metadata.is_declaration_only() {
107            declaration = true;
108        } else {
109            definition = true;
110        }
111    }
112    match (declaration, definition) {
113        (true, false) => CppCallableUnitRole::DeclarationOnly,
114        (false, true) => CppCallableUnitRole::Definition,
115        (true, true) => CppCallableUnitRole::Both,
116        (false, false) => CppCallableUnitRole::Unknown,
117    }
118}
119
120pub fn cpp_indexed_callable_linkage(
121    index: &dyn CodeUnitIndex,
122    callable: &CodeUnit,
123) -> Option<CallableLinkage> {
124    cpp_callable_linkage_from_metadata(index.signature_metadata(callable))
125}
126
127pub fn cpp_callable_linkage_from_metadata(
128    metadata: impl IntoIterator<Item = SignatureMetadata>,
129) -> Option<CallableLinkage> {
130    let mut external = false;
131    for metadata in metadata {
132        match metadata.callable_linkage() {
133            Some(CallableLinkage::Internal) => return Some(CallableLinkage::Internal),
134            Some(CallableLinkage::External) => external = true,
135            None => {}
136        }
137    }
138    external.then_some(CallableLinkage::External)
139}
140
141/// Whether `left` and `right` are the same callable seen twice.
142///
143/// `header_body_related` is the include-evidence predicate; the analysis wrapper
144/// supplies it because reaching the include graph needs the analyzer downcast
145/// this crate cannot perform.
146pub fn cpp_callable_definitions_share_identity_evidence(
147    index: &dyn CodeUnitIndex,
148    left: &CodeUnit,
149    right: &CodeUnit,
150    header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
151) -> bool {
152    left.fq_name() == right.fq_name()
153        && left.signature() == right.signature()
154        && (left.source() == right.source()
155            || (matches!(
156                cpp_indexed_callable_linkage(index, left),
157                Some(CallableLinkage::External)
158            ) && matches!(
159                cpp_indexed_callable_linkage(index, right),
160                Some(CallableLinkage::External)
161            ) && header_body_related(left.source(), right.source())))
162}
163
164/// The same evidence, with the signature-string conjunct answered by the
165/// resolved parameter comparison instead.
166///
167/// The persisted signature embeds each parameter type exactly as it was
168/// spelled, and a header declaration and its out-of-line body routinely spell
169/// one type two ways - `msg_t *` inside `namespace zmq` against `zmq::msg_t *`
170/// at file scope - so string equality alone refuses to unify the very pair this
171/// predicate exists to unify (#2010). [`VisibilityIndex::same_logical_callable`]
172/// compares the strings first and resolves the written parameter names only
173/// when they differ, so this is strictly the wider relation; every other
174/// conjunct - equal `fq_name`, external linkage on both sides, and the include
175/// evidence relating the two files - is unchanged.
176///
177/// Definition lookup uses this variant. The workspace-scale scans keep the
178/// string form above, where the candidate set is the whole index.
179pub fn cpp_callable_definitions_share_identity_evidence_with_visibility(
180    analyzer: &CppGraphSource<'_>,
181    visibility: &VisibilityIndex<'_>,
182    left: &CodeUnit,
183    right: &CodeUnit,
184    header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
185) -> bool {
186    left.fq_name() == right.fq_name()
187        && visibility.same_logical_callable(analyzer, left, right)
188        && (left.source() == right.source()
189            || (matches!(
190                visibility.callable_linkage(left),
191                Some(CallableLinkage::External)
192            ) && matches!(
193                visibility.callable_linkage(right),
194                Some(CallableLinkage::External)
195            ) && header_body_related(left.source(), right.source())))
196}
197
198/// Return whether `node` is one of the names declared by a range-for
199/// declarator. Follow only declarator fields. This keeps identifiers in array
200/// bounds and attributes in the range-for header as references.
201pub fn cpp_is_range_for_binding_name(node: Node<'_>) -> bool {
202    let mut current = Some(node);
203    while let Some(candidate) = current {
204        let Some(parent) = candidate.parent() else {
205            return false;
206        };
207        if parent.kind() == "for_range_loop" {
208            return parent
209                .child_by_field_name("declarator")
210                .is_some_and(|declarator| {
211                    cpp_range_for_declarator_contains_name(declarator, node)
212                });
213        }
214        current = Some(parent);
215    }
216    false
217}
218
219/// Return whether `node` is part of a conversion operator's target type.
220///
221/// A C++ conversion operator has no return type. Its target is part of the
222/// operator declaration identity, so inverse usage analysis deliberately does
223/// not report it as a type reference (#1489). Keep census probes on the same
224/// semantic surface by recognizing only components structurally contained by
225/// tree-sitter's `operator_cast` node. Types used in the function body and
226/// ordinary or overloaded-operator return types never cross that node.
227pub fn cpp_is_conversion_operator_target_type(mut node: Node<'_>) -> bool {
228    while let Some(parent) = node.parent() {
229        if parent.kind() == "operator_cast" {
230            return true;
231        }
232        if matches!(
233            parent.kind(),
234            "function_declarator" | "declaration" | "function_definition" | "translation_unit"
235        ) {
236            return false;
237        }
238        node = parent;
239    }
240    false
241}
242
243/// Return whether `node` is a character token that C++ recovery parsed as an
244/// unnamed parameter type in a class-scope macro invocation.
245///
246/// Tree-sitter does not expand macros. A call such as `STRING_(Name, 'x')`
247/// where declarations are expected can therefore become a malformed function
248/// declaration. Its character argument has an exact CST shape: the opening and
249/// closing quote are separate direct `ERROR` siblings around an otherwise
250/// clean `parameter_declaration(type_identifier)`. That middle identifier is
251/// neither a declaration nor a reference.
252pub fn cpp_is_recovered_macro_character_token_type(node: Node<'_>) -> bool {
253    if node.kind() != "type_identifier" {
254        return false;
255    }
256    let Some(parameter) = node.parent() else {
257        return false;
258    };
259    if parameter.kind() != "parameter_declaration"
260        || parameter.child_by_field_name("type") != Some(node)
261        || parameter.child_by_field_name("declarator").is_some()
262        || parameter
263            .parent()
264            .is_none_or(|parent| parent.kind() != "parameter_list")
265    {
266        return false;
267    }
268
269    parameter
270        .prev_named_sibling()
271        .is_some_and(cpp_is_recovered_character_quote)
272        && parameter
273            .next_named_sibling()
274            .is_some_and(cpp_is_recovered_character_quote)
275}
276
277fn cpp_is_recovered_character_quote(node: Node<'_>) -> bool {
278    node.is_error()
279        && node.child_count() == 1
280        && node
281            .child(0)
282            .is_some_and(|quote| !quote.is_named() && quote.kind() == "'")
283}
284
285/// Return whether `node` is the declarator name of a constructor or destructor
286/// -- the declaration occurrence itself, never a reference to one.
287///
288/// A declaration site is not a usage probe, so the reference differential must
289/// not seed one (#1834). The indexed-declaration-name filter the seeder already
290/// applies misses these two shapes:
291///
292/// * The identifier inside a `destructor_name`. The census proposes both
293///   `~Foo` and its inner `Foo`, while the indexed declaration name range
294///   covers only the `~Foo` span, so the inner identifier survives the filter.
295/// * A declarator the parse never recovered as a declaration. `class MACRO Foo
296///   { ... }` and a bare `MACRO_NAMESPACE_BEGIN` before `class Foo { ... }`
297///   both recover as a `function_definition` whose declarator is a lone
298///   identifier -- a shape valid C++ cannot produce -- with the class body as
299///   its `compound_statement`. Every declaration inside it that the grammar can
300///   read as an expression becomes one, so `Foo();` reads as a call of `Foo`,
301///   which the census then grades as a tier-1 forward gap.
302///
303/// Both tests are structural. Constructor calls stay references: `new Foo(...)`
304/// is a `new_expression` type, `Foo x(...)` is a declaration whose type field
305/// holds the name, and `: base_(x)` is a `field_initializer`. None of them is a
306/// `function_declarator` declarator or a callee inside a recovered class body.
307pub fn cpp_is_constructor_or_destructor_declarator_name(node: Node<'_>, source: &str) -> bool {
308    cpp_is_declared_constructor_or_destructor_name(node)
309        || cpp_is_recovered_constructor_or_destructor_name(node, source)
310}
311
312/// The parsed-as-declared shape: the grammar's `constructor_or_destructor_
313/// declaration` and `constructor_or_destructor_definition`, both aliased to
314/// `declaration`/`function_definition` and both recognizable by the absence of
315/// a `type` field -- exactly what distinguishes a constructor or destructor
316/// from every other C++ callable, which must name a return type.
317fn cpp_is_declared_constructor_or_destructor_name(node: Node<'_>) -> bool {
318    let mut name = node;
319    if let Some(parent) = name.parent()
320        && parent.kind() == "destructor_name"
321    {
322        name = parent;
323    }
324    // `Foo::Foo`, `A::B::Foo` and `Foo<T>::~Foo` reach the declarator through
325    // the qualified name's `name` field. The `scope` segments stay references:
326    // they name the owning type.
327    while let Some(parent) = name.parent() {
328        if parent.kind() != "qualified_identifier"
329            || parent.child_by_field_name("name") != Some(name)
330        {
331            break;
332        }
333        name = parent;
334    }
335    let Some(declarator) = name.parent() else {
336        return false;
337    };
338    if declarator.kind() != "function_declarator"
339        || declarator.child_by_field_name("declarator") != Some(name)
340    {
341        return false;
342    }
343    let Some(owner) = declarator.parent() else {
344        return false;
345    };
346    matches!(owner.kind(), "declaration" | "function_definition")
347        && owner.child_by_field_name("declarator") == Some(declarator)
348        && owner.child_by_field_name("type").is_none()
349}
350
351/// The recovered shape: a callee that names the class whose body the parse
352/// turned into a `compound_statement`.
353///
354/// Two conditions hold together, and both are needed. The nearest enclosing
355/// `function_definition` must declare a bare `identifier` -- valid C++ always
356/// declares a `function_declarator` there, so this shape only ever comes out of
357/// the class-body recovery. And the callee must name one of the identifiers in
358/// that recovery's header, which is where the class name is: `class MACRO Foo`
359/// and `class MACRO Foo : public Base` both keep `Foo` in the header even
360/// though the second leaves `Base` as the recovered declarator.
361///
362/// Together they keep genuine calls references. A recursive `f(n - 1);` sits in
363/// a real body, whose declarator is a `function_declarator`. A method body
364/// inside the recovered class body is itself a real `function_definition`, so
365/// `RAPIDJSON_ASSERT(false)` inside one keeps its own nearest owner. A macro
366/// invocation such as `DISALLOW_COPY_AND_ASSIGN(Foo);` in the recovered body
367/// does not name the class, so it stays proposed.
368fn cpp_is_recovered_constructor_or_destructor_name(node: Node<'_>, source: &str) -> bool {
369    if node.kind() != "identifier" {
370        return false;
371    }
372    let Some(call) = node.parent() else {
373        return false;
374    };
375    if call.kind() != "call_expression" || call.child_by_field_name("function") != Some(node) {
376        return false;
377    }
378    let mut current = call.parent();
379    while let Some(ancestor) = current {
380        if ancestor.kind() == "function_definition" {
381            return ancestor
382                .child_by_field_name("declarator")
383                .is_some_and(|declarator| declarator.kind() == "identifier")
384                && cpp_recovered_class_header_names(ancestor, node_text(node, source), source);
385        }
386        current = ancestor.parent();
387    }
388    false
389}
390
391/// Whether `name` is spelled by an identifier in the recovered class header --
392/// everything the recovery kept before the body it mistook for a function body.
393fn cpp_recovered_class_header_names(definition: Node<'_>, name: &str, source: &str) -> bool {
394    let header_end = definition
395        .child_by_field_name("body")
396        .map_or_else(|| definition.end_byte(), |body| body.start_byte());
397    let mut stack = vec![definition];
398    while let Some(node) = stack.pop() {
399        if node.start_byte() >= header_end {
400            continue;
401        }
402        if matches!(
403            node.kind(),
404            "identifier" | "type_identifier" | "namespace_identifier"
405        ) && node_text(node, source) == name
406        {
407            return true;
408        }
409        let mut cursor = node.walk();
410        for child in node.named_children(&mut cursor) {
411            stack.push(child);
412        }
413    }
414    false
415}
416
417fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
418    let mut pending = vec![declarator];
419    while let Some(candidate) = pending.pop() {
420        match candidate.kind() {
421            "identifier" | "field_identifier" => {
422                if cpp_same_node(candidate, target) {
423                    return true;
424                }
425            }
426            "structured_binding_declarator" => {
427                let mut cursor = candidate.walk();
428                if candidate
429                    .named_children(&mut cursor)
430                    .any(|name| cpp_same_node(name, target))
431                {
432                    return true;
433                }
434            }
435            "pointer_declarator"
436            | "reference_declarator"
437            | "array_declarator"
438            | "attributed_declarator"
439            | "parenthesized_declarator"
440            | "function_declarator"
441            | "init_declarator" => {
442                if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
443                    pending.push(inner);
444                }
445            }
446            _ => {}
447        }
448    }
449    false
450}
451
452fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
453    node.child_by_field_name("declarator").or_else(|| {
454        let mut cursor = node.walk();
455        node.named_children(&mut cursor).find(|child| {
456            matches!(
457                child.kind(),
458                "identifier"
459                    | "field_identifier"
460                    | "structured_binding_declarator"
461                    | "pointer_declarator"
462                    | "reference_declarator"
463                    | "array_declarator"
464                    | "attributed_declarator"
465                    | "parenthesized_declarator"
466                    | "function_declarator"
467                    | "init_declarator"
468            )
469        })
470    })
471}
472
473/// Include evidence relates one header declaration to one implementation file
474/// without pretending that every external name in a workspace belongs to one
475/// linker unit.
476///
477/// The question is whether the implementation file's translation unit sees the
478/// declaring header at all, and a translation unit sees every header in its
479/// `#include` closure, not only the ones it names itself. `llama-vocab.cpp`
480/// defines `llama_tokenize`, declared in `llama.h`, while including only
481/// `llama-vocab.h`, which includes `llama.h`; reading the direct include list
482/// refused that pair and left definition navigation with a bodiless
483/// declaration (#2909).
484///
485/// [`cpp_include_closure_reaches`] is that closure. It resolves each include
486/// the way visibility does, to a unique target or to nothing, so two headers
487/// sharing a basename still relate only to the translation units that
488/// unambiguously name them.
489///
490/// A body can also live in a header: fmt's `format-inl.h` defines
491/// `detail::vformat_to`, declared in `base.h`, and includes `format.h`, which
492/// includes `base.h`. Every translation unit that compiles that body also sees
493/// the body file's include closure, so the same closure proves the pair. For
494/// two headers either closure may reach the other, because a declaring header
495/// can instead include its inline-implementation header at its end (#6138).
496/// Two implementation files still never relate: neither is part of the other's
497/// translation unit.
498pub fn cpp_header_body_files_are_related(
499    source: &dyn CppSource,
500    token: QueryToken<'_>,
501    left: &ProjectFile,
502    right: &ProjectFile,
503) -> bool {
504    match (
505        cpp_source_path_is_header(left),
506        cpp_source_path_is_header(right),
507    ) {
508        (true, false) => cpp_include_closure_reaches(source, token, right, left),
509        (false, true) => cpp_include_closure_reaches(source, token, left, right),
510        (true, true) => {
511            cpp_include_closure_reaches(source, token, left, right)
512                || cpp_include_closure_reaches(source, token, right, left)
513        }
514        (false, false) => false,
515    }
516}
517
518pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
519    let path = rel_path_string(source).to_ascii_lowercase();
520    matches!(
521        path.rsplit('.').next(),
522        Some("h" | "hin" | "hh" | "hpp" | "hxx")
523    )
524}
525
526pub(crate) fn cpp_occurrence_role_for_node(
527    node: Node<'_>,
528    source: &str,
529    candidate: &CodeUnit,
530) -> CppOccurrenceRole {
531    if !candidate.is_callable() && !candidate.is_class() {
532        return CppOccurrenceRole::Both;
533    }
534    if candidate.is_callable() {
535        return if subtree_contains(node, |descendant| {
536            descendant.kind() == "function_definition"
537                && descendant.child_by_field_name("body").is_some()
538        }) {
539            CppOccurrenceRole::Definition
540        } else {
541            CppOccurrenceRole::DeclarationOnly
542        };
543    }
544    // A typedef or `using` declaration both declares and defines the alias it
545    // names, even when it mentions a bodiless elaborated type such as
546    // `typedef struct node_t *node;` (#2430).
547    if cpp_node_declares_alias(node, source, candidate) {
548        return CppOccurrenceRole::Both;
549    }
550    if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
551        return CppOccurrenceRole::Definition;
552    }
553    if !subtree_contains(node, |descendant| {
554        matches!(
555            descendant.kind(),
556            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
557        )
558    }) {
559        return CppOccurrenceRole::Both;
560    }
561    if subtree_contains(node, |descendant| {
562        matches!(
563            descendant.kind(),
564            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
565        ) && descendant.child_by_field_name("body").is_some()
566    }) {
567        CppOccurrenceRole::Definition
568    } else {
569        CppOccurrenceRole::DeclarationOnly
570    }
571}
572
573pub fn cpp_occurrence_role_for_range(
574    recovered_export_classes: &CppRecoveredExportClassIndex,
575    root: Node<'_>,
576    source: &str,
577    candidate: &CodeUnit,
578    range: &Range,
579) -> CppOccurrenceRole {
580    if !candidate.is_callable() && !candidate.is_class() {
581        return CppOccurrenceRole::Both;
582    }
583    let Some(node) = cpp_declaration_node_for_range(root, range) else {
584        return CppOccurrenceRole::Unknown;
585    };
586    if candidate.is_callable()
587        && subtree_contains(node, |descendant| {
588            descendant.kind() == "function_definition"
589                && descendant.child_by_field_name("body").is_some()
590        })
591    {
592        return CppOccurrenceRole::Definition;
593    }
594    // A callable recovered from a mangled region owns no node of its own,
595    // so the climb above landed on the container the parser left -- an
596    // access label, an `ERROR`, a statement -- and the scan answered for
597    // whatever else that container holds. Ask the recovery about this exact
598    // range instead, the same way a recovered class is asked below. An
599    // ordinary declaration lands on a declaration node and never gets here.
600    if candidate.is_callable()
601        && !matches!(
602            node.kind(),
603            "declaration" | "field_declaration" | "function_definition"
604        )
605        && let Some(has_body) = recovered_callable_body_at(source, range)
606    {
607        return if has_body {
608            CppOccurrenceRole::Definition
609        } else {
610            CppOccurrenceRole::DeclarationOnly
611        };
612    }
613    if candidate.is_class()
614        && node.kind() == "function_definition"
615        && node.child_by_field_name("body").is_some()
616    {
617        return CppOccurrenceRole::Definition;
618    }
619    // A recovered export-macro class owns no node of its own, so the fallback
620    // climb lands on the enclosing container and the specifier scan below would
621    // answer for whatever else that container holds. In a header where every
622    // class is macro decorated, no plain specifier has a body and the scan
623    // calls every class a forward declaration. Ask the recovery shapes about
624    // this exact range first; they are the same answer the resolver's
625    // declaration strength uses.
626    if candidate.is_class()
627        && let Some(has_body) = recovered_class_body_at(
628            recovered_export_classes,
629            root,
630            source,
631            candidate.identifier(),
632            range,
633        )
634    {
635        return if has_body {
636            CppOccurrenceRole::Definition
637        } else {
638            CppOccurrenceRole::DeclarationOnly
639        };
640    }
641    cpp_occurrence_role_for_node(node, source, candidate)
642}
643
644/// Whether `node` is the typedef or `using` alias declaration of `candidate`.
645fn cpp_node_declares_alias(node: Node<'_>, source: &str, candidate: &CodeUnit) -> bool {
646    match node.kind() {
647        "alias_declaration" => {
648            node.child_by_field_name("name")
649                .and_then(|name| name.utf8_text(source.as_bytes()).ok())
650                == Some(candidate.identifier())
651        }
652        "type_definition" => {
653            let mut cursor = node.walk();
654            node.children_by_field_name("declarator", &mut cursor)
655                .any(|declarator| {
656                    crate::declarations::extract_typedef_declarator_name(declarator, source)
657                        .as_deref()
658                        == Some(candidate.identifier())
659                })
660        }
661        _ => false,
662    }
663}
664
665/// Whether the occurrence at `range` is a pure-virtual member declaration
666/// (`virtual T f() = 0;`).
667///
668/// A pure virtual is the C++ spelling of an abstract method: `= 0` says the
669/// declaration has no body, so the declaration is itself the definition site.
670/// Definition navigation answers with it the way the JVM adapters answer an
671/// interface method with its declaration, instead of reporting that the
672/// candidates hold no implementation body (#2178). A pure virtual may still
673/// carry an out-of-line body; that occurrence is a real definition and is
674/// selected on its own role.
675pub fn cpp_range_is_pure_virtual_declaration(root: Node<'_>, source: &str, range: &Range) -> bool {
676    let Some(node) = cpp_declaration_node_for_range(root, range) else {
677        return false;
678    };
679    let mut current = Some(node);
680    while let Some(node) = current {
681        match node.kind() {
682            "field_declaration" => {
683                // `int data = 0;` and `int (*hook)() = 0;` share this
684                // `default_value` shape, so the declarator has to be a real
685                // function declarator rather than a function-pointer member.
686                return node
687                    .child_by_field_name("default_value")
688                    .is_some_and(|value| {
689                        value.kind() == "number_literal" && node_text(value, source) == "0"
690                    })
691                    && node
692                        .child_by_field_name("declarator")
693                        .and_then(extract_function_declarator)
694                        .is_some();
695            }
696            // Anything at or above the member list is a different construct:
697            // the occurrence range names the declaration or one of its parts.
698            "field_declaration_list" | "function_definition" | "translation_unit" => return false,
699            _ => current = node.parent(),
700        }
701    }
702    false
703}
704
705fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
706    node_for_exact_range(root, range).or_else(|| {
707        root.descendant_for_byte_range(range.start_byte, range.end_byte)
708            .and_then(|mut node| {
709                while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
710                    node = node.parent()?;
711                }
712                Some(node)
713            })
714    })
715}
716
717/// The #1134 resolution-time identity-reconciliation overlay for one queried
718/// canonical `fq_name`.
719///
720/// For each out-of-line member definition whose per-file provisional identity
721/// the include-visible class table re-keys to this name, it holds a *re-keyed*
722/// `CodeUnit` -- a synthetic unit carrying the canonical identity but the
723/// definition's real `.cpp` source -- so a canonical query resolves the
724/// definition alongside its header declaration across every resolution surface
725/// (`definitions`, source blocks, occurrence roles, canonical selectors). The
726/// re-keyed unit is not in the store, so `provisional_of` maps it back to the
727/// stored provisional unit for range and signature-metadata lookups.
728#[derive(Default)]
729pub struct CppReconciledDefinitionIndex {
730    /// Re-keyed definitions belonging under the queried canonical `fq_name`.
731    pub rekeyed: Vec<CodeUnit>,
732    /// Re-keyed unit -> the stored provisional unit its indexed data lives under.
733    pub provisional_of: HashMap<CodeUnit, CodeUnit>,
734}
735
736/// One bounded reconciliation cohort, with both identity directions. Physical
737/// units remain the keys of immutable extraction facts; canonical units retain
738/// their source, signature and synthetic provenance.
739#[derive(Default)]
740pub struct CppReconciledGroup {
741    pub by_name: HashMap<String, Arc<CppReconciledDefinitionIndex>>,
742    pub canonical_of: HashMap<CodeUnit, CodeUnit>,
743}
744
745/// Derive a cohort from recorded segment boundaries. Physical declarations
746/// must use this path instead of re-parsing their rendered protocol name.
747pub fn cpp_reconcile_group_key_for_fq(
748    fq: &brokk_bifrost_core::analyzer::fq_name::FqName,
749) -> Option<CppReconcileGroupKey> {
750    let interner = segment_interner();
751    let (member_identifier, _) = interner.resolve(fq.last()?);
752    if member_identifier.is_empty() {
753        return None;
754    }
755    let owner_terminal = fq
756        .segments()
757        .len()
758        .checked_sub(2)
759        .map(|index| interner.resolve(fq.segments()[index]).0.to_string());
760    Some(CppReconcileGroupKey {
761        member_identifier: member_identifier.to_string(),
762        owner_terminal,
763    })
764}
765
766/// Which candidates one reconcile group covers: a member identifier, and the
767/// terminal owner component that #1566's pre-filter admits.
768///
769/// This -- not the queried fq name -- is the unit reconciliation is a function
770/// of, and keying the memo by it is the #1908 fix. The old key was the queried
771/// fq name, so a bare identifier that 1,277 distinct owners answer produced
772/// 1,277 distinct keys, none of which ever hit, each re-running the identical
773/// `lookup_candidates_by_identifier` store read and re-scanning the identical
774/// 2,898-candidate set: 3.70M candidate evaluations for one request.
775///
776/// The owner terminal stays in the key rather than being dropped for a plain
777/// per-identifier map. Dropping it would mean reconciling every same-named
778/// candidate in the workspace on the first query for that identifier, which is
779/// exactly the cost #1566 removed -- chromium paid ~75 s per member query that
780/// way. `reconcile_skips_same_named_members_of_unrelated_classes_1566` pins it.
781#[derive(Debug, Clone, PartialEq, Eq, Hash)]
782pub struct CppReconcileGroupKey {
783    /// The queried name's terminal segment: the member identifier the
784    /// persisted identifier index is probed with.
785    pub member_identifier: String,
786    /// The terminal component of the queried name's penultimate segment, or
787    /// `None` for a single-segment (bare) query, where #1566's pre-filter is
788    /// inert and every candidate has to be reconciled.
789    pub owner_terminal: Option<String>,
790}
791
792/// Which member identifier and owner terminal a queried canonical name asks
793/// about, or `None` when the name has no terminal segment to probe with.
794///
795/// Parsed through the sanctioned input-edge parser rather than split here, and
796/// note `$` is not a segment boundary for it -- a nested owner chain stays one
797/// segment, so the terminal really is the member.
798pub fn cpp_reconcile_group_key(fq_name: &str) -> Option<CppReconcileGroupKey> {
799    let interner = segment_interner();
800    let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
801    let (member_identifier, _) = interner.resolve(query_fq.last()?);
802    if member_identifier.is_empty() {
803        return None;
804    }
805    // #1566 owner-terminal pre-filter: the reconciler only re-partitions a
806    // candidate's qualifier -- the class chain it emits is always a suffix
807    // of the candidate's owner segments (`reconcile.rs`) -- so the terminal
808    // `$` component of any identity it can produce equals the candidate's
809    // terminal owner segment. A candidate whose terminal owner differs
810    // from the queried name's penultimate segment can therefore never
811    // re-key onto it, and skipping it avoids the role check and, on
812    // whale repos, an include-closure class-table build per same-named
813    // candidate in the repo.
814    let owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
815        let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
816        // fqname-M4: the input-edge parser above deliberately keeps a nested
817        // owner chain as one `$`-joined segment (no structured sub-segments
818        // exist at this surface), so the terminal component must come from
819        // the raw text.
820        text.rsplit_once('$')
821            .map_or(text, |(_, tail)| tail)
822            .to_string()
823    });
824    Some(CppReconcileGroupKey {
825        member_identifier: member_identifier.to_string(),
826        owner_terminal,
827    })
828}
829
830/// Every callable declaration in the workspace sharing one member identifier,
831/// bucketed by its terminal owner segment.
832///
833/// One store read and one pass over the candidate set per identifier, memoized
834/// on the analyzer. Before #1908 both were re-run once per queried fq name.
835pub struct CppReconcileCandidates {
836    by_owner_terminal: HashMap<String, Vec<CodeUnit>>,
837    /// Every bucketed candidate, in the order the sorted candidate set
838    /// produced them. What a bare query has to reconcile, since #1566's
839    /// pre-filter cannot narrow it.
840    all: Vec<CodeUnit>,
841}
842
843impl CppReconcileCandidates {
844    /// The candidates a group key admits: one owner-terminal bucket, or every
845    /// candidate for a bare query.
846    fn for_group(&self, key: &CppReconcileGroupKey) -> &[CodeUnit] {
847        match &key.owner_terminal {
848            Some(owner_terminal) => self
849                .by_owner_terminal
850                .get(owner_terminal)
851                .map_or(&[][..], Vec::as_slice),
852            None => &self.all,
853        }
854    }
855
856    /// Every bucketed candidate, once. What a cache weigher has to charge for.
857    pub fn iter(&self) -> impl Iterator<Item = &CodeUnit> {
858        self.all.iter()
859    }
860
861    /// How many bucket entries reference those candidates. A candidate with no
862    /// owner segment is in no bucket, so this is not `len`.
863    pub fn bucketed_len(&self) -> usize {
864        self.by_owner_terminal.values().map(Vec::len).sum()
865    }
866}
867
868/// Bucket an already-retrieved identifier cohort for reconciliation.
869///
870/// The analysis crate's relational executor obtains this cohort in the same
871/// batch as the caller's questions. Keeping retrieval outside this semantic
872/// function prevents the C++ crate from choosing a second, legacy data-access
873/// path while preserving one implementation of the structured owner bucketing.
874pub fn cpp_reconcile_candidates_from_units(
875    candidates: impl IntoIterator<Item = CodeUnit>,
876    keep_going: &dyn Fn() -> bool,
877) -> Option<CppReconcileCandidates> {
878    let mut candidates = candidates.into_iter().collect::<Vec<_>>();
879    candidates.sort();
880    candidates.dedup();
881    let interner = segment_interner();
882    let mut by_owner_terminal: HashMap<String, Vec<CodeUnit>> = HashMap::default();
883    let mut all = Vec::new();
884    for (index, unit) in candidates.into_iter().enumerate() {
885        // Bucketing is a segment walk per candidate, cheap next to the role
886        // check and class-table build the groups pay, so the poll runs per
887        // batch rather than per candidate.
888        if index % CANDIDATE_BUCKETING_POLL_STRIDE == 0 && !keep_going() {
889            return None;
890        }
891        if !unit.is_callable() {
892            continue;
893        }
894        let owner_terminal = unit
895            .fq()
896            .segments()
897            .iter()
898            .filter_map(|&segment| {
899                let (text, kind) = interner.resolve(segment);
900                // Candidate fq segments carry real boundaries (each nested
901                // class is its own `SegmentKind::Nested` segment), so the
902                // segment text is already the terminal component.
903                matches!(
904                    kind,
905                    SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
906                )
907                .then_some(text)
908            })
909            .last();
910        if let Some(owner_terminal) = owner_terminal {
911            by_owner_terminal
912                .entry(owner_terminal.to_string())
913                .or_default()
914                .push(unit.clone());
915        }
916        all.push(unit);
917    }
918    // The final batch can be shorter than the polling stride, or empty.
919    // Refuse publication when its deadline expires after the last loop poll.
920    if !keep_going() {
921        return None;
922    }
923    Some(CppReconcileCandidates {
924        by_owner_terminal,
925        all,
926    })
927}
928
929/// How many candidates the bucketing pass walks between deadline polls.
930const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
931
932/// Reconcile every candidate one group key admits, grouped by the canonical
933/// `fq_name` each re-keyed definition belongs under.
934///
935/// Deliberately **not** a workspace-wide index: building one would need a full
936/// declaration scan, and a warm forward lookup must not trigger one
937/// (`tests/analyzer_persistence.rs`'s candidate-bounded contract). Instead each
938/// group reconciles only the candidates the persisted terminal identifier
939/// index already offers, which is the same bounded lookup the ordinary
940/// resolution path uses.
941///
942/// A definition whose reconciled identity equals its provisional one (the
943/// overwhelming majority, including genuine `ns1::ns2::Klass::method` namespace
944/// chains) contributes nothing.
945///
946/// `None` means `keep_going` went false mid-scan. Nothing may be memoized
947/// then; see [`cpp_reconcile_candidates`].
948pub fn cpp_reconcile_group(
949    cpp: &dyn CppSource,
950    token: QueryToken<'_>,
951    key: &CppReconcileGroupKey,
952    candidates: &CppReconcileCandidates,
953    keep_going: &dyn Fn() -> bool,
954    on_candidate: &dyn Fn(),
955) -> Option<CppReconciledGroup> {
956    let _scope = profiling::scope_with(|| {
957        format!(
958            "cpp.reconciled.build[{}#{}]",
959            key.member_identifier,
960            key.owner_terminal.as_deref().unwrap_or("*")
961        )
962    });
963    let mut groups: HashMap<String, CppReconciledDefinitionIndex> = HashMap::default();
964    let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
965    for unit in candidates.for_group(key) {
966        // Per candidate, not per batch: each iteration below can run a role
967        // check (0.265 ms mean in the #1908 trace) and an include-visible
968        // class-table read, so an atomic load per iteration is free by
969        // comparison.
970        if !keep_going() {
971            return None;
972        }
973        on_candidate();
974        // Lazy: `fq_name` clones a String, and this loop runs once per
975        // same-named candidate the group admits.
976        let _candidate =
977            profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
978        let role = {
979            let _role = profiling::scope("cpp.reconcile.role");
980            cpp.stored_callable_unit_role(unit)
981        };
982        if !matches!(
983            role,
984            CppCallableUnitRole::Definition | CppCallableUnitRole::Both
985        ) {
986            continue;
987        }
988        let Some(reconciled) =
989            cpp_reconcile_definition_identity(cpp, token, unit, &mut using_by_file, keep_going)
990        else {
991            continue;
992        };
993        let canonical_fq = reconciled.fq_name();
994        // A candidate that already carries the canonical identity is the
995        // stored declaration, not a re-keying of it. Before #1908 this read
996        // `unit.fq_name() == fq_name` against the queried name, checked before
997        // the reconcile; against the group's canonical key it is the same
998        // predicate for the same (query, candidate) pair, because a candidate
999        // only ever lands under its own reconciled identity.
1000        if unit.fq_name() == canonical_fq {
1001            continue;
1002        }
1003        // Re-key onto the canonical identity while keeping the definition's
1004        // real `.cpp` source and signature, so it resolves as a definition
1005        // alongside its header declaration under the canonical `fq_name`.
1006        // The structured `FqName` is rebuilt from the *canonical* package and
1007        // owner chain through the same emission helper extraction uses, so
1008        // the re-keyed unit carries real segment boundaries: owner lookup
1009        // (`default_parent_fq_name`) is a pure segment pop, where an empty
1010        // `fq` would mean "no owner" rather than "not yet migrated".
1011        let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
1012        let fq = cpp_member_fq(&reconciled.package, &short_name);
1013        let rekeyed = CodeUnit::with_signature_and_fq(
1014            unit.source().clone(),
1015            unit.kind(),
1016            reconciled.package,
1017            short_name,
1018            unit.signature().map(str::to_string),
1019            unit.is_synthetic(),
1020            fq,
1021        );
1022        let index = groups.entry(canonical_fq).or_default();
1023        index.rekeyed.push(rekeyed.clone());
1024        index.provisional_of.insert(rekeyed, unit.clone());
1025    }
1026    let canonical_of = groups
1027        .values()
1028        .flat_map(|index| {
1029            index
1030                .provisional_of
1031                .iter()
1032                .map(|(canonical, physical)| (physical.clone(), canonical.clone()))
1033        })
1034        .collect();
1035    let group = CppReconciledGroup {
1036        canonical_of,
1037        by_name: groups
1038            .into_iter()
1039            .map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
1040            .collect(),
1041    };
1042    // A deadline can expire in the last candidate or provenance assembly.
1043    // The cache must receive only a fully completed, uncancelled cohort.
1044    keep_going().then_some(group)
1045}
1046
1047/// Reconcile one out-of-line member definition's provisional identity against
1048/// the class table visible to its file. Returns `None` for anything that is
1049/// not a re-keyable out-of-line member or that the class table does not
1050/// confirm. A one-segment class qualifier is valid when a structured using
1051/// namespace and the visible class table confirm its package.
1052fn cpp_reconcile_definition_identity(
1053    cpp: &dyn CppSource,
1054    token: QueryToken<'_>,
1055    unit: &CodeUnit,
1056    using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
1057    keep_going: &dyn Fn() -> bool,
1058) -> Option<ReconciledIdentity> {
1059    // Read the full source-order qualifier off the definition's *structured*
1060    // `FqName` -- the namespace (`Package`) segments followed by the
1061    // class-nesting (`Type`/`Nested`) ones, with the terminal `Member` as the
1062    // member name. The segment boundaries were recorded at extraction, so
1063    // nothing here re-infers them by splitting the rendered name on a guessed
1064    // delimiter (the shape `tests/no_stringly_name_parsing.rs` guards). The
1065    // reconciler then re-partitions this whole sequence against the class
1066    // table, so extraction need not have decided where the namespace ends and
1067    // the class chain begins.
1068    let interner = segment_interner();
1069    let mut provisional_owner_segments: Vec<&str> = Vec::new();
1070    let mut member: Option<&str> = None;
1071    for &segment in unit.fq().segments() {
1072        let (text, kind) = interner.resolve(segment);
1073        match kind {
1074            SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
1075                // A `Member` is always terminal in a cpp callable's chain; a
1076                // qualifier segment after one would mean the identity is not
1077                // the plain `namespace... class... member` shape this handles.
1078                if member.is_some() {
1079                    return None;
1080                }
1081                if !text.is_empty() {
1082                    provisional_owner_segments.push(text);
1083                }
1084            }
1085            SegmentKind::Member => member = Some(text),
1086            _ => return None,
1087        }
1088    }
1089    let member = member?;
1090    // Keep multi-owner/template reconciliation on the existing provisional
1091    // structured chain. A single written owner needs independent lexical proof
1092    // before replacing that chain (which may include a guessed using package).
1093    let single_owner = cpp_single_owner_context(cpp, token, unit, keep_going)?;
1094    let owner_segments = single_owner.as_ref().map_or_else(
1095        || provisional_owner_segments,
1096        |context| vec![context.owner.as_str()],
1097    );
1098    if owner_segments.is_empty() {
1099        return None;
1100    }
1101
1102    let visible = {
1103        let _visible = profiling::scope_with(|| {
1104            format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
1105        });
1106        cpp.visible_type_units(unit.source())
1107    };
1108    let class_table: Vec<VisibleClass> = visible
1109        .iter()
1110        .filter(|candidate| candidate.is_class())
1111        .map(|candidate| VisibleClass {
1112            package: candidate.package_name(),
1113            nested_short_name: candidate.short_name(),
1114        })
1115        .collect();
1116
1117    if let Some(context) = &single_owner {
1118        if context.global {
1119            // Leading :: excludes lexical and using-namespace candidates.
1120            return keep_going().then(|| {
1121                reconcile_out_of_line_member_identity(&owner_segments, member, &[""], &class_table)
1122            })?;
1123        }
1124        // Namespace proof is not a guessed class-owner chain. Keep each
1125        // namespace tier separate from the written owner, so a global nested
1126        // class cannot win by repartitioning those proven namespace segments.
1127        for tier in
1128            lexical_component_tiers(&[], false, &context.lexical).take(context.lexical.len())
1129        {
1130            if !keep_going() {
1131                return None;
1132            }
1133            let namespace = tier.join("::");
1134            if let Some(identity) = reconcile_out_of_line_member_identity(
1135                &owner_segments,
1136                member,
1137                &[namespace.as_str()],
1138                &class_table,
1139            ) {
1140                return keep_going().then_some(identity);
1141            }
1142        }
1143    }
1144    if !keep_going() {
1145        return None;
1146    }
1147    let using = using_by_file
1148        .entry(unit.source().clone())
1149        .or_insert_with(|| {
1150            Arc::new(
1151                cpp.declaration_source_facts(token, unit.source())
1152                    .map(|source| {
1153                        source
1154                            .facts
1155                            .using_namespaces
1156                            .iter()
1157                            .map(|(_, name)| name.clone())
1158                            .collect()
1159                    })
1160                    .unwrap_or_default(),
1161            )
1162        })
1163        .clone();
1164    let mut namespace_candidates: Vec<&str> = vec![""];
1165    namespace_candidates.extend(using.iter().map(String::as_str));
1166
1167    reconcile_out_of_line_member_identity(
1168        &owner_segments,
1169        member,
1170        &namespace_candidates,
1171        &class_table,
1172    )
1173}
1174
1175#[derive(PartialEq, Eq)]
1176struct CppSingleOwnerContext {
1177    owner: String,
1178    global: bool,
1179    lexical: Vec<String>,
1180}
1181
1182/// Exact physical declaration/name anchors prove namespace context, never a
1183/// guessed Package or a containing malformed wrapper. All records must agree.
1184fn cpp_single_owner_context(
1185    cpp: &dyn CppSource,
1186    token: QueryToken<'_>,
1187    unit: &CodeUnit,
1188    keep_going: &dyn Fn() -> bool,
1189) -> Option<Option<CppSingleOwnerContext>> {
1190    let facts = cpp.declaration_source_facts(token, unit.source())?;
1191    // Some(None) keeps the legacy multi-owner/template path unchanged. Once
1192    // any record supplies a single owner, every applicable record must prove
1193    // that same owner/context; missing or conflicting records cannot fall back.
1194    let mut has_single = false;
1195    for fact in facts.for_unit(unit) {
1196        if !keep_going() {
1197            return None;
1198        }
1199        has_single |= fact.written_owner.len() == 1;
1200    }
1201    if !has_single {
1202        return keep_going().then_some(None);
1203    }
1204    let syntax = cpp.prepared_syntax(token, unit.source())?;
1205    if !keep_going() {
1206        return None;
1207    }
1208    let scopes = cpp.orphaned_namespace_scopes(token, unit.source());
1209    let root = syntax.tree().root_node();
1210    let source = syntax.source();
1211    let mut agreed = None;
1212    for fact in facts.for_unit(unit) {
1213        if !keep_going() {
1214            return None;
1215        }
1216        let declaration = facts.source.declaration(fact.declaration);
1217        let occurrence = facts.source.occurrence(declaration.occurrence).range;
1218        let name_range = facts.source.occurrence(declaration.name?).range;
1219        let mut node =
1220            root.descendant_for_byte_range(occurrence.start_byte, occurrence.end_byte)?;
1221        // Exact spans can be shared by declarator wrappers. Ascend only along
1222        // that span; accepting a larger containing envelope is not proof.
1223        let selected = loop {
1224            if !keep_going()
1225                || node.start_byte() != occurrence.start_byte
1226                || node.end_byte() != occurrence.end_byte
1227            {
1228                return None;
1229            }
1230            if matches!(
1231                node.kind(),
1232                "function_definition" | "function_declarator" | "declaration" | "field_declaration"
1233            ) {
1234                break node;
1235            }
1236            node = node.parent()?;
1237        };
1238        let callable = if selected.kind() == "function_declarator" {
1239            selected
1240        } else {
1241            selected
1242                .child_by_field_name("declarator")
1243                .and_then(extract_function_declarator)?
1244        };
1245        let name = declarator_name_node(callable)?;
1246        let terminal = cpp_name_component_nodes(name)?.pop()?;
1247        if name.is_missing()
1248            || name.has_error()
1249            || terminal.is_missing()
1250            || terminal.start_byte() != name_range.start_byte
1251            || terminal.end_byte() != name_range.end_byte
1252            || node_text(terminal, source) != unit.identifier()
1253        {
1254            return None;
1255        }
1256        let qualified = qualified_owner_components(name, source)?;
1257        if qualified.names.len() != 1 || qualified.names != fact.written_owner {
1258            return None;
1259        }
1260        let proof = CppSingleOwnerContext {
1261            owner: qualified.names.into_iter().next()?,
1262            global: qualified.global,
1263            lexical: scopes.enclosing_namespace_components(selected, source),
1264        };
1265        if !keep_going() || agreed.as_ref().is_some_and(|prior| prior != &proof) {
1266            return None;
1267        }
1268        agreed = Some(proof);
1269    }
1270    if !keep_going() {
1271        return None;
1272    }
1273    Some(Some(agreed?))
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278    use super::*;
1279    use brokk_bifrost_test_macros::hardwon;
1280
1281    #[test]
1282    fn physical_identity_cohorts_keep_recorded_owner_and_member_boundaries() {
1283        use brokk_bifrost_core::analyzer::fq_name::FqName;
1284        let mut fq = FqName::new();
1285        for (text, kind) in [
1286            ("ns", SegmentKind::Package),
1287            ("Owner$Literal", SegmentKind::Type),
1288            ("operator std::string", SegmentKind::Member),
1289        ] {
1290            fq.push(segment_interner().intern(text, kind));
1291        }
1292        let key = cpp_reconcile_group_key_for_fq(&fq).unwrap();
1293        assert_eq!(key.owner_terminal.as_deref(), Some("Owner$Literal"));
1294        assert_eq!(key.member_identifier, "operator std::string");
1295    }
1296
1297    #[test]
1298    #[hardwon("#2488: terminal cancellation must not publish identity cohorts")]
1299    fn candidate_cohorts_recheck_terminal_cancellation_before_publish() {
1300        use std::cell::Cell;
1301
1302        let unit = CodeUnit::new(
1303            ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1304            brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1305            "",
1306            "Owner.run",
1307        );
1308        let cancel_after_last_candidate = {
1309            let active = Cell::new(true);
1310            move || active.replace(false)
1311        };
1312        assert!(
1313            cpp_reconcile_candidates_from_units(
1314                std::iter::once(unit.clone()),
1315                &cancel_after_last_candidate,
1316            )
1317            .is_none(),
1318            "a cancellation observed after the last sub-256 candidate must prevent publication"
1319        );
1320
1321        let candidates = (0..=CANDIDATE_BUCKETING_POLL_STRIDE)
1322            .map(|index| {
1323                CodeUnit::new(
1324                    ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1325                    brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1326                    "",
1327                    format!("Owner{index}.run"),
1328                )
1329            })
1330            .collect::<Vec<_>>();
1331        let cancel_at_bucket_boundary = {
1332            let active = Cell::new(true);
1333            move || active.replace(false)
1334        };
1335        assert!(
1336            cpp_reconcile_candidates_from_units(candidates, &cancel_at_bucket_boundary).is_none(),
1337            "a cancellation at the bucket boundary must prevent a partial cohort from publishing"
1338        );
1339
1340        let cancelled = || false;
1341        assert!(
1342            cpp_reconcile_candidates_from_units(Vec::<CodeUnit>::new(), &cancelled).is_none(),
1343            "an empty cancelled cohort must not publish"
1344        );
1345    }
1346
1347    fn parse_cpp(source: &str) -> Tree {
1348        crate::diagnostics::parse_cpp_tree(source)
1349    }
1350
1351    fn is_declarator_name(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1352        let end = start + text.len();
1353        assert_eq!(&source[start..end], text, "the probe must name the token");
1354        let node = tree
1355            .root_node()
1356            .named_descendant_for_byte_range(start, end)
1357            .expect("a node spans the probed range");
1358        assert_eq!(
1359            (node.start_byte(), node.end_byte()),
1360            (start, end),
1361            "the probed range must be exactly one node: {}",
1362            node.to_sexp()
1363        );
1364        cpp_is_constructor_or_destructor_declarator_name(node, source)
1365    }
1366
1367    fn is_conversion_target(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1368        let end = start + text.len();
1369        assert_eq!(&source[start..end], text, "the probe must name the token");
1370        let node = tree
1371            .root_node()
1372            .named_descendant_for_byte_range(start, end)
1373            .expect("a node spans the probed range");
1374        assert_eq!(
1375            (node.start_byte(), node.end_byte()),
1376            (start, end),
1377            "the probed range must be exactly one node: {}",
1378            node.to_sexp()
1379        );
1380        cpp_is_conversion_operator_target_type(node)
1381    }
1382
1383    fn is_recovered_macro_character_type(
1384        tree: &Tree,
1385        source: &str,
1386        start: usize,
1387        text: &str,
1388    ) -> bool {
1389        let end = start + text.len();
1390        assert_eq!(&source[start..end], text, "the probe must name the token");
1391        let node = tree
1392            .root_node()
1393            .named_descendant_for_byte_range(start, end)
1394            .expect("a node spans the probed range");
1395        cpp_is_recovered_macro_character_token_type(node)
1396    }
1397
1398    #[test]
1399    #[hardwon(
1400        "b114d87215ca: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1401    )]
1402    fn recovered_macro_character_tokens_are_not_type_references() {
1403        let source = concat!(
1404            "struct I {};\n",
1405            "#define STRING_TOKEN_(name, ...)\n",
1406            "struct Schema {\n",
1407            "  STRING_TOKEN_(MaxItems, 'm', 'I')\n",
1408            "  void ordinary(I value);\n",
1409            "  void malformed(I value, @);\n",
1410            "  void use() { I value; consume('I'); }\n",
1411            "};\n",
1412        );
1413        let tree = parse_cpp(source);
1414        let recovered_m = source.find("'m'").expect("recovered m") + 1;
1415        let recovered_i = source.find("'I'").expect("recovered I") + 1;
1416        for (label, start, text) in [
1417            ("lowercase character token", recovered_m, "m"),
1418            ("uppercase character token", recovered_i, "I"),
1419        ] {
1420            assert!(
1421                is_recovered_macro_character_type(&tree, source, start, text),
1422                "{label} must match the exact recovery role"
1423            );
1424        }
1425
1426        let macro_first_argument = source.find("MaxItems").expect("macro first argument");
1427        let ordinary = source.find("ordinary(I").expect("ordinary parameter") + "ordinary(".len();
1428        let malformed =
1429            source.find("malformed(I").expect("malformed parameter") + "malformed(".len();
1430        let local = source
1431            .find("I value; consume")
1432            .expect("local type reference");
1433        let expression_character = source.rfind("'I'").expect("expression character") + 1;
1434        for (label, start, text) in [
1435            ("unquoted macro argument", macro_first_argument, "MaxItems"),
1436            ("ordinary parameter type", ordinary, "I"),
1437            ("parameter beside another error", malformed, "I"),
1438            ("local type reference", local, "I"),
1439            ("expression character literal", expression_character, "I"),
1440        ] {
1441            assert!(
1442                !is_recovered_macro_character_type(&tree, source, start, text),
1443                "{label} must remain outside the recovery role"
1444            );
1445        }
1446    }
1447
1448    #[test]
1449    #[hardwon(
1450        "0733cadab142: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1451    )]
1452    fn conversion_operator_target_components_are_identity_syntax_only() {
1453        let source = concat!(
1454            "namespace other { struct Target {}; template<class T> struct Box {}; }\n",
1455            "using other::Target;\n",
1456            "struct Source {\n",
1457            "  operator Target() const;\n",
1458            "  operator other::Target() const { return other::Target{}; }\n",
1459            "  template<class T> operator other::Box<T>() const { return {}; }\n",
1460            "  operator other::Target const&() const;\n",
1461            "  operator other::Target*() const;\n",
1462            "  other::Target ordinary() const {\n",
1463            "    return reinterpret_cast<other::Target&>(*this);\n",
1464            "  }\n",
1465            "  other::Target operator+() const { return {}; }\n",
1466            "};\n",
1467        );
1468        let tree = parse_cpp(source);
1469
1470        let bare = source.find("operator Target").expect("bare target") + "operator ".len();
1471        let qualified = source
1472            .find("operator other::Target()")
1473            .expect("qualified target")
1474            + "operator ".len();
1475        let template = source
1476            .find("operator other::Box<T>")
1477            .expect("template target")
1478            + "operator ".len();
1479        let cv_reference = source
1480            .find("operator other::Target const&")
1481            .expect("cv-reference target")
1482            + "operator other::".len();
1483        let pointer = source
1484            .find("operator other::Target*")
1485            .expect("pointer target")
1486            + "operator other::".len();
1487
1488        for (label, start, text) in [
1489            ("bare target", bare, "Target"),
1490            ("qualified target scope", qualified, "other"),
1491            (
1492                "qualified target name",
1493                qualified + "other::".len(),
1494                "Target",
1495            ),
1496            ("template target scope", template, "other"),
1497            ("template target name", template + "other::".len(), "Box"),
1498            (
1499                "template target argument",
1500                template + "other::Box<".len(),
1501                "T",
1502            ),
1503            ("cv-reference target", cv_reference, "Target"),
1504            ("pointer target", pointer, "Target"),
1505        ] {
1506            assert!(
1507                is_conversion_target(&tree, source, start, text),
1508                "the {label} at byte {start} belongs to the conversion identity"
1509            );
1510        }
1511
1512        let ordinary_return = source
1513            .find("other::Target ordinary")
1514            .expect("ordinary return");
1515        let body_cast = source
1516            .find("reinterpret_cast<other::Target")
1517            .expect("body cast")
1518            + "reinterpret_cast<".len();
1519        let overloaded_return = source
1520            .find("other::Target operator+")
1521            .expect("overloaded operator return");
1522        for (label, start) in [
1523            ("ordinary return type", ordinary_return),
1524            ("body cast target", body_cast),
1525            ("overloaded-operator return type", overloaded_return),
1526        ] {
1527            assert!(
1528                !is_conversion_target(&tree, source, start, "other"),
1529                "the {label} at byte {start} stays a reference"
1530            );
1531        }
1532    }
1533
1534    /// The parsed-as-declared shape, in class and out of line. The names that
1535    /// surround a declarator stay references: the owning scope of an out-of-line
1536    /// definition, a parameter type that happens to be the class, and the class
1537    /// name itself.
1538    #[test]
1539    #[hardwon(
1540        "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1541    )]
1542    fn declared_constructor_and_destructor_declarator_names_are_not_references() {
1543        let source = concat!(
1544            "class Foo {\n",
1545            "public:\n",
1546            "  Foo();\n",
1547            "  Foo(const Foo&);\n",
1548            "  ~Foo();\n",
1549            "  void m();\n",
1550            "};\n",
1551            "Foo::Foo() {}\n",
1552            "Foo::~Foo() {}\n",
1553            "void Foo::m() {}\n",
1554        );
1555        let tree = parse_cpp(source);
1556
1557        for (label, start, text) in [
1558            (
1559                "constructor declaration",
1560                source.find("Foo();").expect("ctor"),
1561                "Foo",
1562            ),
1563            (
1564                "copy constructor declaration",
1565                source.find("Foo(const Foo&);").expect("copy ctor"),
1566                "Foo",
1567            ),
1568            (
1569                "destructor name",
1570                source.find("~Foo();").expect("dtor"),
1571                "~Foo",
1572            ),
1573            (
1574                "identifier inside the destructor name",
1575                source.find("~Foo();").expect("dtor") + "~".len(),
1576                "Foo",
1577            ),
1578            (
1579                "out-of-line constructor definition name",
1580                source.find("Foo::Foo() {}").expect("out-of-line ctor") + "Foo::".len(),
1581                "Foo",
1582            ),
1583            (
1584                "out-of-line destructor definition name",
1585                source.find("Foo::~Foo() {}").expect("out-of-line dtor") + "Foo::".len(),
1586                "~Foo",
1587            ),
1588        ] {
1589            assert!(
1590                is_declarator_name(&tree, source, start, text),
1591                "the {label} at byte {start} is a declaration occurrence"
1592            );
1593        }
1594
1595        for (label, start, text) in [
1596            (
1597                "class name",
1598                source.find("class Foo {").expect("class") + "class ".len(),
1599                "Foo",
1600            ),
1601            (
1602                "parameter type",
1603                source.find("const Foo&").expect("parameter type") + "const ".len(),
1604                "Foo",
1605            ),
1606            (
1607                "owning scope of an out-of-line constructor",
1608                source.find("Foo::Foo() {}").expect("out-of-line ctor"),
1609                "Foo",
1610            ),
1611            (
1612                "owning scope of an out-of-line destructor",
1613                source.find("Foo::~Foo() {}").expect("out-of-line dtor"),
1614                "Foo",
1615            ),
1616            (
1617                "out-of-line method name",
1618                source.find("void Foo::m() {}").expect("out-of-line method") + "void Foo::".len(),
1619                "m",
1620            ),
1621        ] {
1622            assert!(
1623                !is_declarator_name(&tree, source, start, text),
1624                "the {label} at byte {start} stays a reference"
1625            );
1626        }
1627    }
1628
1629    /// Constructor CALL sites are references. `new D(...)`, the direct
1630    /// initialization `D x(...)`, a member initializer and a bare temporary
1631    /// statement all name the type, and none of them is a declarator.
1632    #[test]
1633    #[hardwon(
1634        "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1635    )]
1636    fn constructor_call_sites_stay_references() {
1637        let source = concat!(
1638            "struct B { B(int); };\n",
1639            "struct D : B {\n",
1640            "  D(int x) : B(x), base_(x) {}\n",
1641            "  int base_;\n",
1642            "};\n",
1643            "void g() {\n",
1644            "  D* p = new D(1);\n",
1645            "  D x(2);\n",
1646            "  D(3);\n",
1647            "  g();\n",
1648            "}\n",
1649        );
1650        let tree = parse_cpp(source);
1651
1652        let inline_declarator = source.find("D(int x)").expect("inline constructor");
1653        assert!(
1654            is_declarator_name(&tree, source, inline_declarator, "D"),
1655            "an inline constructor definition name is still a declarator"
1656        );
1657
1658        for (label, start, text) in [
1659            (
1660                "base member initializer",
1661                source.find(": B(x)").expect("base initializer") + ": ".len(),
1662                "B",
1663            ),
1664            (
1665                "field member initializer",
1666                source.find("base_(x) {}").expect("field initializer"),
1667                "base_",
1668            ),
1669            (
1670                "new expression type",
1671                source.find("new D(1)").expect("new expression") + "new ".len(),
1672                "D",
1673            ),
1674            (
1675                "direct initialization type",
1676                source.find("D x(2)").expect("direct initialization"),
1677                "D",
1678            ),
1679            (
1680                "temporary construction statement",
1681                source.find("D(3)").expect("temporary"),
1682                "D",
1683            ),
1684            (
1685                "recursive call in a real body",
1686                source.find("g();").expect("recursive call"),
1687                "g",
1688            ),
1689        ] {
1690            assert!(
1691                !is_declarator_name(&tree, source, start, text),
1692                "the {label} at byte {start} is a reference"
1693            );
1694        }
1695    }
1696
1697    /// The recovered shape (#1834): an export macro between `class` and the
1698    /// class name makes the parse read the class body as a function body and
1699    /// every constructor declaration in it as a call of the class's own name.
1700    /// The bodies the recovery left intact keep their references.
1701    #[test]
1702    #[hardwon(
1703        "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1704    )]
1705    fn a_constructor_declarator_the_parse_read_as_a_call_is_not_a_reference() {
1706        let source = concat!(
1707            "class SAMPLE_EXPORT Properties {\n",
1708            "  public:\n",
1709            "    Properties();\n",
1710            "    DISALLOW_COPY_AND_ASSIGN(Properties);\n",
1711            "    int size() const;\n",
1712            "    int total() { return size(); }\n",
1713            "};\n",
1714        );
1715        let tree = parse_cpp(source);
1716
1717        let recovered = source.find("Properties();").expect("recovered constructor");
1718        assert!(
1719            is_declarator_name(&tree, source, recovered, "Properties"),
1720            "a constructor declaration the parse read as a call is still a declarator"
1721        );
1722
1723        for (label, start, text) in [
1724            (
1725                "class name in the recovered header",
1726                source
1727                    .find("class SAMPLE_EXPORT Properties")
1728                    .expect("class")
1729                    + "class SAMPLE_EXPORT ".len(),
1730                "Properties",
1731            ),
1732            (
1733                "macro invocation in the recovered body",
1734                source
1735                    .find("DISALLOW_COPY_AND_ASSIGN(Properties);")
1736                    .expect("macro invocation"),
1737                "DISALLOW_COPY_AND_ASSIGN",
1738            ),
1739            (
1740                "call inside a method body the recovery kept",
1741                source.find("return size();").expect("member call") + "return ".len(),
1742                "size",
1743            ),
1744        ] {
1745            assert!(
1746                !is_declarator_name(&tree, source, start, text),
1747                "the {label} at byte {start} stays a reference"
1748            );
1749        }
1750    }
1751
1752    /// Every class role in one file, so a header can be classified with no
1753    /// ordinary body-bearing class in it.
1754    fn class_roles(source: &str, name: &str) -> Vec<CppOccurrenceRole> {
1755        let tree = parse_cpp(source);
1756        let file = ProjectFile::new(std::env::temp_dir(), "occurrence-role.hpp");
1757        let parsed = crate::adapter::parse_cpp_file(&file, source, &tree);
1758        let unit = parsed
1759            .declarations()
1760            .iter()
1761            .find(|unit| unit.is_class() && unit.fq_name() == name)
1762            .unwrap_or_else(|| panic!("missing class {name}: {parsed:#?}"));
1763        let ranges = parsed.declaration_ranges(unit);
1764        assert!(!ranges.is_empty(), "{name} must have a declaration range");
1765        ranges
1766            .iter()
1767            .map(|range| {
1768                cpp_occurrence_role_for_range(
1769                    &CppRecoveredExportClassIndex::build(tree.root_node(), source),
1770                    tree.root_node(),
1771                    source,
1772                    unit,
1773                    range,
1774                )
1775            })
1776            .collect()
1777    }
1778
1779    /// #2557: the role of a recovered `class MACRO(2, 0) Name` comes from the
1780    /// recovery itself. The fallback climb lands on the enclosing container, so
1781    /// a header whose classes are all export-macro decorated used to hold no
1782    /// body-bearing `class_specifier` at all and every class in it read as a
1783    /// forward declaration, which dropped it from navigation.
1784    #[test]
1785    #[hardwon(
1786        "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1787    )]
1788    fn recovered_export_macro_classes_are_definitions_without_an_ordinary_class() {
1789        let source = concat!(
1790            "namespace api {\n",
1791            "class PROJECT_PUBLIC_API(2, 0) Name final {\n",
1792            "  public:\n",
1793            "    Name();\n",
1794            "};\n",
1795            "class PROJECT_PUBLIC_API(2, 0) Other : public Name {\n",
1796            "  public:\n",
1797            "    Other();\n",
1798            "};\n",
1799            "} // namespace api\n",
1800        );
1801        for name in ["api.Name", "api.Other"] {
1802            assert_eq!(
1803                class_roles(source, name),
1804                vec![CppOccurrenceRole::Definition],
1805                "{name} is a complete recovered definition"
1806            );
1807        }
1808    }
1809
1810    #[test]
1811    #[hardwon(
1812        "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1813    )]
1814    fn ordinary_class_roles_keep_their_plain_specifier_reading() {
1815        assert_eq!(
1816            class_roles("class Plain;\n", "Plain"),
1817            vec![CppOccurrenceRole::DeclarationOnly],
1818            "a forward declaration stays a declaration"
1819        );
1820        assert_eq!(
1821            class_roles("class Plain { };\n", "Plain"),
1822            vec![CppOccurrenceRole::Definition],
1823            "a complete class stays a definition"
1824        );
1825    }
1826}