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