1use crate::declarations::{
8 CppVisitor, collect_cpp_identifiers, collect_cpp_includes, recover_quoted_includes,
9};
10use crate::graph::resolver::OrphanedNamespaceScopeIndex;
11use crate::graph::syntax::ObjectMacroReplacement;
12use brokk_bifrost_core::analyzer::ProjectFile;
13use brokk_bifrost_core::analyzer::cognitive_complexity;
14use brokk_bifrost_core::analyzer::model::{Language, LanguageDialect};
15use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
16use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
17use brokk_bifrost_core::hash::{HashMap, HashSet};
18use std::sync::LazyLock;
19use tree_sitter::{Node, Tree};
20
21#[derive(Debug)]
24pub struct CppPrimaryReading {
25 pub parsed: ParsedFile,
26 pub c_tag_scope_witness: bool,
27}
28
29pub const CPP_FILE_EXTENSION: &str = "cpp";
32
33pub static CPP_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
36 LazyLock::new(|| cognitive_complexity::Config {
37 if_types: &["if_statement"],
38 loop_types: &["for_statement", "while_statement", "do_statement"],
39 catch_types: &["catch_clause"],
40 conditional_types: &["conditional_expression"],
41 case_types: &["case_statement"],
42 binary_types: &["binary_expression"],
43 logical_operators: &["&&", "||", "and", "or"],
44 jump_types: &["break_statement", "continue_statement"],
45 named_function_boundary_types: &["function_definition"],
46 anonymous_function_types: &["lambda_expression"],
47 else_clause_types: &["else_clause"],
48 default_case_predicate: Some(cpp_is_default_case),
49 ..cognitive_complexity::Config::empty()
50 });
51
52fn cpp_is_default_case(node: Node<'_>, _source: &str) -> bool {
53 node.child_by_field_name("value").is_none()
54}
55
56pub fn parse_cpp_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
58 parse_cpp_file_in_dialect(
59 file,
60 source,
61 tree,
62 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
63 )
64}
65
66pub fn parse_cpp_file_with_object_macro_fields(
71 file: &ProjectFile,
72 source: &str,
73 tree: &Tree,
74 object_macro_fields: HashMap<String, ObjectMacroReplacement>,
75) -> ParsedFile {
76 let root = tree.root_node();
77 let ancestry = ParentIndex::new(root);
78 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
79 parse_cpp_reading_with_object_macro_fields(
80 file,
81 source,
82 root,
83 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
84 &ancestry,
85 object_macro_fields,
86 &orphaned_namespaces,
87 )
88 .parsed
89}
90
91pub fn parse_cpp_file_in_dialect(
106 file: &ProjectFile,
107 source: &str,
108 tree: &Tree,
109 dialect: LanguageDialect,
110) -> ParsedFile {
111 let root = tree.root_node();
112 let ancestry = ParentIndex::new(root);
113 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
114 parse_cpp_reading(file, source, root, dialect, &ancestry, &orphaned_namespaces).parsed
115}
116
117pub fn parse_cpp_file_with_ancestry<'tree>(
124 file: &ProjectFile,
125 source: &str,
126 root: Node<'tree>,
127 ancestry: &ParentIndex<'tree>,
128) -> ParsedFile {
129 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
130 parse_cpp_file_with_ancestry_and_orphaned_namespaces(
131 file,
132 source,
133 root,
134 ancestry,
135 &orphaned_namespaces,
136 )
137 .parsed
138}
139
140pub fn parse_cpp_file_with_ancestry_and_orphaned_namespaces<'tree>(
149 file: &ProjectFile,
150 source: &str,
151 root: Node<'tree>,
152 ancestry: &ParentIndex<'tree>,
153 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
154) -> CppPrimaryReading {
155 parse_cpp_reading(
156 file,
157 source,
158 root,
159 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
160 ancestry,
161 orphaned_namespaces,
162 )
163}
164
165pub fn parse_cpp_c_reading<'tree>(
177 file: &ProjectFile,
178 source: &str,
179 root: Node<'tree>,
180 ancestry: &ParentIndex<'tree>,
181 primary: &ParsedFile,
182) -> ParsedFile {
183 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
184 parse_cpp_c_reading_with_orphaned_namespaces(
185 file,
186 source,
187 root,
188 ancestry,
189 primary,
190 &orphaned_namespaces,
191 )
192}
193
194pub fn parse_cpp_c_reading_with_orphaned_namespaces<'tree>(
200 file: &ProjectFile,
201 source: &str,
202 root: Node<'tree>,
203 ancestry: &ParentIndex<'tree>,
204 primary: &ParsedFile,
205 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
206) -> ParsedFile {
207 let mut parsed = ParsedFile::new(String::new());
208 parsed.imports = primary.imports.clone();
209 parsed.type_identifiers = primary.type_identifiers.clone();
210 {
211 let mut visitor = CppVisitor {
212 file,
213 source,
214 parsed: &mut parsed,
215 c_tag_semantics: true,
216 c_tag_scope_witness: false,
217 recovered_class_sibling_scopes: HashMap::default(),
218 consumed_fragment_regions: Vec::new(),
219 orphaned_namespaces,
220 partitioned_regions: Vec::new(),
221 namespace_forward_scans: HashMap::default(),
222 field_owners: None,
223 recovery_captures: Vec::new(),
224 object_macro_fields: HashMap::default(),
225 ambiguous_object_macro_fields: HashSet::default(),
226 };
227 visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
228 }
229 parsed.finalize_deferred_replacements();
230
231 #[cfg(debug_assertions)]
232 {
233 let mut recomputed = ParsedFile::new(String::new());
234 collect_cpp_includes(root, source, &mut recomputed);
235 collect_cpp_identifiers(root, source, &mut recomputed.type_identifiers);
236 recover_quoted_includes(source, &mut recomputed);
237 assert_eq!(
238 parsed.imports, recomputed.imports,
239 "the C reading's includes are the C++ reading's includes: {:?}",
240 file
241 );
242 assert_eq!(
243 parsed.type_identifiers, recomputed.type_identifiers,
244 "the C reading's identifiers are the C++ reading's identifiers: {:?}",
245 file
246 );
247 }
248
249 parsed
250}
251
252fn parse_cpp_reading<'tree>(
254 file: &ProjectFile,
255 source: &str,
256 root: Node<'tree>,
257 dialect: LanguageDialect,
258 ancestry: &ParentIndex<'tree>,
259 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
260) -> CppPrimaryReading {
261 parse_cpp_reading_with_object_macro_fields(
262 file,
263 source,
264 root,
265 dialect,
266 ancestry,
267 HashMap::default(),
268 orphaned_namespaces,
269 )
270}
271
272fn parse_cpp_reading_with_object_macro_fields<'tree>(
273 file: &ProjectFile,
274 source: &str,
275 root: Node<'tree>,
276 dialect: LanguageDialect,
277 ancestry: &ParentIndex<'tree>,
278 object_macro_fields: HashMap<String, ObjectMacroReplacement>,
279 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
280) -> CppPrimaryReading {
281 let mut parsed = ParsedFile::new(String::new());
282
283 collect_cpp_includes(root, source, &mut parsed);
284 collect_cpp_identifiers(root, source, &mut parsed.type_identifiers);
285
286 let c_tag_scope_witness;
287 {
288 let mut visitor = CppVisitor {
289 file,
290 source,
291 parsed: &mut parsed,
292 c_tag_semantics: dialect == LanguageDialect::CppC,
293 c_tag_scope_witness: false,
294 recovered_class_sibling_scopes: HashMap::default(),
295 consumed_fragment_regions: Vec::new(),
296 orphaned_namespaces,
297 partitioned_regions: Vec::new(),
298 namespace_forward_scans: HashMap::default(),
299 field_owners: None,
300 recovery_captures: Vec::new(),
301 object_macro_fields,
302 ambiguous_object_macro_fields: HashSet::default(),
303 };
304 visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
305 c_tag_scope_witness = visitor.c_tag_scope_witness;
306 }
307 recover_quoted_includes(source, &mut parsed);
311 parsed.finalize_deferred_replacements();
312 CppPrimaryReading {
313 parsed,
314 c_tag_scope_witness,
315 }
316}
317
318pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
328 left.declarations() != right.declarations()
329 || left.top_level_declarations != right.top_level_declarations
330 || left.definition_lookup_units != right.definition_lookup_units
331 || left.children != right.children
332 || left.ranges != right.ranges
333 || left.signatures != right.signatures
334 || left.type_aliases != right.type_aliases
335}
336
337pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
338 let trimmed = reference.trim();
339 let before_args = trimmed
340 .split_once('(')
341 .map(|(head, _)| head)
342 .unwrap_or(trimmed);
343 before_args
344 .rsplit_once("::")
345 .or_else(|| before_args.rsplit_once('.'))
346 .map(|(receiver, _)| receiver.to_string())
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352 use tree_sitter::Parser;
353
354 fn cpp_tree(source: &str) -> tree_sitter::Tree {
355 let mut parser = Parser::new();
356 parser
357 .set_language(&tree_sitter_cpp::LANGUAGE.into())
358 .expect("C++ grammar");
359 parser.parse(source, None).expect("C++ tree")
360 }
361
362 fn published_facts(parsed: &ParsedFile) -> Vec<String> {
365 let mut facts = vec![
366 format!("package={}", parsed.package_name),
367 format!("content_qualifier={}", parsed.content_qualifier),
368 format!("top_level={:?}", parsed.top_level_declarations),
369 format!("imports={:?}", parsed.imports),
370 format!("materializations={:?}", parsed.materialization_records),
371 format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
372 ];
373 let mut unordered = |label: &str, mut entries: Vec<String>| {
374 entries.sort();
375 facts.push(format!("{label}={entries:?}"));
376 };
377 unordered(
378 "declarations",
379 parsed.declarations().iter().map(debug_of).collect(),
380 );
381 unordered(
382 "definition_lookup",
383 parsed
384 .definition_lookup_units
385 .iter()
386 .map(debug_of)
387 .collect(),
388 );
389 unordered(
390 "type_identifiers",
391 parsed.type_identifiers.iter().map(debug_of).collect(),
392 );
393 unordered(
394 "type_aliases",
395 parsed.type_aliases.iter().map(debug_of).collect(),
396 );
397 unordered(
398 "scala_traits",
399 parsed.scala_traits.iter().map(debug_of).collect(),
400 );
401 unordered(
402 "test_region_units",
403 parsed.test_region_units.iter().map(debug_of).collect(),
404 );
405 unordered(
406 "navigation_truncated",
407 parsed
408 .navigation_ranges_truncated
409 .iter()
410 .map(debug_of)
411 .collect(),
412 );
413 unordered("children", pairs(&parsed.children));
414 unordered("ranges", pairs(&parsed.ranges));
415 unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
416 unordered("signatures", pairs(&parsed.signatures));
417 unordered("signature_metadata", pairs(&parsed.signature_metadata));
418 unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
419 unordered(
420 "supertype_lookup_paths",
421 pairs(&parsed.supertype_lookup_paths),
422 );
423 unordered("scala_exports", pairs(&parsed.scala_exports));
424 unordered(
425 "cpp_template_metadata",
426 pairs(&parsed.cpp_template_metadata),
427 );
428 unordered(
429 "ruby_method_dispatch_modes",
430 pairs(&parsed.ruby_method_dispatch_modes),
431 );
432 facts
433 }
434
435 fn debug_of<T: std::fmt::Debug>(value: T) -> String {
436 format!("{value:?}")
437 }
438
439 fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
440 map: &brokk_bifrost_core::hash::HashMap<K, V>,
441 ) -> Vec<String> {
442 map.iter().map(|entry| format!("{entry:?}")).collect()
443 }
444
445 #[test]
460 fn a_shared_reading_publishes_what_an_independent_one_publishes() {
461 let fixtures: &[(&str, &str, bool, bool)] = &[
462 (
463 "nested tag inside an aggregate, plus a nested include",
464 r#"
465#include <vector>
466struct outer {
467#include "member_list.def"
468 struct inner { int v; } i;
469};
470struct inner *p;
471 "#,
472 true,
473 true,
474 ),
475 (
476 "a quoted include only the line scan can recover",
477 r#"
478#include "visible.h"
479class Broken {
480 void method(
481#include "hidden.h"
482 "#,
483 false,
484 false,
485 ),
486 (
487 "forward declarations replaced by their definitions",
488 r#"
489typedef unsigned long long u64;
490namespace generated {
491struct tag0;
492struct tag1;
493struct tag1 {
494 struct nested { int v; } n;
495 u64 first;
496};
497struct tag0 { int second; };
498}
499 "#,
500 true,
501 true,
502 ),
503 (
504 "a fragmented export-macro class body",
505 r#"
506#define SIMPLECPP_LIB
507namespace simplecpp {
508using TokenString = std::string;
509struct Location { int line{}; };
510class SIMPLECPP_LIB Token {
511 TokenString prefix;
512 void prefix_method() {}
513 public:
514 Token(const TokenString &s, const Location &loc, bool wsahead = false) :
515 whitespaceahead(wsahead), location(loc), string(s)
516 {
517 flags();
518 }
519 struct Nested { int v; } nested;
520 TokenString string;
521 bool whitespaceahead;
522 Location location;
523 private:
524 void flags() {
525 whitespaceahead = true;
526 }
527};
528}
529 "#,
530 true,
531 true,
532 ),
533 (
534 "nested struct, union, and enum tags",
535 r#"
536struct StructOwner { struct StructTag { int value; }; };
537struct UnionOwner { union UnionTag { int value; }; };
538struct EnumOwner { enum EnumTag { value }; };
539"#,
540 true,
541 true,
542 ),
543 (
544 "C++ class and anonymous tag near misses",
545 r#"
546struct Owner {
547 class NestedClass { int value; };
548 struct { int anonymous_value; } anonymous_member;
549};
550struct TopLevel { int value; };
551"#,
552 false,
553 false,
554 ),
555 (
556 "tag reparsed under a recovered export class",
557 r#"
558namespace api {
559
560/**
561* Doc comment
562*/
563class PROJECT_PUBLIC_API(2, 0) RecoveryOwner : public virtual BaseKey {
564 public:
565 /** Construct from a point. */
566 RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
567
568#if defined(PROJECT_HAS_LEGACY_POINT)
569 /** Construct from a legacy point. */
570 RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
571#endif
572
573 struct RecoveredTag { int value; };
574 std::string algo_name() const override;
575 AlgorithmIdentifier algorithm_identifier() const override;
576};
577}
578"#,
579 true,
580 true,
581 ),
582 ];
583
584 for (name, source, expected_witness, expected_differs) in fixtures {
585 let file = ProjectFile::new(
586 std::env::current_dir().expect("test working directory must be available"),
587 "src/widget.h",
588 );
589 let tree = cpp_tree(source);
590 let root = tree.root_node();
591
592 let independent_primary = parse_cpp_file(&file, source, &tree);
593 let independent_c =
594 parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
595 let independent_primary_facts = published_facts(&independent_primary);
596 let independent_c_facts = published_facts(&independent_c);
597 let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
598 assert_eq!(
599 independent_differs,
600 *expected_differs,
601 "independent C/C++ projection difference for {name}; root.has_error()={}; CST={}; C++ declarations={:#?}; C declarations={:#?}; C++ facts: {independent_primary_facts:#?}; C facts: {independent_c_facts:#?}",
602 root.has_error(),
603 root.to_sexp(),
604 independent_primary.declarations(),
605 independent_c.declarations(),
606 );
607
608 let ancestry = ParentIndex::new(root);
609 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
610 let primary = parse_cpp_file_with_ancestry_and_orphaned_namespaces(
611 &file,
612 source,
613 root,
614 &ancestry,
615 &orphaned_namespaces,
616 );
617 let c_tag_scope_witness = primary.c_tag_scope_witness;
618 let shared_primary = primary.parsed;
619
620 if *name == "tag reparsed under a recovered export class" {
621 assert!(
622 root.has_error(),
623 "the recovery fixture must parse with errors"
624 );
625 let recovered_owner = independent_primary
626 .declarations()
627 .iter()
628 .find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
629 .expect("the malformed export-class head must recover its owner");
630 let owner_range = independent_primary
631 .ranges
632 .get(recovered_owner)
633 .and_then(|ranges| ranges.first())
634 .expect("the recovered owner must retain its class range");
635 let mut pending = vec![root];
636 let mut recognized_recovery = false;
637 while let Some(node) = pending.pop() {
638 if crate::declarations::recovered_fragmented_class_has_body(
639 node,
640 source,
641 "RecoveryOwner",
642 owner_range,
643 ) {
644 recognized_recovery = true;
645 break;
646 }
647 let mut cursor = node.walk();
648 pending.extend(node.named_children(&mut cursor));
649 }
650 assert!(
651 recognized_recovery,
652 "the fixture must enter the fragmented export-class recovery path"
653 );
654 let mut pending = vec![root];
655 let mut tag = None;
656 while let Some(node) = pending.pop() {
657 if matches!(
658 node.kind(),
659 "struct_specifier" | "union_specifier" | "enum_specifier"
660 ) && node.child_by_field_name("name").is_some_and(|name| {
661 name.utf8_text(source.as_bytes())
662 .is_ok_and(|text| text == "RecoveredTag")
663 }) {
664 tag = Some(node);
665 break;
666 }
667 let mut cursor = node.walk();
668 pending.extend(node.named_children(&mut cursor));
669 }
670 let tag = tag.expect("the original recovered fixture exposes its tag node");
671 let mut ancestor = ancestry.parent(tag);
672 while let Some(node) = ancestor {
673 assert!(
674 !matches!(
675 node.kind(),
676 "class_specifier" | "struct_specifier" | "union_specifier"
677 ),
678 "the original CST must not already give RecoveredTag an aggregate owner"
679 );
680 ancestor = ancestry.parent(node);
681 }
682 }
683
684 assert_eq!(
685 c_tag_scope_witness,
686 *expected_witness,
687 "unexpected C tag-scope witness for {name}; root.has_error()={}; CST={}; C++ declarations={:#?}; C declarations={:#?}; independent C++ facts: {independent_primary_facts:#?}; independent C facts: {independent_c_facts:#?}",
688 root.has_error(),
689 root.to_sexp(),
690 independent_primary.declarations(),
691 independent_c.declarations(),
692 );
693 assert_eq!(
694 independent_primary_facts,
695 published_facts(&shared_primary),
696 "C++ reading of {name}"
697 );
698 if *expected_witness {
699 let shared_c = parse_cpp_c_reading_with_orphaned_namespaces(
700 &file,
701 source,
702 root,
703 &ancestry,
704 &shared_primary,
705 &orphaned_namespaces,
706 );
707 assert_eq!(
708 independent_c_facts,
709 published_facts(&shared_c),
710 "C reading of {name}"
711 );
712 } else {
713 assert_eq!(
714 independent_primary_facts, independent_c_facts,
715 "a false witness must imply identical complete readings for {name}"
716 );
717 assert!(
718 !independent_differs,
719 "a false witness must make the existing publication comparator agree for {name}"
720 );
721 }
722 }
723 }
724
725 #[test]
729 fn the_nested_tag_fixture_really_has_two_readings() {
730 let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
731 let file = ProjectFile::new(
732 std::env::current_dir().expect("test working directory must be available"),
733 "src/widget.h",
734 );
735 let tree = cpp_tree(source);
736 let root = tree.root_node();
737 let ancestry = ParentIndex::new(root);
738 let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
739 let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
740 assert!(
741 cpp_projections_differ(&primary, &c_reading),
742 "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
743 primary.declarations(),
744 c_reading.declarations()
745 );
746 }
747
748 #[test]
758 fn includes_are_recorded_at_every_depth() {
759 let source = r#"
760#include <vector>
761
762class Widget {
763public:
764 int value() const;
765};
766
767int run() {
768 switch (0) {
769#include <sycl/info/aspects.def>
770 default:
771 return 0;
772 }
773}
774"#;
775 let file = ProjectFile::new(
776 std::env::current_dir().expect("test working directory must be available"),
777 "src/widget.cpp",
778 );
779 let mut parser = Parser::new();
780 parser
781 .set_language(&tree_sitter_cpp::LANGUAGE.into())
782 .expect("C++ grammar");
783 let tree = parser.parse(source, None).expect("C++ tree");
784
785 let parsed = parse_cpp_file(&file, source, &tree);
786 let includes = parsed
787 .imports
788 .iter()
789 .map(|import| import.raw_snippet.clone())
790 .collect::<Vec<_>>();
791
792 assert_eq!(
793 includes,
794 vec![
795 "#include <vector>".to_string(),
796 "#include <sycl/info/aspects.def>".to_string(),
797 ]
798 );
799 assert!(!parsed.declarations().is_empty());
800 }
801}