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 cell that memoizes [`cpp_reconciled_definitions`] per queried name
14//!   stays on the analyzer, as does every other cache, so `IAnalyzer::update`
15//!   keeps rebuilding them wholesale.
16
17use crate::declarations::{cpp_file_using_namespaces, cpp_member_fq};
18use crate::graph_support::CppSource;
19use crate::imports::{IncludeTargetIndex, include_paths, resolve_include_targets_with_index};
20use crate::reconcile::{ReconciledIdentity, VisibleClass, reconcile_out_of_line_member_identity};
21use brokk_bifrost_core::analyzer::fq_name::{SegmentKind, segment_interner};
22use brokk_bifrost_core::analyzer::model::{CallableLinkage, Range};
23use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path_fq;
24use brokk_bifrost_core::analyzer::tree_walk::{node_for_exact_range, subtree_contains};
25use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
26use brokk_bifrost_core::hash::HashMap;
27use brokk_bifrost_core::path_utils::rel_path_string;
28use brokk_bifrost_core::profiling;
29use std::collections::BTreeSet;
30use std::sync::Arc;
31use tree_sitter::{Node, Parser, Tree};
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum CppCallableUnitRole {
35    DeclarationOnly,
36    Definition,
37    Both,
38    Unknown,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum CppOccurrenceRole {
43    DeclarationOnly,
44    Definition,
45    Both,
46    Unknown,
47}
48
49impl CppOccurrenceRole {
50    pub fn api_label(self) -> Option<&'static str> {
51        match self {
52            Self::DeclarationOnly => Some("declaration"),
53            Self::Definition => Some("definition"),
54            Self::Both | Self::Unknown => None,
55        }
56    }
57}
58
59pub struct CppOccurrenceClassifier {
60    tree: Tree,
61}
62
63impl CppOccurrenceClassifier {
64    pub fn new(source: &str) -> Option<Self> {
65        let mut parser = Parser::new();
66        parser
67            .set_language(&tree_sitter_cpp::LANGUAGE.into())
68            .ok()?;
69        parser.parse(source, None).map(|tree| Self { tree })
70    }
71
72    pub fn classify(&self, candidate: &CodeUnit, range: &Range) -> CppOccurrenceRole {
73        cpp_occurrence_role_for_range(self.tree.root_node(), candidate, range)
74    }
75}
76
77pub fn cpp_callable_unit_role(
78    index: &dyn CodeUnitIndex,
79    callable: &CodeUnit,
80) -> CppCallableUnitRole {
81    if !callable.is_callable() {
82        return CppCallableUnitRole::Unknown;
83    }
84    let mut declaration = false;
85    let mut definition = false;
86    for metadata in index.signature_metadata(callable) {
87        if metadata.is_declaration_only() {
88            declaration = true;
89        } else {
90            definition = true;
91        }
92    }
93    match (declaration, definition) {
94        (true, false) => CppCallableUnitRole::DeclarationOnly,
95        (false, true) => CppCallableUnitRole::Definition,
96        (true, true) => CppCallableUnitRole::Both,
97        (false, false) => CppCallableUnitRole::Unknown,
98    }
99}
100
101pub fn cpp_indexed_callable_linkage(
102    index: &dyn CodeUnitIndex,
103    callable: &CodeUnit,
104) -> Option<CallableLinkage> {
105    let mut external = false;
106    for metadata in index.signature_metadata(callable) {
107        match metadata.callable_linkage() {
108            Some(CallableLinkage::Internal) => return Some(CallableLinkage::Internal),
109            Some(CallableLinkage::External) => external = true,
110            None => {}
111        }
112    }
113    external.then_some(CallableLinkage::External)
114}
115
116/// Whether `left` and `right` are the same callable seen twice.
117///
118/// `header_body_related` is the include-evidence predicate; the analysis wrapper
119/// supplies it because reaching an `IncludeTargetIndex` needs the analyzer
120/// downcast this crate cannot perform.
121pub fn cpp_callable_definitions_share_identity_evidence(
122    index: &dyn CodeUnitIndex,
123    left: &CodeUnit,
124    right: &CodeUnit,
125    header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
126) -> bool {
127    left.source() == right.source()
128        || (left.fq_name() == right.fq_name()
129            && left.signature() == right.signature()
130            && matches!(
131                cpp_indexed_callable_linkage(index, left),
132                Some(CallableLinkage::External)
133            )
134            && matches!(
135                cpp_indexed_callable_linkage(index, right),
136                Some(CallableLinkage::External)
137            )
138            && header_body_related(left.source(), right.source()))
139}
140
141/// Return whether `node` is one of the names declared by a range-for
142/// declarator. Follow only declarator fields. This keeps identifiers in array
143/// bounds and attributes in the range-for header as references.
144pub fn cpp_is_range_for_binding_name(node: Node<'_>) -> bool {
145    let mut current = Some(node);
146    while let Some(candidate) = current {
147        let Some(parent) = candidate.parent() else {
148            return false;
149        };
150        if parent.kind() == "for_range_loop" {
151            return parent
152                .child_by_field_name("declarator")
153                .is_some_and(|declarator| {
154                    cpp_range_for_declarator_contains_name(declarator, node)
155                });
156        }
157        current = Some(parent);
158    }
159    false
160}
161
162fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
163    let mut pending = vec![declarator];
164    while let Some(candidate) = pending.pop() {
165        match candidate.kind() {
166            "identifier" | "field_identifier" => {
167                if cpp_same_node(candidate, target) {
168                    return true;
169                }
170            }
171            "structured_binding_declarator" => {
172                let mut cursor = candidate.walk();
173                if candidate
174                    .named_children(&mut cursor)
175                    .any(|name| cpp_same_node(name, target))
176                {
177                    return true;
178                }
179            }
180            "pointer_declarator"
181            | "reference_declarator"
182            | "array_declarator"
183            | "attributed_declarator"
184            | "parenthesized_declarator"
185            | "function_declarator"
186            | "init_declarator" => {
187                if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
188                    pending.push(inner);
189                }
190            }
191            _ => {}
192        }
193    }
194    false
195}
196
197fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
198    node.child_by_field_name("declarator").or_else(|| {
199        let mut cursor = node.walk();
200        node.named_children(&mut cursor).find(|child| {
201            matches!(
202                child.kind(),
203                "identifier"
204                    | "field_identifier"
205                    | "structured_binding_declarator"
206                    | "pointer_declarator"
207                    | "reference_declarator"
208                    | "array_declarator"
209                    | "attributed_declarator"
210                    | "parenthesized_declarator"
211                    | "function_declarator"
212                    | "init_declarator"
213            )
214        })
215    })
216}
217
218fn cpp_same_node(left: Node<'_>, right: Node<'_>) -> bool {
219    left.id() == right.id()
220        && left.start_byte() == right.start_byte()
221        && left.end_byte() == right.end_byte()
222}
223
224/// Direct include evidence relates one header declaration to one implementation
225/// file without pretending that every external name in a workspace belongs to
226/// one linker unit.
227///
228/// `implementation_imports` are that file's raw `#include` lines; the analysis
229/// wrapper reads them off the analyzer along with `include_targets`.
230pub fn cpp_header_body_files_are_related(
231    left: &ProjectFile,
232    right: &ProjectFile,
233    implementation_imports: &[String],
234    include_targets: &IncludeTargetIndex,
235) -> bool {
236    let (header, implementation) = if cpp_source_path_is_header(left) {
237        (left, right)
238    } else if cpp_source_path_is_header(right) {
239        (right, left)
240    } else {
241        return false;
242    };
243    if cpp_source_path_is_header(implementation) {
244        return false;
245    }
246    implementation_imports
247        .iter()
248        .flat_map(|import| include_paths(std::slice::from_ref(import)))
249        .any(|include| {
250            let targets =
251                resolve_include_targets_with_index(implementation, &include, include_targets);
252            targets.len() == 1 && targets.first() == Some(header)
253        })
254}
255
256/// Which of `left`/`right` the include evidence would read as the header, if
257/// either. The analysis wrapper uses this to decide which file's imports to read
258/// before paying for them.
259pub fn cpp_header_body_implementation_file<'a>(
260    left: &'a ProjectFile,
261    right: &'a ProjectFile,
262) -> Option<&'a ProjectFile> {
263    let implementation = if cpp_source_path_is_header(left) {
264        right
265    } else if cpp_source_path_is_header(right) {
266        left
267    } else {
268        return None;
269    };
270    (!cpp_source_path_is_header(implementation)).then_some(implementation)
271}
272
273pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
274    let path = rel_path_string(source).to_ascii_lowercase();
275    matches!(path.rsplit('.').next(), Some("h" | "hh" | "hpp" | "hxx"))
276}
277
278pub fn cpp_occurrence_role_for_range(
279    root: Node<'_>,
280    candidate: &CodeUnit,
281    range: &Range,
282) -> CppOccurrenceRole {
283    if !candidate.is_callable() && !candidate.is_class() {
284        return CppOccurrenceRole::Both;
285    }
286    let Some(node) = cpp_declaration_node_for_range(root, range) else {
287        return CppOccurrenceRole::Unknown;
288    };
289    if candidate.is_callable() {
290        return if subtree_contains(node, |descendant| {
291            descendant.kind() == "function_definition"
292                && descendant.child_by_field_name("body").is_some()
293        }) {
294            CppOccurrenceRole::Definition
295        } else {
296            CppOccurrenceRole::DeclarationOnly
297        };
298    }
299    if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
300        return CppOccurrenceRole::Definition;
301    }
302    if !subtree_contains(node, |descendant| {
303        matches!(
304            descendant.kind(),
305            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
306        )
307    }) {
308        return CppOccurrenceRole::Both;
309    }
310    if subtree_contains(node, |descendant| {
311        matches!(
312            descendant.kind(),
313            "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
314        ) && descendant.child_by_field_name("body").is_some()
315    }) {
316        CppOccurrenceRole::Definition
317    } else {
318        CppOccurrenceRole::DeclarationOnly
319    }
320}
321
322fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
323    node_for_exact_range(root, range).or_else(|| {
324        root.descendant_for_byte_range(range.start_byte, range.end_byte)
325            .and_then(|mut node| {
326                while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
327                    node = node.parent()?;
328                }
329                Some(node)
330            })
331    })
332}
333
334/// The #1134 resolution-time identity-reconciliation overlay for one queried
335/// canonical `fq_name`.
336///
337/// For each out-of-line member definition whose per-file provisional identity
338/// the include-visible class table re-keys to this name, it holds a *re-keyed*
339/// `CodeUnit` -- a synthetic unit carrying the canonical identity but the
340/// definition's real `.cpp` source -- so a canonical query resolves the
341/// definition alongside its header declaration across every resolution surface
342/// (`definitions`, source blocks, occurrence roles, canonical selectors). The
343/// re-keyed unit is not in the store, so `provisional_of` maps it back to the
344/// stored provisional unit for range and signature-metadata lookups.
345#[derive(Default)]
346pub struct CppReconciledDefinitionIndex {
347    /// Re-keyed definitions belonging under the queried canonical `fq_name`.
348    pub rekeyed: Vec<CodeUnit>,
349    /// Re-keyed unit -> the stored provisional unit its indexed data lives under.
350    pub provisional_of: HashMap<CodeUnit, CodeUnit>,
351}
352
353/// Reconcile the bounded candidate set for one queried canonical `fq_name`:
354/// every out-of-line member definition sharing its terminal identifier whose
355/// provisional per-file identity the include-visible class table re-keys onto
356/// exactly this name (the two ambiguous shapes left by #1121). A definition
357/// whose reconciled identity equals its provisional one (the overwhelming
358/// majority, including genuine `ns1::ns2::Klass::method` namespace chains)
359/// contributes nothing.
360///
361/// Deliberately **not** a workspace-wide index: building one would need a full
362/// declaration scan, and a warm forward lookup must not trigger one
363/// (`tests/analyzer_persistence.rs`'s candidate-bounded contract). Instead each
364/// queried name reconciles only the candidates the persisted terminal identifier
365/// index already offers, which is the same bounded lookup the ordinary
366/// resolution path uses.
367pub fn cpp_reconciled_definitions(
368    cpp: &dyn CppSource,
369    fq_name: &str,
370) -> CppReconciledDefinitionIndex {
371    let _scope = profiling::scope_with(|| format!("cpp.reconciled.build[{fq_name}]"));
372    let mut index = CppReconciledDefinitionIndex::default();
373    let interner = segment_interner();
374    // The queried name's terminal segment is the member identifier to probe
375    // the identifier index with. Parsed through the sanctioned input-edge
376    // parser rather than split here, and note `$` is not a segment boundary
377    // for it -- a nested owner chain stays one segment, so the terminal
378    // really is the member.
379    let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
380    let Some(member_segment) = query_fq.last() else {
381        return index;
382    };
383    let (member_identifier, _) = interner.resolve(member_segment);
384    if member_identifier.is_empty() {
385        return index;
386    }
387
388    // #1566 owner-terminal pre-filter: the reconciler only re-partitions a
389    // candidate's qualifier -- the class chain it emits is always a suffix
390    // of the candidate's owner segments (`reconcile.rs`) -- so the terminal
391    // `$` component of any identity it can produce equals the candidate's
392    // terminal owner segment. A candidate whose terminal owner differs
393    // from the queried name's penultimate segment can therefore never
394    // re-key onto it, and skipping it here avoids the role check and, on
395    // whale repos, an include-closure class-table build per same-named
396    // candidate in the repo (chromium paid ~75s per member query that way:
397    // one BFS per same-named candidate file, 2.5M declaration queries per
398    // probe file for a gtest-shaped member name).
399    let query_owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
400        let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
401        // fqname-M4: the input-edge parser above deliberately keeps a nested
402        // owner chain as one `$`-joined segment (no structured sub-segments
403        // exist at this surface), so the terminal component must come from
404        // the raw text.
405        text.rsplit_once('$').map_or(text, |(_, tail)| tail)
406    });
407
408    let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
409    let candidates: BTreeSet<CodeUnit> = {
410        let _lookup =
411            profiling::scope_with(|| format!("cpp.reconcile.lookup[{member_identifier}]"));
412        cpp.lookup_candidates_by_identifier(member_identifier)
413    };
414    profiling::note_with(|| {
415        format!(
416            "cpp.reconcile.candidates[{member_identifier}] n={}",
417            candidates.len()
418        )
419    });
420    for unit in candidates {
421        // Lazy: `fq_name` clones a String, and this loop runs once per
422        // same-named candidate in the repo (2.5M per probe file on chromium).
423        let _candidate =
424            profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
425        let candidate_owner_terminal = unit
426            .fq()
427            .segments()
428            .iter()
429            .filter_map(|&segment| {
430                let (text, kind) = interner.resolve(segment);
431                // Candidate fq segments carry real boundaries (each nested
432                // class is its own `SegmentKind::Nested` segment), so the
433                // segment text is already the terminal component.
434                matches!(
435                    kind,
436                    SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
437                )
438                .then_some(text)
439            })
440            .last();
441        if let Some(query_terminal) = query_owner_terminal
442            && candidate_owner_terminal != Some(query_terminal)
443        {
444            continue;
445        }
446        if !unit.is_callable() || unit.fq_name() == fq_name {
447            continue;
448        }
449        let role = {
450            let _role = profiling::scope("cpp.reconcile.role");
451            cpp_callable_unit_role(cpp, &unit)
452        };
453        if !matches!(
454            role,
455            CppCallableUnitRole::Definition | CppCallableUnitRole::Both
456        ) {
457            continue;
458        }
459        let Some(reconciled) = cpp_reconcile_definition_identity(cpp, &unit, &mut using_by_file)
460        else {
461            continue;
462        };
463        let canonical_fq = reconciled.fq_name();
464        if canonical_fq != fq_name {
465            continue;
466        }
467        // Re-key onto the canonical identity while keeping the definition's
468        // real `.cpp` source and signature, so it resolves as a definition
469        // alongside its header declaration under the canonical `fq_name`.
470        // The structured `FqName` is rebuilt from the *canonical* package and
471        // owner chain through the same emission helper extraction uses, so
472        // the re-keyed unit carries real segment boundaries: owner lookup
473        // (`default_parent_fq_name`) is a pure segment pop, where an empty
474        // `fq` would mean "no owner" rather than "not yet migrated".
475        let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
476        let fq = cpp_member_fq(&reconciled.package, &short_name);
477        let rekeyed = CodeUnit::with_signature_and_fq(
478            unit.source().clone(),
479            unit.kind(),
480            reconciled.package,
481            short_name,
482            unit.signature().map(str::to_string),
483            unit.is_synthetic(),
484            fq,
485        );
486        index.rekeyed.push(rekeyed.clone());
487        index.provisional_of.insert(rekeyed, unit);
488    }
489    index
490}
491
492/// Reconcile one out-of-line member definition's provisional identity against
493/// the class table visible to its file. Returns `None` for anything that is
494/// not a re-keyable out-of-line member (free functions with no owner, single
495/// segment qualifiers) or that the class table does not confirm.
496fn cpp_reconcile_definition_identity(
497    cpp: &dyn CppSource,
498    unit: &CodeUnit,
499    using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
500) -> Option<ReconciledIdentity> {
501    // Read the full source-order qualifier off the definition's *structured*
502    // `FqName` -- the namespace (`Package`) segments followed by the
503    // class-nesting (`Type`/`Nested`) ones, with the terminal `Member` as the
504    // member name. The segment boundaries were recorded at extraction, so
505    // nothing here re-infers them by splitting the rendered name on a guessed
506    // delimiter (the shape `tests/no_stringly_name_parsing.rs` guards). The
507    // reconciler then re-partitions this whole sequence against the class
508    // table, so extraction need not have decided where the namespace ends and
509    // the class chain begins.
510    let interner = segment_interner();
511    let mut owner_segments: Vec<&str> = Vec::new();
512    let mut member: Option<&str> = None;
513    for &segment in unit.fq().segments() {
514        let (text, kind) = interner.resolve(segment);
515        match kind {
516            SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
517                // A `Member` is always terminal in a cpp callable's chain; a
518                // qualifier segment after one would mean the identity is not
519                // the plain `namespace... class... member` shape this handles.
520                if member.is_some() {
521                    return None;
522                }
523                if !text.is_empty() {
524                    owner_segments.push(text);
525                }
526            }
527            SegmentKind::Member => member = Some(text),
528            _ => return None,
529        }
530    }
531    let member = member?;
532    if owner_segments.len() < 2 {
533        return None;
534    }
535
536    let using = using_by_file
537        .entry(unit.source().clone())
538        .or_insert_with(|| {
539            Arc::new(
540                cpp.file_source(unit.source())
541                    .map(|source| cpp_file_using_namespaces(&source))
542                    .unwrap_or_default(),
543            )
544        })
545        .clone();
546    let mut namespace_candidates: Vec<&str> = vec![""];
547    namespace_candidates.extend(using.iter().map(String::as_str));
548
549    let visible = {
550        let _visible = profiling::scope_with(|| {
551            format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
552        });
553        cpp.visible_type_units(unit.source())
554    };
555    let class_table: Vec<VisibleClass> = visible
556        .iter()
557        .filter(|candidate| candidate.is_class())
558        .map(|candidate| VisibleClass {
559            package: candidate.package_name(),
560            nested_short_name: candidate.short_name(),
561        })
562        .collect();
563
564    reconcile_out_of_line_member_identity(
565        &owner_segments,
566        member,
567        &namespace_candidates,
568        &class_table,
569    )
570}