1use crate::declarations::{
18 CppRecoveredExportClassIndex, cpp_member_fq, extract_function_declarator, node_text,
19 recovered_callable_body_at, recovered_class_body_at,
20};
21use crate::graph::CppGraphSource;
22use crate::graph::resolver::{VisibilityIndex, cpp_include_closure_reaches};
23use crate::graph_support::CppSource;
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, SignatureMetadata};
27use brokk_bifrost_core::analyzer::query_token::QueryToken;
28use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path_fq;
29use brokk_bifrost_core::analyzer::tree_walk::{node_for_exact_range, subtree_contains};
30pub use brokk_bifrost_core::analyzer::usages::common::same_node as cpp_same_node;
31use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
32use brokk_bifrost_core::hash::HashMap;
33use brokk_bifrost_core::path_utils::rel_path_string;
34use brokk_bifrost_core::profiling;
35use std::sync::Arc;
36use tree_sitter::Node;
37use tree_sitter::{Parser, Tree};
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub enum CppCallableUnitRole {
41 DeclarationOnly,
42 Definition,
43 Both,
44 Unknown,
45}
46
47pub use brokk_bifrost_core::analyzer::cpp_facts::CppOccurrenceRole;
48pub struct CppOccurrenceClassifier {
49 tree: Tree,
50 source: String,
53 recovered_export_classes: CppRecoveredExportClassIndex,
56}
57
58impl CppOccurrenceClassifier {
59 pub fn new(source: &str) -> Option<Self> {
60 let mut parser = Parser::new();
61 parser
62 .set_language(&tree_sitter_cpp::LANGUAGE.into())
63 .ok()?;
64 parser.parse(source, None).map(|tree| {
65 let recovered_export_classes =
66 CppRecoveredExportClassIndex::build(tree.root_node(), source);
67 Self {
68 tree,
69 source: source.to_owned(),
70 recovered_export_classes,
71 }
72 })
73 }
74
75 pub fn classify(&self, candidate: &CodeUnit, range: &Range) -> CppOccurrenceRole {
76 cpp_occurrence_role_for_range(
77 &self.recovered_export_classes,
78 self.tree.root_node(),
79 &self.source,
80 candidate,
81 range,
82 )
83 }
84}
85
86pub fn cpp_callable_unit_role(
87 index: &dyn CodeUnitIndex,
88 callable: &CodeUnit,
89) -> CppCallableUnitRole {
90 cpp_callable_unit_role_from_metadata(callable, index.signature_metadata(callable))
91}
92
93fn cpp_callable_unit_role_from_metadata(
94 callable: &CodeUnit,
95 metadata: impl IntoIterator<Item = SignatureMetadata>,
96) -> CppCallableUnitRole {
97 if !callable.is_callable() {
98 return CppCallableUnitRole::Unknown;
99 }
100 let mut declaration = false;
101 let mut definition = false;
102 for metadata in metadata {
103 if metadata.is_declaration_only() {
104 declaration = true;
105 } else {
106 definition = true;
107 }
108 }
109 match (declaration, definition) {
110 (true, false) => CppCallableUnitRole::DeclarationOnly,
111 (false, true) => CppCallableUnitRole::Definition,
112 (true, true) => CppCallableUnitRole::Both,
113 (false, false) => CppCallableUnitRole::Unknown,
114 }
115}
116
117pub fn cpp_indexed_callable_linkage(
118 index: &dyn CodeUnitIndex,
119 callable: &CodeUnit,
120) -> Option<CallableLinkage> {
121 cpp_callable_linkage_from_metadata(index.signature_metadata(callable))
122}
123
124pub fn cpp_callable_linkage_from_metadata(
125 metadata: impl IntoIterator<Item = SignatureMetadata>,
126) -> Option<CallableLinkage> {
127 let mut external = false;
128 for metadata in metadata {
129 match metadata.callable_linkage() {
130 Some(CallableLinkage::Internal) => return Some(CallableLinkage::Internal),
131 Some(CallableLinkage::External) => external = true,
132 None => {}
133 }
134 }
135 external.then_some(CallableLinkage::External)
136}
137
138pub fn cpp_callable_definitions_share_identity_evidence(
144 index: &dyn CodeUnitIndex,
145 left: &CodeUnit,
146 right: &CodeUnit,
147 header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
148) -> bool {
149 left.fq_name() == right.fq_name()
150 && left.signature() == right.signature()
151 && (left.source() == right.source()
152 || (matches!(
153 cpp_indexed_callable_linkage(index, left),
154 Some(CallableLinkage::External)
155 ) && matches!(
156 cpp_indexed_callable_linkage(index, right),
157 Some(CallableLinkage::External)
158 ) && header_body_related(left.source(), right.source())))
159}
160
161pub fn cpp_callable_definitions_share_identity_evidence_with_visibility(
177 analyzer: &CppGraphSource<'_>,
178 visibility: &VisibilityIndex<'_>,
179 left: &CodeUnit,
180 right: &CodeUnit,
181 header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
182) -> bool {
183 left.fq_name() == right.fq_name()
184 && visibility.same_logical_callable(analyzer, left, right)
185 && (left.source() == right.source()
186 || (matches!(
187 visibility.callable_linkage(left),
188 Some(CallableLinkage::External)
189 ) && matches!(
190 visibility.callable_linkage(right),
191 Some(CallableLinkage::External)
192 ) && header_body_related(left.source(), right.source())))
193}
194
195pub fn cpp_is_range_for_binding_name(node: Node<'_>) -> bool {
199 let mut current = Some(node);
200 while let Some(candidate) = current {
201 let Some(parent) = candidate.parent() else {
202 return false;
203 };
204 if parent.kind() == "for_range_loop" {
205 return parent
206 .child_by_field_name("declarator")
207 .is_some_and(|declarator| {
208 cpp_range_for_declarator_contains_name(declarator, node)
209 });
210 }
211 current = Some(parent);
212 }
213 false
214}
215
216pub fn cpp_is_conversion_operator_target_type(mut node: Node<'_>) -> bool {
225 while let Some(parent) = node.parent() {
226 if parent.kind() == "operator_cast" {
227 return true;
228 }
229 if matches!(
230 parent.kind(),
231 "function_declarator" | "declaration" | "function_definition" | "translation_unit"
232 ) {
233 return false;
234 }
235 node = parent;
236 }
237 false
238}
239
240pub fn cpp_is_recovered_macro_character_token_type(node: Node<'_>) -> bool {
250 if node.kind() != "type_identifier" {
251 return false;
252 }
253 let Some(parameter) = node.parent() else {
254 return false;
255 };
256 if parameter.kind() != "parameter_declaration"
257 || parameter.child_by_field_name("type") != Some(node)
258 || parameter.child_by_field_name("declarator").is_some()
259 || parameter
260 .parent()
261 .is_none_or(|parent| parent.kind() != "parameter_list")
262 {
263 return false;
264 }
265
266 parameter
267 .prev_named_sibling()
268 .is_some_and(cpp_is_recovered_character_quote)
269 && parameter
270 .next_named_sibling()
271 .is_some_and(cpp_is_recovered_character_quote)
272}
273
274fn cpp_is_recovered_character_quote(node: Node<'_>) -> bool {
275 node.is_error()
276 && node.child_count() == 1
277 && node
278 .child(0)
279 .is_some_and(|quote| !quote.is_named() && quote.kind() == "'")
280}
281
282pub fn cpp_is_constructor_or_destructor_declarator_name(node: Node<'_>, source: &str) -> bool {
305 cpp_is_declared_constructor_or_destructor_name(node)
306 || cpp_is_recovered_constructor_or_destructor_name(node, source)
307}
308
309fn cpp_is_declared_constructor_or_destructor_name(node: Node<'_>) -> bool {
315 let mut name = node;
316 if let Some(parent) = name.parent()
317 && parent.kind() == "destructor_name"
318 {
319 name = parent;
320 }
321 while let Some(parent) = name.parent() {
325 if parent.kind() != "qualified_identifier"
326 || parent.child_by_field_name("name") != Some(name)
327 {
328 break;
329 }
330 name = parent;
331 }
332 let Some(declarator) = name.parent() else {
333 return false;
334 };
335 if declarator.kind() != "function_declarator"
336 || declarator.child_by_field_name("declarator") != Some(name)
337 {
338 return false;
339 }
340 let Some(owner) = declarator.parent() else {
341 return false;
342 };
343 matches!(owner.kind(), "declaration" | "function_definition")
344 && owner.child_by_field_name("declarator") == Some(declarator)
345 && owner.child_by_field_name("type").is_none()
346}
347
348fn cpp_is_recovered_constructor_or_destructor_name(node: Node<'_>, source: &str) -> bool {
366 if node.kind() != "identifier" {
367 return false;
368 }
369 let Some(call) = node.parent() else {
370 return false;
371 };
372 if call.kind() != "call_expression" || call.child_by_field_name("function") != Some(node) {
373 return false;
374 }
375 let mut current = call.parent();
376 while let Some(ancestor) = current {
377 if ancestor.kind() == "function_definition" {
378 return ancestor
379 .child_by_field_name("declarator")
380 .is_some_and(|declarator| declarator.kind() == "identifier")
381 && cpp_recovered_class_header_names(ancestor, node_text(node, source), source);
382 }
383 current = ancestor.parent();
384 }
385 false
386}
387
388fn cpp_recovered_class_header_names(definition: Node<'_>, name: &str, source: &str) -> bool {
391 let header_end = definition
392 .child_by_field_name("body")
393 .map_or_else(|| definition.end_byte(), |body| body.start_byte());
394 let mut stack = vec![definition];
395 while let Some(node) = stack.pop() {
396 if node.start_byte() >= header_end {
397 continue;
398 }
399 if matches!(
400 node.kind(),
401 "identifier" | "type_identifier" | "namespace_identifier"
402 ) && node_text(node, source) == name
403 {
404 return true;
405 }
406 let mut cursor = node.walk();
407 for child in node.named_children(&mut cursor) {
408 stack.push(child);
409 }
410 }
411 false
412}
413
414fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
415 let mut pending = vec![declarator];
416 while let Some(candidate) = pending.pop() {
417 match candidate.kind() {
418 "identifier" | "field_identifier" => {
419 if cpp_same_node(candidate, target) {
420 return true;
421 }
422 }
423 "structured_binding_declarator" => {
424 let mut cursor = candidate.walk();
425 if candidate
426 .named_children(&mut cursor)
427 .any(|name| cpp_same_node(name, target))
428 {
429 return true;
430 }
431 }
432 "pointer_declarator"
433 | "reference_declarator"
434 | "array_declarator"
435 | "attributed_declarator"
436 | "parenthesized_declarator"
437 | "function_declarator"
438 | "init_declarator" => {
439 if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
440 pending.push(inner);
441 }
442 }
443 _ => {}
444 }
445 }
446 false
447}
448
449fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
450 node.child_by_field_name("declarator").or_else(|| {
451 let mut cursor = node.walk();
452 node.named_children(&mut cursor).find(|child| {
453 matches!(
454 child.kind(),
455 "identifier"
456 | "field_identifier"
457 | "structured_binding_declarator"
458 | "pointer_declarator"
459 | "reference_declarator"
460 | "array_declarator"
461 | "attributed_declarator"
462 | "parenthesized_declarator"
463 | "function_declarator"
464 | "init_declarator"
465 )
466 })
467 })
468}
469
470pub fn cpp_header_body_files_are_related(
487 source: &dyn CppSource,
488 token: QueryToken<'_>,
489 left: &ProjectFile,
490 right: &ProjectFile,
491) -> bool {
492 let (header, implementation) = if cpp_source_path_is_header(left) {
493 (left, right)
494 } else if cpp_source_path_is_header(right) {
495 (right, left)
496 } else {
497 return false;
498 };
499 if cpp_source_path_is_header(implementation) {
500 return false;
501 }
502 cpp_include_closure_reaches(source, token, implementation, header)
503}
504
505pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
506 let path = rel_path_string(source).to_ascii_lowercase();
507 matches!(
508 path.rsplit('.').next(),
509 Some("h" | "hin" | "hh" | "hpp" | "hxx")
510 )
511}
512
513pub(crate) fn cpp_occurrence_role_for_node(
514 node: Node<'_>,
515 source: &str,
516 candidate: &CodeUnit,
517) -> CppOccurrenceRole {
518 if !candidate.is_callable() && !candidate.is_class() {
519 return CppOccurrenceRole::Both;
520 }
521 if candidate.is_callable() {
522 return if subtree_contains(node, |descendant| {
523 descendant.kind() == "function_definition"
524 && descendant.child_by_field_name("body").is_some()
525 }) {
526 CppOccurrenceRole::Definition
527 } else {
528 CppOccurrenceRole::DeclarationOnly
529 };
530 }
531 if cpp_node_declares_alias(node, source, candidate) {
535 return CppOccurrenceRole::Both;
536 }
537 if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
538 return CppOccurrenceRole::Definition;
539 }
540 if !subtree_contains(node, |descendant| {
541 matches!(
542 descendant.kind(),
543 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
544 )
545 }) {
546 return CppOccurrenceRole::Both;
547 }
548 if subtree_contains(node, |descendant| {
549 matches!(
550 descendant.kind(),
551 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
552 ) && descendant.child_by_field_name("body").is_some()
553 }) {
554 CppOccurrenceRole::Definition
555 } else {
556 CppOccurrenceRole::DeclarationOnly
557 }
558}
559
560pub fn cpp_occurrence_role_for_range(
561 recovered_export_classes: &CppRecoveredExportClassIndex,
562 root: Node<'_>,
563 source: &str,
564 candidate: &CodeUnit,
565 range: &Range,
566) -> CppOccurrenceRole {
567 if !candidate.is_callable() && !candidate.is_class() {
568 return CppOccurrenceRole::Both;
569 }
570 let Some(node) = cpp_declaration_node_for_range(root, range) else {
571 return CppOccurrenceRole::Unknown;
572 };
573 if candidate.is_callable()
574 && subtree_contains(node, |descendant| {
575 descendant.kind() == "function_definition"
576 && descendant.child_by_field_name("body").is_some()
577 })
578 {
579 return CppOccurrenceRole::Definition;
580 }
581 if candidate.is_callable()
588 && !matches!(
589 node.kind(),
590 "declaration" | "field_declaration" | "function_definition"
591 )
592 && let Some(has_body) = recovered_callable_body_at(source, range)
593 {
594 return if has_body {
595 CppOccurrenceRole::Definition
596 } else {
597 CppOccurrenceRole::DeclarationOnly
598 };
599 }
600 if candidate.is_class()
601 && node.kind() == "function_definition"
602 && node.child_by_field_name("body").is_some()
603 {
604 return CppOccurrenceRole::Definition;
605 }
606 if candidate.is_class()
614 && let Some(has_body) = recovered_class_body_at(
615 recovered_export_classes,
616 root,
617 source,
618 candidate.identifier(),
619 range,
620 )
621 {
622 return if has_body {
623 CppOccurrenceRole::Definition
624 } else {
625 CppOccurrenceRole::DeclarationOnly
626 };
627 }
628 cpp_occurrence_role_for_node(node, source, candidate)
629}
630
631fn cpp_node_declares_alias(node: Node<'_>, source: &str, candidate: &CodeUnit) -> bool {
633 match node.kind() {
634 "alias_declaration" => {
635 node.child_by_field_name("name")
636 .and_then(|name| name.utf8_text(source.as_bytes()).ok())
637 == Some(candidate.identifier())
638 }
639 "type_definition" => {
640 let mut cursor = node.walk();
641 node.children_by_field_name("declarator", &mut cursor)
642 .any(|declarator| {
643 crate::declarations::extract_typedef_declarator_name(declarator, source)
644 .as_deref()
645 == Some(candidate.identifier())
646 })
647 }
648 _ => false,
649 }
650}
651
652pub fn cpp_range_is_pure_virtual_declaration(root: Node<'_>, source: &str, range: &Range) -> bool {
663 let Some(node) = cpp_declaration_node_for_range(root, range) else {
664 return false;
665 };
666 let mut current = Some(node);
667 while let Some(node) = current {
668 match node.kind() {
669 "field_declaration" => {
670 return node
674 .child_by_field_name("default_value")
675 .is_some_and(|value| {
676 value.kind() == "number_literal" && node_text(value, source) == "0"
677 })
678 && node
679 .child_by_field_name("declarator")
680 .and_then(extract_function_declarator)
681 .is_some();
682 }
683 "field_declaration_list" | "function_definition" | "translation_unit" => return false,
686 _ => current = node.parent(),
687 }
688 }
689 false
690}
691
692fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
693 node_for_exact_range(root, range).or_else(|| {
694 root.descendant_for_byte_range(range.start_byte, range.end_byte)
695 .and_then(|mut node| {
696 while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
697 node = node.parent()?;
698 }
699 Some(node)
700 })
701 })
702}
703
704#[derive(Default)]
716pub struct CppReconciledDefinitionIndex {
717 pub rekeyed: Vec<CodeUnit>,
719 pub provisional_of: HashMap<CodeUnit, CodeUnit>,
721}
722
723#[derive(Default)]
727pub struct CppReconciledGroup {
728 pub by_name: HashMap<String, Arc<CppReconciledDefinitionIndex>>,
729 pub canonical_of: HashMap<CodeUnit, CodeUnit>,
730}
731
732pub fn cpp_reconcile_group_key_for_fq(
735 fq: &brokk_bifrost_core::analyzer::fq_name::FqName,
736) -> Option<CppReconcileGroupKey> {
737 let interner = segment_interner();
738 let (member_identifier, _) = interner.resolve(fq.last()?);
739 if member_identifier.is_empty() {
740 return None;
741 }
742 let owner_terminal = fq
743 .segments()
744 .len()
745 .checked_sub(2)
746 .map(|index| interner.resolve(fq.segments()[index]).0.to_string());
747 Some(CppReconcileGroupKey {
748 member_identifier: member_identifier.to_string(),
749 owner_terminal,
750 })
751}
752
753#[derive(Debug, Clone, PartialEq, Eq, Hash)]
769pub struct CppReconcileGroupKey {
770 pub member_identifier: String,
773 pub owner_terminal: Option<String>,
777}
778
779pub fn cpp_reconcile_group_key(fq_name: &str) -> Option<CppReconcileGroupKey> {
786 let interner = segment_interner();
787 let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
788 let (member_identifier, _) = interner.resolve(query_fq.last()?);
789 if member_identifier.is_empty() {
790 return None;
791 }
792 let owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
802 let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
803 text.rsplit_once('$')
808 .map_or(text, |(_, tail)| tail)
809 .to_string()
810 });
811 Some(CppReconcileGroupKey {
812 member_identifier: member_identifier.to_string(),
813 owner_terminal,
814 })
815}
816
817pub struct CppReconcileCandidates {
823 by_owner_terminal: HashMap<String, Vec<CodeUnit>>,
824 all: Vec<CodeUnit>,
828}
829
830impl CppReconcileCandidates {
831 fn for_group(&self, key: &CppReconcileGroupKey) -> &[CodeUnit] {
834 match &key.owner_terminal {
835 Some(owner_terminal) => self
836 .by_owner_terminal
837 .get(owner_terminal)
838 .map_or(&[][..], Vec::as_slice),
839 None => &self.all,
840 }
841 }
842
843 pub fn iter(&self) -> impl Iterator<Item = &CodeUnit> {
845 self.all.iter()
846 }
847
848 pub fn bucketed_len(&self) -> usize {
851 self.by_owner_terminal.values().map(Vec::len).sum()
852 }
853}
854
855pub fn cpp_reconcile_candidates_from_units(
862 candidates: impl IntoIterator<Item = CodeUnit>,
863 keep_going: &dyn Fn() -> bool,
864) -> Option<CppReconcileCandidates> {
865 let mut candidates = candidates.into_iter().collect::<Vec<_>>();
866 candidates.sort();
867 candidates.dedup();
868 let interner = segment_interner();
869 let mut by_owner_terminal: HashMap<String, Vec<CodeUnit>> = HashMap::default();
870 let mut all = Vec::new();
871 for (index, unit) in candidates.into_iter().enumerate() {
872 if index % CANDIDATE_BUCKETING_POLL_STRIDE == 0 && !keep_going() {
876 return None;
877 }
878 if !unit.is_callable() {
879 continue;
880 }
881 let owner_terminal = unit
882 .fq()
883 .segments()
884 .iter()
885 .filter_map(|&segment| {
886 let (text, kind) = interner.resolve(segment);
887 matches!(
891 kind,
892 SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
893 )
894 .then_some(text)
895 })
896 .last();
897 if let Some(owner_terminal) = owner_terminal {
898 by_owner_terminal
899 .entry(owner_terminal.to_string())
900 .or_default()
901 .push(unit.clone());
902 }
903 all.push(unit);
904 }
905 if !keep_going() {
908 return None;
909 }
910 Some(CppReconcileCandidates {
911 by_owner_terminal,
912 all,
913 })
914}
915
916const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
918
919pub fn cpp_reconcile_group(
936 cpp: &dyn CppSource,
937 token: QueryToken<'_>,
938 key: &CppReconcileGroupKey,
939 candidates: &CppReconcileCandidates,
940 keep_going: &dyn Fn() -> bool,
941 on_candidate: &dyn Fn(),
942) -> Option<CppReconciledGroup> {
943 let _scope = profiling::scope_with(|| {
944 format!(
945 "cpp.reconciled.build[{}#{}]",
946 key.member_identifier,
947 key.owner_terminal.as_deref().unwrap_or("*")
948 )
949 });
950 let mut groups: HashMap<String, CppReconciledDefinitionIndex> = HashMap::default();
951 let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
952 for unit in candidates.for_group(key) {
953 if !keep_going() {
958 return None;
959 }
960 on_candidate();
961 let _candidate =
964 profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
965 let role = {
966 let _role = profiling::scope("cpp.reconcile.role");
967 cpp.stored_callable_unit_role(unit)
968 };
969 if !matches!(
970 role,
971 CppCallableUnitRole::Definition | CppCallableUnitRole::Both
972 ) {
973 continue;
974 }
975 let Some(reconciled) =
976 cpp_reconcile_definition_identity(cpp, token, unit, &mut using_by_file)
977 else {
978 continue;
979 };
980 let canonical_fq = reconciled.fq_name();
981 if unit.fq_name() == canonical_fq {
988 continue;
989 }
990 let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
999 let fq = cpp_member_fq(&reconciled.package, &short_name);
1000 let rekeyed = CodeUnit::with_signature_and_fq(
1001 unit.source().clone(),
1002 unit.kind(),
1003 reconciled.package,
1004 short_name,
1005 unit.signature().map(str::to_string),
1006 unit.is_synthetic(),
1007 fq,
1008 );
1009 let index = groups.entry(canonical_fq).or_default();
1010 index.rekeyed.push(rekeyed.clone());
1011 index.provisional_of.insert(rekeyed, unit.clone());
1012 }
1013 let canonical_of = groups
1014 .values()
1015 .flat_map(|index| {
1016 index
1017 .provisional_of
1018 .iter()
1019 .map(|(canonical, physical)| (physical.clone(), canonical.clone()))
1020 })
1021 .collect();
1022 let group = CppReconciledGroup {
1023 canonical_of,
1024 by_name: groups
1025 .into_iter()
1026 .map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
1027 .collect(),
1028 };
1029 keep_going().then_some(group)
1032}
1033
1034fn cpp_reconcile_definition_identity(
1040 cpp: &dyn CppSource,
1041 token: QueryToken<'_>,
1042 unit: &CodeUnit,
1043 using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
1044) -> Option<ReconciledIdentity> {
1045 let interner = segment_interner();
1055 let mut provisional_owner_segments: Vec<&str> = Vec::new();
1056 let mut member: Option<&str> = None;
1057 for &segment in unit.fq().segments() {
1058 let (text, kind) = interner.resolve(segment);
1059 match kind {
1060 SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
1061 if member.is_some() {
1065 return None;
1066 }
1067 if !text.is_empty() {
1068 provisional_owner_segments.push(text);
1069 }
1070 }
1071 SegmentKind::Member => member = Some(text),
1072 _ => return None,
1073 }
1074 }
1075 let member = member?;
1076 let structured_owner_segments = cpp_structured_out_of_line_owner_segments(cpp, token, unit)
1081 .filter(|segments| segments.len() == 1);
1082 let owner_segments = structured_owner_segments.as_ref().map_or_else(
1083 || provisional_owner_segments,
1084 |segments| segments.iter().map(String::as_str).collect(),
1085 );
1086 if owner_segments.is_empty() {
1087 return None;
1088 }
1089
1090 let using = using_by_file
1091 .entry(unit.source().clone())
1092 .or_insert_with(|| {
1093 Arc::new(
1094 cpp.declaration_source_facts(token, unit.source())
1095 .map(|source| {
1096 source
1097 .facts
1098 .using_namespaces
1099 .iter()
1100 .map(|(_, name)| name.clone())
1101 .collect()
1102 })
1103 .unwrap_or_default(),
1104 )
1105 })
1106 .clone();
1107 let mut namespace_candidates: Vec<&str> = vec![""];
1108 namespace_candidates.extend(using.iter().map(String::as_str));
1109
1110 let visible = {
1111 let _visible = profiling::scope_with(|| {
1112 format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
1113 });
1114 cpp.visible_type_units(unit.source())
1115 };
1116 let class_table: Vec<VisibleClass> = visible
1117 .iter()
1118 .filter(|candidate| candidate.is_class())
1119 .map(|candidate| VisibleClass {
1120 package: candidate.package_name(),
1121 nested_short_name: candidate.short_name(),
1122 })
1123 .collect();
1124
1125 reconcile_out_of_line_member_identity(
1126 &owner_segments,
1127 member,
1128 &namespace_candidates,
1129 &class_table,
1130 )
1131}
1132
1133fn cpp_structured_out_of_line_owner_segments(
1140 cpp: &dyn CppSource,
1141 token: QueryToken<'_>,
1142 unit: &CodeUnit,
1143) -> Option<Vec<String>> {
1144 let source = cpp.declaration_source_facts(token, unit.source())?;
1147 let mut owners = source
1148 .for_unit(unit)
1149 .filter(|fact| !fact.written_owner.is_empty());
1150 let first = &owners.next()?.written_owner;
1151 owners
1152 .all(|fact| &fact.written_owner == first)
1153 .then(|| first.clone())
1154}
1155
1156#[cfg(test)]
1157mod tests {
1158 use super::*;
1159 use brokk_bifrost_test_macros::hardwon;
1160
1161 #[test]
1162 fn physical_identity_cohorts_keep_recorded_owner_and_member_boundaries() {
1163 use brokk_bifrost_core::analyzer::fq_name::FqName;
1164 let mut fq = FqName::new();
1165 for (text, kind) in [
1166 ("ns", SegmentKind::Package),
1167 ("Owner$Literal", SegmentKind::Type),
1168 ("operator std::string", SegmentKind::Member),
1169 ] {
1170 fq.push(segment_interner().intern(text, kind));
1171 }
1172 let key = cpp_reconcile_group_key_for_fq(&fq).unwrap();
1173 assert_eq!(key.owner_terminal.as_deref(), Some("Owner$Literal"));
1174 assert_eq!(key.member_identifier, "operator std::string");
1175 }
1176
1177 #[test]
1178 #[hardwon("#2488: terminal cancellation must not publish identity cohorts")]
1179 fn candidate_cohorts_recheck_terminal_cancellation_before_publish() {
1180 use std::cell::Cell;
1181
1182 let unit = CodeUnit::new(
1183 ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1184 brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1185 "",
1186 "Owner.run",
1187 );
1188 let cancel_after_last_candidate = {
1189 let active = Cell::new(true);
1190 move || active.replace(false)
1191 };
1192 assert!(
1193 cpp_reconcile_candidates_from_units(
1194 std::iter::once(unit.clone()),
1195 &cancel_after_last_candidate,
1196 )
1197 .is_none(),
1198 "a cancellation observed after the last sub-256 candidate must prevent publication"
1199 );
1200
1201 let candidates = (0..=CANDIDATE_BUCKETING_POLL_STRIDE)
1202 .map(|index| {
1203 CodeUnit::new(
1204 ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1205 brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1206 "",
1207 format!("Owner{index}.run"),
1208 )
1209 })
1210 .collect::<Vec<_>>();
1211 let cancel_at_bucket_boundary = {
1212 let active = Cell::new(true);
1213 move || active.replace(false)
1214 };
1215 assert!(
1216 cpp_reconcile_candidates_from_units(candidates, &cancel_at_bucket_boundary).is_none(),
1217 "a cancellation at the bucket boundary must prevent a partial cohort from publishing"
1218 );
1219
1220 let cancelled = || false;
1221 assert!(
1222 cpp_reconcile_candidates_from_units(Vec::<CodeUnit>::new(), &cancelled).is_none(),
1223 "an empty cancelled cohort must not publish"
1224 );
1225 }
1226
1227 fn parse_cpp(source: &str) -> Tree {
1228 crate::diagnostics::parse_cpp_tree(source)
1229 }
1230
1231 fn is_declarator_name(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1232 let end = start + text.len();
1233 assert_eq!(&source[start..end], text, "the probe must name the token");
1234 let node = tree
1235 .root_node()
1236 .named_descendant_for_byte_range(start, end)
1237 .expect("a node spans the probed range");
1238 assert_eq!(
1239 (node.start_byte(), node.end_byte()),
1240 (start, end),
1241 "the probed range must be exactly one node: {}",
1242 node.to_sexp()
1243 );
1244 cpp_is_constructor_or_destructor_declarator_name(node, source)
1245 }
1246
1247 fn is_conversion_target(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1248 let end = start + text.len();
1249 assert_eq!(&source[start..end], text, "the probe must name the token");
1250 let node = tree
1251 .root_node()
1252 .named_descendant_for_byte_range(start, end)
1253 .expect("a node spans the probed range");
1254 assert_eq!(
1255 (node.start_byte(), node.end_byte()),
1256 (start, end),
1257 "the probed range must be exactly one node: {}",
1258 node.to_sexp()
1259 );
1260 cpp_is_conversion_operator_target_type(node)
1261 }
1262
1263 fn is_recovered_macro_character_type(
1264 tree: &Tree,
1265 source: &str,
1266 start: usize,
1267 text: &str,
1268 ) -> bool {
1269 let end = start + text.len();
1270 assert_eq!(&source[start..end], text, "the probe must name the token");
1271 let node = tree
1272 .root_node()
1273 .named_descendant_for_byte_range(start, end)
1274 .expect("a node spans the probed range");
1275 cpp_is_recovered_macro_character_token_type(node)
1276 }
1277
1278 #[test]
1279 #[hardwon(
1280 "b114d87215ca: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1281 )]
1282 fn recovered_macro_character_tokens_are_not_type_references() {
1283 let source = concat!(
1284 "struct I {};\n",
1285 "#define STRING_TOKEN_(name, ...)\n",
1286 "struct Schema {\n",
1287 " STRING_TOKEN_(MaxItems, 'm', 'I')\n",
1288 " void ordinary(I value);\n",
1289 " void malformed(I value, @);\n",
1290 " void use() { I value; consume('I'); }\n",
1291 "};\n",
1292 );
1293 let tree = parse_cpp(source);
1294 let recovered_m = source.find("'m'").expect("recovered m") + 1;
1295 let recovered_i = source.find("'I'").expect("recovered I") + 1;
1296 for (label, start, text) in [
1297 ("lowercase character token", recovered_m, "m"),
1298 ("uppercase character token", recovered_i, "I"),
1299 ] {
1300 assert!(
1301 is_recovered_macro_character_type(&tree, source, start, text),
1302 "{label} must match the exact recovery role"
1303 );
1304 }
1305
1306 let macro_first_argument = source.find("MaxItems").expect("macro first argument");
1307 let ordinary = source.find("ordinary(I").expect("ordinary parameter") + "ordinary(".len();
1308 let malformed =
1309 source.find("malformed(I").expect("malformed parameter") + "malformed(".len();
1310 let local = source
1311 .find("I value; consume")
1312 .expect("local type reference");
1313 let expression_character = source.rfind("'I'").expect("expression character") + 1;
1314 for (label, start, text) in [
1315 ("unquoted macro argument", macro_first_argument, "MaxItems"),
1316 ("ordinary parameter type", ordinary, "I"),
1317 ("parameter beside another error", malformed, "I"),
1318 ("local type reference", local, "I"),
1319 ("expression character literal", expression_character, "I"),
1320 ] {
1321 assert!(
1322 !is_recovered_macro_character_type(&tree, source, start, text),
1323 "{label} must remain outside the recovery role"
1324 );
1325 }
1326 }
1327
1328 #[test]
1329 #[hardwon(
1330 "0733cadab142: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1331 )]
1332 fn conversion_operator_target_components_are_identity_syntax_only() {
1333 let source = concat!(
1334 "namespace other { struct Target {}; template<class T> struct Box {}; }\n",
1335 "using other::Target;\n",
1336 "struct Source {\n",
1337 " operator Target() const;\n",
1338 " operator other::Target() const { return other::Target{}; }\n",
1339 " template<class T> operator other::Box<T>() const { return {}; }\n",
1340 " operator other::Target const&() const;\n",
1341 " operator other::Target*() const;\n",
1342 " other::Target ordinary() const {\n",
1343 " return reinterpret_cast<other::Target&>(*this);\n",
1344 " }\n",
1345 " other::Target operator+() const { return {}; }\n",
1346 "};\n",
1347 );
1348 let tree = parse_cpp(source);
1349
1350 let bare = source.find("operator Target").expect("bare target") + "operator ".len();
1351 let qualified = source
1352 .find("operator other::Target()")
1353 .expect("qualified target")
1354 + "operator ".len();
1355 let template = source
1356 .find("operator other::Box<T>")
1357 .expect("template target")
1358 + "operator ".len();
1359 let cv_reference = source
1360 .find("operator other::Target const&")
1361 .expect("cv-reference target")
1362 + "operator other::".len();
1363 let pointer = source
1364 .find("operator other::Target*")
1365 .expect("pointer target")
1366 + "operator other::".len();
1367
1368 for (label, start, text) in [
1369 ("bare target", bare, "Target"),
1370 ("qualified target scope", qualified, "other"),
1371 (
1372 "qualified target name",
1373 qualified + "other::".len(),
1374 "Target",
1375 ),
1376 ("template target scope", template, "other"),
1377 ("template target name", template + "other::".len(), "Box"),
1378 (
1379 "template target argument",
1380 template + "other::Box<".len(),
1381 "T",
1382 ),
1383 ("cv-reference target", cv_reference, "Target"),
1384 ("pointer target", pointer, "Target"),
1385 ] {
1386 assert!(
1387 is_conversion_target(&tree, source, start, text),
1388 "the {label} at byte {start} belongs to the conversion identity"
1389 );
1390 }
1391
1392 let ordinary_return = source
1393 .find("other::Target ordinary")
1394 .expect("ordinary return");
1395 let body_cast = source
1396 .find("reinterpret_cast<other::Target")
1397 .expect("body cast")
1398 + "reinterpret_cast<".len();
1399 let overloaded_return = source
1400 .find("other::Target operator+")
1401 .expect("overloaded operator return");
1402 for (label, start) in [
1403 ("ordinary return type", ordinary_return),
1404 ("body cast target", body_cast),
1405 ("overloaded-operator return type", overloaded_return),
1406 ] {
1407 assert!(
1408 !is_conversion_target(&tree, source, start, "other"),
1409 "the {label} at byte {start} stays a reference"
1410 );
1411 }
1412 }
1413
1414 #[test]
1419 #[hardwon(
1420 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1421 )]
1422 fn declared_constructor_and_destructor_declarator_names_are_not_references() {
1423 let source = concat!(
1424 "class Foo {\n",
1425 "public:\n",
1426 " Foo();\n",
1427 " Foo(const Foo&);\n",
1428 " ~Foo();\n",
1429 " void m();\n",
1430 "};\n",
1431 "Foo::Foo() {}\n",
1432 "Foo::~Foo() {}\n",
1433 "void Foo::m() {}\n",
1434 );
1435 let tree = parse_cpp(source);
1436
1437 for (label, start, text) in [
1438 (
1439 "constructor declaration",
1440 source.find("Foo();").expect("ctor"),
1441 "Foo",
1442 ),
1443 (
1444 "copy constructor declaration",
1445 source.find("Foo(const Foo&);").expect("copy ctor"),
1446 "Foo",
1447 ),
1448 (
1449 "destructor name",
1450 source.find("~Foo();").expect("dtor"),
1451 "~Foo",
1452 ),
1453 (
1454 "identifier inside the destructor name",
1455 source.find("~Foo();").expect("dtor") + "~".len(),
1456 "Foo",
1457 ),
1458 (
1459 "out-of-line constructor definition name",
1460 source.find("Foo::Foo() {}").expect("out-of-line ctor") + "Foo::".len(),
1461 "Foo",
1462 ),
1463 (
1464 "out-of-line destructor definition name",
1465 source.find("Foo::~Foo() {}").expect("out-of-line dtor") + "Foo::".len(),
1466 "~Foo",
1467 ),
1468 ] {
1469 assert!(
1470 is_declarator_name(&tree, source, start, text),
1471 "the {label} at byte {start} is a declaration occurrence"
1472 );
1473 }
1474
1475 for (label, start, text) in [
1476 (
1477 "class name",
1478 source.find("class Foo {").expect("class") + "class ".len(),
1479 "Foo",
1480 ),
1481 (
1482 "parameter type",
1483 source.find("const Foo&").expect("parameter type") + "const ".len(),
1484 "Foo",
1485 ),
1486 (
1487 "owning scope of an out-of-line constructor",
1488 source.find("Foo::Foo() {}").expect("out-of-line ctor"),
1489 "Foo",
1490 ),
1491 (
1492 "owning scope of an out-of-line destructor",
1493 source.find("Foo::~Foo() {}").expect("out-of-line dtor"),
1494 "Foo",
1495 ),
1496 (
1497 "out-of-line method name",
1498 source.find("void Foo::m() {}").expect("out-of-line method") + "void Foo::".len(),
1499 "m",
1500 ),
1501 ] {
1502 assert!(
1503 !is_declarator_name(&tree, source, start, text),
1504 "the {label} at byte {start} stays a reference"
1505 );
1506 }
1507 }
1508
1509 #[test]
1513 #[hardwon(
1514 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1515 )]
1516 fn constructor_call_sites_stay_references() {
1517 let source = concat!(
1518 "struct B { B(int); };\n",
1519 "struct D : B {\n",
1520 " D(int x) : B(x), base_(x) {}\n",
1521 " int base_;\n",
1522 "};\n",
1523 "void g() {\n",
1524 " D* p = new D(1);\n",
1525 " D x(2);\n",
1526 " D(3);\n",
1527 " g();\n",
1528 "}\n",
1529 );
1530 let tree = parse_cpp(source);
1531
1532 let inline_declarator = source.find("D(int x)").expect("inline constructor");
1533 assert!(
1534 is_declarator_name(&tree, source, inline_declarator, "D"),
1535 "an inline constructor definition name is still a declarator"
1536 );
1537
1538 for (label, start, text) in [
1539 (
1540 "base member initializer",
1541 source.find(": B(x)").expect("base initializer") + ": ".len(),
1542 "B",
1543 ),
1544 (
1545 "field member initializer",
1546 source.find("base_(x) {}").expect("field initializer"),
1547 "base_",
1548 ),
1549 (
1550 "new expression type",
1551 source.find("new D(1)").expect("new expression") + "new ".len(),
1552 "D",
1553 ),
1554 (
1555 "direct initialization type",
1556 source.find("D x(2)").expect("direct initialization"),
1557 "D",
1558 ),
1559 (
1560 "temporary construction statement",
1561 source.find("D(3)").expect("temporary"),
1562 "D",
1563 ),
1564 (
1565 "recursive call in a real body",
1566 source.find("g();").expect("recursive call"),
1567 "g",
1568 ),
1569 ] {
1570 assert!(
1571 !is_declarator_name(&tree, source, start, text),
1572 "the {label} at byte {start} is a reference"
1573 );
1574 }
1575 }
1576
1577 #[test]
1582 #[hardwon(
1583 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1584 )]
1585 fn a_constructor_declarator_the_parse_read_as_a_call_is_not_a_reference() {
1586 let source = concat!(
1587 "class SAMPLE_EXPORT Properties {\n",
1588 " public:\n",
1589 " Properties();\n",
1590 " DISALLOW_COPY_AND_ASSIGN(Properties);\n",
1591 " int size() const;\n",
1592 " int total() { return size(); }\n",
1593 "};\n",
1594 );
1595 let tree = parse_cpp(source);
1596
1597 let recovered = source.find("Properties();").expect("recovered constructor");
1598 assert!(
1599 is_declarator_name(&tree, source, recovered, "Properties"),
1600 "a constructor declaration the parse read as a call is still a declarator"
1601 );
1602
1603 for (label, start, text) in [
1604 (
1605 "class name in the recovered header",
1606 source
1607 .find("class SAMPLE_EXPORT Properties")
1608 .expect("class")
1609 + "class SAMPLE_EXPORT ".len(),
1610 "Properties",
1611 ),
1612 (
1613 "macro invocation in the recovered body",
1614 source
1615 .find("DISALLOW_COPY_AND_ASSIGN(Properties);")
1616 .expect("macro invocation"),
1617 "DISALLOW_COPY_AND_ASSIGN",
1618 ),
1619 (
1620 "call inside a method body the recovery kept",
1621 source.find("return size();").expect("member call") + "return ".len(),
1622 "size",
1623 ),
1624 ] {
1625 assert!(
1626 !is_declarator_name(&tree, source, start, text),
1627 "the {label} at byte {start} stays a reference"
1628 );
1629 }
1630 }
1631
1632 fn class_roles(source: &str, name: &str) -> Vec<CppOccurrenceRole> {
1635 let tree = parse_cpp(source);
1636 let file = ProjectFile::new(std::env::temp_dir(), "occurrence-role.hpp");
1637 let parsed = crate::adapter::parse_cpp_file(&file, source, &tree);
1638 let unit = parsed
1639 .declarations()
1640 .iter()
1641 .find(|unit| unit.is_class() && unit.fq_name() == name)
1642 .unwrap_or_else(|| panic!("missing class {name}: {parsed:#?}"));
1643 let ranges = parsed.declaration_ranges(unit);
1644 assert!(!ranges.is_empty(), "{name} must have a declaration range");
1645 ranges
1646 .iter()
1647 .map(|range| {
1648 cpp_occurrence_role_for_range(
1649 &CppRecoveredExportClassIndex::build(tree.root_node(), source),
1650 tree.root_node(),
1651 source,
1652 unit,
1653 range,
1654 )
1655 })
1656 .collect()
1657 }
1658
1659 #[test]
1665 #[hardwon(
1666 "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1667 )]
1668 fn recovered_export_macro_classes_are_definitions_without_an_ordinary_class() {
1669 let source = concat!(
1670 "namespace api {\n",
1671 "class PROJECT_PUBLIC_API(2, 0) Name final {\n",
1672 " public:\n",
1673 " Name();\n",
1674 "};\n",
1675 "class PROJECT_PUBLIC_API(2, 0) Other : public Name {\n",
1676 " public:\n",
1677 " Other();\n",
1678 "};\n",
1679 "} // namespace api\n",
1680 );
1681 for name in ["api.Name", "api.Other"] {
1682 assert_eq!(
1683 class_roles(source, name),
1684 vec![CppOccurrenceRole::Definition],
1685 "{name} is a complete recovered definition"
1686 );
1687 }
1688 }
1689
1690 #[test]
1691 #[hardwon(
1692 "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1693 )]
1694 fn ordinary_class_roles_keep_their_plain_specifier_reading() {
1695 assert_eq!(
1696 class_roles("class Plain;\n", "Plain"),
1697 vec![CppOccurrenceRole::DeclarationOnly],
1698 "a forward declaration stays a declaration"
1699 );
1700 assert_eq!(
1701 class_roles("class Plain { };\n", "Plain"),
1702 vec![CppOccurrenceRole::Definition],
1703 "a complete class stays a definition"
1704 );
1705 }
1706}