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