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