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