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