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(
180 analyzer: &CppGraphSource<'_>,
181 visibility: &VisibilityIndex<'_>,
182 left: &CodeUnit,
183 right: &CodeUnit,
184 header_body_related: impl Fn(&ProjectFile, &ProjectFile) -> bool,
185) -> bool {
186 left.fq_name() == right.fq_name()
187 && visibility.same_logical_callable(analyzer, left, right)
188 && (left.source() == right.source()
189 || (matches!(
190 visibility.callable_linkage(left),
191 Some(CallableLinkage::External)
192 ) && matches!(
193 visibility.callable_linkage(right),
194 Some(CallableLinkage::External)
195 ) && header_body_related(left.source(), right.source())))
196}
197
198pub fn cpp_is_range_for_binding_name(node: Node<'_>) -> bool {
202 let mut current = Some(node);
203 while let Some(candidate) = current {
204 let Some(parent) = candidate.parent() else {
205 return false;
206 };
207 if parent.kind() == "for_range_loop" {
208 return parent
209 .child_by_field_name("declarator")
210 .is_some_and(|declarator| {
211 cpp_range_for_declarator_contains_name(declarator, node)
212 });
213 }
214 current = Some(parent);
215 }
216 false
217}
218
219pub fn cpp_is_conversion_operator_target_type(mut node: Node<'_>) -> bool {
228 while let Some(parent) = node.parent() {
229 if parent.kind() == "operator_cast" {
230 return true;
231 }
232 if matches!(
233 parent.kind(),
234 "function_declarator" | "declaration" | "function_definition" | "translation_unit"
235 ) {
236 return false;
237 }
238 node = parent;
239 }
240 false
241}
242
243pub fn cpp_is_recovered_macro_character_token_type(node: Node<'_>) -> bool {
253 if node.kind() != "type_identifier" {
254 return false;
255 }
256 let Some(parameter) = node.parent() else {
257 return false;
258 };
259 if parameter.kind() != "parameter_declaration"
260 || parameter.child_by_field_name("type") != Some(node)
261 || parameter.child_by_field_name("declarator").is_some()
262 || parameter
263 .parent()
264 .is_none_or(|parent| parent.kind() != "parameter_list")
265 {
266 return false;
267 }
268
269 parameter
270 .prev_named_sibling()
271 .is_some_and(cpp_is_recovered_character_quote)
272 && parameter
273 .next_named_sibling()
274 .is_some_and(cpp_is_recovered_character_quote)
275}
276
277fn cpp_is_recovered_character_quote(node: Node<'_>) -> bool {
278 node.is_error()
279 && node.child_count() == 1
280 && node
281 .child(0)
282 .is_some_and(|quote| !quote.is_named() && quote.kind() == "'")
283}
284
285pub fn cpp_is_constructor_or_destructor_declarator_name(node: Node<'_>, source: &str) -> bool {
308 cpp_is_declared_constructor_or_destructor_name(node)
309 || cpp_is_recovered_constructor_or_destructor_name(node, source)
310}
311
312fn cpp_is_declared_constructor_or_destructor_name(node: Node<'_>) -> bool {
318 let mut name = node;
319 if let Some(parent) = name.parent()
320 && parent.kind() == "destructor_name"
321 {
322 name = parent;
323 }
324 while let Some(parent) = name.parent() {
328 if parent.kind() != "qualified_identifier"
329 || parent.child_by_field_name("name") != Some(name)
330 {
331 break;
332 }
333 name = parent;
334 }
335 let Some(declarator) = name.parent() else {
336 return false;
337 };
338 if declarator.kind() != "function_declarator"
339 || declarator.child_by_field_name("declarator") != Some(name)
340 {
341 return false;
342 }
343 let Some(owner) = declarator.parent() else {
344 return false;
345 };
346 matches!(owner.kind(), "declaration" | "function_definition")
347 && owner.child_by_field_name("declarator") == Some(declarator)
348 && owner.child_by_field_name("type").is_none()
349}
350
351fn cpp_is_recovered_constructor_or_destructor_name(node: Node<'_>, source: &str) -> bool {
369 if node.kind() != "identifier" {
370 return false;
371 }
372 let Some(call) = node.parent() else {
373 return false;
374 };
375 if call.kind() != "call_expression" || call.child_by_field_name("function") != Some(node) {
376 return false;
377 }
378 let mut current = call.parent();
379 while let Some(ancestor) = current {
380 if ancestor.kind() == "function_definition" {
381 return ancestor
382 .child_by_field_name("declarator")
383 .is_some_and(|declarator| declarator.kind() == "identifier")
384 && cpp_recovered_class_header_names(ancestor, node_text(node, source), source);
385 }
386 current = ancestor.parent();
387 }
388 false
389}
390
391fn cpp_recovered_class_header_names(definition: Node<'_>, name: &str, source: &str) -> bool {
394 let header_end = definition
395 .child_by_field_name("body")
396 .map_or_else(|| definition.end_byte(), |body| body.start_byte());
397 let mut stack = vec![definition];
398 while let Some(node) = stack.pop() {
399 if node.start_byte() >= header_end {
400 continue;
401 }
402 if matches!(
403 node.kind(),
404 "identifier" | "type_identifier" | "namespace_identifier"
405 ) && node_text(node, source) == name
406 {
407 return true;
408 }
409 let mut cursor = node.walk();
410 for child in node.named_children(&mut cursor) {
411 stack.push(child);
412 }
413 }
414 false
415}
416
417fn cpp_range_for_declarator_contains_name(declarator: Node<'_>, target: Node<'_>) -> bool {
418 let mut pending = vec![declarator];
419 while let Some(candidate) = pending.pop() {
420 match candidate.kind() {
421 "identifier" | "field_identifier" => {
422 if cpp_same_node(candidate, target) {
423 return true;
424 }
425 }
426 "structured_binding_declarator" => {
427 let mut cursor = candidate.walk();
428 if candidate
429 .named_children(&mut cursor)
430 .any(|name| cpp_same_node(name, target))
431 {
432 return true;
433 }
434 }
435 "pointer_declarator"
436 | "reference_declarator"
437 | "array_declarator"
438 | "attributed_declarator"
439 | "parenthesized_declarator"
440 | "function_declarator"
441 | "init_declarator" => {
442 if let Some(inner) = cpp_range_for_inner_declarator(candidate) {
443 pending.push(inner);
444 }
445 }
446 _ => {}
447 }
448 }
449 false
450}
451
452fn cpp_range_for_inner_declarator(node: Node<'_>) -> Option<Node<'_>> {
453 node.child_by_field_name("declarator").or_else(|| {
454 let mut cursor = node.walk();
455 node.named_children(&mut cursor).find(|child| {
456 matches!(
457 child.kind(),
458 "identifier"
459 | "field_identifier"
460 | "structured_binding_declarator"
461 | "pointer_declarator"
462 | "reference_declarator"
463 | "array_declarator"
464 | "attributed_declarator"
465 | "parenthesized_declarator"
466 | "function_declarator"
467 | "init_declarator"
468 )
469 })
470 })
471}
472
473pub fn cpp_header_body_files_are_related(
499 source: &dyn CppSource,
500 token: QueryToken<'_>,
501 left: &ProjectFile,
502 right: &ProjectFile,
503) -> bool {
504 match (
505 cpp_source_path_is_header(left),
506 cpp_source_path_is_header(right),
507 ) {
508 (true, false) => cpp_include_closure_reaches(source, token, right, left),
509 (false, true) => cpp_include_closure_reaches(source, token, left, right),
510 (true, true) => {
511 cpp_include_closure_reaches(source, token, left, right)
512 || cpp_include_closure_reaches(source, token, right, left)
513 }
514 (false, false) => false,
515 }
516}
517
518pub fn cpp_source_path_is_header(source: &ProjectFile) -> bool {
519 let path = rel_path_string(source).to_ascii_lowercase();
520 matches!(
521 path.rsplit('.').next(),
522 Some("h" | "hin" | "hh" | "hpp" | "hxx")
523 )
524}
525
526pub(crate) fn cpp_occurrence_role_for_node(
527 node: Node<'_>,
528 source: &str,
529 candidate: &CodeUnit,
530) -> CppOccurrenceRole {
531 if !candidate.is_callable() && !candidate.is_class() {
532 return CppOccurrenceRole::Both;
533 }
534 if candidate.is_callable() {
535 return if subtree_contains(node, |descendant| {
536 descendant.kind() == "function_definition"
537 && descendant.child_by_field_name("body").is_some()
538 }) {
539 CppOccurrenceRole::Definition
540 } else {
541 CppOccurrenceRole::DeclarationOnly
542 };
543 }
544 if cpp_node_declares_alias(node, source, candidate) {
548 return CppOccurrenceRole::Both;
549 }
550 if node.kind() == "function_definition" && node.child_by_field_name("body").is_some() {
551 return CppOccurrenceRole::Definition;
552 }
553 if !subtree_contains(node, |descendant| {
554 matches!(
555 descendant.kind(),
556 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
557 )
558 }) {
559 return CppOccurrenceRole::Both;
560 }
561 if subtree_contains(node, |descendant| {
562 matches!(
563 descendant.kind(),
564 "class_specifier" | "struct_specifier" | "union_specifier" | "enum_specifier"
565 ) && descendant.child_by_field_name("body").is_some()
566 }) {
567 CppOccurrenceRole::Definition
568 } else {
569 CppOccurrenceRole::DeclarationOnly
570 }
571}
572
573pub fn cpp_occurrence_role_for_range(
574 recovered_export_classes: &CppRecoveredExportClassIndex,
575 root: Node<'_>,
576 source: &str,
577 candidate: &CodeUnit,
578 range: &Range,
579) -> CppOccurrenceRole {
580 if !candidate.is_callable() && !candidate.is_class() {
581 return CppOccurrenceRole::Both;
582 }
583 let Some(node) = cpp_declaration_node_for_range(root, range) else {
584 return CppOccurrenceRole::Unknown;
585 };
586 if candidate.is_callable()
587 && subtree_contains(node, |descendant| {
588 descendant.kind() == "function_definition"
589 && descendant.child_by_field_name("body").is_some()
590 })
591 {
592 return CppOccurrenceRole::Definition;
593 }
594 if candidate.is_callable()
601 && !matches!(
602 node.kind(),
603 "declaration" | "field_declaration" | "function_definition"
604 )
605 && let Some(has_body) = recovered_callable_body_at(source, range)
606 {
607 return if has_body {
608 CppOccurrenceRole::Definition
609 } else {
610 CppOccurrenceRole::DeclarationOnly
611 };
612 }
613 if candidate.is_class()
614 && node.kind() == "function_definition"
615 && node.child_by_field_name("body").is_some()
616 {
617 return CppOccurrenceRole::Definition;
618 }
619 if candidate.is_class()
627 && let Some(has_body) = recovered_class_body_at(
628 recovered_export_classes,
629 root,
630 source,
631 candidate.identifier(),
632 range,
633 )
634 {
635 return if has_body {
636 CppOccurrenceRole::Definition
637 } else {
638 CppOccurrenceRole::DeclarationOnly
639 };
640 }
641 cpp_occurrence_role_for_node(node, source, candidate)
642}
643
644fn cpp_node_declares_alias(node: Node<'_>, source: &str, candidate: &CodeUnit) -> bool {
646 match node.kind() {
647 "alias_declaration" => {
648 node.child_by_field_name("name")
649 .and_then(|name| name.utf8_text(source.as_bytes()).ok())
650 == Some(candidate.identifier())
651 }
652 "type_definition" => {
653 let mut cursor = node.walk();
654 node.children_by_field_name("declarator", &mut cursor)
655 .any(|declarator| {
656 crate::declarations::extract_typedef_declarator_name(declarator, source)
657 .as_deref()
658 == Some(candidate.identifier())
659 })
660 }
661 _ => false,
662 }
663}
664
665pub fn cpp_range_is_pure_virtual_declaration(root: Node<'_>, source: &str, range: &Range) -> bool {
676 let Some(node) = cpp_declaration_node_for_range(root, range) else {
677 return false;
678 };
679 let mut current = Some(node);
680 while let Some(node) = current {
681 match node.kind() {
682 "field_declaration" => {
683 return node
687 .child_by_field_name("default_value")
688 .is_some_and(|value| {
689 value.kind() == "number_literal" && node_text(value, source) == "0"
690 })
691 && node
692 .child_by_field_name("declarator")
693 .and_then(extract_function_declarator)
694 .is_some();
695 }
696 "field_declaration_list" | "function_definition" | "translation_unit" => return false,
699 _ => current = node.parent(),
700 }
701 }
702 false
703}
704
705fn cpp_declaration_node_for_range<'tree>(root: Node<'tree>, range: &Range) -> Option<Node<'tree>> {
706 node_for_exact_range(root, range).or_else(|| {
707 root.descendant_for_byte_range(range.start_byte, range.end_byte)
708 .and_then(|mut node| {
709 while node.start_byte() > range.start_byte || node.end_byte() < range.end_byte {
710 node = node.parent()?;
711 }
712 Some(node)
713 })
714 })
715}
716
717#[derive(Default)]
729pub struct CppReconciledDefinitionIndex {
730 pub rekeyed: Vec<CodeUnit>,
732 pub provisional_of: HashMap<CodeUnit, CodeUnit>,
734}
735
736#[derive(Default)]
740pub struct CppReconciledGroup {
741 pub by_name: HashMap<String, Arc<CppReconciledDefinitionIndex>>,
742 pub canonical_of: HashMap<CodeUnit, CodeUnit>,
743}
744
745pub fn cpp_reconcile_group_key_for_fq(
748 fq: &brokk_bifrost_core::analyzer::fq_name::FqName,
749) -> Option<CppReconcileGroupKey> {
750 let interner = segment_interner();
751 let (member_identifier, _) = interner.resolve(fq.last()?);
752 if member_identifier.is_empty() {
753 return None;
754 }
755 let owner_terminal = fq
756 .segments()
757 .len()
758 .checked_sub(2)
759 .map(|index| interner.resolve(fq.segments()[index]).0.to_string());
760 Some(CppReconcileGroupKey {
761 member_identifier: member_identifier.to_string(),
762 owner_terminal,
763 })
764}
765
766#[derive(Debug, Clone, PartialEq, Eq, Hash)]
782pub struct CppReconcileGroupKey {
783 pub member_identifier: String,
786 pub owner_terminal: Option<String>,
790}
791
792pub fn cpp_reconcile_group_key(fq_name: &str) -> Option<CppReconcileGroupKey> {
799 let interner = segment_interner();
800 let query_fq = parse_symbol_path_fq(Language::Cpp, fq_name, interner);
801 let (member_identifier, _) = interner.resolve(query_fq.last()?);
802 if member_identifier.is_empty() {
803 return None;
804 }
805 let owner_terminal = query_fq.segments().len().checked_sub(2).map(|penultimate| {
815 let (text, _) = interner.resolve(query_fq.segments()[penultimate]);
816 text.rsplit_once('$')
821 .map_or(text, |(_, tail)| tail)
822 .to_string()
823 });
824 Some(CppReconcileGroupKey {
825 member_identifier: member_identifier.to_string(),
826 owner_terminal,
827 })
828}
829
830pub struct CppReconcileCandidates {
836 by_owner_terminal: HashMap<String, Vec<CodeUnit>>,
837 all: Vec<CodeUnit>,
841}
842
843impl CppReconcileCandidates {
844 fn for_group(&self, key: &CppReconcileGroupKey) -> &[CodeUnit] {
847 match &key.owner_terminal {
848 Some(owner_terminal) => self
849 .by_owner_terminal
850 .get(owner_terminal)
851 .map_or(&[][..], Vec::as_slice),
852 None => &self.all,
853 }
854 }
855
856 pub fn iter(&self) -> impl Iterator<Item = &CodeUnit> {
858 self.all.iter()
859 }
860
861 pub fn bucketed_len(&self) -> usize {
864 self.by_owner_terminal.values().map(Vec::len).sum()
865 }
866}
867
868pub fn cpp_reconcile_candidates_from_units(
875 candidates: impl IntoIterator<Item = CodeUnit>,
876 keep_going: &dyn Fn() -> bool,
877) -> Option<CppReconcileCandidates> {
878 let mut candidates = candidates.into_iter().collect::<Vec<_>>();
879 candidates.sort();
880 candidates.dedup();
881 let interner = segment_interner();
882 let mut by_owner_terminal: HashMap<String, Vec<CodeUnit>> = HashMap::default();
883 let mut all = Vec::new();
884 for (index, unit) in candidates.into_iter().enumerate() {
885 if index % CANDIDATE_BUCKETING_POLL_STRIDE == 0 && !keep_going() {
889 return None;
890 }
891 if !unit.is_callable() {
892 continue;
893 }
894 let owner_terminal = unit
895 .fq()
896 .segments()
897 .iter()
898 .filter_map(|&segment| {
899 let (text, kind) = interner.resolve(segment);
900 matches!(
904 kind,
905 SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested
906 )
907 .then_some(text)
908 })
909 .last();
910 if let Some(owner_terminal) = owner_terminal {
911 by_owner_terminal
912 .entry(owner_terminal.to_string())
913 .or_default()
914 .push(unit.clone());
915 }
916 all.push(unit);
917 }
918 if !keep_going() {
921 return None;
922 }
923 Some(CppReconcileCandidates {
924 by_owner_terminal,
925 all,
926 })
927}
928
929const CANDIDATE_BUCKETING_POLL_STRIDE: usize = 256;
931
932pub fn cpp_reconcile_group(
949 cpp: &dyn CppSource,
950 token: QueryToken<'_>,
951 key: &CppReconcileGroupKey,
952 candidates: &CppReconcileCandidates,
953 keep_going: &dyn Fn() -> bool,
954 on_candidate: &dyn Fn(),
955) -> Option<CppReconciledGroup> {
956 let _scope = profiling::scope_with(|| {
957 format!(
958 "cpp.reconciled.build[{}#{}]",
959 key.member_identifier,
960 key.owner_terminal.as_deref().unwrap_or("*")
961 )
962 });
963 let mut groups: HashMap<String, CppReconciledDefinitionIndex> = HashMap::default();
964 let mut using_by_file: HashMap<ProjectFile, Arc<Vec<String>>> = HashMap::default();
965 for unit in candidates.for_group(key) {
966 if !keep_going() {
971 return None;
972 }
973 on_candidate();
974 let _candidate =
977 profiling::scope_with(|| format!("cpp.reconcile.candidate[{}]", unit.fq_name()));
978 let role = {
979 let _role = profiling::scope("cpp.reconcile.role");
980 cpp.stored_callable_unit_role(unit)
981 };
982 if !matches!(
983 role,
984 CppCallableUnitRole::Definition | CppCallableUnitRole::Both
985 ) {
986 continue;
987 }
988 let Some(reconciled) =
989 cpp_reconcile_definition_identity(cpp, token, unit, &mut using_by_file, keep_going)
990 else {
991 continue;
992 };
993 let canonical_fq = reconciled.fq_name();
994 if unit.fq_name() == canonical_fq {
1001 continue;
1002 }
1003 let short_name = format!("{}.{}", reconciled.owner_chain, reconciled.member);
1012 let fq = cpp_member_fq(&reconciled.package, &short_name);
1013 let rekeyed = CodeUnit::with_signature_and_fq(
1014 unit.source().clone(),
1015 unit.kind(),
1016 reconciled.package,
1017 short_name,
1018 unit.signature().map(str::to_string),
1019 unit.is_synthetic(),
1020 fq,
1021 );
1022 let index = groups.entry(canonical_fq).or_default();
1023 index.rekeyed.push(rekeyed.clone());
1024 index.provisional_of.insert(rekeyed, unit.clone());
1025 }
1026 let canonical_of = groups
1027 .values()
1028 .flat_map(|index| {
1029 index
1030 .provisional_of
1031 .iter()
1032 .map(|(canonical, physical)| (physical.clone(), canonical.clone()))
1033 })
1034 .collect();
1035 let group = CppReconciledGroup {
1036 canonical_of,
1037 by_name: groups
1038 .into_iter()
1039 .map(|(canonical_fq, index)| (canonical_fq, Arc::new(index)))
1040 .collect(),
1041 };
1042 keep_going().then_some(group)
1045}
1046
1047fn cpp_reconcile_definition_identity(
1053 cpp: &dyn CppSource,
1054 token: QueryToken<'_>,
1055 unit: &CodeUnit,
1056 using_by_file: &mut HashMap<ProjectFile, Arc<Vec<String>>>,
1057 keep_going: &dyn Fn() -> bool,
1058) -> Option<ReconciledIdentity> {
1059 let interner = segment_interner();
1069 let mut provisional_owner_segments: Vec<&str> = Vec::new();
1070 let mut member: Option<&str> = None;
1071 for &segment in unit.fq().segments() {
1072 let (text, kind) = interner.resolve(segment);
1073 match kind {
1074 SegmentKind::Package | SegmentKind::Type | SegmentKind::Nested => {
1075 if member.is_some() {
1079 return None;
1080 }
1081 if !text.is_empty() {
1082 provisional_owner_segments.push(text);
1083 }
1084 }
1085 SegmentKind::Member => member = Some(text),
1086 _ => return None,
1087 }
1088 }
1089 let member = member?;
1090 let single_owner = cpp_single_owner_context(cpp, token, unit, keep_going)?;
1094 let owner_segments = single_owner.as_ref().map_or_else(
1095 || provisional_owner_segments,
1096 |context| vec![context.owner.as_str()],
1097 );
1098 if owner_segments.is_empty() {
1099 return None;
1100 }
1101
1102 let visible = {
1103 let _visible = profiling::scope_with(|| {
1104 format!("cpp.reconcile.visible[{}]", rel_path_string(unit.source()))
1105 });
1106 cpp.visible_type_units(unit.source())
1107 };
1108 let class_table: Vec<VisibleClass> = visible
1109 .iter()
1110 .filter(|candidate| candidate.is_class())
1111 .map(|candidate| VisibleClass {
1112 package: candidate.package_name(),
1113 nested_short_name: candidate.short_name(),
1114 })
1115 .collect();
1116
1117 if let Some(context) = &single_owner {
1118 if context.global {
1119 return keep_going().then(|| {
1121 reconcile_out_of_line_member_identity(&owner_segments, member, &[""], &class_table)
1122 })?;
1123 }
1124 for tier in
1128 lexical_component_tiers(&[], false, &context.lexical).take(context.lexical.len())
1129 {
1130 if !keep_going() {
1131 return None;
1132 }
1133 let namespace = tier.join("::");
1134 if let Some(identity) = reconcile_out_of_line_member_identity(
1135 &owner_segments,
1136 member,
1137 &[namespace.as_str()],
1138 &class_table,
1139 ) {
1140 return keep_going().then_some(identity);
1141 }
1142 }
1143 }
1144 if !keep_going() {
1145 return None;
1146 }
1147 let using = using_by_file
1148 .entry(unit.source().clone())
1149 .or_insert_with(|| {
1150 Arc::new(
1151 cpp.declaration_source_facts(token, unit.source())
1152 .map(|source| {
1153 source
1154 .facts
1155 .using_namespaces
1156 .iter()
1157 .map(|(_, name)| name.clone())
1158 .collect()
1159 })
1160 .unwrap_or_default(),
1161 )
1162 })
1163 .clone();
1164 let mut namespace_candidates: Vec<&str> = vec![""];
1165 namespace_candidates.extend(using.iter().map(String::as_str));
1166
1167 reconcile_out_of_line_member_identity(
1168 &owner_segments,
1169 member,
1170 &namespace_candidates,
1171 &class_table,
1172 )
1173}
1174
1175#[derive(PartialEq, Eq)]
1176struct CppSingleOwnerContext {
1177 owner: String,
1178 global: bool,
1179 lexical: Vec<String>,
1180}
1181
1182fn cpp_single_owner_context(
1185 cpp: &dyn CppSource,
1186 token: QueryToken<'_>,
1187 unit: &CodeUnit,
1188 keep_going: &dyn Fn() -> bool,
1189) -> Option<Option<CppSingleOwnerContext>> {
1190 let facts = cpp.declaration_source_facts(token, unit.source())?;
1191 let mut has_single = false;
1195 for fact in facts.for_unit(unit) {
1196 if !keep_going() {
1197 return None;
1198 }
1199 has_single |= fact.written_owner.len() == 1;
1200 }
1201 if !has_single {
1202 return keep_going().then_some(None);
1203 }
1204 let syntax = cpp.prepared_syntax(token, unit.source())?;
1205 if !keep_going() {
1206 return None;
1207 }
1208 let scopes = cpp.orphaned_namespace_scopes(token, unit.source());
1209 let root = syntax.tree().root_node();
1210 let source = syntax.source();
1211 let mut agreed = None;
1212 for fact in facts.for_unit(unit) {
1213 if !keep_going() {
1214 return None;
1215 }
1216 let declaration = facts.source.declaration(fact.declaration);
1217 let occurrence = facts.source.occurrence(declaration.occurrence).range;
1218 let name_range = facts.source.occurrence(declaration.name?).range;
1219 let mut node =
1220 root.descendant_for_byte_range(occurrence.start_byte, occurrence.end_byte)?;
1221 let selected = loop {
1224 if !keep_going()
1225 || node.start_byte() != occurrence.start_byte
1226 || node.end_byte() != occurrence.end_byte
1227 {
1228 return None;
1229 }
1230 if matches!(
1231 node.kind(),
1232 "function_definition" | "function_declarator" | "declaration" | "field_declaration"
1233 ) {
1234 break node;
1235 }
1236 node = node.parent()?;
1237 };
1238 let callable = if selected.kind() == "function_declarator" {
1239 selected
1240 } else {
1241 selected
1242 .child_by_field_name("declarator")
1243 .and_then(extract_function_declarator)?
1244 };
1245 let name = declarator_name_node(callable)?;
1246 let terminal = cpp_name_component_nodes(name)?.pop()?;
1247 if name.is_missing()
1248 || name.has_error()
1249 || terminal.is_missing()
1250 || terminal.start_byte() != name_range.start_byte
1251 || terminal.end_byte() != name_range.end_byte
1252 || node_text(terminal, source) != unit.identifier()
1253 {
1254 return None;
1255 }
1256 let qualified = qualified_owner_components(name, source)?;
1257 if qualified.names.len() != 1 || qualified.names != fact.written_owner {
1258 return None;
1259 }
1260 let proof = CppSingleOwnerContext {
1261 owner: qualified.names.into_iter().next()?,
1262 global: qualified.global,
1263 lexical: scopes.enclosing_namespace_components(selected, source),
1264 };
1265 if !keep_going() || agreed.as_ref().is_some_and(|prior| prior != &proof) {
1266 return None;
1267 }
1268 agreed = Some(proof);
1269 }
1270 if !keep_going() {
1271 return None;
1272 }
1273 Some(Some(agreed?))
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279 use brokk_bifrost_test_macros::hardwon;
1280
1281 #[test]
1282 fn physical_identity_cohorts_keep_recorded_owner_and_member_boundaries() {
1283 use brokk_bifrost_core::analyzer::fq_name::FqName;
1284 let mut fq = FqName::new();
1285 for (text, kind) in [
1286 ("ns", SegmentKind::Package),
1287 ("Owner$Literal", SegmentKind::Type),
1288 ("operator std::string", SegmentKind::Member),
1289 ] {
1290 fq.push(segment_interner().intern(text, kind));
1291 }
1292 let key = cpp_reconcile_group_key_for_fq(&fq).unwrap();
1293 assert_eq!(key.owner_terminal.as_deref(), Some("Owner$Literal"));
1294 assert_eq!(key.member_identifier, "operator std::string");
1295 }
1296
1297 #[test]
1298 #[hardwon("#2488: terminal cancellation must not publish identity cohorts")]
1299 fn candidate_cohorts_recheck_terminal_cancellation_before_publish() {
1300 use std::cell::Cell;
1301
1302 let unit = CodeUnit::new(
1303 ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1304 brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1305 "",
1306 "Owner.run",
1307 );
1308 let cancel_after_last_candidate = {
1309 let active = Cell::new(true);
1310 move || active.replace(false)
1311 };
1312 assert!(
1313 cpp_reconcile_candidates_from_units(
1314 std::iter::once(unit.clone()),
1315 &cancel_after_last_candidate,
1316 )
1317 .is_none(),
1318 "a cancellation observed after the last sub-256 candidate must prevent publication"
1319 );
1320
1321 let candidates = (0..=CANDIDATE_BUCKETING_POLL_STRIDE)
1322 .map(|index| {
1323 CodeUnit::new(
1324 ProjectFile::new(std::env::temp_dir(), "identity-cancellation.cpp"),
1325 brokk_bifrost_core::analyzer::model::CodeUnitType::Function,
1326 "",
1327 format!("Owner{index}.run"),
1328 )
1329 })
1330 .collect::<Vec<_>>();
1331 let cancel_at_bucket_boundary = {
1332 let active = Cell::new(true);
1333 move || active.replace(false)
1334 };
1335 assert!(
1336 cpp_reconcile_candidates_from_units(candidates, &cancel_at_bucket_boundary).is_none(),
1337 "a cancellation at the bucket boundary must prevent a partial cohort from publishing"
1338 );
1339
1340 let cancelled = || false;
1341 assert!(
1342 cpp_reconcile_candidates_from_units(Vec::<CodeUnit>::new(), &cancelled).is_none(),
1343 "an empty cancelled cohort must not publish"
1344 );
1345 }
1346
1347 fn parse_cpp(source: &str) -> Tree {
1348 crate::diagnostics::parse_cpp_tree(source)
1349 }
1350
1351 fn is_declarator_name(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1352 let end = start + text.len();
1353 assert_eq!(&source[start..end], text, "the probe must name the token");
1354 let node = tree
1355 .root_node()
1356 .named_descendant_for_byte_range(start, end)
1357 .expect("a node spans the probed range");
1358 assert_eq!(
1359 (node.start_byte(), node.end_byte()),
1360 (start, end),
1361 "the probed range must be exactly one node: {}",
1362 node.to_sexp()
1363 );
1364 cpp_is_constructor_or_destructor_declarator_name(node, source)
1365 }
1366
1367 fn is_conversion_target(tree: &Tree, source: &str, start: usize, text: &str) -> bool {
1368 let end = start + text.len();
1369 assert_eq!(&source[start..end], text, "the probe must name the token");
1370 let node = tree
1371 .root_node()
1372 .named_descendant_for_byte_range(start, end)
1373 .expect("a node spans the probed range");
1374 assert_eq!(
1375 (node.start_byte(), node.end_byte()),
1376 (start, end),
1377 "the probed range must be exactly one node: {}",
1378 node.to_sexp()
1379 );
1380 cpp_is_conversion_operator_target_type(node)
1381 }
1382
1383 fn is_recovered_macro_character_type(
1384 tree: &Tree,
1385 source: &str,
1386 start: usize,
1387 text: &str,
1388 ) -> bool {
1389 let end = start + text.len();
1390 assert_eq!(&source[start..end], text, "the probe must name the token");
1391 let node = tree
1392 .root_node()
1393 .named_descendant_for_byte_range(start, end)
1394 .expect("a node spans the probed range");
1395 cpp_is_recovered_macro_character_token_type(node)
1396 }
1397
1398 #[test]
1399 #[hardwon(
1400 "b114d87215ca: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1401 )]
1402 fn recovered_macro_character_tokens_are_not_type_references() {
1403 let source = concat!(
1404 "struct I {};\n",
1405 "#define STRING_TOKEN_(name, ...)\n",
1406 "struct Schema {\n",
1407 " STRING_TOKEN_(MaxItems, 'm', 'I')\n",
1408 " void ordinary(I value);\n",
1409 " void malformed(I value, @);\n",
1410 " void use() { I value; consume('I'); }\n",
1411 "};\n",
1412 );
1413 let tree = parse_cpp(source);
1414 let recovered_m = source.find("'m'").expect("recovered m") + 1;
1415 let recovered_i = source.find("'I'").expect("recovered I") + 1;
1416 for (label, start, text) in [
1417 ("lowercase character token", recovered_m, "m"),
1418 ("uppercase character token", recovered_i, "I"),
1419 ] {
1420 assert!(
1421 is_recovered_macro_character_type(&tree, source, start, text),
1422 "{label} must match the exact recovery role"
1423 );
1424 }
1425
1426 let macro_first_argument = source.find("MaxItems").expect("macro first argument");
1427 let ordinary = source.find("ordinary(I").expect("ordinary parameter") + "ordinary(".len();
1428 let malformed =
1429 source.find("malformed(I").expect("malformed parameter") + "malformed(".len();
1430 let local = source
1431 .find("I value; consume")
1432 .expect("local type reference");
1433 let expression_character = source.rfind("'I'").expect("expression character") + 1;
1434 for (label, start, text) in [
1435 ("unquoted macro argument", macro_first_argument, "MaxItems"),
1436 ("ordinary parameter type", ordinary, "I"),
1437 ("parameter beside another error", malformed, "I"),
1438 ("local type reference", local, "I"),
1439 ("expression character literal", expression_character, "I"),
1440 ] {
1441 assert!(
1442 !is_recovered_macro_character_type(&tree, source, start, text),
1443 "{label} must remain outside the recovery role"
1444 );
1445 }
1446 }
1447
1448 #[test]
1449 #[hardwon(
1450 "0733cadab142: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1451 )]
1452 fn conversion_operator_target_components_are_identity_syntax_only() {
1453 let source = concat!(
1454 "namespace other { struct Target {}; template<class T> struct Box {}; }\n",
1455 "using other::Target;\n",
1456 "struct Source {\n",
1457 " operator Target() const;\n",
1458 " operator other::Target() const { return other::Target{}; }\n",
1459 " template<class T> operator other::Box<T>() const { return {}; }\n",
1460 " operator other::Target const&() const;\n",
1461 " operator other::Target*() const;\n",
1462 " other::Target ordinary() const {\n",
1463 " return reinterpret_cast<other::Target&>(*this);\n",
1464 " }\n",
1465 " other::Target operator+() const { return {}; }\n",
1466 "};\n",
1467 );
1468 let tree = parse_cpp(source);
1469
1470 let bare = source.find("operator Target").expect("bare target") + "operator ".len();
1471 let qualified = source
1472 .find("operator other::Target()")
1473 .expect("qualified target")
1474 + "operator ".len();
1475 let template = source
1476 .find("operator other::Box<T>")
1477 .expect("template target")
1478 + "operator ".len();
1479 let cv_reference = source
1480 .find("operator other::Target const&")
1481 .expect("cv-reference target")
1482 + "operator other::".len();
1483 let pointer = source
1484 .find("operator other::Target*")
1485 .expect("pointer target")
1486 + "operator other::".len();
1487
1488 for (label, start, text) in [
1489 ("bare target", bare, "Target"),
1490 ("qualified target scope", qualified, "other"),
1491 (
1492 "qualified target name",
1493 qualified + "other::".len(),
1494 "Target",
1495 ),
1496 ("template target scope", template, "other"),
1497 ("template target name", template + "other::".len(), "Box"),
1498 (
1499 "template target argument",
1500 template + "other::Box<".len(),
1501 "T",
1502 ),
1503 ("cv-reference target", cv_reference, "Target"),
1504 ("pointer target", pointer, "Target"),
1505 ] {
1506 assert!(
1507 is_conversion_target(&tree, source, start, text),
1508 "the {label} at byte {start} belongs to the conversion identity"
1509 );
1510 }
1511
1512 let ordinary_return = source
1513 .find("other::Target ordinary")
1514 .expect("ordinary return");
1515 let body_cast = source
1516 .find("reinterpret_cast<other::Target")
1517 .expect("body cast")
1518 + "reinterpret_cast<".len();
1519 let overloaded_return = source
1520 .find("other::Target operator+")
1521 .expect("overloaded operator return");
1522 for (label, start) in [
1523 ("ordinary return type", ordinary_return),
1524 ("body cast target", body_cast),
1525 ("overloaded-operator return type", overloaded_return),
1526 ] {
1527 assert!(
1528 !is_conversion_target(&tree, source, start, "other"),
1529 "the {label} at byte {start} stays a reference"
1530 );
1531 }
1532 }
1533
1534 #[test]
1539 #[hardwon(
1540 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1541 )]
1542 fn declared_constructor_and_destructor_declarator_names_are_not_references() {
1543 let source = concat!(
1544 "class Foo {\n",
1545 "public:\n",
1546 " Foo();\n",
1547 " Foo(const Foo&);\n",
1548 " ~Foo();\n",
1549 " void m();\n",
1550 "};\n",
1551 "Foo::Foo() {}\n",
1552 "Foo::~Foo() {}\n",
1553 "void Foo::m() {}\n",
1554 );
1555 let tree = parse_cpp(source);
1556
1557 for (label, start, text) in [
1558 (
1559 "constructor declaration",
1560 source.find("Foo();").expect("ctor"),
1561 "Foo",
1562 ),
1563 (
1564 "copy constructor declaration",
1565 source.find("Foo(const Foo&);").expect("copy ctor"),
1566 "Foo",
1567 ),
1568 (
1569 "destructor name",
1570 source.find("~Foo();").expect("dtor"),
1571 "~Foo",
1572 ),
1573 (
1574 "identifier inside the destructor name",
1575 source.find("~Foo();").expect("dtor") + "~".len(),
1576 "Foo",
1577 ),
1578 (
1579 "out-of-line constructor definition name",
1580 source.find("Foo::Foo() {}").expect("out-of-line ctor") + "Foo::".len(),
1581 "Foo",
1582 ),
1583 (
1584 "out-of-line destructor definition name",
1585 source.find("Foo::~Foo() {}").expect("out-of-line dtor") + "Foo::".len(),
1586 "~Foo",
1587 ),
1588 ] {
1589 assert!(
1590 is_declarator_name(&tree, source, start, text),
1591 "the {label} at byte {start} is a declaration occurrence"
1592 );
1593 }
1594
1595 for (label, start, text) in [
1596 (
1597 "class name",
1598 source.find("class Foo {").expect("class") + "class ".len(),
1599 "Foo",
1600 ),
1601 (
1602 "parameter type",
1603 source.find("const Foo&").expect("parameter type") + "const ".len(),
1604 "Foo",
1605 ),
1606 (
1607 "owning scope of an out-of-line constructor",
1608 source.find("Foo::Foo() {}").expect("out-of-line ctor"),
1609 "Foo",
1610 ),
1611 (
1612 "owning scope of an out-of-line destructor",
1613 source.find("Foo::~Foo() {}").expect("out-of-line dtor"),
1614 "Foo",
1615 ),
1616 (
1617 "out-of-line method name",
1618 source.find("void Foo::m() {}").expect("out-of-line method") + "void Foo::".len(),
1619 "m",
1620 ),
1621 ] {
1622 assert!(
1623 !is_declarator_name(&tree, source, start, text),
1624 "the {label} at byte {start} stays a reference"
1625 );
1626 }
1627 }
1628
1629 #[test]
1633 #[hardwon(
1634 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1635 )]
1636 fn constructor_call_sites_stay_references() {
1637 let source = concat!(
1638 "struct B { B(int); };\n",
1639 "struct D : B {\n",
1640 " D(int x) : B(x), base_(x) {}\n",
1641 " int base_;\n",
1642 "};\n",
1643 "void g() {\n",
1644 " D* p = new D(1);\n",
1645 " D x(2);\n",
1646 " D(3);\n",
1647 " g();\n",
1648 "}\n",
1649 );
1650 let tree = parse_cpp(source);
1651
1652 let inline_declarator = source.find("D(int x)").expect("inline constructor");
1653 assert!(
1654 is_declarator_name(&tree, source, inline_declarator, "D"),
1655 "an inline constructor definition name is still a declarator"
1656 );
1657
1658 for (label, start, text) in [
1659 (
1660 "base member initializer",
1661 source.find(": B(x)").expect("base initializer") + ": ".len(),
1662 "B",
1663 ),
1664 (
1665 "field member initializer",
1666 source.find("base_(x) {}").expect("field initializer"),
1667 "base_",
1668 ),
1669 (
1670 "new expression type",
1671 source.find("new D(1)").expect("new expression") + "new ".len(),
1672 "D",
1673 ),
1674 (
1675 "direct initialization type",
1676 source.find("D x(2)").expect("direct initialization"),
1677 "D",
1678 ),
1679 (
1680 "temporary construction statement",
1681 source.find("D(3)").expect("temporary"),
1682 "D",
1683 ),
1684 (
1685 "recursive call in a real body",
1686 source.find("g();").expect("recursive call"),
1687 "g",
1688 ),
1689 ] {
1690 assert!(
1691 !is_declarator_name(&tree, source, start, text),
1692 "the {label} at byte {start} is a reference"
1693 );
1694 }
1695 }
1696
1697 #[test]
1702 #[hardwon(
1703 "246fc91b2fb6: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1704 )]
1705 fn a_constructor_declarator_the_parse_read_as_a_call_is_not_a_reference() {
1706 let source = concat!(
1707 "class SAMPLE_EXPORT Properties {\n",
1708 " public:\n",
1709 " Properties();\n",
1710 " DISALLOW_COPY_AND_ASSIGN(Properties);\n",
1711 " int size() const;\n",
1712 " int total() { return size(); }\n",
1713 "};\n",
1714 );
1715 let tree = parse_cpp(source);
1716
1717 let recovered = source.find("Properties();").expect("recovered constructor");
1718 assert!(
1719 is_declarator_name(&tree, source, recovered, "Properties"),
1720 "a constructor declaration the parse read as a call is still a declarator"
1721 );
1722
1723 for (label, start, text) in [
1724 (
1725 "class name in the recovered header",
1726 source
1727 .find("class SAMPLE_EXPORT Properties")
1728 .expect("class")
1729 + "class SAMPLE_EXPORT ".len(),
1730 "Properties",
1731 ),
1732 (
1733 "macro invocation in the recovered body",
1734 source
1735 .find("DISALLOW_COPY_AND_ASSIGN(Properties);")
1736 .expect("macro invocation"),
1737 "DISALLOW_COPY_AND_ASSIGN",
1738 ),
1739 (
1740 "call inside a method body the recovery kept",
1741 source.find("return size();").expect("member call") + "return ".len(),
1742 "size",
1743 ),
1744 ] {
1745 assert!(
1746 !is_declarator_name(&tree, source, start, text),
1747 "the {label} at byte {start} stays a reference"
1748 );
1749 }
1750 }
1751
1752 fn class_roles(source: &str, name: &str) -> Vec<CppOccurrenceRole> {
1755 let tree = parse_cpp(source);
1756 let file = ProjectFile::new(std::env::temp_dir(), "occurrence-role.hpp");
1757 let parsed = crate::adapter::parse_cpp_file(&file, source, &tree);
1758 let unit = parsed
1759 .declarations()
1760 .iter()
1761 .find(|unit| unit.is_class() && unit.fq_name() == name)
1762 .unwrap_or_else(|| panic!("missing class {name}: {parsed:#?}"));
1763 let ranges = parsed.declaration_ranges(unit);
1764 assert!(!ranges.is_empty(), "{name} must have a declaration range");
1765 ranges
1766 .iter()
1767 .map(|range| {
1768 cpp_occurrence_role_for_range(
1769 &CppRecoveredExportClassIndex::build(tree.root_node(), source),
1770 tree.root_node(),
1771 source,
1772 unit,
1773 range,
1774 )
1775 })
1776 .collect()
1777 }
1778
1779 #[test]
1785 #[hardwon(
1786 "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1787 )]
1788 fn recovered_export_macro_classes_are_definitions_without_an_ordinary_class() {
1789 let source = concat!(
1790 "namespace api {\n",
1791 "class PROJECT_PUBLIC_API(2, 0) Name final {\n",
1792 " public:\n",
1793 " Name();\n",
1794 "};\n",
1795 "class PROJECT_PUBLIC_API(2, 0) Other : public Name {\n",
1796 " public:\n",
1797 " Other();\n",
1798 "};\n",
1799 "} // namespace api\n",
1800 );
1801 for name in ["api.Name", "api.Other"] {
1802 assert_eq!(
1803 class_roles(source, name),
1804 vec![CppOccurrenceRole::Definition],
1805 "{name} is a complete recovered definition"
1806 );
1807 }
1808 }
1809
1810 #[test]
1811 #[hardwon(
1812 "ec6b972248cc: C++ FIRD census assigned a declaration token the wrong reference/definition role"
1813 )]
1814 fn ordinary_class_roles_keep_their_plain_specifier_reading() {
1815 assert_eq!(
1816 class_roles("class Plain;\n", "Plain"),
1817 vec![CppOccurrenceRole::DeclarationOnly],
1818 "a forward declaration stays a declaration"
1819 );
1820 assert_eq!(
1821 class_roles("class Plain { };\n", "Plain"),
1822 vec![CppOccurrenceRole::Definition],
1823 "a complete class stays a definition"
1824 );
1825 }
1826}