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