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