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::{
23 VisibilityIndex, cpp_include_closure_reaches, cpp_name_component_nodes, declarator_name_node,
24 lexical_component_tiers, qualified_owner_components,
25};
26use crate::graph_support::CppSource;
27use crate::reconcile::{ReconciledIdentity, VisibleClass, reconcile_out_of_line_member_identity};
28use crate::template_heads::cpp_out_of_line_owner_names;
29use brokk_bifrost_core::analyzer::fq_name::{SegmentKind, segment_interner};
30use brokk_bifrost_core::analyzer::model::{CallableLinkage, Range, SignatureMetadata};
31use brokk_bifrost_core::analyzer::query_token::QueryToken;
32use brokk_bifrost_core::analyzer::symbol_path::parse_symbol_path_fq;
33use brokk_bifrost_core::analyzer::tree_walk::{node_for_exact_range, subtree_contains};
34pub use brokk_bifrost_core::analyzer::usages::common::same_node as cpp_same_node;
35use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, Language, ProjectFile};
36use brokk_bifrost_core::hash::HashMap;
37use brokk_bifrost_core::path_utils::rel_path_string;
38use brokk_bifrost_core::profiling;
39use std::sync::Arc;
40use tree_sitter::Node;
41use tree_sitter::{Parser, Tree};
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum CppCallableUnitRole {
45 DeclarationOnly,
46 Definition,
47 Both,
48 Unknown,
49}
50
51pub use brokk_bifrost_core::analyzer::cpp_facts::CppOccurrenceRole;
52pub struct CppOccurrenceClassifier {
53 tree: Tree,
54 source: String,
57 recovered_export_classes: CppRecoveredExportClassIndex,
60}
61
62impl CppOccurrenceClassifier {
63 pub fn new(source: &str) -> Option<Self> {
64 let mut parser = Parser::new();
65 parser
66 .set_language(&tree_sitter_cpp::LANGUAGE.into())
67 .ok()?;
68 parser.parse(source, None).map(|tree| {
69 let recovered_export_classes =
70 CppRecoveredExportClassIndex::build(tree.root_node(), source);
71 Self {
72 tree,
73 source: source.to_owned(),
74 recovered_export_classes,
75 }
76 })
77 }
78
79 pub fn classify(&self, candidate: &CodeUnit, range: &Range) -> CppOccurrenceRole {
80 cpp_occurrence_role_for_range(
81 &self.recovered_export_classes,
82 self.tree.root_node(),
83 &self.source,
84 candidate,
85 range,
86 )
87 }
88}
89
90pub fn cpp_callable_unit_role(
91 index: &dyn CodeUnitIndex,
92 callable: &CodeUnit,
93) -> CppCallableUnitRole {
94 cpp_callable_unit_role_from_metadata(callable, index.signature_metadata(callable))
95}
96
97fn cpp_callable_unit_role_from_metadata(
98 callable: &CodeUnit,
99 metadata: impl IntoIterator<Item = SignatureMetadata>,
100) -> CppCallableUnitRole {
101 if !callable.is_callable() {
102 return CppCallableUnitRole::Unknown;
103 }
104 let mut declaration = false;
105 let mut definition = false;
106 for metadata in metadata {
107 if metadata.is_declaration_only() {
108 declaration = true;
109 } else {
110 definition = true;
111 }
112 }
113 match (declaration, definition) {
114 (true, false) => CppCallableUnitRole::DeclarationOnly,
115 (false, true) => CppCallableUnitRole::Definition,
116 (true, true) => CppCallableUnitRole::Both,
117 (false, false) => CppCallableUnitRole::Unknown,
118 }
119}
120
121pub fn cpp_indexed_callable_linkage(
122 index: &dyn CodeUnitIndex,
123 callable: &CodeUnit,
124) -> Option<CallableLinkage> {
125 cpp_callable_linkage_from_metadata(index.signature_metadata(callable))
126}
127
128pub fn cpp_callable_linkage_from_metadata(
129 metadata: impl IntoIterator<Item = SignatureMetadata>,
130) -> Option<CallableLinkage> {
131 let mut external = false;
132 for metadata in metadata {
133 match metadata.callable_linkage() {
134 Some(CallableLinkage::Internal) => return Some(CallableLinkage::Internal),
135 Some(CallableLinkage::External) => external = true,
136 None => {}
137 }
138 }
139 external.then_some(CallableLinkage::External)
140}
141
142pub fn cpp_callable_definitions_share_identity_evidence(
148 index: &dyn CodeUnitIndex,
149 left: &CodeUnit,
150 right: &CodeUnit,
151 header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
152) -> bool {
153 left.fq_name() == right.fq_name()
154 && left.signature() == right.signature()
155 && (left.source() == right.source()
156 || (matches!(
157 cpp_indexed_callable_linkage(index, left),
158 Some(CallableLinkage::External)
159 ) && matches!(
160 cpp_indexed_callable_linkage(index, right),
161 Some(CallableLinkage::External)
162 ) && header_body_related(left.source(), right.source())))
163}
164
165pub fn cpp_callable_definitions_share_identity_evidence_with_visibility(
183 analyzer: &CppGraphSource<'_>,
184 visibility: &VisibilityIndex<'_>,
185 left: &CodeUnit,
186 right: &CodeUnit,
187 header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
188) -> bool {
189 left.fq_name() == right.fq_name()
190 && visibility.same_logical_callable(analyzer, left, right)
191 && (left.source() == right.source()
192 || (matches!(
193 visibility.callable_linkage(left),
194 Some(CallableLinkage::External)
195 ) && matches!(
196 visibility.callable_linkage(right),
197 Some(CallableLinkage::External)
198 ) && (header_body_related(left.source(), right.source())
199 || visibility.member_declaration_bridges_to_body(
200 analyzer,
201 left,
202 right,
203 &header_body_related,
204 )
205 || visibility.member_declaration_bridges_to_body(
206 analyzer,
207 right,
208 left,
209 &header_body_related,
210 ))))
211}
212
213pub fn cpp_is_range_for_binding_name(node: Node<'_>) -> bool {
217 let mut current = Some(node);
218 while let Some(candidate) = current {
219 let Some(parent) = candidate.parent() else {
220 return false;
221 };
222 if parent.kind() == "for_range_loop" {
223 return parent
224 .child_by_field_name("declarator")
225 .is_some_and(|declarator| {
226 cpp_range_for_declarator_contains_name(declarator, node)
227 });
228 }
229 current = Some(parent);
230 }
231 false
232}
233
234pub fn cpp_is_conversion_operator_target_type(mut node: Node<'_>) -> bool {
243 while let Some(parent) = node.parent() {
244 if parent.kind() == "operator_cast" {
245 return true;
246 }
247 if matches!(
248 parent.kind(),
249 "function_declarator" | "declaration" | "function_definition" | "translation_unit"
250 ) {
251 return false;
252 }
253 node = parent;
254 }
255 false
256}
257
258pub fn cpp_is_recovered_macro_character_token_type(node: Node<'_>) -> bool {
268 if node.kind() != "type_identifier" {
269 return false;
270 }
271 let Some(parameter) = node.parent() else {
272 return false;
273 };
274 if parameter.kind() != "parameter_declaration"
275 || parameter.child_by_field_name("type") != Some(node)
276 || parameter.child_by_field_name("declarator").is_some()
277 || parameter
278 .parent()
279 .is_none_or(|parent| parent.kind() != "parameter_list")
280 {
281 return false;
282 }
283
284 parameter
285 .prev_named_sibling()
286 .is_some_and(cpp_is_recovered_character_quote)
287 && parameter
288 .next_named_sibling()
289 .is_some_and(cpp_is_recovered_character_quote)
290}
291
292fn cpp_is_recovered_character_quote(node: Node<'_>) -> bool {
293 node.is_error()
294 && node.child_count() == 1
295 && node
296 .child(0)
297 .is_some_and(|quote| !quote.is_named() && quote.kind() == "'")
298}
299
300pub fn cpp_is_constructor_or_destructor_declarator_name(node: Node<'_>, source: &str) -> bool {
323 cpp_is_declared_constructor_or_destructor_name(node)
324 || cpp_is_recovered_constructor_or_destructor_name(node, source)
325}
326
327fn cpp_is_declared_constructor_or_destructor_name(node: Node<'_>) -> bool {
333 let mut name = node;
334 if let Some(parent) = name.parent()
335 && parent.kind() == "destructor_name"
336 {
337 name = parent;
338 }
339 while let Some(parent) = name.parent() {
343 if parent.kind() != "qualified_identifier"
344 || parent.child_by_field_name("name") != Some(name)
345 {
346 break;
347 }
348 name = parent;
349 }
350 let Some(declarator) = name.parent() else {
351 return false;
352 };
353 if declarator.kind() != "function_declarator"
354 || declarator.child_by_field_name("declarator") != Some(name)
355 {
356 return false;
357 }
358 let Some(owner) = declarator.parent() else {
359 return false;
360 };
361 matches!(owner.kind(), "declaration" | "function_definition")
362 && owner.child_by_field_name("declarator") == Some(declarator)
363 && owner.child_by_field_name("type").is_none()
364}
365
366fn cpp_is_recovered_constructor_or_destructor_name(node: Node<'_>, source: &str) -> bool {
384 if node.kind() != "identifier" {
385 return false;
386 }
387 let Some(call) = node.parent() else {
388 return false;
389 };
390 if call.kind() != "call_expression" || call.child_by_field_name("function") != Some(node) {
391 return false;
392 }
393 let mut current = call.parent();
394 while let Some(ancestor) = current {
395 if ancestor.kind() == "function_definition" {
396 return ancestor
397 .child_by_field_name("declarator")
398 .is_some_and(|declarator| declarator.kind() == "identifier")
399 && cpp_recovered_class_header_names(ancestor, node_text(node, source), source);
400 }
401 current = ancestor.parent();
402 }
403 false
404}
405
406fn cpp_recovered_class_header_names(definition: Node<'_>, name: &str, source: &str) -> bool {
409 let header_end = definition
410 .child_by_field_name("body")
411 .map_or_else(|| definition.end_byte(), |body| body.start_byte());
412 let mut stack = vec![definition];
413 while let Some(node) = stack.pop() {
414 if node.start_byte() >= header_end {
415 continue;
416 }
417 if matches!(
418 node.kind(),
419 "identifier" | "type_identifier" | "namespace_identifier"
420 ) && node_text(node, source) == name
421 {
422 return true;
423 }
424 let mut cursor = node.walk();
425 for child in node.named_children(&mut cursor) {
426 stack.push(child);
427 }
428 }
429 false
430}
431
432fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
433 let mut pending = vec![declarator];
434 while let Some(candidate) = pending.pop() {
435 match candidate.kind() {
436 "identifier" | "field_identifier" => {
437 if cpp_same_node(candidate, target) {
438 return true;
439 }
440 }
441 "structured_binding_declarator" => {
442 let mut cursor = candidate.walk();
443 if candidate
444 .named_children(&mut cursor)
445 .any(|name| cpp_same_node(name, target))
446 {
447 return true;
448 }
449 }
450 "pointer_declarator"
451 | "reference_declarator"
452 | "array_declarator"
453 | "attributed_declarator"
454 | "parenthesized_declarator"
455 | "function_declarator"
456 | "init_declarator" => {
457 if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
458 pending.push(inner);
459 }
460 }
461 _ => {}
462 }
463 }
464 false
465}
466
467fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
468 node.child_by_field_name("declarator").or_else(|| {
469 let mut cursor = node.walk();
470 node.named_children(&mut cursor).find(|child| {
471 matches!(
472 child.kind(),
473 "identifier"
474 | "field_identifier"
475 | "structured_binding_declarator"
476 | "pointer_declarator"
477 | "reference_declarator"
478 | "array_declarator"
479 | "attributed_declarator"
480 | "parenthesized_declarator"
481 | "function_declarator"
482 | "init_declarator"
483 )
484 })
485 })
486}
487
488pub fn cpp_header_body_files_are_related(
514 source: &dyn CppSource,
515 token: QueryToken<'_>,
516 left: &ProjectFile,
517 right: &ProjectFile,
518) -> bool {
519 match (
520 cpp_source_path_is_header(left),
521 cpp_source_path_is_header(right),
522 ) {
523 (true, false) => cpp_include_closure_reaches(source, token, right, left),
524 (false, true) => cpp_include_closure_reaches(source, token, left, right),
525 (true, true) => {
526 cpp_include_closure_reaches(source, token, left, right)
527 || cpp_include_closure_reaches(source, token, right, left)
528 }
529 (false, false) => false,
530 }
531}
532
533pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
534 let path = rel_path_string(source).to_ascii_lowercase();
535 matches!(
536 path.rsplit('.').next(),
537 Some("h" | "hin" | "hh" | "hpp" | "hxx")
538 )
539}
540
541pub(crate) fn cpp_occurrence_role_for_node(
542 node: Node<'_>,
543 source: &str,
544 candidate: &CodeUnit,
545) -> CppOccurrenceRole {
546 if !candidate.is_callable() && !candidate.is_class() {
547 return CppOccurrenceRole::Both;
548 }
549 if candidate.is_callable() {
550 return if subtree_contains(node, |descendant| {
551 descendant.kind() == "function_definition"
552 && descendant.child_by_field_name("body").is_some()
553 }) {
554 CppOccurrenceRole::Definition
555 } else {
556 CppOccurrenceRole::DeclarationOnly
557 };
558 }
559 if cpp_node_declares_alias(node, source, candidate) {
563 return CppOccurrenceRole::Both;
564 }
565 if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
566 return CppOccurrenceRole::Definition;
567 }
568 if !subtree_contains(node, |descendant| {
569 matches!(
570 descendant.kind(),
571 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
572 )
573 }) {
574 return CppOccurrenceRole::Both;
575 }
576 if subtree_contains(node, |descendant| {
577 matches!(
578 descendant.kind(),
579 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
580 ) && descendant.child_by_field_name("body").is_some()
581 }) {
582 CppOccurrenceRole::Definition
583 } else {
584 CppOccurrenceRole::DeclarationOnly
585 }
586}
587
588pub fn cpp_occurrence_role_for_range(
589 recovered_export_classes: &CppRecoveredExportClassIndex,
590 root: Node<'_>,
591 source: &str,
592 candidate: &CodeUnit,
593 range: &Range,
594) -> CppOccurrenceRole {
595 if !candidate.is_callable() && !candidate.is_class() {
596 return CppOccurrenceRole::Both;
597 }
598 let Some(node) = cpp_declaration_node_for_range(root, range) else {
599 return CppOccurrenceRole::Unknown;
600 };
601 if candidate.is_callable()
602 && subtree_contains(node, |descendant| {
603 descendant.kind() == "function_definition"
604 && descendant.child_by_field_name("body").is_some()
605 })
606 {
607 return CppOccurrenceRole::Definition;
608 }
609 if candidate.is_callable()
616 && !matches!(
617 node.kind(),
618 "declaration" | "field_declaration" | "function_definition"
619 )
620 && let Some(has_body) = recovered_callable_body_at(source, range)
621 {
622 return if has_body {
623 CppOccurrenceRole::Definition
624 } else {
625 CppOccurrenceRole::DeclarationOnly
626 };
627 }
628 if candidate.is_class()
629 && node.kind() == "function_definition"
630 && node.child_by_field_name("body").is_some()
631 {
632 return CppOccurrenceRole::Definition;
633 }
634 if candidate.is_class()
642 && let Some(has_body) = recovered_class_body_at(
643 recovered_export_classes,
644 root,
645 source,
646 candidate.identifier(),
647 range,
648 )
649 {
650 return if has_body {
651 CppOccurrenceRole::Definition
652 } else {
653 CppOccurrenceRole::DeclarationOnly
654 };
655 }
656 cpp_occurrence_role_for_node(node, source, candidate)
657}
658
659fn cpp_node_declares_alias(node: Node<'_>, source: &str, candidate: &CodeUnit) -> bool {
661 match node.kind() {
662 "alias_declaration" => {
663 node.child_by_field_name("name")
664 .and_then(|name| name.utf8_text(source.as_bytes()).ok())
665 == Some(candidate.identifier())
666 }
667 "type_definition" => {
668 let mut cursor = node.walk();
669 node.children_by_field_name("declarator", &mut cursor)
670 .any(|declarator| {
671 crate::declarations::extract_typedef_declarator_name(declarator, source)
672 .as_deref()
673 == Some(candidate.identifier())
674 })
675 }
676 _ => false,
677 }
678}
679
680pub fn cpp_range_is_pure_virtual_declaration(root: Node<'_>, source: &str, range: &Range) -> bool {
691 let Some(node) = cpp_declaration_node_for_range(root, range) else {
692 return false;
693 };
694 let mut current = Some(node);
695 while let Some(node) = current {
696 match node.kind() {
697 "field_declaration" => {
698 return node
702 .child_by_field_name("default_value")
703 .is_some_and(|value| {
704 value.kind() == "number_literal" && node_text(value, source) == "0"
705 })
706 && node
707 .child_by_field_name("declarator")
708 .and_then(extract_function_declarator)
709 .is_some();
710 }
711 "field_declaration_list" | "function_definition" | "translation_unit" => return false,
714 _ => current = node.parent(),
715 }
716 }
717 false
718}
719
720fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
721 node_for_exact_range(root, range).or_else(|| {
722 root.descendant_for_byte_range(range.start_byte, range.end_byte)
723 .and_then(|mut node| {
724 while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
725 node = node.parent()?;
726 }
727 Some(node)
728 })
729 })
730}
731
732#[derive(Default)]
744pub struct CppReconciledDefinitionIndex {
745 pub rekeyed: Vec<CodeUnit>,
747 pub provisional_of: HashMap<CodeUnit, CodeUnit>,
749}
750
751#[derive(Default)]
755pub struct CppReconciledGroup {
756 pub by_name: HashMap<String, Arc<CppReconciledDefinitionIndex>>,
757 pub canonical_of: HashMap<CodeUnit, CodeUnit>,
758}
759
760pub fn cpp_reconcile_group_key_for_fq(
763 fq: &brokk_bifrost_core::analyzer::fq_name::FqName,
764) -> Option<CppReconcileGroupKey> {
765 let interner = segment_interner();
766 let (member_identifier, _) = interner.resolve(fq.last()?);
767 if member_identifier.is_empty() {
768 return None;
769 }
770 let owner_terminal = fq
771 .segments()
772 .len()
773 .checked_sub(2)
774 .map(|index| interner.resolve(fq.segments()[index]).0.to_string());
775 Some(CppReconcileGroupKey {
776 member_identifier: member_identifier.to_string(),
777 owner_terminal,
778 })
779}
780
781#[derive(Debug, Clone, PartialEq, Eq, Hash)]
797pub struct CppReconcileGroupKey {
798 pub member_identifier: String,
801 pub owner_terminal: Option<String>,
805}
806
807pub fn cpp_reconcile_group_key(fq_name: &str) -> Option<CppReconcileGroupKey> {
814 let interner = segment_interner();
815 let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
816 let (member_identifier, _) = interner.resolve(query_fq.last()?);
817 if member_identifier.is_empty() {
818 return None;
819 }
820 let owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
830 let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
831 text.rsplit_once('$')
836 .map_or(text, |(_, tail)| tail)
837 .to_string()
838 });
839 Some(CppReconcileGroupKey {
840 member_identifier: member_identifier.to_string(),
841 owner_terminal,
842 })
843}
844
845pub struct CppReconcileCandidates {
851 by_owner_terminal: HashMap<String, Vec<CodeUnit>>,
852 all: Vec<CodeUnit>,
856}
857
858impl CppReconcileCandidates {
859 fn for_group(&self, key: &CppReconcileGroupKey) -> &[CodeUnit] {
862 match &key.owner_terminal {
863 Some(owner_terminal) => self
864 .by_owner_terminal
865 .get(owner_terminal)
866 .map_or(&[][..], Vec::as_slice),
867 None => &self.all,
868 }
869 }
870
871 pub fn iter(&self) -> impl Iterator<Item = &CodeUnit> {
873 self.all.iter()
874 }
875
876 pub fn bucketed_len(&self) -> usize {
879 self.by_owner_terminal.values().map(Vec::len).sum()
880 }
881}
882
883pub fn cpp_reconcile_candidates_from_units(
890 candidates: impl IntoIterator<Item = CodeUnit>,
891 keep_going: &dyn Fn() -> bool,
892) -> Option<CppReconcileCandidates> {
893 let mut candidates = candidates.into_iter().collect::<Vec<_>>();
894 candidates.sort();
895 candidates.dedup();
896 let interner = segment_interner();
897 let mut by_owner_terminal: HashMap<String, Vec<CodeUnit>> = HashMap::default();
898 let mut all = Vec::new();
899 for (index, unit) in candidates.into_iter().enumerate() {
900 if index % CANDIDATE_BUCKETING_POLL_STRIDE == 0 && !keep_going() {
904 return None;
905 }
906 if !unit.is_callable() {
907 continue;
908 }
909 let owner_terminal = unit
910 .fq()
911 .segments()
912 .iter()
913 .filter_map(|&segment| {
914 let (text, kind) = interner.resolve(segment);
915 matches!(
919 kind,
920 SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
921 )
922 .then_some(text)
923 })
924 .last();
925 if let Some(owner_terminal) = owner_terminal {
926 by_owner_terminal
927 .entry(owner_terminal.to_string())
928 .or_default()
929 .push(unit.clone());
930 }
931 all.push(unit);
932 }
933 if !keep_going() {
936 return None;
937 }
938 Some(CppReconcileCandidates {
939 by_owner_terminal,
940 all,
941 })
942}
943
944const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
946
947pub fn cpp_reconcile_group(
964 cpp: &dyn CppSource,
965 token: QueryToken<'_>,
966 key: &CppReconcileGroupKey,
967 candidates: &CppReconcileCandidates,
968 keep_going: &dyn Fn() -> bool,
969 on_candidate: &dyn Fn(),
970) -> Option<CppReconciledGroup> {
971 let _scope = profiling::scope_with(|| {
972 format!(
973 "cpp.reconciled.build[{}#{}]",
974 key.member_identifier,
975 key.owner_terminal.as_deref().unwrap_or("*")
976 )
977 });
978 let mut groups: HashMap<String, CppReconciledDefinitionIndex> = HashMap::default();
979 let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
980 for unit in candidates.for_group(key) {
981 if !keep_going() {
986 return None;
987 }
988 on_candidate();
989 let _candidate =
992 profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
993 let role = {
994 let _role = profiling::scope("cpp.reconcile.role");
995 cpp.stored_callable_unit_role(unit)
996 };
997 if !matches!(
998 role,
999 CppCallableUnitRole::Definition | CppCallableUnitRole::Both
1000 ) {
1001 continue;
1002 }
1003 let Some(reconciled) =
1004 cpp_reconcile_definition_identity(cpp, token, unit, &mut using_by_file, keep_going)
1005 else {
1006 continue;
1007 };
1008 let canonical_fq = reconciled.fq_name();
1009 if unit.fq_name() == canonical_fq {
1016 continue;
1017 }
1018 let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
1027 let fq = cpp_member_fq(&reconciled.package, &short_name);
1028 let rekeyed = CodeUnit::with_signature_and_fq(
1029 unit.source().clone(),
1030 unit.kind(),
1031 reconciled.package,
1032 short_name,
1033 unit.signature().map(str::to_string),
1034 unit.is_synthetic(),
1035 fq,
1036 );
1037 let index = groups.entry(canonical_fq).or_default();
1038 index.rekeyed.push(rekeyed.clone());
1039 index.provisional_of.insert(rekeyed, unit.clone());
1040 }
1041 let canonical_of = groups
1042 .values()
1043 .flat_map(|index| {
1044 index
1045 .provisional_of
1046 .iter()
1047 .map(|(canonical, physical)| (physical.clone(), canonical.clone()))
1048 })
1049 .collect();
1050 let group = CppReconciledGroup {
1051 canonical_of,
1052 by_name: groups
1053 .into_iter()
1054 .map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
1055 .collect(),
1056 };
1057 keep_going().then_some(group)
1060}
1061
1062fn cpp_reconcile_definition_identity(
1068 cpp: &dyn CppSource,
1069 token: QueryToken<'_>,
1070 unit: &CodeUnit,
1071 using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
1072 keep_going: &dyn Fn() -> bool,
1073) -> Option<ReconciledIdentity> {
1074 let interner = segment_interner();
1084 let mut provisional_owner_segments: Vec<&str> = Vec::new();
1085 let mut member: Option<&str> = None;
1086 for &segment in unit.fq().segments() {
1087 let (text, kind) = interner.resolve(segment);
1088 match kind {
1089 SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
1090 if member.is_some() {
1094 return None;
1095 }
1096 if !text.is_empty() {
1097 provisional_owner_segments.push(text);
1098 }
1099 }
1100 SegmentKind::Member => member = Some(text),
1101 _ => return None,
1102 }
1103 }
1104 let member = member?;
1105 let single_owner = cpp_single_owner_context(cpp, token, unit, keep_going)?;
1109 let owner_segments = single_owner.as_ref().map_or_else(
1110 || provisional_owner_segments,
1111 |context| vec![context.owner.as_str()],
1112 );
1113 if owner_segments.is_empty() {
1114 return None;
1115 }
1116
1117 let visible = {
1118 let _visible = profiling::scope_with(|| {
1119 format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
1120 });
1121 cpp.visible_type_units(unit.source())
1122 };
1123 let class_table: Vec<VisibleClass> = visible
1124 .iter()
1125 .filter(|candidate| candidate.is_class())
1126 .map(|candidate| VisibleClass {
1127 package: candidate.package_name(),
1128 nested_short_name: candidate.short_name(),
1129 })
1130 .collect();
1131
1132 if let Some(context) = &single_owner {
1133 if context.global {
1134 return keep_going().then(|| {
1136 reconcile_out_of_line_member_identity(&owner_segments, member, &[""], &class_table)
1137 })?;
1138 }
1139 for tier in
1143 lexical_component_tiers(&[], false, &context.lexical).take(context.lexical.len())
1144 {
1145 if !keep_going() {
1146 return None;
1147 }
1148 let namespace = tier.join("::");
1149 if let Some(identity) = reconcile_out_of_line_member_identity(
1150 &owner_segments,
1151 member,
1152 &[namespace.as_str()],
1153 &class_table,
1154 ) {
1155 return keep_going().then_some(identity);
1156 }
1157 }
1158 }
1159 if !keep_going() {
1160 return None;
1161 }
1162 let using = using_by_file
1163 .entry(unit.source().clone())
1164 .or_insert_with(|| {
1165 Arc::new(
1166 cpp.declaration_source_facts(token, unit.source())
1167 .map(|source| {
1168 source
1169 .facts
1170 .using_namespaces
1171 .iter()
1172 .map(|(_, name)| name.clone())
1173 .collect()
1174 })
1175 .unwrap_or_default(),
1176 )
1177 })
1178 .clone();
1179 let mut namespace_candidates: Vec<&str> = vec![""];
1180 namespace_candidates.extend(using.iter().map(String::as_str));
1181
1182 reconcile_out_of_line_member_identity(
1183 &owner_segments,
1184 member,
1185 &namespace_candidates,
1186 &class_table,
1187 )
1188}
1189
1190#[derive(PartialEq, Eq)]
1191struct CppSingleOwnerContext {
1192 owner: String,
1193 global: bool,
1194 lexical: Vec<String>,
1195}
1196
1197fn cpp_single_owner_context(
1200 cpp: &dyn CppSource,
1201 token: QueryToken<'_>,
1202 unit: &CodeUnit,
1203 keep_going: &dyn Fn() -> bool,
1204) -> Option<Option<CppSingleOwnerContext>> {
1205 let facts = cpp.declaration_source_facts(token, unit.source())?;
1206 let mut has_single = false;
1210 for fact in facts.for_unit(unit) {
1211 if !keep_going() {
1212 return None;
1213 }
1214 has_single |= fact.written_owner.len() == 1;
1215 }
1216 if !has_single {
1217 return keep_going().then_some(None);
1218 }
1219 let syntax = cpp.prepared_syntax(token, unit.source())?;
1220 if !keep_going() {
1221 return None;
1222 }
1223 let scopes = cpp.orphaned_namespace_scopes(token, unit.source());
1224 let root = syntax.tree().root_node();
1225 let source = syntax.source();
1226 let mut agreed = None;
1227 for fact in facts.for_unit(unit) {
1228 if !keep_going() {
1229 return None;
1230 }
1231 let declaration = facts.source.declaration(fact.declaration);
1232 let occurrence = facts.source.occurrence(declaration.occurrence).range;
1233 let name_range = facts.source.occurrence(declaration.name?).range;
1234 let mut node =
1235 root.descendant_for_byte_range(occurrence.start_byte, occurrence.end_byte)?;
1236 let selected = loop {
1239 if !keep_going()
1240 || node.start_byte() != occurrence.start_byte
1241 || node.end_byte() != occurrence.end_byte
1242 {
1243 return None;
1244 }
1245 if matches!(
1246 node.kind(),
1247 "function_definition" | "function_declarator" | "declaration" | "field_declaration"
1248 ) {
1249 break node;
1250 }
1251 node = node.parent()?;
1252 };
1253 let callable = if selected.kind() == "function_declarator" {
1254 selected
1255 } else {
1256 selected
1257 .child_by_field_name("declarator")
1258 .and_then(extract_function_declarator)?
1259 };
1260 let name = declarator_name_node(callable)?;
1261 let terminal = cpp_name_component_nodes(name)?.pop()?;
1262 if name.is_missing()
1263 || name.has_error()
1264 || terminal.is_missing()
1265 || terminal.start_byte() != name_range.start_byte
1266 || terminal.end_byte() != name_range.end_byte
1267 || node_text(terminal, source) != unit.identifier()
1268 {
1269 return None;
1270 }
1271 let qualified = qualified_owner_components(name, source)?;
1272 if qualified.names.len() != 1 || qualified.names != fact.written_owner {
1273 return None;
1274 }
1275 let owner = match cpp_out_of_line_owner_names(callable, source) {
1278 Some(mut names) if names.len() == 1 => names.pop()?,
1279 _ => qualified.names.into_iter().next()?,
1280 };
1281 let proof = CppSingleOwnerContext {
1282 owner,
1283 global: qualified.global,
1284 lexical: scopes.enclosing_namespace_components(selected, source),
1285 };
1286 if !keep_going() || agreed.as_ref().is_some_and(|prior| prior != &proof) {
1287 return None;
1288 }
1289 agreed = Some(proof);
1290 }
1291 if !keep_going() {
1292 return None;
1293 }
1294 Some(Some(agreed?))
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299
1300 #[test]
1301 fn range_for_binder_grammar() {
1302 use crate::grammar_test_support::{SOURCE, at, field, nodes, tree};
1303
1304 let parsed = tree(SOURCE);
1305 let cases = [
1306 (
1307 314..348,
1308 "structured_binding_declarator",
1309 324..336,
1310 None,
1311 339..344,
1312 &[(325..328, "key"), (330..335, "value")][..],
1313 ),
1314 (
1315 350..376,
1316 "reference_declarator",
1317 360..365,
1318 None,
1319 368..372,
1320 &[(361..365, "same")][..],
1321 ),
1322 (
1323 378..407,
1324 "pointer_declarator",
1325 388..392,
1326 Some(389..392),
1327 395..403,
1328 &[(389..392, "ptr")][..],
1329 ),
1330 (
1331 409..452,
1332 "attributed_declarator",
1333 419..440,
1334 None,
1335 443..448,
1336 &[(419..423, "item")][..],
1337 ),
1338 (
1339 454..480,
1340 "reference_declarator",
1341 464..467,
1342 None,
1343 470..476,
1344 &[(465..467, "\u{3bb}")][..],
1345 ),
1346 ];
1347 for (span, kind, declarator_span, inner_field, iterable_span, binders) in cases {
1348 let range = at(parsed.root_node(), "for_range_loop", span);
1349 field(range, "declarator", kind, declarator_span);
1350 field(range, "right", "identifier", iterable_span.clone());
1351 let declarator = range.child_by_field_name("declarator").unwrap();
1352 assert_eq!(
1353 declarator
1354 .child_by_field_name("declarator")
1355 .map(|node| node.byte_range()),
1356 inner_field
1357 );
1358 for (binder_span, text) in binders {
1359 let binder = at(range, "identifier", binder_span.clone());
1360 assert_eq!(binder.utf8_text(SOURCE.as_bytes()).unwrap(), *text);
1361 assert!(crate::identity::cpp_is_range_for_binding_name(binder));
1362 }
1363 for identifier in nodes(range)
1366 .into_iter()
1367 .filter(|node| node.kind() == "identifier")
1368 {
1369 let expected = binders
1370 .iter()
1371 .any(|(span, _)| identifier.byte_range() == *span);
1372 assert_eq!(
1373 crate::identity::cpp_is_range_for_binding_name(identifier),
1374 expected,
1375 "identifier {:?}",
1376 identifier.byte_range()
1377 );
1378 }
1379 assert!(!crate::identity::cpp_is_range_for_binding_name(at(
1380 range,
1381 "identifier",
1382 iterable_span
1383 )));
1384 }
1385 }
1386
1387 use super::*;
1388 use brokk_bifrost_test_macros::hardwon;
1389
1390 #[test]
1391 fn physical_identity_cohorts_keep_recorded_owner_and_member_boundaries() {
1392 use brokk_bifrost_core::analyzer::fq_name::FqName;
1393 let mut fq = FqName::new();
1394 for (text, kind) in [
1395 ("ns", SegmentKind::Package),
1396 ("Owner$Literal", SegmentKind::Type),
1397 ("operator std::string", SegmentKind::Member),
1398 ] {
1399 fq.push(segment_interner().intern(text, kind));
1400 }
1401 let key = cpp_reconcile_group_key_for_fq(&fq).unwrap();
1402 assert_eq!(key.owner_terminal.as_deref(), Some("Owner$Literal"));
1403 assert_eq!(key.member_identifier, "operator std::string");
1404 }
1405
1406 #[test]
1407 #[hardwon("#2488: terminal cancellation must not publish identity cohorts")]
1408 fn candidate_cohorts_recheck_terminal_cancellation_before_publish() {
1409 use std::cell::Cell;
1410
1411 let unit = CodeUnit::new(
1412 ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1413 brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1414 "",
1415 "Owner.run",
1416 );
1417 let cancel_after_last_candidate = {
1418 let active = Cell::new(true);
1419 move || active.replace(false)
1420 };
1421 assert!(
1422 cpp_reconcile_candidates_from_units(
1423 std::iter::once(unit.clone()),
1424 &cancel_after_last_candidate,
1425 )
1426 .is_none(),
1427 "a cancellation observed after the last sub-256 candidate must prevent publication"
1428 );
1429
1430 let candidates = (0..=CANDIDATE_BUCKETING_POLL_STRIDE)
1431 .map(|index| {
1432 CodeUnit::new(
1433 ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1434 brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1435 "",
1436 format!("Owner{index}.run"),
1437 )
1438 })
1439 .collect::<Vec<_>>();
1440 let cancel_at_bucket_boundary = {
1441 let active = Cell::new(true);
1442 move || active.replace(false)
1443 };
1444 assert!(
1445 cpp_reconcile_candidates_from_units(candidates, &cancel_at_bucket_boundary).is_none(),
1446 "a cancellation at the bucket boundary must prevent a partial cohort from publishing"
1447 );
1448
1449 let cancelled = || false;
1450 assert!(
1451 cpp_reconcile_candidates_from_units(Vec::<CodeUnit>::new(), &cancelled).is_none(),
1452 "an empty cancelled cohort must not publish"
1453 );
1454 }
1455
1456 fn parse_cpp(source: &str) -> Tree {
1457 crate::diagnostics::parse_cpp_tree(source)
1458 }
1459
1460 fn is_declarator_name(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1461 let end = start + text.len();
1462 assert_eq!(&source[start..end], text, "the probe must name the token");
1463 let node = tree
1464 .root_node()
1465 .named_descendant_for_byte_range(start, end)
1466 .expect("a node spans the probed range");
1467 assert_eq!(
1468 (node.start_byte(), node.end_byte()),
1469 (start, end),
1470 "the probed range must be exactly one node: {}",
1471 node.to_sexp()
1472 );
1473 cpp_is_constructor_or_destructor_declarator_name(node, source)
1474 }
1475
1476 fn is_conversion_target(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1477 let end = start + text.len();
1478 assert_eq!(&source[start..end], text, "the probe must name the token");
1479 let node = tree
1480 .root_node()
1481 .named_descendant_for_byte_range(start, end)
1482 .expect("a node spans the probed range");
1483 assert_eq!(
1484 (node.start_byte(), node.end_byte()),
1485 (start, end),
1486 "the probed range must be exactly one node: {}",
1487 node.to_sexp()
1488 );
1489 cpp_is_conversion_operator_target_type(node)
1490 }
1491
1492 fn is_recovered_macro_character_type(
1493 tree: &Tree,
1494 source: &str,
1495 start: usize,
1496 text: &str,
1497 ) -> bool {
1498 let end = start + text.len();
1499 assert_eq!(&source[start..end], text, "the probe must name the token");
1500 let node = tree
1501 .root_node()
1502 .named_descendant_for_byte_range(start, end)
1503 .expect("a node spans the probed range");
1504 cpp_is_recovered_macro_character_token_type(node)
1505 }
1506
1507 #[test]
1508 #[hardwon(
1509 "b114d87215ca: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1510 )]
1511 fn recovered_macro_character_tokens_are_not_type_references() {
1512 let source = concat!(
1513 "struct I {};\n",
1514 "#define STRING_TOKEN_(name, ...)\n",
1515 "struct Schema {\n",
1516 " STRING_TOKEN_(MaxItems, 'm', 'I')\n",
1517 " void ordinary(I value);\n",
1518 " void malformed(I value, @);\n",
1519 " void use() { I value; consume('I'); }\n",
1520 "};\n",
1521 );
1522 let tree = parse_cpp(source);
1523 let recovered_m = source.find("'m'").expect("recovered m") + 1;
1524 let recovered_i = source.find("'I'").expect("recovered I") + 1;
1525 for (label, start, text) in [
1526 ("lowercase character token", recovered_m, "m"),
1527 ("uppercase character token", recovered_i, "I"),
1528 ] {
1529 assert!(
1530 is_recovered_macro_character_type(&tree, source, start, text),
1531 "{label} must match the exact recovery role"
1532 );
1533 }
1534
1535 let macro_first_argument = source.find("MaxItems").expect("macro first argument");
1536 let ordinary = source.find("ordinary(I").expect("ordinary parameter") + "ordinary(".len();
1537 let malformed =
1538 source.find("malformed(I").expect("malformed parameter") + "malformed(".len();
1539 let local = source
1540 .find("I value; consume")
1541 .expect("local type reference");
1542 let expression_character = source.rfind("'I'").expect("expression character") + 1;
1543 for (label, start, text) in [
1544 ("unquoted macro argument", macro_first_argument, "MaxItems"),
1545 ("ordinary parameter type", ordinary, "I"),
1546 ("parameter beside another error", malformed, "I"),
1547 ("local type reference", local, "I"),
1548 ("expression character literal", expression_character, "I"),
1549 ] {
1550 assert!(
1551 !is_recovered_macro_character_type(&tree, source, start, text),
1552 "{label} must remain outside the recovery role"
1553 );
1554 }
1555 }
1556
1557 #[test]
1558 #[hardwon(
1559 "0733cadab142: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1560 )]
1561 fn conversion_operator_target_components_are_identity_syntax_only() {
1562 let source = concat!(
1563 "namespace other { struct Target {}; template<class T> struct Box {}; }\n",
1564 "using other::Target;\n",
1565 "struct Source {\n",
1566 " operator Target() const;\n",
1567 " operator other::Target() const { return other::Target{}; }\n",
1568 " template<class T> operator other::Box<T>() const { return {}; }\n",
1569 " operator other::Target const&() const;\n",
1570 " operator other::Target*() const;\n",
1571 " other::Target ordinary() const {\n",
1572 " return reinterpret_cast<other::Target&>(*this);\n",
1573 " }\n",
1574 " other::Target operator+() const { return {}; }\n",
1575 "};\n",
1576 );
1577 let tree = parse_cpp(source);
1578
1579 let bare = source.find("operator Target").expect("bare target") + "operator ".len();
1580 let qualified = source
1581 .find("operator other::Target()")
1582 .expect("qualified target")
1583 + "operator ".len();
1584 let template = source
1585 .find("operator other::Box<T>")
1586 .expect("template target")
1587 + "operator ".len();
1588 let cv_reference = source
1589 .find("operator other::Target const&")
1590 .expect("cv-reference target")
1591 + "operator other::".len();
1592 let pointer = source
1593 .find("operator other::Target*")
1594 .expect("pointer target")
1595 + "operator other::".len();
1596
1597 for (label, start, text) in [
1598 ("bare target", bare, "Target"),
1599 ("qualified target scope", qualified, "other"),
1600 (
1601 "qualified target name",
1602 qualified + "other::".len(),
1603 "Target",
1604 ),
1605 ("template target scope", template, "other"),
1606 ("template target name", template + "other::".len(), "Box"),
1607 (
1608 "template target argument",
1609 template + "other::Box<".len(),
1610 "T",
1611 ),
1612 ("cv-reference target", cv_reference, "Target"),
1613 ("pointer target", pointer, "Target"),
1614 ] {
1615 assert!(
1616 is_conversion_target(&tree, source, start, text),
1617 "the {label} at byte {start} belongs to the conversion identity"
1618 );
1619 }
1620
1621 let ordinary_return = source
1622 .find("other::Target ordinary")
1623 .expect("ordinary return");
1624 let body_cast = source
1625 .find("reinterpret_cast<other::Target")
1626 .expect("body cast")
1627 + "reinterpret_cast<".len();
1628 let overloaded_return = source
1629 .find("other::Target operator+")
1630 .expect("overloaded operator return");
1631 for (label, start) in [
1632 ("ordinary return type", ordinary_return),
1633 ("body cast target", body_cast),
1634 ("overloaded-operator return type", overloaded_return),
1635 ] {
1636 assert!(
1637 !is_conversion_target(&tree, source, start, "other"),
1638 "the {label} at byte {start} stays a reference"
1639 );
1640 }
1641 }
1642
1643 #[test]
1648 #[hardwon(
1649 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1650 )]
1651 fn declared_constructor_and_destructor_declarator_names_are_not_references() {
1652 let source = concat!(
1653 "class Foo {\n",
1654 "public:\n",
1655 " Foo();\n",
1656 " Foo(const Foo&);\n",
1657 " ~Foo();\n",
1658 " void m();\n",
1659 "};\n",
1660 "Foo::Foo() {}\n",
1661 "Foo::~Foo() {}\n",
1662 "void Foo::m() {}\n",
1663 );
1664 let tree = parse_cpp(source);
1665
1666 for (label, start, text) in [
1667 (
1668 "constructor declaration",
1669 source.find("Foo();").expect("ctor"),
1670 "Foo",
1671 ),
1672 (
1673 "copy constructor declaration",
1674 source.find("Foo(const Foo&);").expect("copy ctor"),
1675 "Foo",
1676 ),
1677 (
1678 "destructor name",
1679 source.find("~Foo();").expect("dtor"),
1680 "~Foo",
1681 ),
1682 (
1683 "identifier inside the destructor name",
1684 source.find("~Foo();").expect("dtor") + "~".len(),
1685 "Foo",
1686 ),
1687 (
1688 "out-of-line constructor definition name",
1689 source.find("Foo::Foo() {}").expect("out-of-line ctor") + "Foo::".len(),
1690 "Foo",
1691 ),
1692 (
1693 "out-of-line destructor definition name",
1694 source.find("Foo::~Foo() {}").expect("out-of-line dtor") + "Foo::".len(),
1695 "~Foo",
1696 ),
1697 ] {
1698 assert!(
1699 is_declarator_name(&tree, source, start, text),
1700 "the {label} at byte {start} is a declaration occurrence"
1701 );
1702 }
1703
1704 for (label, start, text) in [
1705 (
1706 "class name",
1707 source.find("class Foo {").expect("class") + "class ".len(),
1708 "Foo",
1709 ),
1710 (
1711 "parameter type",
1712 source.find("const Foo&").expect("parameter type") + "const ".len(),
1713 "Foo",
1714 ),
1715 (
1716 "owning scope of an out-of-line constructor",
1717 source.find("Foo::Foo() {}").expect("out-of-line ctor"),
1718 "Foo",
1719 ),
1720 (
1721 "owning scope of an out-of-line destructor",
1722 source.find("Foo::~Foo() {}").expect("out-of-line dtor"),
1723 "Foo",
1724 ),
1725 (
1726 "out-of-line method name",
1727 source.find("void Foo::m() {}").expect("out-of-line method") + "void Foo::".len(),
1728 "m",
1729 ),
1730 ] {
1731 assert!(
1732 !is_declarator_name(&tree, source, start, text),
1733 "the {label} at byte {start} stays a reference"
1734 );
1735 }
1736 }
1737
1738 #[test]
1742 #[hardwon(
1743 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1744 )]
1745 fn constructor_call_sites_stay_references() {
1746 let source = concat!(
1747 "struct B { B(int); };\n",
1748 "struct D : B {\n",
1749 " D(int x) : B(x), base_(x) {}\n",
1750 " int base_;\n",
1751 "};\n",
1752 "void g() {\n",
1753 " D* p = new D(1);\n",
1754 " D x(2);\n",
1755 " D(3);\n",
1756 " g();\n",
1757 "}\n",
1758 );
1759 let tree = parse_cpp(source);
1760
1761 let inline_declarator = source.find("D(int x)").expect("inline constructor");
1762 assert!(
1763 is_declarator_name(&tree, source, inline_declarator, "D"),
1764 "an inline constructor definition name is still a declarator"
1765 );
1766
1767 for (label, start, text) in [
1768 (
1769 "base member initializer",
1770 source.find(": B(x)").expect("base initializer") + ": ".len(),
1771 "B",
1772 ),
1773 (
1774 "field member initializer",
1775 source.find("base_(x) {}").expect("field initializer"),
1776 "base_",
1777 ),
1778 (
1779 "new expression type",
1780 source.find("new D(1)").expect("new expression") + "new ".len(),
1781 "D",
1782 ),
1783 (
1784 "direct initialization type",
1785 source.find("D x(2)").expect("direct initialization"),
1786 "D",
1787 ),
1788 (
1789 "temporary construction statement",
1790 source.find("D(3)").expect("temporary"),
1791 "D",
1792 ),
1793 (
1794 "recursive call in a real body",
1795 source.find("g();").expect("recursive call"),
1796 "g",
1797 ),
1798 ] {
1799 assert!(
1800 !is_declarator_name(&tree, source, start, text),
1801 "the {label} at byte {start} is a reference"
1802 );
1803 }
1804 }
1805
1806 #[test]
1811 #[hardwon(
1812 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1813 )]
1814 fn a_constructor_declarator_the_parse_read_as_a_call_is_not_a_reference() {
1815 let source = concat!(
1816 "class SAMPLE_EXPORT Properties {\n",
1817 " public:\n",
1818 " Properties();\n",
1819 " DISALLOW_COPY_AND_ASSIGN(Properties);\n",
1820 " int size() const;\n",
1821 " int total() { return size(); }\n",
1822 "};\n",
1823 );
1824 let tree = parse_cpp(source);
1825
1826 let recovered = source.find("Properties();").expect("recovered constructor");
1827 assert!(
1828 is_declarator_name(&tree, source, recovered, "Properties"),
1829 "a constructor declaration the parse read as a call is still a declarator"
1830 );
1831
1832 for (label, start, text) in [
1833 (
1834 "class name in the recovered header",
1835 source
1836 .find("class SAMPLE_EXPORT Properties")
1837 .expect("class")
1838 + "class SAMPLE_EXPORT ".len(),
1839 "Properties",
1840 ),
1841 (
1842 "macro invocation in the recovered body",
1843 source
1844 .find("DISALLOW_COPY_AND_ASSIGN(Properties);")
1845 .expect("macro invocation"),
1846 "DISALLOW_COPY_AND_ASSIGN",
1847 ),
1848 (
1849 "call inside a method body the recovery kept",
1850 source.find("return size();").expect("member call") + "return ".len(),
1851 "size",
1852 ),
1853 ] {
1854 assert!(
1855 !is_declarator_name(&tree, source, start, text),
1856 "the {label} at byte {start} stays a reference"
1857 );
1858 }
1859 }
1860
1861 fn class_roles(source: &str, name: &str) -> Vec<CppOccurrenceRole> {
1864 let tree = parse_cpp(source);
1865 let file = ProjectFile::new(std::env::temp_dir(), "occurrence-role.hpp");
1866 let parsed = crate::adapter::parse_cpp_file(&file, source, &tree);
1867 let unit = parsed
1868 .declarations()
1869 .iter()
1870 .find(|unit| unit.is_class() && unit.fq_name() == name)
1871 .unwrap_or_else(|| panic!("missing class {name}: {parsed:#?}"));
1872 let ranges = parsed.declaration_ranges(unit);
1873 assert!(!ranges.is_empty(), "{name} must have a declaration range");
1874 ranges
1875 .iter()
1876 .map(|range| {
1877 cpp_occurrence_role_for_range(
1878 &CppRecoveredExportClassIndex::build(tree.root_node(), source),
1879 tree.root_node(),
1880 source,
1881 unit,
1882 range,
1883 )
1884 })
1885 .collect()
1886 }
1887
1888 #[test]
1894 #[hardwon(
1895 "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1896 )]
1897 fn recovered_export_macro_classes_are_definitions_without_an_ordinary_class() {
1898 let source = concat!(
1899 "namespace api {\n",
1900 "class PROJECT_PUBLIC_API(2, 0) Name final {\n",
1901 " public:\n",
1902 " Name();\n",
1903 "};\n",
1904 "class PROJECT_PUBLIC_API(2, 0) Other : public Name {\n",
1905 " public:\n",
1906 " Other();\n",
1907 "};\n",
1908 "} // namespace api\n",
1909 );
1910 for name in ["api.Name", "api.Other"] {
1911 assert_eq!(
1912 class_roles(source, name),
1913 vec![CppOccurrenceRole::Definition],
1914 "{name} is a complete recovered definition"
1915 );
1916 }
1917 }
1918
1919 #[test]
1920 #[hardwon(
1921 "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1922 )]
1923 fn ordinary_class_roles_keep_their_plain_specifier_reading() {
1924 assert_eq!(
1925 class_roles("class Plain;\n", "Plain"),
1926 vec![CppOccurrenceRole::DeclarationOnly],
1927 "a forward declaration stays a declaration"
1928 );
1929 assert_eq!(
1930 class_roles("class Plain { };\n", "Plain"),
1931 vec![CppOccurrenceRole::Definition],
1932 "a complete class stays a definition"
1933 );
1934 }
1935
1936 #[test]
1937 fn partial_declaration_range_grammar_boundary() {
1938 for prefix in ["", "// offset\n"] {
1939 let source = format!("{prefix}void first(); void later() {{}}");
1940 let mut parser = tree_sitter::Parser::new();
1941 parser
1942 .set_language(&tree_sitter_cpp::LANGUAGE.into())
1943 .unwrap();
1944 let tree = parser.parse(&source, None).unwrap();
1945 assert!(!tree.root_node().has_error());
1946 let declaration = tree
1947 .root_node()
1948 .named_child(if prefix.is_empty() { 0 } else { 1 })
1949 .unwrap();
1950 assert_eq!(declaration.kind(), "declaration");
1951 assert_eq!(declaration.byte_range(), prefix.len()..prefix.len() + 13);
1952 let mut range = brokk_bifrost_core::analyzer::tree_walk::node_range(declaration);
1953 range.end_byte -= 1; assert!(super::node_for_exact_range(tree.root_node(), &range).is_none());
1955 let selected = super::cpp_declaration_node_for_range(tree.root_node(), &range).unwrap();
1956 assert_eq!(
1957 (selected.kind(), selected.byte_range()),
1958 ("declaration", prefix.len()..prefix.len() + 13)
1959 );
1960 }
1961 }
1962}