1use crate::declarations::{
8 CppVisitor, collect_cpp_identifiers, collect_cpp_includes, recover_quoted_includes,
9};
10use brokk_bifrost_core::analyzer::ProjectFile;
11use brokk_bifrost_core::analyzer::cognitive_complexity;
12use brokk_bifrost_core::analyzer::model::{Language, LanguageDialect};
13use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
14use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
15use brokk_bifrost_core::hash::HashMap;
16use std::sync::LazyLock;
17use tree_sitter::{Node, Tree};
18
19pub const CPP_FILE_EXTENSION: &str = "cpp";
22
23pub static CPP_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
26 LazyLock::new(|| cognitive_complexity::Config {
27 if_types: &["if_statement"],
28 loop_types: &["for_statement", "while_statement", "do_statement"],
29 catch_types: &["catch_clause"],
30 conditional_types: &["conditional_expression"],
31 case_types: &["case_statement"],
32 binary_types: &["binary_expression"],
33 logical_operators: &["&&", "||", "and", "or"],
34 jump_types: &["break_statement", "continue_statement"],
35 named_function_boundary_types: &["function_definition"],
36 anonymous_function_types: &["lambda_expression"],
37 else_clause_types: &["else_clause"],
38 default_case_predicate: Some(cpp_is_default_case),
39 ..cognitive_complexity::Config::empty()
40 });
41
42fn cpp_is_default_case(node: Node<'_>, _source: &str) -> bool {
43 node.child_by_field_name("value").is_none()
44}
45
46pub fn parse_cpp_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
48 parse_cpp_file_in_dialect(
49 file,
50 source,
51 tree,
52 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
53 )
54}
55
56pub fn parse_cpp_file_in_dialect(
71 file: &ProjectFile,
72 source: &str,
73 tree: &Tree,
74 dialect: LanguageDialect,
75) -> ParsedFile {
76 let root = tree.root_node();
77 let ancestry = ParentIndex::new(root);
78 parse_cpp_reading(file, source, root, dialect, &ancestry)
79}
80
81pub fn parse_cpp_file_with_ancestry<'tree>(
88 file: &ProjectFile,
89 source: &str,
90 root: Node<'tree>,
91 ancestry: &ParentIndex<'tree>,
92) -> ParsedFile {
93 parse_cpp_reading(
94 file,
95 source,
96 root,
97 LanguageDialect::for_path(Language::Cpp, file.rel_path()),
98 ancestry,
99 )
100}
101
102pub fn parse_cpp_c_reading<'tree>(
114 file: &ProjectFile,
115 source: &str,
116 root: Node<'tree>,
117 ancestry: &ParentIndex<'tree>,
118 primary: &ParsedFile,
119) -> ParsedFile {
120 let mut parsed = ParsedFile::new(String::new());
121 parsed.imports = primary.imports.clone();
122 parsed.type_identifiers = primary.type_identifiers.clone();
123 walk_cpp_declarations(
124 file,
125 source,
126 root,
127 LanguageDialect::CppC,
128 ancestry,
129 &mut parsed,
130 );
131 parsed.finalize_deferred_replacements();
132
133 #[cfg(debug_assertions)]
134 {
135 let mut recomputed = ParsedFile::new(String::new());
136 collect_cpp_includes(root, source, &mut recomputed);
137 collect_cpp_identifiers(root, source, &mut recomputed.type_identifiers);
138 recover_quoted_includes(source, &mut recomputed);
139 assert_eq!(
140 parsed.imports, recomputed.imports,
141 "the C reading's includes are the C++ reading's includes: {:?}",
142 file
143 );
144 assert_eq!(
145 parsed.type_identifiers, recomputed.type_identifiers,
146 "the C reading's identifiers are the C++ reading's identifiers: {:?}",
147 file
148 );
149 }
150
151 parsed
152}
153
154fn parse_cpp_reading<'tree>(
156 file: &ProjectFile,
157 source: &str,
158 root: Node<'tree>,
159 dialect: LanguageDialect,
160 ancestry: &ParentIndex<'tree>,
161) -> ParsedFile {
162 let mut parsed = ParsedFile::new(String::new());
163
164 collect_cpp_includes(root, source, &mut parsed);
165 collect_cpp_identifiers(root, source, &mut parsed.type_identifiers);
166
167 walk_cpp_declarations(file, source, root, dialect, ancestry, &mut parsed);
168 recover_quoted_includes(source, &mut parsed);
172 parsed.finalize_deferred_replacements();
173 parsed
174}
175
176fn walk_cpp_declarations<'tree>(
180 file: &ProjectFile,
181 source: &str,
182 root: Node<'tree>,
183 dialect: LanguageDialect,
184 ancestry: &ParentIndex<'tree>,
185 parsed: &mut ParsedFile,
186) {
187 let mut visitor = CppVisitor {
188 file,
189 source,
190 parsed,
191 c_tag_semantics: dialect == LanguageDialect::CppC,
192 recovered_class_sibling_scopes: HashMap::default(),
193 consumed_fragment_regions: Vec::new(),
194 namespace_forward_scans: HashMap::default(),
195 field_owners: None,
196 recovery_captures: Vec::new(),
197 };
198 visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
199}
200
201pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
211 left.declarations() != right.declarations()
212 || left.top_level_declarations != right.top_level_declarations
213 || left.definition_lookup_units != right.definition_lookup_units
214 || left.children != right.children
215 || left.ranges != right.ranges
216 || left.signatures != right.signatures
217 || left.type_aliases != right.type_aliases
218}
219
220pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
221 let trimmed = reference.trim();
222 let before_args = trimmed
223 .split_once('(')
224 .map(|(head, _)| head)
225 .unwrap_or(trimmed);
226 before_args
227 .rsplit_once("::")
228 .or_else(|| before_args.rsplit_once('.'))
229 .map(|(receiver, _)| receiver.to_string())
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use tree_sitter::Parser;
236
237 fn cpp_tree(source: &str) -> tree_sitter::Tree {
238 let mut parser = Parser::new();
239 parser
240 .set_language(&tree_sitter_cpp::LANGUAGE.into())
241 .expect("C++ grammar");
242 parser.parse(source, None).expect("C++ tree")
243 }
244
245 fn published_facts(parsed: &ParsedFile) -> Vec<String> {
248 let mut facts = vec![
249 format!("package={}", parsed.package_name),
250 format!("content_qualifier={}", parsed.content_qualifier),
251 format!("top_level={:?}", parsed.top_level_declarations),
252 format!("imports={:?}", parsed.imports),
253 format!("materializations={:?}", parsed.materialization_records),
254 format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
255 ];
256 let mut unordered = |label: &str, mut entries: Vec<String>| {
257 entries.sort();
258 facts.push(format!("{label}={entries:?}"));
259 };
260 unordered(
261 "declarations",
262 parsed.declarations().iter().map(debug_of).collect(),
263 );
264 unordered(
265 "definition_lookup",
266 parsed
267 .definition_lookup_units
268 .iter()
269 .map(debug_of)
270 .collect(),
271 );
272 unordered(
273 "type_identifiers",
274 parsed.type_identifiers.iter().map(debug_of).collect(),
275 );
276 unordered(
277 "type_aliases",
278 parsed.type_aliases.iter().map(debug_of).collect(),
279 );
280 unordered(
281 "scala_traits",
282 parsed.scala_traits.iter().map(debug_of).collect(),
283 );
284 unordered(
285 "test_region_units",
286 parsed.test_region_units.iter().map(debug_of).collect(),
287 );
288 unordered(
289 "navigation_truncated",
290 parsed
291 .navigation_ranges_truncated
292 .iter()
293 .map(debug_of)
294 .collect(),
295 );
296 unordered("children", pairs(&parsed.children));
297 unordered("ranges", pairs(&parsed.ranges));
298 unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
299 unordered("signatures", pairs(&parsed.signatures));
300 unordered("signature_metadata", pairs(&parsed.signature_metadata));
301 unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
302 unordered(
303 "supertype_lookup_paths",
304 pairs(&parsed.supertype_lookup_paths),
305 );
306 unordered("scala_exports", pairs(&parsed.scala_exports));
307 unordered(
308 "cpp_template_metadata",
309 pairs(&parsed.cpp_template_metadata),
310 );
311 unordered(
312 "ruby_method_dispatch_modes",
313 pairs(&parsed.ruby_method_dispatch_modes),
314 );
315 facts
316 }
317
318 fn debug_of<T: std::fmt::Debug>(value: T) -> String {
319 format!("{value:?}")
320 }
321
322 fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
323 map: &brokk_bifrost_core::hash::HashMap<K, V>,
324 ) -> Vec<String> {
325 map.iter().map(|entry| format!("{entry:?}")).collect()
326 }
327
328 #[test]
343 fn a_shared_reading_publishes_what_an_independent_one_publishes() {
344 let fixtures: &[(&str, &str)] = &[
345 (
346 "nested tag inside an aggregate, plus a nested include",
347 r#"
348#include <vector>
349struct outer {
350#include "member_list.def"
351 struct inner { int v; } i;
352};
353struct inner *p;
354"#,
355 ),
356 (
357 "a quoted include only the line scan can recover",
358 r#"
359#include "visible.h"
360class Broken {
361 void method(
362#include "hidden.h"
363"#,
364 ),
365 (
366 "forward declarations replaced by their definitions",
367 r#"
368typedef unsigned long long u64;
369namespace generated {
370struct tag0;
371struct tag1;
372struct tag1 {
373 struct nested { int v; } n;
374 u64 first;
375};
376struct tag0 { int second; };
377}
378"#,
379 ),
380 (
381 "a fragmented export-macro class body",
382 r#"
383#define SIMPLECPP_LIB
384namespace simplecpp {
385using TokenString = std::string;
386struct Location { int line{}; };
387class SIMPLECPP_LIB Token {
388 TokenString prefix;
389 void prefix_method() {}
390 public:
391 Token(const TokenString &s, const Location &loc, bool wsahead = false) :
392 whitespaceahead(wsahead), location(loc), string(s)
393 {
394 flags();
395 }
396 struct Nested { int v; } nested;
397 TokenString string;
398 bool whitespaceahead;
399 Location location;
400 private:
401 void flags() {
402 whitespaceahead = true;
403 }
404};
405}
406"#,
407 ),
408 ];
409
410 for (name, source) 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
422 let ancestry = ParentIndex::new(root);
423 let shared_primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
424 let shared_c = parse_cpp_c_reading(&file, source, root, &ancestry, &shared_primary);
425
426 assert_eq!(
427 published_facts(&independent_primary),
428 published_facts(&shared_primary),
429 "C++ reading of {name}"
430 );
431 assert_eq!(
432 published_facts(&independent_c),
433 published_facts(&shared_c),
434 "C reading of {name}"
435 );
436 }
437 }
438
439 #[test]
443 fn the_nested_tag_fixture_really_has_two_readings() {
444 let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
445 let file = ProjectFile::new(
446 std::env::current_dir().expect("test working directory must be available"),
447 "src/widget.h",
448 );
449 let tree = cpp_tree(source);
450 let root = tree.root_node();
451 let ancestry = ParentIndex::new(root);
452 let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
453 let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
454 assert!(
455 cpp_projections_differ(&primary, &c_reading),
456 "the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
457 primary.declarations(),
458 c_reading.declarations()
459 );
460 }
461
462 #[test]
472 fn includes_are_recorded_at_every_depth() {
473 let source = r#"
474#include <vector>
475
476class Widget {
477public:
478 int value() const;
479};
480
481int run() {
482 switch (0) {
483#include <sycl/info/aspects.def>
484 default:
485 return 0;
486 }
487}
488"#;
489 let file = ProjectFile::new(
490 std::env::current_dir().expect("test working directory must be available"),
491 "src/widget.cpp",
492 );
493 let mut parser = Parser::new();
494 parser
495 .set_language(&tree_sitter_cpp::LANGUAGE.into())
496 .expect("C++ grammar");
497 let tree = parser.parse(source, None).expect("C++ tree");
498
499 let parsed = parse_cpp_file(&file, source, &tree);
500 let includes = parsed
501 .imports
502 .iter()
503 .map(|import| import.raw_snippet.clone())
504 .collect::<Vec<_>>();
505
506 assert_eq!(
507 includes,
508 vec![
509 "#include <vector>".to_string(),
510 "#include <sycl/info/aspects.def>".to_string(),
511 ]
512 );
513 assert!(!parsed.declarations().is_empty());
514 }
515}