1use crate::declarations::{
8 CppVisitor, collect_cpp_identifiers, collect_cpp_includes, recover_quoted_includes,
9};
10use crate::graph::resolver::OrphanedNamespaceScopeIndex;
11use crate::graph::syntax::MacroReplacementField;
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, Vec<MacroReplacementField>>,
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, Vec<MacroReplacementField>>,
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, Vec<MacroReplacementField>>,
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 namespace_forward_scans: HashMap::default(),
248 field_owners: None,
249 recovery_captures: Vec::new(),
250 object_macro_fields,
251 ambiguous_object_macro_fields: HashSet::default(),
252 };
253 visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
254}
255
256pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
266 left.declarations() != right.declarations()
267 || left.top_level_declarations != right.top_level_declarations
268 || left.definition_lookup_units != right.definition_lookup_units
269 || left.children != right.children
270 || left.ranges != right.ranges
271 || left.signatures != right.signatures
272 || left.type_aliases != right.type_aliases
273}
274
275pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
276 let trimmed = reference.trim();
277 let before_args = trimmed
278 .split_once('(')
279 .map(|(head, _)| head)
280 .unwrap_or(trimmed);
281 before_args
282 .rsplit_once("::")
283 .or_else(|| before_args.rsplit_once('.'))
284 .map(|(receiver, _)| receiver.to_string())
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use tree_sitter::Parser;
291
292 fn cpp_tree(source: &str) -> tree_sitter::Tree {
293 let mut parser = Parser::new();
294 parser
295 .set_language(&tree_sitter_cpp::LANGUAGE.into())
296 .expect("C++ grammar");
297 parser.parse(source, None).expect("C++ tree")
298 }
299
300 fn published_facts(parsed: &ParsedFile) -> Vec<String> {
303 let mut facts = vec![
304 format!("package={}", parsed.package_name),
305 format!("content_qualifier={}", parsed.content_qualifier),
306 format!("top_level={:?}", parsed.top_level_declarations),
307 format!("imports={:?}", parsed.imports),
308 format!("materializations={:?}", parsed.materialization_records),
309 format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
310 ];
311 let mut unordered = |label: &str, mut entries: Vec<String>| {
312 entries.sort();
313 facts.push(format!("{label}={entries:?}"));
314 };
315 unordered(
316 "declarations",
317 parsed.declarations().iter().map(debug_of).collect(),
318 );
319 unordered(
320 "definition_lookup",
321 parsed
322 .definition_lookup_units
323 .iter()
324 .map(debug_of)
325 .collect(),
326 );
327 unordered(
328 "type_identifiers",
329 parsed.type_identifiers.iter().map(debug_of).collect(),
330 );
331 unordered(
332 "type_aliases",
333 parsed.type_aliases.iter().map(debug_of).collect(),
334 );
335 unordered(
336 "scala_traits",
337 parsed.scala_traits.iter().map(debug_of).collect(),
338 );
339 unordered(
340 "test_region_units",
341 parsed.test_region_units.iter().map(debug_of).collect(),
342 );
343 unordered(
344 "navigation_truncated",
345 parsed
346 .navigation_ranges_truncated
347 .iter()
348 .map(debug_of)
349 .collect(),
350 );
351 unordered("children", pairs(&parsed.children));
352 unordered("ranges", pairs(&parsed.ranges));
353 unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
354 unordered("signatures", pairs(&parsed.signatures));
355 unordered("signature_metadata", pairs(&parsed.signature_metadata));
356 unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
357 unordered(
358 "supertype_lookup_paths",
359 pairs(&parsed.supertype_lookup_paths),
360 );
361 unordered("scala_exports", pairs(&parsed.scala_exports));
362 unordered(
363 "cpp_template_metadata",
364 pairs(&parsed.cpp_template_metadata),
365 );
366 unordered(
367 "ruby_method_dispatch_modes",
368 pairs(&parsed.ruby_method_dispatch_modes),
369 );
370 facts
371 }
372
373 fn debug_of<T: std::fmt::Debug>(value: T) -> String {
374 format!("{value:?}")
375 }
376
377 fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
378 map: &brokk_bifrost_core::hash::HashMap<K, V>,
379 ) -> Vec<String> {
380 map.iter().map(|entry| format!("{entry:?}")).collect()
381 }
382
383 #[test]
398 fn a_shared_reading_publishes_what_an_independent_one_publishes() {
399 let fixtures: &[(&str, &str)] = &[
400 (
401 "nested tag inside an aggregate, plus a nested include",
402 r#"
403#include <vector>
404struct outer {
405#include "member_list.def"
406 struct inner { int v; } i;
407};
408struct inner *p;
409"#,
410 ),
411 (
412 "a quoted include only the line scan can recover",
413 r#"
414#include "visible.h"
415class Broken {
416 void method(
417#include "hidden.h"
418"#,
419 ),
420 (
421 "forward declarations replaced by their definitions",
422 r#"
423typedef unsigned long long u64;
424namespace generated {
425struct tag0;
426struct tag1;
427struct tag1 {
428 struct nested { int v; } n;
429 u64 first;
430};
431struct tag0 { int second; };
432}
433"#,
434 ),
435 (
436 "a fragmented export-macro class body",
437 r#"
438#define SIMPLECPP_LIB
439namespace simplecpp {
440using TokenString = std::string;
441struct Location { int line{}; };
442class SIMPLECPP_LIB Token {
443 TokenString prefix;
444 void prefix_method() {}
445 public:
446 Token(const TokenString &s, const Location &loc, bool wsahead = false) :
447 whitespaceahead(wsahead), location(loc), string(s)
448 {
449 flags();
450 }
451 struct Nested { int v; } nested;
452 TokenString string;
453 bool whitespaceahead;
454 Location location;
455 private:
456 void flags() {
457 whitespaceahead = true;
458 }
459};
460}
461"#,
462 ),
463 ];
464
465 for (name, source) in fixtures {
466 let file = ProjectFile::new(
467 std::env::current_dir().expect("test working directory must be available"),
468 "src/widget.h",
469 );
470 let tree = cpp_tree(source);
471 let root = tree.root_node();
472
473 let independent_primary = parse_cpp_file(&file, source, &tree);
474 let independent_c =
475 parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
476
477 let ancestry = ParentIndex::new(root);
478 let shared_primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
479 let shared_c = parse_cpp_c_reading(&file, source, root, &ancestry, &shared_primary);
480
481 assert_eq!(
482 published_facts(&independent_primary),
483 published_facts(&shared_primary),
484 "C++ reading of {name}"
485 );
486 assert_eq!(
487 published_facts(&independent_c),
488 published_facts(&shared_c),
489 "C reading of {name}"
490 );
491 }
492 }
493
494 #[test]
498 fn the_nested_tag_fixture_really_has_two_readings() {
499 let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
500 let file = ProjectFile::new(
501 std::env::current_dir().expect("test working directory must be available"),
502 "src/widget.h",
503 );
504 let tree = cpp_tree(source);
505 let root = tree.root_node();
506 let ancestry = ParentIndex::new(root);
507 let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
508 let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
509 assert!(
510 cpp_projections_differ(&primary, &c_reading),
511 "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
512 primary.declarations(),
513 c_reading.declarations()
514 );
515 }
516
517 #[test]
527 fn includes_are_recorded_at_every_depth() {
528 let source = r#"
529#include <vector>
530
531class Widget {
532public:
533 int value() const;
534};
535
536int run() {
537 switch (0) {
538#include <sycl/info/aspects.def>
539 default:
540 return 0;
541 }
542}
543"#;
544 let file = ProjectFile::new(
545 std::env::current_dir().expect("test working directory must be available"),
546 "src/widget.cpp",
547 );
548 let mut parser = Parser::new();
549 parser
550 .set_language(&tree_sitter_cpp::LANGUAGE.into())
551 .expect("C++ grammar");
552 let tree = parser.parse(source, None).expect("C++ tree");
553
554 let parsed = parse_cpp_file(&file, source, &tree);
555 let includes = parsed
556 .imports
557 .iter()
558 .map(|import| import.raw_snippet.clone())
559 .collect::<Vec<_>>();
560
561 assert_eq!(
562 includes,
563 vec![
564 "#include <vector>".to_string(),
565 "#include <sycl/info/aspects.def>".to_string(),
566 ]
567 );
568 assert!(!parsed.declarations().is_empty());
569 }
570}