use crate::declarations::{
CppCallableObserver, parse_cpp_readings, parse_cpp_readings_observed,
parse_cpp_readings_with_object_macro_fields,
};
use crate::graph::syntax::ObjectMacroReplacement;
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(crate) fn parse_cpp_file_with_callable_observer(
file: &ProjectFile,
source: &str,
tree: &Tree,
observer: &dyn CppCallableObserver,
) -> ParsedFile {
let root = tree.root_node();
let ancestry = ParentIndex::new(root);
parse_cpp_readings_observed(
file,
source,
root,
&ancestry,
&[LanguageDialect::for_path(Language::Cpp, file.rel_path())],
HashMap::default(),
observer,
)
.pop()
.expect("one dialect reading")
}
pub fn parse_cpp_file_with_object_macro_fields(
file: &ProjectFile,
source: &str,
tree: &Tree,
object_macro_fields: HashMap<String, ObjectMacroReplacement>,
) -> ParsedFile {
let root = tree.root_node();
let ancestry = ParentIndex::new(root);
parse_cpp_readings_with_object_macro_fields(
file,
source,
root,
&ancestry,
&[LanguageDialect::for_path(Language::Cpp, file.rel_path())],
object_macro_fields,
)
.pop()
.expect("one dialect reading")
}
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_file_from_root(file, source, root, &ancestry, dialect)
}
fn parse_cpp_file_from_root<'tree>(
file: &ProjectFile,
source: &str,
root: Node<'tree>,
ancestry: &ParentIndex<'tree>,
dialect: LanguageDialect,
) -> ParsedFile {
parse_cpp_readings(file, source, root, ancestry, &[dialect])
.pop()
.expect("one dialect reading")
}
pub fn parse_cpp_file_with_readings<'tree>(
file: &ProjectFile,
source: &str,
root: Node<'tree>,
ancestry: &ParentIndex<'tree>,
) -> (ParsedFile, Option<ParsedFile>) {
let mut readings = parse_cpp_readings(
file,
source,
root,
ancestry,
&[
LanguageDialect::for_path(Language::Cpp, file.rel_path()),
LanguageDialect::CppC,
],
);
let c = (readings.len() == 2).then(|| readings.pop().expect("C reading"));
let primary = readings.pop().expect("primary reading");
(primary, c)
}
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
|| left.resolution_facts != right.resolution_facts
}
pub fn cpp_extract_call_receiver(reference: &str) -> Option<String> {
brokk_bifrost_core::analyzer::common::extract_call_reference_parts(reference, &["::", "."])
.map(|(receiver, _)| receiver.to_string())
}
pub const CPP_LINE_DATA_KINDS: brokk_bifrost_core::analyzer::tree_walk::LineDataKinds =
brokk_bifrost_core::analyzer::tree_walk::LineDataKinds {
literals: &[
"string_literal",
"raw_string_literal",
"char_literal",
"number_literal",
],
interpolations: &[],
composites: &[
("initializer_list", &["{", "}", ","]),
("unary_expression", &["-", "+"]),
("concatenated_string", &[]),
],
};
#[cfg(test)]
mod tests {
#[test]
fn numeric_initializer_grammar_and_cancellation() {
use crate::grammar_test_support::{SOURCE, at, field, nodes, tree};
use brokk_bifrost_core::analyzer::tree_walk::maximal_data_subtrees;
use std::ops::ControlFlow;
let parsed = tree(SOURCE);
let cases = [
(
499..517,
true,
&[
("initializer_list", 499..517),
("{", 499..500),
("initializer_list", 500..507),
("{", 500..501),
("number_literal", 501..502),
(",", 502..503),
("number_literal", 504..506),
("}", 506..507),
(",", 507..508),
("initializer_list", 509..516),
("{", 509..510),
("number_literal", 510..512),
(",", 512..513),
("number_literal", 514..515),
("}", 515..516),
("}", 516..517),
][..],
),
(
535..552,
false,
&[
("initializer_list", 535..552),
("{", 535..536),
("number_literal", 536..537),
(",", 537..538),
("comment", 539..549),
("number_literal", 550..551),
("}", 551..552),
][..],
),
(
568..575,
false,
&[
("initializer_list", 568..575),
("{", 568..569),
("identifier", 569..574),
("}", 574..575),
][..],
),
(
591..599,
false,
&[
("initializer_list", 591..599),
("{", 591..592),
("call_expression", 592..598),
("identifier", 592..596),
("argument_list", 596..598),
("(", 596..597),
(")", 597..598),
("}", 598..599),
][..],
),
(
616..622,
false,
&[
("initializer_list", 616..622),
("{", 616..617),
("number_literal", 617..618),
(",", 618..619),
("ERROR", 620..621),
("ERROR", 620..621),
("}", 621..622),
][..],
),
(
640..645,
false,
&[
("initializer_list", 640..645),
("{", 640..641),
("number_literal", 641..642),
("ERROR", 643..644),
("number_literal", 643..644),
("}", 644..645),
][..],
),
(
662..672,
true,
&[
("initializer_list", 662..672),
("{", 662..663),
("unary_expression", 663..666),
("-", 663..664),
("number_literal", 665..666),
(",", 666..667),
("unary_expression", 668..671),
("+", 668..669),
("number_literal", 670..671),
("}", 671..672),
][..],
),
(
696..704,
false,
&[
("initializer_list", 696..704),
("{", 696..697),
("call_expression", 697..703),
("identifier", 697..701),
("argument_list", 701..703),
("(", 701..702),
("number_literal", 702..703),
(")", 703..703),
("}", 703..704),
][..],
),
];
for (span, admitted, manifest) in cases {
let initializer = at(parsed.root_node(), "initializer_list", span.clone());
let actual = nodes(initializer);
assert_eq!(
actual
.iter()
.map(|node| (node.kind(), node.byte_range()))
.collect::<Vec<_>>(),
manifest
);
for (index, node) in actual.iter().enumerate() {
if node.kind() == "unary_expression" {
let (operator_kind, operator_span) = &manifest[index + 1];
field(*node, "operator", operator_kind, operator_span.clone());
field(
*node,
"argument",
"number_literal",
manifest[index + 2].1.clone(),
);
assert!(!node.child_by_field_name("operator").unwrap().is_named());
}
if node.kind() == "number_literal" && admitted {
assert!(matches!(
node.parent().unwrap().kind(),
"initializer_list" | "unary_expression"
));
assert_eq!(node.child_count(), 0);
}
if node.kind() == ")" && node.start_byte() == node.end_byte() {
assert!(node.is_missing());
assert_eq!(node.parent().unwrap().kind(), "argument_list");
}
}
let mut visited = 0;
let selected =
maximal_data_subtrees(initializer, &CPP_LINE_DATA_KINDS, |_| true, &mut || {
visited += 1;
false
});
let ControlFlow::Continue(selected) = selected else {
panic!("an unstopped walk completes");
};
assert_eq!(
selected.iter().any(|subtree| subtree.range == span),
admitted,
"{span:?}"
);
assert_eq!(visited, manifest.len(), "poll manifest for {span:?}");
for cancel_at in 1..=manifest.len() {
let mut visited = 0;
assert_eq!(
maximal_data_subtrees(initializer, &CPP_LINE_DATA_KINDS, |_| true, &mut || {
visited += 1;
visited == cancel_at
}),
ControlFlow::Break(()),
"cancel at {cancel_at} in {span:?}"
);
assert_eq!(visited, cancel_at);
}
}
}
use super::*;
use brokk_bifrost_core::analyzer::resolution_facts::{ResolutionNameFact, ResolutionNameId};
use tree_sitter::Parser;
fn cpp_tree(source: &str) -> tree_sitter::Tree {
crate::diagnostics::parse_cpp_tree(source)
}
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, bool, bool)] = &[
(
"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;
"#,
true,
true,
),
(
"a quoted include only the line scan can recover",
r#"
#include "visible.h"
class Broken {
void method(
#include "hidden.h"
"#,
false,
false,
),
(
"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; };
}
"#,
true,
true,
),
(
"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;
}
};
}
"#,
true,
true,
),
(
"nested struct, union, and enum tags",
r#"
struct StructOwner { struct StructTag { int value; }; };
struct UnionOwner { union UnionTag { int value; }; };
struct EnumOwner { enum EnumTag { value }; };
"#,
true,
true,
),
(
"C++ class and anonymous tag near misses",
r#"
struct Owner {
class NestedClass { int value; };
struct { int anonymous_value; } anonymous_member;
};
struct TopLevel { int value; };
"#,
false,
false,
),
(
"tag reparsed under a recovered export class",
r#"
namespace api {
/**
* Doc comment
*/
class PROJECT_PUBLIC_API RecoveryOwner : public virtual BaseKey {
public:
/** Construct from a point. */
RecoveryOwner(const Group& group, const Point& point) : BaseKey(group, point) {}
#if defined(PROJECT_HAS_LEGACY_POINT)
/** Construct from a legacy point. */
RecoveryOwner(const Group& group, const LegacyPoint& point) : BaseKey(group, point) {}
#endif
struct RecoveredTag { int value; };
std::string algo_name() const override;
AlgorithmIdentifier algorithm_identifier() const override;
};
}
"#,
true,
true,
),
];
for (name, source, expected_witness, expected_differs) 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 independent_primary_facts = published_facts(&independent_primary);
let independent_c_facts = published_facts(&independent_c);
let independent_differs = cpp_projections_differ(&independent_primary, &independent_c);
assert_eq!(
independent_differs,
*expected_differs,
"independent C/C++ projection difference for {name}; root.has_error()={}; CST={}; C++ declarations={:#?}; C declarations={:#?}; C++ facts: {independent_primary_facts:#?}; C facts: {independent_c_facts:#?}",
root.has_error(),
root.to_sexp(),
independent_primary.declarations(),
independent_c.declarations(),
);
let ancestry = ParentIndex::new(root);
let (shared_primary, shared_c) =
parse_cpp_file_with_readings(&file, source, root, &ancestry);
let c_tag_scope_witness = shared_c.is_some();
if *name == "tag reparsed under a recovered export class" {
assert!(
root.has_error(),
"the recovery fixture must parse with errors"
);
let recovered_owner = independent_primary
.declarations()
.iter()
.find(|unit| unit.is_class() && unit.fq_name() == "api.RecoveryOwner")
.expect("the malformed export-class head must recover its owner");
let owner_range = independent_primary
.ranges
.get(recovered_owner)
.and_then(|ranges| ranges.first())
.expect("the recovered owner must retain its class range");
assert!(
owner_range.start_byte < owner_range.end_byte,
"the recovered owner must retain a nonempty class range"
);
assert!(
independent_primary.materialization_records.iter().any(|record| matches!(
record,
brokk_bifrost_core::analyzer::structural::materialization::MaterializationRecord::RecoveredDeclaration { unit, .. }
if unit == recovered_owner
)),
"the fixture must enter export-class recovery: {:#?}",
independent_primary.materialization_records
);
let mut pending = vec![root];
let mut tag = None;
while let Some(node) = pending.pop() {
if matches!(
node.kind(),
"struct_specifier" | "union_specifier" | "enum_specifier"
) && node.child_by_field_name("name").is_some_and(|name| {
name.utf8_text(source.as_bytes())
.is_ok_and(|text| text == "RecoveredTag")
}) {
tag = Some(node);
break;
}
let mut cursor = node.walk();
pending.extend(node.named_children(&mut cursor));
}
let tag = tag.expect("the original recovered fixture exposes its tag node");
let mut ancestor = ancestry.parent(tag);
while let Some(node) = ancestor {
assert!(
!matches!(
node.kind(),
"class_specifier" | "struct_specifier" | "union_specifier"
),
"the original CST must not already give RecoveredTag an aggregate owner"
);
ancestor = ancestry.parent(node);
}
}
assert_eq!(
c_tag_scope_witness,
*expected_witness,
"unexpected C tag-scope witness for {name}; root.has_error()={}; CST={}; C++ declarations={:#?}; C declarations={:#?}; independent C++ facts: {independent_primary_facts:#?}; independent C facts: {independent_c_facts:#?}",
root.has_error(),
root.to_sexp(),
independent_primary.declarations(),
independent_c.declarations(),
);
assert_eq!(
independent_primary_facts,
published_facts(&shared_primary),
"C++ reading of {name}"
);
if *expected_witness {
let shared_c = shared_c.expect("a witnessed C reading is emitted");
assert_eq!(
independent_c_facts,
published_facts(&shared_c),
"C reading of {name}"
);
} else {
assert_eq!(
independent_primary_facts, independent_c_facts,
"a false witness must imply identical complete readings for {name}"
);
assert!(
!independent_differs,
"a false witness must make the existing publication comparator agree for {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, c_reading) = parse_cpp_file_with_readings(&file, source, root, &ancestry);
let c_reading = c_reading.expect("nested tag requires a C reading");
assert!(
cpp_projections_differ(&primary, &c_reading),
"the C reading should mint `inner` at file scope: {:#?} vs {:#?}",
primary.declarations(),
c_reading.declarations()
);
}
#[test]
fn projection_difference_includes_resolution_facts() {
let source = "struct Widget {};\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 primary = parse_cpp_file(&file, source, &tree);
let mut fact_only_difference = primary.clone();
assert!(!cpp_projections_differ(&primary, &fact_only_difference));
fact_only_difference
.resolution_facts
.names
.push(ResolutionNameFact {
id: ResolutionNameId::new(0),
spelling: "fact-only-difference".to_owned(),
});
assert!(cpp_projections_differ(&primary, &fact_only_difference));
}
#[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());
}
}