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 the declarator name of a constructor or destructor
167/// -- the declaration occurrence itself, never a reference to one.
168///
169/// A declaration site is not a usage probe, so the reference differential must
170/// not seed one (#1834). The indexed-declaration-name filter the seeder already
171/// applies misses these two shapes:
172///
173/// * The identifier inside a `destructor_name`. The census proposes both
174///   `~Foo` and its inner `Foo`, while the indexed declaration name range
175///   covers only the `~Foo` span, so the inner identifier survives the filter.
176/// * A declarator the parse never recovered as a declaration. `class MACRO Foo
177///   { ... }` and a bare `MACRO_NAMESPACE_BEGIN` before `class Foo { ... }`
178///   both recover as a `function_definition` whose declarator is a lone
179///   identifier -- a shape valid C++ cannot produce -- with the class body as
180///   its `compound_statement`. Every declaration inside it that the grammar can
181///   read as an expression becomes one, so `Foo();` reads as a call of `Foo`,
182///   which the census then grades as a tier-1 forward gap.
183///
184/// Both tests are structural. Constructor calls stay references: `new Foo(...)`
185/// is a `new_expression` type, `Foo x(...)` is a declaration whose type field
186/// holds the name, and `: base_(x)` is a `field_initializer`. None of them is a
187/// `function_declarator` declarator or a callee inside a recovered class body.
188pub fn cpp_is_constructor_or_destructor_declarator_name(node: Node<'_>, source: &str) -> bool {
189    cpp_is_declared_constructor_or_destructor_name(node)
190        || cpp_is_recovered_constructor_or_destructor_name(node, source)
191}
192
193/// The parsed-as-declared shape: the grammar's `constructor_or_destructor_
194/// declaration` and `constructor_or_destructor_definition`, both aliased to
195/// `declaration`/`function_definition` and both recognizable by the absence of
196/// a `type` field -- exactly what distinguishes a constructor or destructor
197/// from every other C++ callable, which must name a return type.
198fn cpp_is_declared_constructor_or_destructor_name(node: Node<'_>) -> bool {
199    let mut name = node;
200    if let Some(parent) = name.parent()
201        && parent.kind() == "destructor_name"
202    {
203        name = parent;
204    }
205    // `Foo::Foo`, `A::B::Foo` and `Foo<T>::~Foo` reach the declarator through
206    // the qualified name's `name` field. The `scope` segments stay references:
207    // they name the owning type.
208    while let Some(parent) = name.parent() {
209        if parent.kind() != "qualified_identifier"
210            || parent.child_by_field_name("name") != Some(name)
211        {
212            break;
213        }
214        name = parent;
215    }
216    let Some(declarator) = name.parent() else {
217        return false;
218    };
219    if declarator.kind() != "function_declarator"
220        || declarator.child_by_field_name("declarator") != Some(name)
221    {
222        return false;
223    }
224    let Some(owner) = declarator.parent() else {
225        return false;
226    };
227    matches!(owner.kind(), "declaration" | "function_definition")
228        && owner.child_by_field_name("declarator") == Some(declarator)
229        && owner.child_by_field_name("type").is_none()
230}
231
232/// The recovered shape: a callee that names the class whose body the parse
233/// turned into a `compound_statement`.
234///
235/// Two conditions hold together, and both are needed. The nearest enclosing
236/// `function_definition` must declare a bare `identifier` -- valid C++ always
237/// declares a `function_declarator` there, so this shape only ever comes out of
238/// the class-body recovery. And the callee must name one of the identifiers in
239/// that recovery's header, which is where the class name is: `class MACRO Foo`
240/// and `class MACRO Foo : public Base` both keep `Foo` in the header even
241/// though the second leaves `Base` as the recovered declarator.
242///
243/// Together they keep genuine calls references. A recursive `f(n - 1);` sits in
244/// a real body, whose declarator is a `function_declarator`. A method body
245/// inside the recovered class body is itself a real `function_definition`, so
246/// `RAPIDJSON_ASSERT(false)` inside one keeps its own nearest owner. A macro
247/// invocation such as `DISALLOW_COPY_AND_ASSIGN(Foo);` in the recovered body
248/// does not name the class, so it stays proposed.
249fn cpp_is_recovered_constructor_or_destructor_name(node: Node<'_>, source: &str) -> bool {
250    if node.kind() != "identifier" {
251        return false;
252    }
253    let Some(call) = node.parent() else {
254        return false;
255    };
256    if call.kind() != "call_expression" || call.child_by_field_name("function") != Some(node) {
257        return false;
258    }
259    let mut current = call.parent();
260    while let Some(ancestor) = current {
261        if ancestor.kind() == "function_definition" {
262            return ancestor
263                .child_by_field_name("declarator")
264                .is_some_and(|declarator| declarator.kind() == "identifier")
265                && cpp_recovered_class_header_names(ancestor, node_text(node, source), source);
266        }
267        current = ancestor.parent();
268    }
269    false
270}
271
272/// Whether `name` is spelled by an identifier in the recovered class header --
273/// everything the recovery kept before the body it mistook for a function body.
274fn cpp_recovered_class_header_names(definition: Node<'_>, name: &str, source: &str) -> bool {
275    let header_end = definition
276        .child_by_field_name("body")
277        .map_or_else(|| definition.end_byte(), |body| body.start_byte());
278    let mut stack = vec![definition];
279    while let Some(node) = stack.pop() {
280        if node.start_byte() >= header_end {
281            continue;
282        }
283        if matches!(
284            node.kind(),
285            "identifier" | "type_identifier" | "namespace_identifier"
286        ) && node_text(node, source) == name
287        {
288            return true;
289        }
290        let mut cursor = node.walk();
291        for child in node.named_children(&mut cursor) {
292            stack.push(child);
293        }
294    }
295    false
296}
297
298fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
299    let mut pending = vec![declarator];
300    while let Some(candidate) = pending.pop() {
301        match candidate.kind() {
302            "identifier" | "field_identifier" => {
303                if cpp_same_node(candidate, target) {
304                    return true;
305                }
306            }
307            "structured_binding_declarator" => {
308                let mut cursor = candidate.walk();
309                if candidate
310                    .named_children(&mut cursor)
311                    .any(|name| cpp_same_node(name, target))
312                {
313                    return true;
314                }
315            }
316            "pointer_declarator"
317            | "reference_declarator"
318            | "array_declarator"
319            | "attributed_declarator"
320            | "parenthesized_declarator"
321            | "function_declarator"
322            | "init_declarator" => {
323                if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
324                    pending.push(inner);
325                }
326            }
327            _ => {}
328        }
329    }
330    false
331}
332
333fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
334    node.child_by_field_name("declarator").or_else(|| {
335        let mut cursor = node.walk();
336        node.named_children(&mut cursor).find(|child| {
337            matches!(
338                child.kind(),
339                "identifier"
340                    | "field_identifier"
341                    | "structured_binding_declarator"
342                    | "pointer_declarator"
343                    | "reference_declarator"
344                    | "array_declarator"
345                    | "attributed_declarator"
346                    | "parenthesized_declarator"
347                    | "function_declarator"
348                    | "init_declarator"
349            )
350        })
351    })
352}
353
354fn cpp_same_node(left: Node<'_>, right: Node<'_>) -> bool {
355    left.id() == right.id()
356        && left.start_byte() == right.start_byte()
357        && left.end_byte() == right.end_byte()
358}
359
360/// Direct include evidence relates one header declaration to one implementation
361/// file without pretending that every external name in a workspace belongs to
362/// one linker unit.
363///
364/// `implementation_imports` are that file's raw `#include` lines; the analysis
365/// wrapper reads them off the analyzer along with `include_targets`.
366pub fn cpp_header_body_files_are_related(
367    left: &ProjectFile,
368    right: &ProjectFile,
369    implementation_imports: &[String],
370    include_targets: &IncludeTargetIndex,
371) -> bool {
372    let (header, implementation) = if cpp_source_path_is_header(left) {
373        (left, right)
374    } else if cpp_source_path_is_header(right) {
375        (right, left)
376    } else {
377        return false;
378    };
379    if cpp_source_path_is_header(implementation) {
380        return false;
381    }
382    implementation_imports
383        .iter()
384        .flat_map(|import| include_paths(std::slice::from_ref(import)))
385        .any(|include| {
386            let targets =
387                resolve_include_targets_with_index(implementation, &include, include_targets);
388            targets.len() == 1 && targets.first() == Some(header)
389        })
390}
391
392/// Which of `left`/`right` the include evidence would read as the header, if
393/// either. The analysis wrapper uses this to decide which file's imports to read
394/// before paying for them.
395pub fn cpp_header_body_implementation_file<'a>(
396    left: &'a ProjectFile,
397    right: &'a ProjectFile,
398) -> Option<&'a ProjectFile> {
399    let implementation = if cpp_source_path_is_header(left) {
400        right
401    } else if cpp_source_path_is_header(right) {
402        left
403    } else {
404        return None;
405    };
406    (!cpp_source_path_is_header(implementation)).then_some(implementation)
407}
408
409pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
410    let path = rel_path_string(source).to_ascii_lowercase();
411    matches!(path.rsplit('.').next(), Some("h" | "hh" | "hpp" | "hxx"))
412}
413
414pub fn cpp_occurrence_role_for_range(
415    root: Node<'_>,
416    candidate: &CodeUnit,
417    range: &Range,
418) -> CppOccurrenceRole {
419    if !candidate.is_callable() && !candidate.is_class() {
420        return CppOccurrenceRole::Both;
421    }
422    let Some(node) = cpp_declaration_node_for_range(root, range) else {
423        return CppOccurrenceRole::Unknown;
424    };
425    if candidate.is_callable() {
426        return if subtree_contains(node, |descendant| {
427            descendant.kind() == "function_definition"
428                && descendant.child_by_field_name("body").is_some()
429        }) {
430            CppOccurrenceRole::Definition
431        } else {
432            CppOccurrenceRole::DeclarationOnly
433        };
434    }
435    if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
436        return CppOccurrenceRole::Definition;
437    }
438    if !subtree_contains(node, |descendant| {
439        matches!(
440            descendant.kind(),
441            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
442        )
443    }) {
444        return CppOccurrenceRole::Both;
445    }
446    if subtree_contains(node, |descendant| {
447        matches!(
448            descendant.kind(),
449            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
450        ) && descendant.child_by_field_name("body").is_some()
451    }) {
452        CppOccurrenceRole::Definition
453    } else {
454        CppOccurrenceRole::DeclarationOnly
455    }
456}
457
458fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
459    node_for_exact_range(root, range).or_else(|| {
460        root.descendant_for_byte_range(range.start_byte, range.end_byte)
461            .and_then(|mut node| {
462                while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
463                    node = node.parent()?;
464                }
465                Some(node)
466            })
467    })
468}
469
470/// The #1134 resolution-time identity-reconciliation overlay for one queried
471/// canonical `fq_name`.
472///
473/// For each out-of-line member definition whose per-file provisional identity
474/// the include-visible class table re-keys to this name, it holds a *re-keyed*
475/// `CodeUnit` -- a synthetic unit carrying the canonical identity but the
476/// definition's real `.cpp` source -- so a canonical query resolves the
477/// definition alongside its header declaration across every resolution surface
478/// (`definitions`, source blocks, occurrence roles, canonical selectors). The
479/// re-keyed unit is not in the store, so `provisional_of` maps it back to the
480/// stored provisional unit for range and signature-metadata lookups.
481#[derive(Default)]
482pub struct CppReconciledDefinitionIndex {
483    /// Re-keyed definitions belonging under the queried canonical `fq_name`.
484    pub rekeyed: Vec<CodeUnit>,
485    /// Re-keyed unit -> the stored provisional unit its indexed data lives under.
486    pub provisional_of: HashMap<CodeUnit, CodeUnit>,
487}
488
489/// Which candidates one reconcile group covers: a member identifier, and the
490/// terminal owner component that #1566's pre-filter admits.
491///
492/// This -- not the queried fq name -- is the unit reconciliation is a function
493/// of, and keying the memo by it is the #1908 fix. The old key was the queried
494/// fq name, so a bare identifier that 1,277 distinct owners answer produced
495/// 1,277 distinct keys, none of which ever hit, each re-running the identical
496/// `lookup_candidates_by_identifier` store read and re-scanning the identical
497/// 2,898-candidate set: 3.70M candidate evaluations for one request.
498///
499/// The owner terminal stays in the key rather than being dropped for a plain
500/// per-identifier map. Dropping it would mean reconciling every same-named
501/// candidate in the workspace on the first query for that identifier, which is
502/// exactly the cost #1566 removed -- chromium paid ~75 s per member query that
503/// way. `reconcile_skips_same_named_members_of_unrelated_classes_1566` pins it.
504#[derive(Debug, Clone, PartialEq, Eq, Hash)]
505pub struct CppReconcileGroupKey {
506    /// The queried name's terminal segment: the member identifier the
507    /// persisted identifier index is probed with.
508    pub member_identifier: String,
509    /// The terminal component of the queried name's penultimate segment, or
510    /// `None` for a single-segment (bare) query, where #1566's pre-filter is
511    /// inert and every candidate has to be reconciled.
512    pub owner_terminal: Option<String>,
513}
514
515/// Which member identifier and owner terminal a queried canonical name asks
516/// about, or `None` when the name has no terminal segment to probe with.
517///
518/// Parsed through the sanctioned input-edge parser rather than split here, and
519/// note `$` is not a segment boundary for it -- a nested owner chain stays one
520/// segment, so the terminal really is the member.
521pub fn cpp_reconcile_group_key(fq_name: &str) -> Option<CppReconcileGroupKey> {
522    let interner = segment_interner();
523    let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
524    let (member_identifier, _) = interner.resolve(query_fq.last()?);
525    if member_identifier.is_empty() {
526        return None;
527    }
528    // #1566 owner-terminal pre-filter: the reconciler only re-partitions a
529    // candidate's qualifier -- the class chain it emits is always a suffix
530    // of the candidate's owner segments (`reconcile.rs`) -- so the terminal
531    // `$` component of any identity it can produce equals the candidate's
532    // terminal owner segment. A candidate whose terminal owner differs
533    // from the queried name's penultimate segment can therefore never
534    // re-key onto it, and skipping it avoids the role check and, on
535    // whale repos, an include-closure class-table build per same-named
536    // candidate in the repo.
537    let owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
538        let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
539        // fqname-M4: the input-edge parser above deliberately keeps a nested
540        // owner chain as one `$`-joined segment (no structured sub-segments
541        // exist at this surface), so the terminal component must come from
542        // the raw text.
543        text.rsplit_once('$')
544            .map_or(text, |(_, tail)| tail)
545            .to_string()
546    });
547    Some(CppReconcileGroupKey {
548        member_identifier: member_identifier.to_string(),
549        owner_terminal,
550    })
551}
552
553/// Every callable declaration in the workspace sharing one member identifier,
554/// bucketed by its terminal owner segment.
555///
556/// One store read and one pass over the candidate set per identifier, memoized
557/// on the analyzer. Before #1908 both were re-run once per queried fq name.
558pub struct CppReconcileCandidates {
559    by_owner_terminal: HashMap<String, Vec<CodeUnit>>,
560    /// Every bucketed candidate, in the order the sorted candidate set
561    /// produced them. What a bare query has to reconcile, since #1566's
562    /// pre-filter cannot narrow it.
563    all: Vec<CodeUnit>,
564}
565
566impl CppReconcileCandidates {
567    /// The candidates a group key admits: one owner-terminal bucket, or every
568    /// candidate for a bare query.
569    fn for_group(&self, key: &CppReconcileGroupKey) -> &[CodeUnit] {
570        match &key.owner_terminal {
571            Some(owner_terminal) => self
572                .by_owner_terminal
573                .get(owner_terminal)
574                .map_or(&[][..], Vec::as_slice),
575            None => &self.all,
576        }
577    }
578
579    /// Every bucketed candidate, once. What a cache weigher has to charge for.
580    pub fn iter(&self) -> impl Iterator<Item = &CodeUnit> {
581        self.all.iter()
582    }
583
584    /// How many bucket entries reference those candidates. A candidate with no
585    /// owner segment is in no bucket, so this is not `len`.
586    pub fn bucketed_len(&self) -> usize {
587        self.by_owner_terminal.values().map(Vec::len).sum()
588    }
589}
590
591/// Read and bucket every callable declaration sharing `member_identifier`.
592///
593/// `keep_going` is polled per candidate batch; `None` means the caller's
594/// deadline expired and nothing may be memoized, because a truncated candidate
595/// set is indistinguishable from an identifier with fewer namesakes and every
596/// later reconcile reading it would silently lose definitions (#1908 fix D,
597/// the same contract `visible_type_units_while` carries).
598pub fn cpp_reconcile_candidates(
599    cpp: &dyn CppSource,
600    member_identifier: &str,
601    keep_going: &dyn Fn() -> bool,
602) -> Option<CppReconcileCandidates> {
603    let candidates: BTreeSet<CodeUnit> = {
604        let _lookup =
605            profiling::scope_with(|| format!("cpp.reconcile.lookup[{member_identifier}]"));
606        cpp.lookup_candidates_by_identifier(member_identifier)
607    };
608    profiling::note_with(|| {
609        format!(
610            "cpp.reconcile.candidates[{member_identifier}] n={}",
611            candidates.len()
612        )
613    });
614
615    let interner = segment_interner();
616    let mut by_owner_terminal: HashMap<String, Vec<CodeUnit>> = HashMap::default();
617    let mut all = Vec::new();
618    for (index, unit) in candidates.into_iter().enumerate() {
619        // Bucketing is a segment walk per candidate, cheap next to the role
620        // check and class-table build the groups pay, so the poll runs per
621        // batch rather than per candidate.
622        if index % CANDIDATE_BUCKETING_POLL_STRIDE == 0 && !keep_going() {
623            return None;
624        }
625        if !unit.is_callable() {
626            continue;
627        }
628        let owner_terminal = unit
629            .fq()
630            .segments()
631            .iter()
632            .filter_map(|&segment| {
633                let (text, kind) = interner.resolve(segment);
634                // Candidate fq segments carry real boundaries (each nested
635                // class is its own `SegmentKind::Nested` segment), so the
636                // segment text is already the terminal component.
637                matches!(
638                    kind,
639                    SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
640                )
641                .then_some(text)
642            })
643            .last();
644        if let Some(owner_terminal) = owner_terminal {
645            by_owner_terminal
646                .entry(owner_terminal.to_string())
647                .or_default()
648                .push(unit.clone());
649        }
650        all.push(unit);
651    }
652    Some(CppReconcileCandidates {
653        by_owner_terminal,
654        all,
655    })
656}
657
658/// How many candidates the bucketing pass walks between deadline polls.
659const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
660
661/// Reconcile every candidate one group key admits, grouped by the canonical
662/// `fq_name` each re-keyed definition belongs under.
663///
664/// Deliberately **not** a workspace-wide index: building one would need a full
665/// declaration scan, and a warm forward lookup must not trigger one
666/// (`tests/analyzer_persistence.rs`'s candidate-bounded contract). Instead each
667/// group reconciles only the candidates the persisted terminal identifier
668/// index already offers, which is the same bounded lookup the ordinary
669/// resolution path uses.
670///
671/// A definition whose reconciled identity equals its provisional one (the
672/// overwhelming majority, including genuine `ns1::ns2::Klass::method` namespace
673/// chains) contributes nothing.
674///
675/// `None` means `keep_going` went false mid-scan. Nothing may be memoized
676/// then; see [`cpp_reconcile_candidates`].
677pub fn cpp_reconcile_group(
678    cpp: &dyn CppSource,
679    key: &CppReconcileGroupKey,
680    candidates: &CppReconcileCandidates,
681    keep_going: &dyn Fn() -> bool,
682    on_candidate: &dyn Fn(),
683) -> Option<HashMap<String, Arc<CppReconciledDefinitionIndex>>> {
684    let _scope = profiling::scope_with(|| {
685        format!(
686            "cpp.reconciled.build[{}#{}]",
687            key.member_identifier,
688            key.owner_terminal.as_deref().unwrap_or("*")
689        )
690    });
691    let mut groups: HashMap<String, CppReconciledDefinitionIndex> = HashMap::default();
692    let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
693    for unit in candidates.for_group(key) {
694        // Per candidate, not per batch: each iteration below can run a role
695        // check (0.265 ms mean in the #1908 trace) and an include-visible
696        // class-table read, so an atomic load per iteration is free by
697        // comparison.
698        if !keep_going() {
699            return None;
700        }
701        on_candidate();
702        // Lazy: `fq_name` clones a String, and this loop runs once per
703        // same-named candidate the group admits.
704        let _candidate =
705            profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
706        let role = {
707            let _role = profiling::scope("cpp.reconcile.role");
708            cpp_callable_unit_role(cpp, unit)
709        };
710        if !matches!(
711            role,
712            CppCallableUnitRole::Definition | CppCallableUnitRole::Both
713        ) {
714            continue;
715        }
716        let Some(reconciled) = cpp_reconcile_definition_identity(cpp, unit, &mut using_by_file)
717        else {
718            continue;
719        };
720        let canonical_fq = reconciled.fq_name();
721        // A candidate that already carries the canonical identity is the
722        // stored declaration, not a re-keying of it. Before #1908 this read
723        // `unit.fq_name() == fq_name` against the queried name, checked before
724        // the reconcile; against the group's canonical key it is the same
725        // predicate for the same (query, candidate) pair, because a candidate
726        // only ever lands under its own reconciled identity.
727        if unit.fq_name() == canonical_fq {
728            continue;
729        }
730        // Re-key onto the canonical identity while keeping the definition's
731        // real `.cpp` source and signature, so it resolves as a definition
732        // alongside its header declaration under the canonical `fq_name`.
733        // The structured `FqName` is rebuilt from the *canonical* package and
734        // owner chain through the same emission helper extraction uses, so
735        // the re-keyed unit carries real segment boundaries: owner lookup
736        // (`default_parent_fq_name`) is a pure segment pop, where an empty
737        // `fq` would mean "no owner" rather than "not yet migrated".
738        let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
739        let fq = cpp_member_fq(&reconciled.package, &short_name);
740        let rekeyed = CodeUnit::with_signature_and_fq(
741            unit.source().clone(),
742            unit.kind(),
743            reconciled.package,
744            short_name,
745            unit.signature().map(str::to_string),
746            unit.is_synthetic(),
747            fq,
748        );
749        let index = groups.entry(canonical_fq).or_default();
750        index.rekeyed.push(rekeyed.clone());
751        index.provisional_of.insert(rekeyed, unit.clone());
752    }
753    Some(
754        groups
755            .into_iter()
756            .map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
757            .collect(),
758    )
759}
760
761/// Reconcile one out-of-line member definition's provisional identity against
762/// the class table visible to its file. Returns `None` for anything that is
763/// not a re-keyable out-of-line member or that the class table does not
764/// confirm. A one-segment class qualifier is valid when a structured using
765/// namespace and the visible class table confirm its package.
766fn cpp_reconcile_definition_identity(
767    cpp: &dyn CppSource,
768    unit: &CodeUnit,
769    using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
770) -> Option<ReconciledIdentity> {
771    // Read the full source-order qualifier off the definition's *structured*
772    // `FqName` -- the namespace (`Package`) segments followed by the
773    // class-nesting (`Type`/`Nested`) ones, with the terminal `Member` as the
774    // member name. The segment boundaries were recorded at extraction, so
775    // nothing here re-infers them by splitting the rendered name on a guessed
776    // delimiter (the shape `tests/no_stringly_name_parsing.rs` guards). The
777    // reconciler then re-partitions this whole sequence against the class
778    // table, so extraction need not have decided where the namespace ends and
779    // the class chain begins.
780    let interner = segment_interner();
781    let mut provisional_owner_segments: Vec<&str> = Vec::new();
782    let mut member: Option<&str> = None;
783    for &segment in unit.fq().segments() {
784        let (text, kind) = interner.resolve(segment);
785        match kind {
786            SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
787                // A `Member` is always terminal in a cpp callable's chain; a
788                // qualifier segment after one would mean the identity is not
789                // the plain `namespace... class... member` shape this handles.
790                if member.is_some() {
791                    return None;
792                }
793                if !text.is_empty() {
794                    provisional_owner_segments.push(text);
795                }
796            }
797            SegmentKind::Member => member = Some(text),
798            _ => return None,
799        }
800    }
801    let member = member?;
802    // Existing multi-segment reconciliation uses the provisional structured
803    // FqName because it preserves template-owner normalization. Read the
804    // declarator only for the previously unsupported one-segment shape, where
805    // extraction can prepend a guessed using namespace.
806    let structured_owner_segments =
807        cpp_structured_out_of_line_owner_segments(cpp, unit).filter(|segments| segments.len() == 1);
808    let owner_segments = structured_owner_segments.as_ref().map_or_else(
809        || provisional_owner_segments,
810        |segments| segments.iter().map(String::as_str).collect(),
811    );
812    if owner_segments.is_empty() {
813        return None;
814    }
815
816    let using = using_by_file
817        .entry(unit.source().clone())
818        .or_insert_with(|| {
819            Arc::new(
820                cpp.file_source(unit.source())
821                    .map(|source| cpp_file_using_namespaces(&source))
822                    .unwrap_or_default(),
823            )
824        })
825        .clone();
826    let mut namespace_candidates: Vec<&str> = vec![""];
827    namespace_candidates.extend(using.iter().map(String::as_str));
828
829    let visible = {
830        let _visible = profiling::scope_with(|| {
831            format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
832        });
833        cpp.visible_type_units(unit.source())
834    };
835    let class_table: Vec<VisibleClass> = visible
836        .iter()
837        .filter(|candidate| candidate.is_class())
838        .map(|candidate| VisibleClass {
839            package: candidate.package_name(),
840            nested_short_name: candidate.short_name(),
841        })
842        .collect();
843
844    reconcile_out_of_line_member_identity(
845        &owner_segments,
846        member,
847        &namespace_candidates,
848        &class_table,
849    )
850}
851
852/// Read the owner qualifier from the definition declarator itself.
853///
854/// Per-file extraction can prepend one guessed using namespace to a bare
855/// `Class::member` definition. That provisional package is not source syntax.
856/// Reconciliation must compare the real qualifier against every structured
857/// using target and let the visible class table select the namespace.
858fn cpp_structured_out_of_line_owner_segments(
859    cpp: &dyn CppSource,
860    unit: &CodeUnit,
861) -> Option<Vec<String>> {
862    let prepared = cpp.prepared_syntax(unit.source())?;
863    let root = prepared.tree().root_node();
864    for range in cpp.ranges(unit) {
865        let mut current = cpp_declaration_node_for_range(root, &range)?;
866        let function = loop {
867            if current.kind() == "function_definition" {
868                break current;
869            }
870            current = current.parent()?;
871        };
872        if function.child_by_field_name("body").is_none() {
873            continue;
874        }
875        let declarator = function.child_by_field_name("declarator")?;
876        let name = declarator_name_node(declarator)?;
877        if !qualified_name_has_concrete_scope_separators(name) {
878            continue;
879        }
880        let mut components = cpp_type_name_components(name, prepared.source())?;
881        components.pop()?;
882        if !components.is_empty() {
883            return Some(components);
884        }
885    }
886    None
887}
888
889#[cfg(test)]
890mod tests {
891    use super::*;
892
893    fn parse_cpp(source: &str) -> Tree {
894        let mut parser = Parser::new();
895        parser
896            .set_language(&tree_sitter_cpp::LANGUAGE.into())
897            .expect("cpp language");
898        parser.parse(source, None).expect("cpp tree")
899    }
900
901    fn is_declarator_name(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
902        let end = start + text.len();
903        assert_eq!(&source[start..end], text, "the probe must name the token");
904        let node = tree
905            .root_node()
906            .named_descendant_for_byte_range(start, end)
907            .expect("a node spans the probed range");
908        assert_eq!(
909            (node.start_byte(), node.end_byte()),
910            (start, end),
911            "the probed range must be exactly one node: {}",
912            node.to_sexp()
913        );
914        cpp_is_constructor_or_destructor_declarator_name(node, source)
915    }
916
917    /// The parsed-as-declared shape, in class and out of line. The names that
918    /// surround a declarator stay references: the owning scope of an out-of-line
919    /// definition, a parameter type that happens to be the class, and the class
920    /// name itself.
921    #[test]
922    fn declared_constructor_and_destructor_declarator_names_are_not_references() {
923        let source = concat!(
924            "class Foo {\n",
925            "public:\n",
926            "  Foo();\n",
927            "  Foo(const Foo&);\n",
928            "  ~Foo();\n",
929            "  void m();\n",
930            "};\n",
931            "Foo::Foo() {}\n",
932            "Foo::~Foo() {}\n",
933            "void Foo::m() {}\n",
934        );
935        let tree = parse_cpp(source);
936
937        for (label, start, text) in [
938            (
939                "constructor declaration",
940                source.find("Foo();").expect("ctor"),
941                "Foo",
942            ),
943            (
944                "copy constructor declaration",
945                source.find("Foo(const Foo&);").expect("copy ctor"),
946                "Foo",
947            ),
948            (
949                "destructor name",
950                source.find("~Foo();").expect("dtor"),
951                "~Foo",
952            ),
953            (
954                "identifier inside the destructor name",
955                source.find("~Foo();").expect("dtor") + "~".len(),
956                "Foo",
957            ),
958            (
959                "out-of-line constructor definition name",
960                source.find("Foo::Foo() {}").expect("out-of-line ctor") + "Foo::".len(),
961                "Foo",
962            ),
963            (
964                "out-of-line destructor definition name",
965                source.find("Foo::~Foo() {}").expect("out-of-line dtor") + "Foo::".len(),
966                "~Foo",
967            ),
968        ] {
969            assert!(
970                is_declarator_name(&tree, source, start, text),
971                "the {label} at byte {start} is a declaration occurrence"
972            );
973        }
974
975        for (label, start, text) in [
976            (
977                "class name",
978                source.find("class Foo {").expect("class") + "class ".len(),
979                "Foo",
980            ),
981            (
982                "parameter type",
983                source.find("const Foo&").expect("parameter type") + "const ".len(),
984                "Foo",
985            ),
986            (
987                "owning scope of an out-of-line constructor",
988                source.find("Foo::Foo() {}").expect("out-of-line ctor"),
989                "Foo",
990            ),
991            (
992                "owning scope of an out-of-line destructor",
993                source.find("Foo::~Foo() {}").expect("out-of-line dtor"),
994                "Foo",
995            ),
996            (
997                "out-of-line method name",
998                source.find("void Foo::m() {}").expect("out-of-line method") + "void Foo::".len(),
999                "m",
1000            ),
1001        ] {
1002            assert!(
1003                !is_declarator_name(&tree, source, start, text),
1004                "the {label} at byte {start} stays a reference"
1005            );
1006        }
1007    }
1008
1009    /// Constructor CALL sites are references. `new D(...)`, the direct
1010    /// initialization `D x(...)`, a member initializer and a bare temporary
1011    /// statement all name the type, and none of them is a declarator.
1012    #[test]
1013    fn constructor_call_sites_stay_references() {
1014        let source = concat!(
1015            "struct B { B(int); };\n",
1016            "struct D : B {\n",
1017            "  D(int x) : B(x), base_(x) {}\n",
1018            "  int base_;\n",
1019            "};\n",
1020            "void g() {\n",
1021            "  D* p = new D(1);\n",
1022            "  D x(2);\n",
1023            "  D(3);\n",
1024            "  g();\n",
1025            "}\n",
1026        );
1027        let tree = parse_cpp(source);
1028
1029        let inline_declarator = source.find("D(int x)").expect("inline constructor");
1030        assert!(
1031            is_declarator_name(&tree, source, inline_declarator, "D"),
1032            "an inline constructor definition name is still a declarator"
1033        );
1034
1035        for (label, start, text) in [
1036            (
1037                "base member initializer",
1038                source.find(": B(x)").expect("base initializer") + ": ".len(),
1039                "B",
1040            ),
1041            (
1042                "field member initializer",
1043                source.find("base_(x) {}").expect("field initializer"),
1044                "base_",
1045            ),
1046            (
1047                "new expression type",
1048                source.find("new D(1)").expect("new expression") + "new ".len(),
1049                "D",
1050            ),
1051            (
1052                "direct initialization type",
1053                source.find("D x(2)").expect("direct initialization"),
1054                "D",
1055            ),
1056            (
1057                "temporary construction statement",
1058                source.find("D(3)").expect("temporary"),
1059                "D",
1060            ),
1061            (
1062                "recursive call in a real body",
1063                source.find("g();").expect("recursive call"),
1064                "g",
1065            ),
1066        ] {
1067            assert!(
1068                !is_declarator_name(&tree, source, start, text),
1069                "the {label} at byte {start} is a reference"
1070            );
1071        }
1072    }
1073
1074    /// The recovered shape (#1834): an export macro between `class` and the
1075    /// class name makes the parse read the class body as a function body and
1076    /// every constructor declaration in it as a call of the class's own name.
1077    /// The bodies the recovery left intact keep their references.
1078    #[test]
1079    fn a_constructor_declarator_the_parse_read_as_a_call_is_not_a_reference() {
1080        let source = concat!(
1081            "class SAMPLE_EXPORT Properties {\n",
1082            "  public:\n",
1083            "    Properties();\n",
1084            "    DISALLOW_COPY_AND_ASSIGN(Properties);\n",
1085            "    int size() const;\n",
1086            "    int total() { return size(); }\n",
1087            "};\n",
1088        );
1089        let tree = parse_cpp(source);
1090
1091        let recovered = source.find("Properties();").expect("recovered constructor");
1092        assert!(
1093            is_declarator_name(&tree, source, recovered, "Properties"),
1094            "a constructor declaration the parse read as a call is still a declarator"
1095        );
1096
1097        for (label, start, text) in [
1098            (
1099                "class name in the recovered header",
1100                source
1101                    .find("class SAMPLE_EXPORT Properties")
1102                    .expect("class")
1103                    + "class SAMPLE_EXPORT ".len(),
1104                "Properties",
1105            ),
1106            (
1107                "macro invocation in the recovered body",
1108                source
1109                    .find("DISALLOW_COPY_AND_ASSIGN(Properties);")
1110                    .expect("macro invocation"),
1111                "DISALLOW_COPY_AND_ASSIGN",
1112            ),
1113            (
1114                "call inside a method body the recovery kept",
1115                source.find("return size();").expect("member call") + "return ".len(),
1116                "size",
1117            ),
1118        ] {
1119            assert!(
1120                !is_declarator_name(&tree, source, start, text),
1121                "the {label} at byte {start} stays a reference"
1122            );
1123        }
1124    }
1125}