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