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