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