use crate::declarations::{
CppVisitor, collect_cpp_identifiers, collect_cpp_includes, recover_quoted_includes,
};
use brokk_bifrost_core::analyzer::ProjectFile;
use brokk_bifrost_core::analyzer::cognitive_complexity;
use brokk_bifrost_core::analyzer::model::{Language, LanguageDialect};
use brokk_bifrost_core::analyzer::parsed_file::ParsedFile;
use brokk_bifrost_core::analyzer::tree_walk::ParentIndex;
use brokk_bifrost_core::hash::HashMap;
use std::sync::LazyLock;
use tree_sitter::{Node, Tree};
pub const CPP_FILE_EXTENSION: &str = "cpp";
pub static CPP_COGNITIVE_CONFIG: LazyLock<cognitive_complexity::Config> =
LazyLock::new(|| cognitive_complexity::Config {
if_types: &["if_statement"],
loop_types: &["for_statement", "while_statement", "do_statement"],
catch_types: &["catch_clause"],
conditional_types: &["conditional_expression"],
case_types: &["case_statement"],
binary_types: &["binary_expression"],
logical_operators: &["&&", "||", "and", "or"],
jump_types: &["break_statement", "continue_statement"],
named_function_boundary_types: &["function_definition"],
anonymous_function_types: &["lambda_expression"],
else_clause_types: &["else_clause"],
default_case_predicate: Some(cpp_is_default_case),
..cognitive_complexity::Config::empty()
});
fn cpp_is_default_case(node: Node<'_>, _source: &str) -> bool {
node.child_by_field_name("value").is_none()
}
pub fn parse_cpp_file(file: &ProjectFile, source: &str, tree: &Tree) -> ParsedFile {
parse_cpp_file_in_dialect(
file,
source,
tree,
LanguageDialect::for_path(Language::Cpp, file.rel_path()),
)
}
pub fn parse_cpp_file_in_dialect(
file: &ProjectFile,
source: &str,
tree: &Tree,
dialect: LanguageDialect,
) -> ParsedFile {
let root = tree.root_node();
let ancestry = ParentIndex::new(root);
parse_cpp_reading(file, source, root, dialect, &ancestry)
}
pub fn parse_cpp_file_with_ancestry<'tree>(
file: &ProjectFile,
source: &str,
root: Node<'tree>,
ancestry: &ParentIndex<'tree>,
) -> ParsedFile {
parse_cpp_reading(
file,
source,
root,
LanguageDialect::for_path(Language::Cpp, file.rel_path()),
ancestry,
)
}
pub fn parse_cpp_c_reading<'tree>(
file: &ProjectFile,
source: &str,
root: Node<'tree>,
ancestry: &ParentIndex<'tree>,
primary: &ParsedFile,
) -> ParsedFile {
let mut parsed = ParsedFile::new(String::new());
parsed.imports = primary.imports.clone();
parsed.type_identifiers = primary.type_identifiers.clone();
walk_cpp_declarations(
file,
source,
root,
LanguageDialect::CppC,
ancestry,
&mut parsed,
);
parsed.finalize_deferred_replacements();
#[cfg(debug_assertions)]
{
let mut recomputed = ParsedFile::new(String::new());
collect_cpp_includes(root, source, &mut recomputed);
collect_cpp_identifiers(root, source, &mut recomputed.type_identifiers);
recover_quoted_includes(source, &mut recomputed);
assert_eq!(
parsed.imports, recomputed.imports,
"the C reading's includes are the C++ reading's includes: {:?}",
file
);
assert_eq!(
parsed.type_identifiers, recomputed.type_identifiers,
"the C reading's identifiers are the C++ reading's identifiers: {:?}",
file
);
}
parsed
}
fn parse_cpp_reading<'tree>(
file: &ProjectFile,
source: &str,
root: Node<'tree>,
dialect: LanguageDialect,
ancestry: &ParentIndex<'tree>,
) -> ParsedFile {
let mut parsed = ParsedFile::new(String::new());
collect_cpp_includes(root, source, &mut parsed);
collect_cpp_identifiers(root, source, &mut parsed.type_identifiers);
walk_cpp_declarations(file, source, root, dialect, ancestry, &mut parsed);
recover_quoted_includes(source, &mut parsed);
parsed.finalize_deferred_replacements();
parsed
}
fn walk_cpp_declarations<'tree>(
file: &ProjectFile,
source: &str,
root: Node<'tree>,
dialect: LanguageDialect,
ancestry: &ParentIndex<'tree>,
parsed: &mut ParsedFile,
) {
let mut visitor = CppVisitor {
file,
source,
parsed,
c_tag_semantics: dialect == LanguageDialect::CppC,
recovered_class_sibling_scopes: HashMap::default(),
consumed_fragment_regions: Vec::new(),
namespace_forward_scans: HashMap::default(),
field_owners: None,
recovery_captures: Vec::new(),
};
visitor.visit_container(root, ancestry, "", None, None, None, Vec::new());
}
pub fn cpp_projections_differ(left: &ParsedFile, right: &ParsedFile) -> bool {
left.declarations() != right.declarations()
|| left.top_level_declarations != right.top_level_declarations
|| left.definition_lookup_units != right.definition_lookup_units
|| left.children != right.children
|| left.ranges != right.ranges
|| left.signatures != right.signatures
|| left.type_aliases != right.type_aliases
}
pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
let trimmed = reference.trim();
let before_args = trimmed
.split_once('(')
.map(|(head, _)| head)
.unwrap_or(trimmed);
before_args
.rsplit_once("::")
.or_else(|| before_args.rsplit_once('.'))
.map(|(receiver, _)| receiver.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use tree_sitter::Parser;
fn cpp_tree(source: &str) -> tree_sitter::Tree {
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
parser.parse(source, None).expect("C++ tree")
}
fn published_facts(parsed: &ParsedFile) -> Vec<String> {
let mut facts = vec![
format!("package={}", parsed.package_name),
format!("content_qualifier={}", parsed.content_qualifier),
format!("top_level={:?}", parsed.top_level_declarations),
format!("imports={:?}", parsed.imports),
format!("materializations={:?}", parsed.materialization_records),
format!("rust_usage_facts={:?}", parsed.rust_usage_facts),
];
let mut unordered = |label: &str, mut entries: Vec<String>| {
entries.sort();
facts.push(format!("{label}={entries:?}"));
};
unordered(
"declarations",
parsed.declarations().iter().map(debug_of).collect(),
);
unordered(
"definition_lookup",
parsed
.definition_lookup_units
.iter()
.map(debug_of)
.collect(),
);
unordered(
"type_identifiers",
parsed.type_identifiers.iter().map(debug_of).collect(),
);
unordered(
"type_aliases",
parsed.type_aliases.iter().map(debug_of).collect(),
);
unordered(
"scala_traits",
parsed.scala_traits.iter().map(debug_of).collect(),
);
unordered(
"test_region_units",
parsed.test_region_units.iter().map(debug_of).collect(),
);
unordered(
"navigation_truncated",
parsed
.navigation_ranges_truncated
.iter()
.map(debug_of)
.collect(),
);
unordered("children", pairs(&parsed.children));
unordered("ranges", pairs(&parsed.ranges));
unordered("navigation_ranges", pairs(&parsed.navigation_ranges));
unordered("signatures", pairs(&parsed.signatures));
unordered("signature_metadata", pairs(&parsed.signature_metadata));
unordered("raw_supertypes", pairs(&parsed.raw_supertypes));
unordered(
"supertype_lookup_paths",
pairs(&parsed.supertype_lookup_paths),
);
unordered("scala_exports", pairs(&parsed.scala_exports));
unordered(
"cpp_template_metadata",
pairs(&parsed.cpp_template_metadata),
);
unordered(
"ruby_method_dispatch_modes",
pairs(&parsed.ruby_method_dispatch_modes),
);
facts
}
fn debug_of<T: std::fmt::Debug>(value: T) -> String {
format!("{value:?}")
}
fn pairs<K: std::fmt::Debug, V: std::fmt::Debug>(
map: &brokk_bifrost_core::hash::HashMap<K, V>,
) -> Vec<String> {
map.iter().map(|entry| format!("{entry:?}")).collect()
}
#[test]
fn a_shared_reading_publishes_what_an_independent_one_publishes() {
let fixtures: &[(&str, &str)] = &[
(
"nested tag inside an aggregate, plus a nested include",
r#"
#include <vector>
struct outer {
#include "member_list.def"
struct inner { int v; } i;
};
struct inner *p;
"#,
),
(
"a quoted include only the line scan can recover",
r#"
#include "visible.h"
class Broken {
void method(
#include "hidden.h"
"#,
),
(
"forward declarations replaced by their definitions",
r#"
typedef unsigned long long u64;
namespace generated {
struct tag0;
struct tag1;
struct tag1 {
struct nested { int v; } n;
u64 first;
};
struct tag0 { int second; };
}
"#,
),
(
"a fragmented export-macro class body",
r#"
#define SIMPLECPP_LIB
namespace simplecpp {
using TokenString = std::string;
struct Location { int line{}; };
class SIMPLECPP_LIB Token {
TokenString prefix;
void prefix_method() {}
public:
Token(const TokenString &s, const Location &loc, bool wsahead = false) :
whitespaceahead(wsahead), location(loc), string(s)
{
flags();
}
struct Nested { int v; } nested;
TokenString string;
bool whitespaceahead;
Location location;
private:
void flags() {
whitespaceahead = true;
}
};
}
"#,
),
];
for (name, source) in fixtures {
let file = ProjectFile::new(
std::env::current_dir().expect("test working directory must be available"),
"src/widget.h",
);
let tree = cpp_tree(source);
let root = tree.root_node();
let independent_primary = parse_cpp_file(&file, source, &tree);
let independent_c =
parse_cpp_file_in_dialect(&file, source, &tree, LanguageDialect::CppC);
let ancestry = ParentIndex::new(root);
let shared_primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
let shared_c = parse_cpp_c_reading(&file, source, root, &ancestry, &shared_primary);
assert_eq!(
published_facts(&independent_primary),
published_facts(&shared_primary),
"C++ reading of {name}"
);
assert_eq!(
published_facts(&independent_c),
published_facts(&shared_c),
"C reading of {name}"
);
}
}
#[test]
fn the_nested_tag_fixture_really_has_two_readings() {
let source = "struct outer { struct inner { int v; } i; };\nstruct inner *p;\n";
let file = ProjectFile::new(
std::env::current_dir().expect("test working directory must be available"),
"src/widget.h",
);
let tree = cpp_tree(source);
let root = tree.root_node();
let ancestry = ParentIndex::new(root);
let primary = parse_cpp_file_with_ancestry(&file, source, root, &ancestry);
let c_reading = parse_cpp_c_reading(&file, source, root, &ancestry, &primary);
assert!(
cpp_projections_differ(&primary, &c_reading),
"the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
primary.declarations(),
c_reading.declarations()
);
}
#[test]
fn includes_are_recorded_at_every_depth() {
let source = r#"
#include <vector>
class Widget {
public:
int value() const;
};
int run() {
switch (0) {
#include <sycl/info/aspects.def>
default:
return 0;
}
}
"#;
let file = ProjectFile::new(
std::env::current_dir().expect("test working directory must be available"),
"src/widget.cpp",
);
let mut parser = Parser::new();
parser
.set_language(&tree_sitter_cpp::LANGUAGE.into())
.expect("C++ grammar");
let tree = parser.parse(source, None).expect("C++ tree");
let parsed = parse_cpp_file(&file, source, &tree);
let includes = parsed
.imports
.iter()
.map(|import| import.raw_snippet.clone())
.collect::<Vec<_>>();
assert_eq!(
includes,
vec![
"#include <vector>".to_string(),
"#include <sycl/info/aspects.def>".to_string(),
]
);
assert!(!parsed.declarations().is_empty());
}
}