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