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
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 fn parse_cpp_file_with_object_macro_fields(
63 file: &ProjectFile,
64 source: &str,
65 tree: &Tree,
66 object_macro_fields: HashMap<String, ObjectMacroReplacement>,
67) -> ParsedFile {
68 let root = tree.root_node();
69 let ancestry = ParentIndex::new(root);
70 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
71 parse_cpp_reading_with_object_macro_fields(
72 file,
73 source,
74 root,
75 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
76 &ancestry,
77 object_macro_fields,
78 &orphaned_namespaces,
79 )
80}
81
82pub fn parse_cpp_file_in_dialect(
97 file: &ProjectFile,
98 source: &str,
99 tree: &Tree,
100 dialect: LanguageDialect,
101) -> ParsedFile {
102 let root = tree.root_node();
103 let ancestry = ParentIndex::new(root);
104 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
105 parse_cpp_reading(file, source, root, dialect, &ancestry, &orphaned_namespaces)
106}
107
108pub fn parse_cpp_file_with_ancestry<'tree>(
115 file: &ProjectFile,
116 source: &str,
117 root: Node<'tree>,
118 ancestry: &ParentIndex<'tree>,
119) -> ParsedFile {
120 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
121 parse_cpp_file_with_ancestry_and_orphaned_namespaces(
122 file,
123 source,
124 root,
125 ancestry,
126 &orphaned_namespaces,
127 )
128}
129
130pub fn parse_cpp_file_with_ancestry_and_orphaned_namespaces<'tree>(
137 file: &ProjectFile,
138 source: &str,
139 root: Node<'tree>,
140 ancestry: &ParentIndex<'tree>,
141 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
142) -> ParsedFile {
143 parse_cpp_reading(
144 file,
145 source,
146 root,
147 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
148 ancestry,
149 orphaned_namespaces,
150 )
151}
152
153pub fn parse_cpp_c_reading<'tree>(
165 file: &ProjectFile,
166 source: &str,
167 root: Node<'tree>,
168 ancestry: &ParentIndex<'tree>,
169 primary: &ParsedFile,
170) -> ParsedFile {
171 let orphaned_namespaces = OrphanedNamespaceScopeIndex::build(root, source);
172 parse_cpp_c_reading_with_orphaned_namespaces(
173 file,
174 source,
175 root,
176 ancestry,
177 primary,
178 &orphaned_namespaces,
179 )
180}
181
182pub fn parse_cpp_c_reading_with_orphaned_namespaces<'tree>(
188 file: &ProjectFile,
189 source: &str,
190 root: Node<'tree>,
191 ancestry: &ParentIndex<'tree>,
192 primary: &ParsedFile,
193 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
194) -> ParsedFile {
195 let mut parsed = ParsedFile::new(String::new());
196 parsed.imports = primary.imports.clone();
197 parsed.type_identifiers = primary.type_identifiers.clone();
198 {
199 let mut visitor = CppVisitor {
200 file,
201 source,
202 parsed: &mut parsed,
203 c_tag_semantics: true,
204 recovered_class_sibling_scopes: HashMap::default(),
205 consumed_fragment_regions: Vec::new(),
206 orphaned_namespaces,
207 partitioned_regions: Vec::new(),
208 namespace_forward_scans: HashMap::default(),
209 field_owners: None,
210 recovery_captures: Vec::new(),
211 object_macro_fields: HashMap::default(),
212 ambiguous_object_macro_fields: HashSet::default(),
213 };
214 visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
215 }
216 parsed.finalize_deferred_replacements();
217
218 #[cfg(debug_assertions)]
219 {
220 let mut recomputed = ParsedFile::new(String::new());
221 collect_cpp_includes(root, source, &mut recomputed);
222 collect_cpp_identifiers(root, source, &mut recomputed.type_identifiers);
223 recover_quoted_includes(source, &mut recomputed);
224 assert_eq!(
225 parsed.imports, recomputed.imports,
226 "the C reading's includes are the C++ reading's includes: {:?}",
227 file
228 );
229 assert_eq!(
230 parsed.type_identifiers, recomputed.type_identifiers,
231 "the C reading's identifiers are the C++ reading's identifiers: {:?}",
232 file
233 );
234 }
235
236 parsed
237}
238
239fn parse_cpp_reading<'tree>(
241 file: &ProjectFile,
242 source: &str,
243 root: Node<'tree>,
244 dialect: LanguageDialect,
245 ancestry: &ParentIndex<'tree>,
246 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
247) -> ParsedFile {
248 parse_cpp_reading_with_object_macro_fields(
249 file,
250 source,
251 root,
252 dialect,
253 ancestry,
254 HashMap::default(),
255 orphaned_namespaces,
256 )
257}
258
259fn parse_cpp_reading_with_object_macro_fields<'tree>(
260 file: &ProjectFile,
261 source: &str,
262 root: Node<'tree>,
263 dialect: LanguageDialect,
264 ancestry: &ParentIndex<'tree>,
265 object_macro_fields: HashMap<String, ObjectMacroReplacement>,
266 orphaned_namespaces: &OrphanedNamespaceScopeIndex,
267) -> ParsedFile {
268 let mut parsed = ParsedFile::new(String::new());
269
270 collect_cpp_includes(root, source, &mut parsed);
271 collect_cpp_identifiers(root, source, &mut parsed.type_identifiers);
272
273 {
274 let mut visitor = CppVisitor {
275 file,
276 source,
277 parsed: &mut parsed,
278 c_tag_semantics: dialect == LanguageDialect::CppC,
279 recovered_class_sibling_scopes: HashMap::default(),
280 consumed_fragment_regions: Vec::new(),
281 orphaned_namespaces,
282 partitioned_regions: Vec::new(),
283 namespace_forward_scans: HashMap::default(),
284 field_owners: None,
285 recovery_captures: Vec::new(),
286 object_macro_fields,
287 ambiguous_object_macro_fields: HashSet::default(),
288 };
289 visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
290 }
291 recover_quoted_includes(source, &mut parsed);
295 parsed.finalize_deferred_replacements();
296 parsed
297}
298
299pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
309 left.declarations() != right.declarations()
310 || left.top_level_declarations != right.top_level_declarations
311 || left.definition_lookup_units != right.definition_lookup_units
312 || left.children != right.children
313 || left.ranges != right.ranges
314 || left.signatures != right.signatures
315 || left.type_aliases != right.type_aliases
316}
317
318pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
319 let trimmed = reference.trim();
320 let before_args = trimmed
321 .split_once('(')
322 .map(|(head, _)| head)
323 .unwrap_or(trimmed);
324 before_args
325 .rsplit_once("::")
326 .or_else(|| before_args.rsplit_once('.'))
327 .map(|(receiver, _)| receiver.to_string())
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use tree_sitter::Parser;
334
335 fn cpp_tree(source: &str) -> tree_sitter::Tree {
336 let mut parser = Parser::new();
337 parser
338 .set_language(&tree_sitter_cpp::LANGUAGE.into())
339 .expect("C++ grammar");
340 parser.parse(source, None).expect("C++ tree")
341 }
342
343 fn published_facts(parsed: &ParsedFile) -> Vec<String> {
346 let mut facts = vec![
347 format!("package={}", parsed.package_name),
348 format!("content_qualifier={}", parsed.content_qualifier),
349 format!("top_level={:?}", parsed.top_level_declarations),
350 format!("imports={:?}", parsed.imports),
351 format!("materializations={:?}", parsed.materialization_records),
352 format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
353 ];
354 let mut unordered = |label: &str, mut entries: Vec<String>| {
355 entries.sort();
356 facts.push(format!("{label}={entries:?}"));
357 };
358 unordered(
359 "declarations",
360 parsed.declarations().iter().map(debug_of).collect(),
361 );
362 unordered(
363 "definition_lookup",
364 parsed
365 .definition_lookup_units
366 .iter()
367 .map(debug_of)
368 .collect(),
369 );
370 unordered(
371 "type_identifiers",
372 parsed.type_identifiers.iter().map(debug_of).collect(),
373 );
374 unordered(
375 "type_aliases",
376 parsed.type_aliases.iter().map(debug_of).collect(),
377 );
378 unordered(
379 "scala_traits",
380 parsed.scala_traits.iter().map(debug_of).collect(),
381 );
382 unordered(
383 "test_region_units",
384 parsed.test_region_units.iter().map(debug_of).collect(),
385 );
386 unordered(
387 "navigation_truncated",
388 parsed
389 .navigation_ranges_truncated
390 .iter()
391 .map(debug_of)
392 .collect(),
393 );
394 unordered("children", pairs(&parsed.children));
395 unordered("ranges", pairs(&parsed.ranges));
396 unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
397 unordered("signatures", pairs(&parsed.signatures));
398 unordered("signature_metadata", pairs(&parsed.signature_metadata));
399 unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
400 unordered(
401 "supertype_lookup_paths",
402 pairs(&parsed.supertype_lookup_paths),
403 );
404 unordered("scala_exports", pairs(&parsed.scala_exports));
405 unordered(
406 "cpp_template_metadata",
407 pairs(&parsed.cpp_template_metadata),
408 );
409 unordered(
410 "ruby_method_dispatch_modes",
411 pairs(&parsed.ruby_method_dispatch_modes),
412 );
413 facts
414 }
415
416 fn debug_of<T: std::fmt::Debug>(value: T) -> String {
417 format!("{value:?}")
418 }
419
420 fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
421 map: &brokk_bifrost_core::hash::HashMap<K, V>,
422 ) -> Vec<String> {
423 map.iter().map(|entry| format!("{entry:?}")).collect()
424 }
425
426 #[test]
441 fn a_shared_reading_publishes_what_an_independent_one_publishes() {
442 let fixtures: &[(&str, &str)] = &[
443 (
444 "nested tag inside an aggregate, plus a nested include",
445 r#"
446#include <vector>
447struct outer {
448#include "member_list.def"
449 struct inner { int v; } i;
450};
451struct inner *p;
452"#,
453 ),
454 (
455 "a quoted include only the line scan can recover",
456 r#"
457#include "visible.h"
458class Broken {
459 void method(
460#include "hidden.h"
461"#,
462 ),
463 (
464 "forward declarations replaced by their definitions",
465 r#"
466typedef unsigned long long u64;
467namespace generated {
468struct tag0;
469struct tag1;
470struct tag1 {
471 struct nested { int v; } n;
472 u64 first;
473};
474struct tag0 { int second; };
475}
476"#,
477 ),
478 (
479 "a fragmented export-macro class body",
480 r#"
481#define SIMPLECPP_LIB
482namespace simplecpp {
483using TokenString = std::string;
484struct Location { int line{}; };
485class SIMPLECPP_LIB Token {
486 TokenString prefix;
487 void prefix_method() {}
488 public:
489 Token(const TokenString &s, const Location &loc, bool wsahead = false) :
490 whitespaceahead(wsahead), location(loc), string(s)
491 {
492 flags();
493 }
494 struct Nested { int v; } nested;
495 TokenString string;
496 bool whitespaceahead;
497 Location location;
498 private:
499 void flags() {
500 whitespaceahead = true;
501 }
502};
503}
504"#,
505 ),
506 ];
507
508 for (name, source) in fixtures {
509 let file = ProjectFile::new(
510 std::env::current_dir().expect("test working directory must be available"),
511 "src/widget.h",
512 );
513 let tree = cpp_tree(source);
514 let root = tree.root_node();
515
516 let independent_primary = parse_cpp_file(&file, source, &tree);
517 let independent_c =
518 parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
519
520 let ancestry = ParentIndex::new(root);
521 let shared_primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
522 let shared_c = parse_cpp_c_reading(&file, source, root, &ancestry, &shared_primary);
523
524 assert_eq!(
525 published_facts(&independent_primary),
526 published_facts(&shared_primary),
527 "C++ reading of {name}"
528 );
529 assert_eq!(
530 published_facts(&independent_c),
531 published_facts(&shared_c),
532 "C reading of {name}"
533 );
534 }
535 }
536
537 #[test]
541 fn the_nested_tag_fixture_really_has_two_readings() {
542 let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
543 let file = ProjectFile::new(
544 std::env::current_dir().expect("test working directory must be available"),
545 "src/widget.h",
546 );
547 let tree = cpp_tree(source);
548 let root = tree.root_node();
549 let ancestry = ParentIndex::new(root);
550 let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
551 let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
552 assert!(
553 cpp_projections_differ(&primary, &c_reading),
554 "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
555 primary.declarations(),
556 c_reading.declarations()
557 );
558 }
559
560 #[test]
570 fn includes_are_recorded_at_every_depth() {
571 let source = r#"
572#include <vector>
573
574class Widget {
575public:
576 int value() const;
577};
578
579int run() {
580 switch (0) {
581#include <sycl/info/aspects.def>
582 default:
583 return 0;
584 }
585}
586"#;
587 let file = ProjectFile::new(
588 std::env::current_dir().expect("test working directory must be available"),
589 "src/widget.cpp",
590 );
591 let mut parser = Parser::new();
592 parser
593 .set_language(&tree_sitter_cpp::LANGUAGE.into())
594 .expect("C++ grammar");
595 let tree = parser.parse(source, None).expect("C++ tree");
596
597 let parsed = parse_cpp_file(&file, source, &tree);
598 let includes = parsed
599 .imports
600 .iter()
601 .map(|import| import.raw_snippet.clone())
602 .collect::<Vec<_>>();
603
604 assert_eq!(
605 includes,
606 vec![
607 "#include <vector>".to_string(),
608 "#include <sycl/info/aspects.def>".to_string(),
609 ]
610 );
611 assert!(!parsed.declarations().is_empty());
612 }
613}