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
101pub fn parse_cpp_file_with_ancestry<'tree>(
103 file: &ProjectFile,
104 source: &str,
105 root: Node<'tree>,
106 ancestry: &ParentIndex<'tree>,
107) -> ParsedFile {
108 let dialect = LanguageDialect::for_path(Language::Cpp, file.rel_path());
109 parse_cpp_file_from_root(file, source, root, ancestry, dialect)
110}
111
112fn parse_cpp_file_from_root<'tree>(
113 file: &ProjectFile,
114 source: &str,
115 root: Node<'tree>,
116 ancestry: &ParentIndex<'tree>,
117 dialect: LanguageDialect,
118) -> ParsedFile {
119 parse_cpp_readings(file, source, root, ancestry, &[dialect])
120 .pop()
121 .expect("one dialect reading")
122}
123
124pub fn parse_cpp_file_with_readings<'tree>(
129 file: &ProjectFile,
130 source: &str,
131 root: Node<'tree>,
132 ancestry: &ParentIndex<'tree>,
133) -> (ParsedFile, Option<ParsedFile>) {
134 let mut readings = parse_cpp_readings(
135 file,
136 source,
137 root,
138 ancestry,
139 &[
140 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
141 LanguageDialect::CppC,
142 ],
143 );
144 let c = (readings.len() == 2).then(|| readings.pop().expect("C reading"));
145 let primary = readings.pop().expect("primary reading");
146 (primary, c)
147}
148
149pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
159 left.declarations() != right.declarations()
160 || left.top_level_declarations != right.top_level_declarations
161 || left.definition_lookup_units != right.definition_lookup_units
162 || left.children != right.children
163 || left.ranges != right.ranges
164 || left.signatures != right.signatures
165 || left.type_aliases != right.type_aliases
166 || left.resolution_facts != right.resolution_facts
167}
168
169pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
170 brokk_bifrost_core::analyzer::common::extract_call_reference_parts(reference, &["::", "."])
171 .map(|(receiver, _)| receiver.to_string())
172}
173
174pub fn is_numeric_initializer_data(
177 node: Node<'_>,
178 should_stop: &mut dyn FnMut() -> bool,
179) -> std::ops::ControlFlow<(), bool> {
180 use std::ops::ControlFlow;
181 if node.kind() != "initializer_list" {
182 return ControlFlow::Continue(false);
183 }
184 use brokk_bifrost_core::analyzer::tree_walk::{WalkControl, walk_tree_preorder};
185 let mut result = ControlFlow::Continue(true);
186 walk_tree_preorder(node, true, |current| {
187 if should_stop() {
188 result = ControlFlow::Break(());
189 return WalkControl::Break;
190 }
191 if current.is_error() || current.is_missing() {
192 result = ControlFlow::Continue(false);
193 return WalkControl::Break;
194 }
195 let allowed = match current.kind() {
196 "initializer_list" => true,
197 "number_literal" => current.child_count() == 0,
198 "unary_expression" => {
199 current.child_count() == 2
200 && current
201 .child_by_field_name("operator")
202 .is_some_and(|op| matches!(op.kind(), "+" | "-"))
203 && current
204 .child_by_field_name("argument")
205 .is_some_and(|arg| arg.kind() == "number_literal")
206 }
207 "{" | "}" | "," => current
208 .parent()
209 .is_some_and(|parent| parent.kind() == "initializer_list"),
210 "+" | "-" => current
211 .parent()
212 .is_some_and(|parent| parent.kind() == "unary_expression"),
213 _ => false,
214 };
215 if allowed {
216 WalkControl::Continue
217 } else {
218 result = ControlFlow::Continue(false);
219 WalkControl::Break
220 }
221 });
222 result
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use brokk_bifrost_core::analyzer::resolution_facts::{ResolutionNameFact, ResolutionNameId};
229 use tree_sitter::Parser;
230
231 fn cpp_tree(source: &str) -> tree_sitter::Tree {
232 crate::diagnostics::parse_cpp_tree(source)
233 }
234
235 fn published_facts(parsed: &ParsedFile) -> Vec<String> {
238 let mut facts = vec![
239 format!("package={}", parsed.package_name),
240 format!("content_qualifier={}", parsed.content_qualifier),
241 format!("top_level={:?}", parsed.top_level_declarations),
242 format!("imports={:?}", parsed.imports),
243 format!("materializations={:?}", parsed.materialization_records),
244 format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
245 ];
246 let mut unordered = |label: &str, mut entries: Vec<String>| {
247 entries.sort();
248 facts.push(format!("{label}={entries:?}"));
249 };
250 unordered(
251 "declarations",
252 parsed.declarations().iter().map(debug_of).collect(),
253 );
254 unordered(
255 "definition_lookup",
256 parsed
257 .definition_lookup_units
258 .iter()
259 .map(debug_of)
260 .collect(),
261 );
262 unordered(
263 "type_identifiers",
264 parsed.type_identifiers.iter().map(debug_of).collect(),
265 );
266 unordered(
267 "type_aliases",
268 parsed.type_aliases.iter().map(debug_of).collect(),
269 );
270 unordered(
271 "scala_traits",
272 parsed.scala_traits.iter().map(debug_of).collect(),
273 );
274 unordered(
275 "test_region_units",
276 parsed.test_region_units.iter().map(debug_of).collect(),
277 );
278 unordered(
279 "navigation_truncated",
280 parsed
281 .navigation_ranges_truncated
282 .iter()
283 .map(debug_of)
284 .collect(),
285 );
286 unordered("children", pairs(&parsed.children));
287 unordered("ranges", pairs(&parsed.ranges));
288 unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
289 unordered("signatures", pairs(&parsed.signatures));
290 unordered("signature_metadata", pairs(&parsed.signature_metadata));
291 unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
292 unordered(
293 "supertype_lookup_paths",
294 pairs(&parsed.supertype_lookup_paths),
295 );
296 unordered("scala_exports", pairs(&parsed.scala_exports));
297 unordered(
298 "cpp_template_metadata",
299 pairs(&parsed.cpp_template_metadata),
300 );
301 unordered(
302 "ruby_method_dispatch_modes",
303 pairs(&parsed.ruby_method_dispatch_modes),
304 );
305 facts
306 }
307
308 fn debug_of<T: std::fmt::Debug>(value: T) -> String {
309 format!("{value:?}")
310 }
311
312 fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
313 map: &brokk_bifrost_core::hash::HashMap<K, V>,
314 ) -> Vec<String> {
315 map.iter().map(|entry| format!("{entry:?}")).collect()
316 }
317
318 #[test]
319 fn a_shared_reading_publishes_what_an_independent_one_publishes() {
320 let fixtures: &[(&str, &str, bool, bool)] = &[
321 (
322 "nested tag inside an aggregate, plus a nested include",
323 r#"
324#include <vector>
325struct outer {
326#include "member_list.def"
327 struct inner { int v; } i;
328};
329struct inner *p;
330 "#,
331 true,
332 true,
333 ),
334 (
335 "a quoted include only the line scan can recover",
336 r#"
337#include "visible.h"
338class Broken {
339 void method(
340#include "hidden.h"
341 "#,
342 false,
343 false,
344 ),
345 (
346 "forward declarations replaced by their definitions",
347 r#"
348typedef unsigned long long u64;
349namespace generated {
350struct tag0;
351struct tag1;
352struct tag1 {
353 struct nested { int v; } n;
354 u64 first;
355};
356struct tag0 { int second; };
357}
358 "#,
359 true,
360 true,
361 ),
362 (
363 "a fragmented export-macro class body",
364 r#"
365#define SIMPLECPP_LIB
366namespace simplecpp {
367using TokenString = std::string;
368struct Location { int line{}; };
369class SIMPLECPP_LIB Token {
370 TokenString prefix;
371 void prefix_method() {}
372 public:
373 Token(const TokenString &s, const Location &loc, bool wsahead = false) :
374 whitespaceahead(wsahead), location(loc), string(s)
375 {
376 flags();
377 }
378 struct Nested { int v; } nested;
379 TokenString string;
380 bool whitespaceahead;
381 Location location;
382 private:
383 void flags() {
384 whitespaceahead = true;
385 }
386};
387}
388 "#,
389 true,
390 true,
391 ),
392 (
393 "nested struct, union, and enum tags",
394 r#"
395struct StructOwner { struct StructTag { int value; }; };
396struct UnionOwner { union UnionTag { int value; }; };
397struct EnumOwner { enum EnumTag { value }; };
398"#,
399 true,
400 true,
401 ),
402 (
403 "C++ class and anonymous tag near misses",
404 r#"
405struct Owner {
406 class NestedClass { int value; };
407 struct { int anonymous_value; } anonymous_member;
408};
409struct TopLevel { int value; };
410"#,
411 false,
412 false,
413 ),
414 (
415 "tag reparsed under a recovered export class",
416 r#"
417namespace api {
418
419/**
420* Doc comment
421*/
422class PROJECT_PUBLIC_API(2, 0) RecoveryOwner : public virtual BaseKey {
423 public:
424 /** Construct from a point. */
425 RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
426
427#if defined(PROJECT_HAS_LEGACY_POINT)
428 /** Construct from a legacy point. */
429 RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
430#endif
431
432 struct RecoveredTag { int value; };
433 std::string algo_name() const override;
434 AlgorithmIdentifier algorithm_identifier() const override;
435};
436}
437"#,
438 true,
439 true,
440 ),
441 ];
442
443 for (name, source, expected_witness, expected_differs) in fixtures {
444 let file = ProjectFile::new(
445 std::env::current_dir().expect("test working directory must be available"),
446 "src/widget.h",
447 );
448 let tree = cpp_tree(source);
449 let root = tree.root_node();
450
451 let independent_primary = parse_cpp_file(&file, source, &tree);
452 let independent_c =
453 parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
454 let independent_primary_facts = published_facts(&independent_primary);
455 let independent_c_facts = published_facts(&independent_c);
456 let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
457 assert_eq!(
458 independent_differs,
459 *expected_differs,
460 "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:#?}",
461 root.has_error(),
462 root.to_sexp(),
463 independent_primary.declarations(),
464 independent_c.declarations(),
465 );
466
467 let ancestry = ParentIndex::new(root);
468 let (shared_primary, shared_c) =
469 parse_cpp_file_with_readings(&file, source, root, &ancestry);
470 let c_tag_scope_witness = shared_c.is_some();
471 if *name == "tag reparsed under a recovered export class" {
472 assert!(
473 root.has_error(),
474 "the recovery fixture must parse with errors"
475 );
476 let recovered_owner = independent_primary
477 .declarations()
478 .iter()
479 .find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
480 .expect("the malformed export-class head must recover its owner");
481 let owner_range = independent_primary
482 .ranges
483 .get(recovered_owner)
484 .and_then(|ranges| ranges.first())
485 .expect("the recovered owner must retain its class range");
486 let mut pending = vec![root];
487 let mut recognized_recovery = false;
488 while let Some(node) = pending.pop() {
489 if crate::declarations::recovered_fragmented_class_has_body(
490 node,
491 source,
492 "RecoveryOwner",
493 owner_range,
494 ) {
495 recognized_recovery = true;
496 break;
497 }
498 let mut cursor = node.walk();
499 pending.extend(node.named_children(&mut cursor));
500 }
501 assert!(
502 recognized_recovery,
503 "the fixture must enter the fragmented export-class recovery path"
504 );
505 let mut pending = vec![root];
506 let mut tag = None;
507 while let Some(node) = pending.pop() {
508 if matches!(
509 node.kind(),
510 "struct_specifier" | "union_specifier" | "enum_specifier"
511 ) && node.child_by_field_name("name").is_some_and(|name| {
512 name.utf8_text(source.as_bytes())
513 .is_ok_and(|text| text == "RecoveredTag")
514 }) {
515 tag = Some(node);
516 break;
517 }
518 let mut cursor = node.walk();
519 pending.extend(node.named_children(&mut cursor));
520 }
521 let tag = tag.expect("the original recovered fixture exposes its tag node");
522 let mut ancestor = ancestry.parent(tag);
523 while let Some(node) = ancestor {
524 assert!(
525 !matches!(
526 node.kind(),
527 "class_specifier" | "struct_specifier" | "union_specifier"
528 ),
529 "the original CST must not already give RecoveredTag an aggregate owner"
530 );
531 ancestor = ancestry.parent(node);
532 }
533 }
534
535 assert_eq!(
536 c_tag_scope_witness,
537 *expected_witness,
538 "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:#?}",
539 root.has_error(),
540 root.to_sexp(),
541 independent_primary.declarations(),
542 independent_c.declarations(),
543 );
544 assert_eq!(
545 independent_primary_facts,
546 published_facts(&shared_primary),
547 "C++ reading of {name}"
548 );
549 if *expected_witness {
550 let shared_c = shared_c.expect("a witnessed C reading is emitted");
551 assert_eq!(
552 independent_c_facts,
553 published_facts(&shared_c),
554 "C reading of {name}"
555 );
556 } else {
557 assert_eq!(
558 independent_primary_facts, independent_c_facts,
559 "a false witness must imply identical complete readings for {name}"
560 );
561 assert!(
562 !independent_differs,
563 "a false witness must make the existing publication comparator agree for {name}"
564 );
565 }
566 }
567 }
568 #[test]
569 fn the_nested_tag_fixture_really_has_two_readings() {
570 let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
571 let file = ProjectFile::new(
572 std::env::current_dir().expect("test working directory must be available"),
573 "src/widget.h",
574 );
575 let tree = cpp_tree(source);
576 let root = tree.root_node();
577 let ancestry = ParentIndex::new(root);
578 let (primary, c_reading) = parse_cpp_file_with_readings(&file, source, root, &ancestry);
579 let c_reading = c_reading.expect("nested tag requires a C reading");
580 assert!(
581 cpp_projections_differ(&primary, &c_reading),
582 "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
583 primary.declarations(),
584 c_reading.declarations()
585 );
586 }
587 #[test]
588 fn projection_difference_includes_resolution_facts() {
589 let source = "struct Widget {};\n";
590 let file = ProjectFile::new(
591 std::env::current_dir().expect("test working directory must be available"),
592 "src/widget.h",
593 );
594 let tree = cpp_tree(source);
595 let primary = parse_cpp_file(&file, source, &tree);
596 let mut fact_only_difference = primary.clone();
597 assert!(!cpp_projections_differ(&primary, &fact_only_difference));
598
599 fact_only_difference
600 .resolution_facts
601 .names
602 .push(ResolutionNameFact {
603 id: ResolutionNameId::new(0),
604 spelling: "fact-only-difference".to_owned(),
605 });
606 assert!(cpp_projections_differ(&primary, &fact_only_difference));
607 }
608 #[test]
609 fn includes_are_recorded_at_every_depth() {
610 let source = r#"
611#include <vector>
612
613class Widget {
614public:
615 int value() const;
616};
617
618int run() {
619 switch (0) {
620#include <sycl/info/aspects.def>
621 default:
622 return 0;
623 }
624}
625"#;
626 let file = ProjectFile::new(
627 std::env::current_dir().expect("test working directory must be available"),
628 "src/widget.cpp",
629 );
630 let mut parser = Parser::new();
631 parser
632 .set_language(&tree_sitter_cpp::LANGUAGE.into())
633 .expect("C++ grammar");
634 let tree = parser.parse(source, None).expect("C++ tree");
635
636 let parsed = parse_cpp_file(&file, source, &tree);
637 let includes = parsed
638 .imports
639 .iter()
640 .map(|import| import.raw_snippet.clone())
641 .collect::<Vec<_>>();
642
643 assert_eq!(
644 includes,
645 vec![
646 "#include <vector>".to_string(),
647 "#include <sycl/info/aspects.def>".to_string(),
648 ]
649 );
650 assert!(!parsed.declarations().is_empty());
651 }
652}