1use crate::declarations::{
8 CppCallableObserver, parse_cpp_readings, parse_cpp_readings_observed,
9 parse_cpp_readings_with_object_macro_fields,
10};
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;
18use std::sync::LazyLock;
19use tree_sitter::{Node, Tree};
20
21pub const CPP_FILE_EXTENSION: &str = "cpp";
24
25pub static CPP_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
28 LazyLock::new(|| cognitive_complexity::Config {
29 if_types: &["if_statement"],
30 loop_types: &["for_statement", "while_statement", "do_statement"],
31 catch_types: &["catch_clause"],
32 conditional_types: &["conditional_expression"],
33 case_types: &["case_statement"],
34 binary_types: &["binary_expression"],
35 logical_operators: &["&&", "||", "and", "or"],
36 jump_types: &["break_statement", "continue_statement"],
37 named_function_boundary_types: &["function_definition"],
38 anonymous_function_types: &["lambda_expression"],
39 else_clause_types: &["else_clause"],
40 default_case_predicate: Some(cpp_is_default_case),
41 ..cognitive_complexity::Config::empty()
42 });
43
44fn cpp_is_default_case(node: Node<'_>, _source: &str) -> bool {
45 node.child_by_field_name("value").is_none()
46}
47
48pub fn parse_cpp_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
50 parse_cpp_file_in_dialect(
51 file,
52 source,
53 tree,
54 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
55 )
56}
57
58pub(crate) fn parse_cpp_file_with_callable_observer(
61 file: &ProjectFile,
62 source: &str,
63 tree: &Tree,
64 observer: &dyn CppCallableObserver,
65) -> ParsedFile {
66 let root = tree.root_node();
67 let ancestry = ParentIndex::new(root);
68 parse_cpp_readings_observed(
69 file,
70 source,
71 root,
72 &ancestry,
73 &[LanguageDialect::for_path(Language::Cpp, file.rel_path())],
74 HashMap::default(),
75 observer,
76 )
77 .pop()
78 .expect("one dialect reading")
79}
80
81pub fn parse_cpp_file_with_object_macro_fields(
86 file: &ProjectFile,
87 source: &str,
88 tree: &Tree,
89 object_macro_fields: HashMap<String, ObjectMacroReplacement>,
90) -> ParsedFile {
91 let root = tree.root_node();
92 let ancestry = ParentIndex::new(root);
93 parse_cpp_readings_with_object_macro_fields(
94 file,
95 source,
96 root,
97 &ancestry,
98 &[LanguageDialect::for_path(Language::Cpp, file.rel_path())],
99 object_macro_fields,
100 )
101 .pop()
102 .expect("one dialect reading")
103}
104
105pub fn parse_cpp_file_in_dialect(
117 file: &ProjectFile,
118 source: &str,
119 tree: &Tree,
120 dialect: LanguageDialect,
121) -> ParsedFile {
122 let root = tree.root_node();
123 let ancestry = ParentIndex::new(root);
124 parse_cpp_file_from_root(file, source, root, &ancestry, dialect)
125}
126
127fn parse_cpp_file_from_root<'tree>(
128 file: &ProjectFile,
129 source: &str,
130 root: Node<'tree>,
131 ancestry: &ParentIndex<'tree>,
132 dialect: LanguageDialect,
133) -> ParsedFile {
134 parse_cpp_readings(file, source, root, ancestry, &[dialect])
135 .pop()
136 .expect("one dialect reading")
137}
138
139pub fn parse_cpp_file_with_readings<'tree>(
144 file: &ProjectFile,
145 source: &str,
146 root: Node<'tree>,
147 ancestry: &ParentIndex<'tree>,
148) -> (ParsedFile, Option<ParsedFile>) {
149 let mut readings = parse_cpp_readings(
150 file,
151 source,
152 root,
153 ancestry,
154 &[
155 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
156 LanguageDialect::CppC,
157 ],
158 );
159 let c = (readings.len() == 2).then(|| readings.pop().expect("C reading"));
160 let primary = readings.pop().expect("primary reading");
161 (primary, c)
162}
163
164pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
174 left.declarations() != right.declarations()
175 || left.top_level_declarations != right.top_level_declarations
176 || left.definition_lookup_units != right.definition_lookup_units
177 || left.children != right.children
178 || left.ranges != right.ranges
179 || left.signatures != right.signatures
180 || left.type_aliases != right.type_aliases
181 || left.resolution_facts != right.resolution_facts
182}
183
184pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
185 brokk_bifrost_core::analyzer::common::extract_call_reference_parts(reference, &["::", "."])
186 .map(|(receiver, _)| receiver.to_string())
187}
188
189pub const CPP_LINE_DATA_KINDS: brokk_bifrost_core::analyzer::tree_walk::LineDataKinds =
193 brokk_bifrost_core::analyzer::tree_walk::LineDataKinds {
194 literals: &[
195 "string_literal",
196 "raw_string_literal",
197 "char_literal",
198 "number_literal",
199 ],
200 interpolations: &[],
201 composites: &[
202 ("initializer_list", &["{", "}", ","]),
203 ("unary_expression", &["-", "+"]),
204 ("concatenated_string", &[]),
205 ],
206 };
207
208#[cfg(test)]
209mod tests {
210
211 #[test]
212 fn numeric_initializer_grammar_and_cancellation() {
213 use crate::grammar_test_support::{SOURCE, at, field, nodes, tree};
214
215 use brokk_bifrost_core::analyzer::tree_walk::maximal_data_subtrees;
216 use std::ops::ControlFlow;
217 let parsed = tree(SOURCE);
218 let cases = [
222 (
223 499..517,
224 true,
225 &[
226 ("initializer_list", 499..517),
227 ("{", 499..500),
228 ("initializer_list", 500..507),
229 ("{", 500..501),
230 ("number_literal", 501..502),
231 (",", 502..503),
232 ("number_literal", 504..506),
233 ("}", 506..507),
234 (",", 507..508),
235 ("initializer_list", 509..516),
236 ("{", 509..510),
237 ("number_literal", 510..512),
238 (",", 512..513),
239 ("number_literal", 514..515),
240 ("}", 515..516),
241 ("}", 516..517),
242 ][..],
243 ),
244 (
245 535..552,
246 false,
247 &[
248 ("initializer_list", 535..552),
249 ("{", 535..536),
250 ("number_literal", 536..537),
251 (",", 537..538),
252 ("comment", 539..549),
253 ("number_literal", 550..551),
254 ("}", 551..552),
255 ][..],
256 ),
257 (
258 568..575,
259 false,
260 &[
261 ("initializer_list", 568..575),
262 ("{", 568..569),
263 ("identifier", 569..574),
264 ("}", 574..575),
265 ][..],
266 ),
267 (
268 591..599,
269 false,
270 &[
271 ("initializer_list", 591..599),
272 ("{", 591..592),
273 ("call_expression", 592..598),
274 ("identifier", 592..596),
275 ("argument_list", 596..598),
276 ("(", 596..597),
277 (")", 597..598),
278 ("}", 598..599),
279 ][..],
280 ),
281 (
282 616..622,
283 false,
284 &[
285 ("initializer_list", 616..622),
286 ("{", 616..617),
287 ("number_literal", 617..618),
288 (",", 618..619),
289 ("ERROR", 620..621),
290 ("ERROR", 620..621),
291 ("}", 621..622),
292 ][..],
293 ),
294 (
295 640..645,
296 false,
297 &[
298 ("initializer_list", 640..645),
299 ("{", 640..641),
300 ("number_literal", 641..642),
301 ("ERROR", 643..644),
302 ("number_literal", 643..644),
303 ("}", 644..645),
304 ][..],
305 ),
306 (
307 662..672,
308 true,
309 &[
310 ("initializer_list", 662..672),
311 ("{", 662..663),
312 ("unary_expression", 663..666),
313 ("-", 663..664),
314 ("number_literal", 665..666),
315 (",", 666..667),
316 ("unary_expression", 668..671),
317 ("+", 668..669),
318 ("number_literal", 670..671),
319 ("}", 671..672),
320 ][..],
321 ),
322 (
323 696..704,
324 false,
325 &[
326 ("initializer_list", 696..704),
327 ("{", 696..697),
328 ("call_expression", 697..703),
329 ("identifier", 697..701),
330 ("argument_list", 701..703),
331 ("(", 701..702),
332 ("number_literal", 702..703),
333 (")", 703..703),
334 ("}", 703..704),
335 ][..],
336 ),
337 ];
338 for (span, admitted, manifest) in cases {
339 let initializer = at(parsed.root_node(), "initializer_list", span.clone());
340 let actual = nodes(initializer);
341 assert_eq!(
342 actual
343 .iter()
344 .map(|node| (node.kind(), node.byte_range()))
345 .collect::<Vec<_>>(),
346 manifest
347 );
348 for (index, node) in actual.iter().enumerate() {
349 if node.kind() == "unary_expression" {
350 let (operator_kind, operator_span) = &manifest[index + 1];
351 field(*node, "operator", operator_kind, operator_span.clone());
352 field(
353 *node,
354 "argument",
355 "number_literal",
356 manifest[index + 2].1.clone(),
357 );
358 assert!(!node.child_by_field_name("operator").unwrap().is_named());
359 }
360 if node.kind() == "number_literal" && admitted {
361 assert!(matches!(
362 node.parent().unwrap().kind(),
363 "initializer_list" | "unary_expression"
364 ));
365 assert_eq!(node.child_count(), 0);
366 }
367 if node.kind() == ")" && node.start_byte() == node.end_byte() {
368 assert!(node.is_missing());
369 assert_eq!(node.parent().unwrap().kind(), "argument_list");
370 }
371 }
372 let mut visited = 0;
373 let selected =
374 maximal_data_subtrees(initializer, &CPP_LINE_DATA_KINDS, |_| true, &mut || {
375 visited += 1;
376 false
377 });
378 let ControlFlow::Continue(selected) = selected else {
379 panic!("an unstopped walk completes");
380 };
381 assert_eq!(
382 selected.iter().any(|subtree| subtree.range == span),
383 admitted,
384 "{span:?}"
385 );
386 assert_eq!(visited, manifest.len(), "poll manifest for {span:?}");
387 for cancel_at in 1..=manifest.len() {
388 let mut visited = 0;
389 assert_eq!(
390 maximal_data_subtrees(initializer, &CPP_LINE_DATA_KINDS, |_| true, &mut || {
391 visited += 1;
392 visited == cancel_at
393 }),
394 ControlFlow::Break(()),
395 "cancel at {cancel_at} in {span:?}"
396 );
397 assert_eq!(visited, cancel_at);
398 }
399 }
400 }
401
402 use super::*;
403 use brokk_bifrost_core::analyzer::resolution_facts::{ResolutionNameFact, ResolutionNameId};
404 use tree_sitter::Parser;
405
406 fn cpp_tree(source: &str) -> tree_sitter::Tree {
407 crate::diagnostics::parse_cpp_tree(source)
408 }
409
410 fn published_facts(parsed: &ParsedFile) -> Vec<String> {
413 let mut facts = vec![
414 format!("package={}", parsed.package_name),
415 format!("content_qualifier={}", parsed.content_qualifier),
416 format!("top_level={:?}", parsed.top_level_declarations),
417 format!("imports={:?}", parsed.imports),
418 format!("materializations={:?}", parsed.materialization_records),
419 format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
420 ];
421 let mut unordered = |label: &str, mut entries: Vec<String>| {
422 entries.sort();
423 facts.push(format!("{label}={entries:?}"));
424 };
425 unordered(
426 "declarations",
427 parsed.declarations().iter().map(debug_of).collect(),
428 );
429 unordered(
430 "definition_lookup",
431 parsed
432 .definition_lookup_units
433 .iter()
434 .map(debug_of)
435 .collect(),
436 );
437 unordered(
438 "type_identifiers",
439 parsed.type_identifiers.iter().map(debug_of).collect(),
440 );
441 unordered(
442 "type_aliases",
443 parsed.type_aliases.iter().map(debug_of).collect(),
444 );
445 unordered(
446 "scala_traits",
447 parsed.scala_traits.iter().map(debug_of).collect(),
448 );
449 unordered(
450 "test_region_units",
451 parsed.test_region_units.iter().map(debug_of).collect(),
452 );
453 unordered(
454 "navigation_truncated",
455 parsed
456 .navigation_ranges_truncated
457 .iter()
458 .map(debug_of)
459 .collect(),
460 );
461 unordered("children", pairs(&parsed.children));
462 unordered("ranges", pairs(&parsed.ranges));
463 unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
464 unordered("signatures", pairs(&parsed.signatures));
465 unordered("signature_metadata", pairs(&parsed.signature_metadata));
466 unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
467 unordered(
468 "supertype_lookup_paths",
469 pairs(&parsed.supertype_lookup_paths),
470 );
471 unordered("scala_exports", pairs(&parsed.scala_exports));
472 unordered(
473 "cpp_template_metadata",
474 pairs(&parsed.cpp_template_metadata),
475 );
476 unordered(
477 "ruby_method_dispatch_modes",
478 pairs(&parsed.ruby_method_dispatch_modes),
479 );
480 facts
481 }
482
483 fn debug_of<T: std::fmt::Debug>(value: T) -> String {
484 format!("{value:?}")
485 }
486
487 fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
488 map: &brokk_bifrost_core::hash::HashMap<K, V>,
489 ) -> Vec<String> {
490 map.iter().map(|entry| format!("{entry:?}")).collect()
491 }
492
493 #[test]
494 fn a_shared_reading_publishes_what_an_independent_one_publishes() {
495 let fixtures: &[(&str, &str, bool, bool)] = &[
496 (
497 "nested tag inside an aggregate, plus a nested include",
498 r#"
499#include <vector>
500struct outer {
501#include "member_list.def"
502 struct inner { int v; } i;
503};
504struct inner *p;
505 "#,
506 true,
507 true,
508 ),
509 (
510 "a quoted include only the line scan can recover",
511 r#"
512#include "visible.h"
513class Broken {
514 void method(
515#include "hidden.h"
516 "#,
517 false,
518 false,
519 ),
520 (
521 "forward declarations replaced by their definitions",
522 r#"
523typedef unsigned long long u64;
524namespace generated {
525struct tag0;
526struct tag1;
527struct tag1 {
528 struct nested { int v; } n;
529 u64 first;
530};
531struct tag0 { int second; };
532}
533 "#,
534 true,
535 true,
536 ),
537 (
538 "a fragmented export-macro class body",
539 r#"
540#define SIMPLECPP_LIB
541namespace simplecpp {
542using TokenString = std::string;
543struct Location { int line{}; };
544class SIMPLECPP_LIB Token {
545 TokenString prefix;
546 void prefix_method() {}
547 public:
548 Token(const TokenString &s, const Location &loc, bool wsahead = false) :
549 whitespaceahead(wsahead), location(loc), string(s)
550 {
551 flags();
552 }
553 struct Nested { int v; } nested;
554 TokenString string;
555 bool whitespaceahead;
556 Location location;
557 private:
558 void flags() {
559 whitespaceahead = true;
560 }
561};
562}
563 "#,
564 true,
565 true,
566 ),
567 (
568 "nested struct, union, and enum tags",
569 r#"
570struct StructOwner { struct StructTag { int value; }; };
571struct UnionOwner { union UnionTag { int value; }; };
572struct EnumOwner { enum EnumTag { value }; };
573"#,
574 true,
575 true,
576 ),
577 (
578 "C++ class and anonymous tag near misses",
579 r#"
580struct Owner {
581 class NestedClass { int value; };
582 struct { int anonymous_value; } anonymous_member;
583};
584struct TopLevel { int value; };
585"#,
586 false,
587 false,
588 ),
589 (
590 "tag reparsed under a recovered export class",
591 r#"
592namespace api {
593
594/**
595* Doc comment
596*/
597class PROJECT_PUBLIC_API RecoveryOwner : public virtual BaseKey {
598 public:
599 /** Construct from a point. */
600 RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
601
602#if defined(PROJECT_HAS_LEGACY_POINT)
603 /** Construct from a legacy point. */
604 RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
605#endif
606
607 struct RecoveredTag { int value; };
608 std::string algo_name() const override;
609 AlgorithmIdentifier algorithm_identifier() const override;
610};
611}
612"#,
613 true,
614 true,
615 ),
616 ];
617
618 for (name, source, expected_witness, expected_differs) in fixtures {
619 let file = ProjectFile::new(
620 std::env::current_dir().expect("test working directory must be available"),
621 "src/widget.h",
622 );
623 let tree = cpp_tree(source);
624 let root = tree.root_node();
625
626 let independent_primary = parse_cpp_file(&file, source, &tree);
627 let independent_c =
628 parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
629 let independent_primary_facts = published_facts(&independent_primary);
630 let independent_c_facts = published_facts(&independent_c);
631 let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
632 assert_eq!(
633 independent_differs,
634 *expected_differs,
635 "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:#?}",
636 root.has_error(),
637 root.to_sexp(),
638 independent_primary.declarations(),
639 independent_c.declarations(),
640 );
641
642 let ancestry = ParentIndex::new(root);
643 let (shared_primary, shared_c) =
644 parse_cpp_file_with_readings(&file, source, root, &ancestry);
645 let c_tag_scope_witness = shared_c.is_some();
646 if *name == "tag reparsed under a recovered export class" {
647 assert!(
648 root.has_error(),
649 "the recovery fixture must parse with errors"
650 );
651 let recovered_owner = independent_primary
652 .declarations()
653 .iter()
654 .find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
655 .expect("the malformed export-class head must recover its owner");
656 let owner_range = independent_primary
657 .ranges
658 .get(recovered_owner)
659 .and_then(|ranges| ranges.first())
660 .expect("the recovered owner must retain its class range");
661 assert!(
666 owner_range.start_byte < owner_range.end_byte,
667 "the recovered owner must retain a nonempty class range"
668 );
669 assert!(
670 independent_primary.materialization_records.iter().any(|record| matches!(
671 record,
672 brokk_bifrost_core::analyzer::structural::materialization::MaterializationRecord::RecoveredDeclaration { unit, .. }
673 if unit == recovered_owner
674 )),
675 "the fixture must enter export-class recovery: {:#?}",
676 independent_primary.materialization_records
677 );
678 let mut pending = vec![root];
679 let mut tag = None;
680 while let Some(node) = pending.pop() {
681 if matches!(
682 node.kind(),
683 "struct_specifier" | "union_specifier" | "enum_specifier"
684 ) && node.child_by_field_name("name").is_some_and(|name| {
685 name.utf8_text(source.as_bytes())
686 .is_ok_and(|text| text == "RecoveredTag")
687 }) {
688 tag = Some(node);
689 break;
690 }
691 let mut cursor = node.walk();
692 pending.extend(node.named_children(&mut cursor));
693 }
694 let tag = tag.expect("the original recovered fixture exposes its tag node");
695 let mut ancestor = ancestry.parent(tag);
696 while let Some(node) = ancestor {
697 assert!(
698 !matches!(
699 node.kind(),
700 "class_specifier" | "struct_specifier" | "union_specifier"
701 ),
702 "the original CST must not already give RecoveredTag an aggregate owner"
703 );
704 ancestor = ancestry.parent(node);
705 }
706 }
707
708 assert_eq!(
709 c_tag_scope_witness,
710 *expected_witness,
711 "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:#?}",
712 root.has_error(),
713 root.to_sexp(),
714 independent_primary.declarations(),
715 independent_c.declarations(),
716 );
717 assert_eq!(
718 independent_primary_facts,
719 published_facts(&shared_primary),
720 "C++ reading of {name}"
721 );
722 if *expected_witness {
723 let shared_c = shared_c.expect("a witnessed C reading is emitted");
724 assert_eq!(
725 independent_c_facts,
726 published_facts(&shared_c),
727 "C reading of {name}"
728 );
729 } else {
730 assert_eq!(
731 independent_primary_facts, independent_c_facts,
732 "a false witness must imply identical complete readings for {name}"
733 );
734 assert!(
735 !independent_differs,
736 "a false witness must make the existing publication comparator agree for {name}"
737 );
738 }
739 }
740 }
741 #[test]
742 fn the_nested_tag_fixture_really_has_two_readings() {
743 let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
744 let file = ProjectFile::new(
745 std::env::current_dir().expect("test working directory must be available"),
746 "src/widget.h",
747 );
748 let tree = cpp_tree(source);
749 let root = tree.root_node();
750 let ancestry = ParentIndex::new(root);
751 let (primary, c_reading) = parse_cpp_file_with_readings(&file, source, root, &ancestry);
752 let c_reading = c_reading.expect("nested tag requires a C reading");
753 assert!(
754 cpp_projections_differ(&primary, &c_reading),
755 "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
756 primary.declarations(),
757 c_reading.declarations()
758 );
759 }
760 #[test]
761 fn projection_difference_includes_resolution_facts() {
762 let source = "struct Widget {};\n";
763 let file = ProjectFile::new(
764 std::env::current_dir().expect("test working directory must be available"),
765 "src/widget.h",
766 );
767 let tree = cpp_tree(source);
768 let primary = parse_cpp_file(&file, source, &tree);
769 let mut fact_only_difference = primary.clone();
770 assert!(!cpp_projections_differ(&primary, &fact_only_difference));
771
772 fact_only_difference
773 .resolution_facts
774 .names
775 .push(ResolutionNameFact {
776 id: ResolutionNameId::new(0),
777 spelling: "fact-only-difference".to_owned(),
778 });
779 assert!(cpp_projections_differ(&primary, &fact_only_difference));
780 }
781 #[test]
782 fn includes_are_recorded_at_every_depth() {
783 let source = r#"
784#include <vector>
785
786class Widget {
787public:
788 int value() const;
789};
790
791int run() {
792 switch (0) {
793#include <sycl/info/aspects.def>
794 default:
795 return 0;
796 }
797}
798"#;
799 let file = ProjectFile::new(
800 std::env::current_dir().expect("test working directory must be available"),
801 "src/widget.cpp",
802 );
803 let mut parser = Parser::new();
804 parser
805 .set_language(&tree_sitter_cpp::LANGUAGE.into())
806 .expect("C++ grammar");
807 let tree = parser.parse(source, None).expect("C++ tree");
808
809 let parsed = parse_cpp_file(&file, source, &tree);
810 let includes = parsed
811 .imports
812 .iter()
813 .map(|import| import.raw_snippet.clone())
814 .collect::<Vec<_>>();
815
816 assert_eq!(
817 includes,
818 vec![
819 "#include <vector>".to_string(),
820 "#include <sycl/info/aspects.def>".to_string(),
821 ]
822 );
823 assert!(!parsed.declarations().is_empty());
824 }
825}