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