use tree_sitter::Node;
use crate::model::{Edge, EdgeType, Language, Node as ModelNode, NodeLabel};
use crate::resolve::FqnGenerator;
use super::dedupe_qn;
use super::error::{ParseError, Result};
use super::extractor::{CallInfo, ExtractResult, Extractor, ImportInfo};
use super::parser_factory::ParserFactory;
pub struct CppExtractor {
_priv: (),
}
impl CppExtractor {
#[must_use]
pub const fn new() -> Self {
Self { _priv: () }
}
}
impl Default for CppExtractor {
fn default() -> Self {
Self::new()
}
}
impl Extractor for CppExtractor {
fn language(&self) -> Language {
Language::Cpp
}
fn extract(&self, source: &str, file_path: &str, project: &str) -> Result<ExtractResult> {
let mut result = ExtractResult::new(file_path, Language::Cpp);
let mut parser = ParserFactory::create_parser(Language::Cpp)?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ParseError::ParseFailed {
file_path: file_path.to_string(),
})?;
let root = tree.root_node();
let ctx = VisitContext {
file_path,
project,
current_func: None,
current_parent: None,
in_template: false,
};
for i in 0..root.named_child_count() as u32 {
if let Some(child) = root.named_child(i) {
visit_node(child, source, &ctx, &mut result);
}
}
Ok(result)
}
}
struct VisitContext<'a> {
file_path: &'a str,
project: &'a str,
current_func: Option<&'a str>,
current_parent: Option<&'a str>,
in_template: bool,
}
fn visit_node(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
match node.kind() {
"function_definition" => {
extract_function(node, source, ctx, result);
let name = function_name(node, source);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: name.as_deref(),
current_parent: ctx.current_parent,
in_template: ctx.in_template,
};
visit_children(node, source, &child_ctx, result);
}
"class_specifier" => {
extract_type(node, source, ctx, result, NodeLabel::Class);
let name = type_name(node, source);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: ctx.current_func,
current_parent: name.as_deref(),
in_template: ctx.in_template,
};
visit_children(node, source, &child_ctx, result);
}
"struct_specifier" => {
extract_type(node, source, ctx, result, NodeLabel::Struct);
let name = type_name(node, source);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: ctx.current_func,
current_parent: name.as_deref(),
in_template: ctx.in_template,
};
visit_children(node, source, &child_ctx, result);
}
"enum_specifier" => {
extract_type(node, source, ctx, result, NodeLabel::Enum);
let name = type_name(node, source);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: ctx.current_func,
current_parent: name.as_deref(),
in_template: ctx.in_template,
};
visit_children(node, source, &child_ctx, result);
}
"namespace_definition" => {
extract_namespace(node, source, ctx, result);
let name = type_name(node, source);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: ctx.current_func,
current_parent: name.as_deref(),
in_template: ctx.in_template,
};
visit_children(node, source, &child_ctx, result);
}
"template_declaration" => {
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: ctx.current_func,
current_parent: ctx.current_parent,
in_template: true,
};
visit_children(node, source, &child_ctx, result);
}
"preproc_include" => {
extract_include(node, source, result);
}
"call_expression" => {
extract_call(node, source, ctx, result);
visit_children(node, source, ctx, result);
}
_ => {
visit_children(node, source, ctx, result);
}
}
}
fn visit_children(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
visit_node(child, source, ctx, result);
}
}
}
fn extract_function(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name) = function_name(node, source) else {
return;
};
let is_inside = is_inside_class_or_struct(node);
let qualifier = extract_qualifier(node, source);
let is_method = is_inside || qualifier.is_some();
let label = if is_method {
NodeLabel::Method
} else {
NodeLabel::Function
};
let parent: Option<&str> = qualifier.as_deref().or(ctx.current_parent);
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, parent),
node.start_position().row as u32 + 1,
result,
);
let signature = node_text(node, source)
.map(signature_first_line)
.map(String::from);
let is_exported = !is_method;
let mut builder = ModelNode::builder(label, name, qn.clone())
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Cpp)
.project(ctx.project)
.is_global(!is_method)
.is_exported(is_exported);
if let Some(p) = parent {
builder = builder.parent_qn(p);
}
if let Some(sig) = signature {
builder = builder.signature(sig);
}
let model_node = builder.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_type(
node: Node,
source: &str,
ctx: &VisitContext<'_>,
result: &mut ExtractResult,
label: NodeLabel,
) {
let Some(name) = type_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let signature = node_text(node, source)
.map(signature_first_line)
.map(String::from);
let mut builder = ModelNode::builder(label, name, qn.clone())
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Cpp)
.project(ctx.project)
.is_global(true)
.is_exported(true);
if let Some(sig) = signature {
builder = builder.signature(sig);
}
let model_node = builder.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
extract_heritage(node, source, ctx, &qn, result);
}
fn extract_heritage(
node: Node,
source: &str,
ctx: &VisitContext<'_>,
class_qn: &str,
result: &mut ExtractResult,
) {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "base_class_clause" {
for_each_type_name(child, source, &mut |parent_name| {
let parent_qn = make_qn(ctx.file_path, &parent_name, ctx.project, None);
result.edges.push(Edge::new(
class_qn.to_string(),
parent_qn,
EdgeType::Extends,
ctx.project,
));
});
}
}
}
}
fn for_each_type_name<F: FnMut(String)>(node: Node, source: &str, f: &mut F) {
match node.kind() {
"type_identifier" | "identifier" | "namespace_identifier" => {
if let Some(text) = node_text(node, source) {
f(text.to_string());
}
}
"qualified_identifier" => {
if let Some(name) = node.child_by_field_name("name") {
for_each_type_name(name, source, f);
}
}
"template_type" => {
if let Some(name) = node.child_by_field_name("name") {
for_each_type_name(name, source, f);
}
}
"base_class_clause" => {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
for_each_type_name(child, source, f);
}
}
}
_ => {}
}
}
fn extract_namespace(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name) = type_name(node, source) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, None),
node.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::Namespace, name, qn)
.file_path(ctx.file_path)
.start_line(node.start_position().row as u32 + 1)
.end_line(node.end_position().row as u32 + 1)
.language(Language::Cpp)
.project(ctx.project)
.is_global(true)
.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
fn extract_include(node: Node, source: &str, result: &mut ExtractResult) {
let line = node.start_position().row as u32 + 1;
if let Some(path_node) = node.child_by_field_name("path") {
if let Some(text) = node_text(path_node, source) {
let cleaned = text
.trim_start_matches('<')
.trim_end_matches('>')
.trim_matches('"');
result.imports.push(ImportInfo {
source_file: cleaned.to_string(),
imported_names: Vec::new(),
line,
});
}
}
}
fn extract_call(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(func_node) = node.child_by_field_name("function") else {
return;
};
let Some(callee) = callee_name(func_node, source) else {
return;
};
let args = call_arguments(node, source);
let caller_qn = ctx
.current_func
.map(|name| make_qn(ctx.file_path, name, ctx.project, ctx.current_parent));
result.calls.push(CallInfo {
caller_qn,
callee_name: callee,
line: node.start_position().row as u32 + 1,
args,
});
}
fn is_inside_class_or_struct(node: Node) -> bool {
let mut cur = node.parent();
while let Some(p) = cur {
match p.kind() {
"class_specifier" | "struct_specifier" => return true,
_ => cur = p.parent(),
}
}
false
}
fn extract_qualifier(node: Node, source: &str) -> Option<String> {
let declarator = node.child_by_field_name("declarator")?;
qualifier_from_declarator(declarator, source)
}
fn qualifier_from_declarator(node: Node, source: &str) -> Option<String> {
match node.kind() {
"qualified_identifier" => {
let scope = node.child_by_field_name("scope")?;
node_text(scope, source).map(String::from)
}
"function_declarator"
| "pointer_declarator"
| "reference_declarator"
| "array_declarator"
| "parenthesized_declarator" => {
let inner = node.child_by_field_name("declarator")?;
qualifier_from_declarator(inner, source)
}
_ => None,
}
}
fn type_name(node: Node, source: &str) -> Option<String> {
let name_node = node.child_by_field_name("name")?;
node_text(name_node, source).map(String::from)
}
fn function_name(node: Node, source: &str) -> Option<String> {
let declarator = node.child_by_field_name("declarator")?;
declarator_name(declarator, source)
}
fn declarator_name(node: Node, source: &str) -> Option<String> {
match node.kind() {
"identifier" | "field_identifier" => node_text(node, source).map(String::from),
"function_declarator"
| "pointer_declarator"
| "reference_declarator"
| "array_declarator"
| "parenthesized_declarator" => {
if let Some(inner) = node.child_by_field_name("declarator") {
declarator_name(inner, source)
} else {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if let Some(name) = declarator_name(child, source) {
return Some(name);
}
}
}
None
}
}
"qualified_identifier" => {
let name = node.child_by_field_name("name")?;
node_text(name, source).map(String::from)
}
"operator_name" => {
node_text(node, source).map(String::from)
}
_ => {
if let Some(inner) = node.child_by_field_name("declarator") {
return declarator_name(inner, source);
}
node_text(node, source).map(String::from)
}
}
}
fn callee_name(node: Node, source: &str) -> Option<String> {
match node.kind() {
"identifier" => node_text(node, source).map(String::from),
"qualified_identifier" => {
let name = node.child_by_field_name("name")?;
node_text(name, source).map(String::from)
}
"field_expression" => {
let field = node.child_by_field_name("field")?;
node_text(field, source).map(String::from)
}
"call_expression" => {
let func = node.child_by_field_name("function")?;
callee_name(func, source)
}
_ => node_text(node, source).map(String::from),
}
}
fn call_arguments(node: Node, source: &str) -> Vec<String> {
let Some(args_node) = node.child_by_field_name("arguments") else {
return Vec::new();
};
let mut args = Vec::new();
for i in 0..args_node.named_child_count() as u32 {
if let Some(arg) = args_node.named_child(i) {
if let Ok(text) = arg.utf8_text(source.as_bytes()) {
args.push(text.to_string());
}
}
}
args
}
fn node_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
node.utf8_text(source.as_bytes()).ok()
}
fn signature_first_line(text: &str) -> &str {
text.lines().next().unwrap_or(text)
}
fn make_qn(file_path: &str, name: &str, project: &str, parent: Option<&str>) -> String {
FqnGenerator::generate(project, file_path, name, Language::Cpp, parent)
}
fn add_definition_edges(
file_path: &str,
project: &str,
node: &ModelNode,
result: &mut ExtractResult,
) {
result.edges.push(Edge::new(
file_path.to_string(),
node.id.clone(),
EdgeType::Defines,
project,
));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::NodeLabel;
fn extract(source: &str) -> ExtractResult {
let ext = CppExtractor::new();
ext.extract(source, "test.cpp", "proj")
.expect("extraction should succeed")
}
#[test]
fn language_returns_cpp() {
assert_eq!(CppExtractor::new().language(), Language::Cpp);
}
#[test]
fn default_creates_extractor() {
let ext = CppExtractor::default();
assert_eq!(ext.language(), Language::Cpp);
}
#[test]
fn extracts_function_definition() {
let result = extract("int add(int a, int b) { return a + b; }\n");
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(
funcs.len(),
1,
"should extract 1 function: {:?}",
result.nodes
);
assert_eq!(funcs[0].name, "add");
assert_eq!(funcs[0].language, Some(Language::Cpp));
assert_eq!(funcs[0].project, "proj");
assert_eq!(funcs[0].file_path.as_deref(), Some("test.cpp"));
assert!(funcs[0].is_global, "top-level function should be global");
}
#[test]
fn extracts_class() {
let result = extract("class Point { public: int x; };\n");
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Class)
.collect();
assert_eq!(
classes.len(),
1,
"should extract 1 class: {:?}",
result.nodes
);
assert_eq!(classes[0].name, "Point");
}
#[test]
fn extracts_struct() {
let result = extract("struct Vec { int x; int y; };\n");
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert_eq!(
structs.len(),
1,
"should extract 1 struct: {:?}",
result.nodes
);
assert_eq!(structs[0].name, "Vec");
}
#[test]
fn extracts_namespace() {
let result = extract("namespace ns { void foo() {} }\n");
let namespaces: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Namespace)
.collect();
assert_eq!(
namespaces.len(),
1,
"should extract 1 namespace: {:?}",
result.nodes
);
assert_eq!(namespaces[0].name, "ns");
}
#[test]
fn namespace_function_fqn_contains_namespace() {
let result = extract("namespace ns { void foo() {} }\n");
let foo = result
.nodes
.iter()
.find(|n| n.name == "foo")
.expect("should find function foo");
assert!(
foo.qualified_name.contains("ns"),
"FQN should contain namespace name: {}",
foo.qualified_name
);
}
#[test]
fn extracts_method_inside_class() {
let result = extract("class C { void m() {} };\n");
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Method)
.collect();
assert_eq!(
methods.len(),
1,
"should extract 1 method: {:?}",
result.nodes
);
assert_eq!(methods[0].name, "m");
assert!(!methods[0].is_global, "method should not be global");
assert_eq!(methods[0].parent_qn.as_deref(), Some("C"));
}
#[test]
fn extracts_template_function() {
let result = extract("template<typename T> T max(T a, T b) { return a > b ? a : b; }\n");
let max_node = result
.nodes
.iter()
.find(|n| n.name == "max")
.expect("should extract a node named max (Function or Template)");
assert!(
max_node.label == NodeLabel::Function || max_node.label == NodeLabel::Template,
"max should be a Function or Template, got {:?}",
max_node.label
);
}
#[test]
fn extracts_template_class() {
let result = extract("template<typename T> class Stack { T data[10]; };\n");
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Class)
.collect();
assert_eq!(classes.len(), 1, "should extract template class Stack");
assert_eq!(classes[0].name, "Stack");
}
#[test]
fn empty_source_returns_empty_result() {
let result = extract("");
assert!(result.is_empty());
}
#[test]
fn result_language_is_cpp() {
let result = extract("int main() { return 0; }\n");
assert_eq!(result.language, Language::Cpp);
assert_eq!(result.file_path, "test.cpp");
}
#[test]
fn creates_defines_edges() {
let result = extract("int add(int a, int b) { return a + b; }\n");
let defines_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Defines)
.count();
let node_count = result.nodes.len();
assert_eq!(defines_count, node_count, "one DEFINES edge per node");
}
#[test]
fn qualified_name_uses_file_path_and_name() {
let result = extract("int add(int a, int b) { return a + b; }\n");
let add = result.nodes.iter().find(|n| n.name == "add").unwrap();
assert_eq!(add.qualified_name, "proj.test.cpp.add");
}
#[test]
fn function_has_signature() {
let result = extract("int add(int a, int b) { return a + b; }\n");
let add = result.nodes.iter().find(|n| n.name == "add").unwrap();
assert!(add.signature.is_some(), "function should have a signature");
assert!(add.signature.as_deref().unwrap().contains("add"));
}
#[test]
fn extracts_call_expression() {
let result = extract("int main() { printf(\"hi\"); return 0; }\n");
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callees.contains(&"printf"),
"should extract call to printf: {:?}",
callees
);
}
#[test]
fn call_has_line_and_args() {
let result = extract("int main() { printf(\"hi\", 1); return 0; }\n");
let call = result
.calls
.iter()
.find(|c| c.callee_name == "printf")
.expect("should find call to printf");
assert_eq!(call.args.len(), 2, "printf(\"hi\", 1) should have 2 args");
}
#[test]
fn extracts_include() {
let result = extract("#include <iostream>\n");
assert_eq!(result.imports.len(), 1, "should extract 1 include");
assert_eq!(result.imports[0].source_file, "iostream");
}
#[test]
fn extracts_local_include() {
let result = extract("#include \"myheader.h\"\n");
assert_eq!(result.imports.len(), 1, "should extract local include");
assert_eq!(result.imports[0].source_file, "myheader.h");
}
#[test]
fn nested_namespace_function_extracts_both() {
let result = extract("namespace outer { void inner_func() {} }\n");
let namespaces: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Namespace)
.collect();
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(namespaces.len(), 1, "should extract namespace outer");
assert_eq!(funcs.len(), 1, "should extract function inner_func");
assert_eq!(namespaces[0].name, "outer");
assert_eq!(funcs[0].name, "inner_func");
}
#[test]
fn class_with_method_and_field_extracts_class_and_method() {
let result = extract("class Point { public: int x; int getX() { return x; } };\n");
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Class)
.collect();
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Method)
.collect();
assert_eq!(classes.len(), 1, "should extract class Point");
assert_eq!(methods.len(), 1, "should extract method getX");
assert_eq!(classes[0].name, "Point");
assert_eq!(methods[0].name, "getX");
}
#[test]
fn function_with_pointer_declarator_is_extracted() {
let result = extract("int *get_ptr(void) { return nullptr; }\n");
let f = result
.nodes
.iter()
.find(|n| n.name == "get_ptr")
.expect("should extract get_ptr");
assert_eq!(f.label, NodeLabel::Function);
}
#[test]
fn function_with_reference_declarator_is_extracted() {
let result = extract("int& ref_get(void) { static int x = 0; return x; }\n");
let f = result
.nodes
.iter()
.find(|n| n.name == "ref_get")
.expect("should extract ref_get");
assert_eq!(f.label, NodeLabel::Function);
}
#[test]
fn function_with_qualified_identifier_name_is_extracted() {
let result = extract("namespace ns { void ns_func() {} }\n");
let f = result
.nodes
.iter()
.find(|n| n.name == "ns_func")
.expect("should extract ns_func");
assert_eq!(f.label, NodeLabel::Function);
}
#[test]
fn operator_overload_is_extracted() {
let result = extract(
"class Foo { public: bool operator==(const Foo& other) const { return true; } };\n",
);
let m = result
.nodes
.iter()
.find(|n| n.name.contains("operator=="))
.expect("should extract operator== as a method");
assert_eq!(m.label, NodeLabel::Method);
}
#[test]
fn call_to_qualified_name_is_extracted() {
let result = extract("int main() { std::swap(1, 2); return 0; }\n");
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callees.contains(&"swap"),
"should extract call to swap via qualified name: {:?}",
callees
);
}
#[test]
fn call_to_method_is_extracted() {
let result =
extract("class C { public: void m() {} };\nint main() { C c; c.m(); return 0; }\n");
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callees.contains(&"m"),
"should extract call to method m via field_expression: {:?}",
callees
);
}
#[test]
fn call_to_chained_invocation_is_extracted() {
let result =
extract("typedef void(*Fn)(); Fn get_fn(); int main() { get_fn()(); return 0; }\n");
assert!(
!result.calls.is_empty(),
"chained call should produce at least one call record: {:?}",
result.calls
);
}
#[test]
fn multi_line_function_signature_uses_first_line() {
let result = extract("int add(int a,\n int b) {\n return a + b;\n}\n");
let add = result.nodes.iter().find(|n| n.name == "add").expect("add");
let sig = add.signature.as_deref().expect("signature should be set");
assert!(
!sig.contains('\n'),
"signature must be a single line, got: {sig:?}"
);
assert!(
sig.contains("add"),
"signature should contain the function name"
);
}
#[test]
fn enum_is_extracted_as_top_level_node() {
let result = extract("enum Color { RED, GREEN, BLUE };\n");
let enums: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Enum)
.collect();
assert_eq!(enums.len(), 1, "should extract 1 enum: {:?}", result.nodes);
assert_eq!(enums[0].name, "Color");
}
#[test]
fn out_of_class_method_definition_is_classified_as_method() {
let src = "class Foo {};\nvoid Foo::bar() {}\n";
let result = extract(src);
let bar = result
.nodes
.iter()
.find(|n| n.name == "bar")
.expect("should extract bar");
assert_eq!(
bar.label,
NodeLabel::Method,
"out-of-class Foo::bar should be Method, not Function: {:?}",
bar.label
);
assert_eq!(
bar.parent_qn.as_deref(),
Some("Foo"),
"parent_qn should be Foo: {:?}",
bar.parent_qn
);
}
#[test]
fn class_extends_base_creates_extends_edge() {
let result = extract("class Base {};\nclass Derived : public Base {};\n");
let extends: Vec<_> = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.collect();
assert_eq!(
extends.len(),
1,
"should have 1 EXTENDS edge: {:?}",
extends
);
assert!(
extends[0].source.contains("Derived"),
"source should be Derived: {}",
extends[0].source
);
assert!(
extends[0].target.contains("Base"),
"target should be Base: {}",
extends[0].target
);
}
#[test]
fn struct_extends_base_creates_extends_edge() {
let result = extract("struct Base {};\nstruct Derived : Base {};\n");
let extends: Vec<_> = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.collect();
assert_eq!(
extends.len(),
1,
"struct should have 1 EXTENDS edge: {:?}",
extends
);
assert!(
extends[0].source.contains("Derived"),
"source should be Derived: {}",
extends[0].source
);
}
#[test]
fn multiple_inheritance_creates_multiple_extends_edges() {
let result = extract(
"class Base1 {};\nclass Base2 {};\nclass Derived : public Base1, public Base2 {};\n",
);
let extends: Vec<_> = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.collect();
assert_eq!(
extends.len(),
2,
"multiple inheritance should have 2 EXTENDS edges: {:?}",
extends
);
}
#[test]
fn class_is_marked_exported() {
let result = extract("class Point { public: int x; };\n");
let cls = result
.nodes
.iter()
.find(|n| n.label == NodeLabel::Class)
.expect("should find class Point");
assert!(
cls.is_exported,
"class should be marked is_exported for TypeResolver strategy 3"
);
}
#[test]
fn struct_is_marked_exported() {
let result = extract("struct Vec { int x; };\n");
let st = result
.nodes
.iter()
.find(|n| n.label == NodeLabel::Struct)
.expect("should find struct Vec");
assert!(
st.is_exported,
"struct should be marked is_exported for TypeResolver strategy 3"
);
}
#[test]
fn cpp_free_function_is_exported() {
let result = extract("int add(int a, int b) { return a + b; }\n");
let func = result.nodes.iter().find(|n| n.name == "add").unwrap();
assert!(
func.is_exported,
"free function should have is_exported=true (BUG-C4 resolved)"
);
}
#[test]
fn class_method_is_not_exported() {
let result = extract("class Calc { public: int add(int a, int b) { return a + b; } };\n");
let method = result.nodes.iter().find(|n| n.name == "add").unwrap();
assert!(
!method.is_exported,
"class method should have is_exported=false"
);
}
#[test]
fn struct_method_is_not_exported() {
let result = extract("struct Vec { int length() { return 0; } };\n");
let method = result.nodes.iter().find(|n| n.name == "length").unwrap();
assert!(
!method.is_exported,
"struct method should have is_exported=false"
);
}
#[test]
fn out_of_class_method_is_not_exported() {
let result = extract("class Foo { public: void bar(); };\nvoid Foo::bar() {}\n");
let methods: Vec<_> = result.nodes.iter().filter(|n| n.name == "bar").collect();
assert!(!methods.is_empty(), "should find bar method");
for m in &methods {
assert!(
!m.is_exported,
"out-of-class method should have is_exported=false"
);
}
}
#[test]
fn cpp_namespace_function_is_exported() {
let result = extract("namespace ns { int helper() { return 42; } }\n");
let func = result.nodes.iter().find(|n| n.name == "helper").unwrap();
assert!(
func.is_exported,
"namespace function should have is_exported=true (BUG-C4 resolved)"
);
}
#[test]
fn private_inheritance_creates_extends_edge() {
let result = extract("class Base {};\nclass Derived : private Base {};\n");
let extends: Vec<_> = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.collect();
assert_eq!(
extends.len(),
1,
"private inheritance should still produce 1 EXTENDS edge: {:?}",
extends
);
}
#[test]
fn qualified_base_class_creates_extends_edge() {
let result =
extract("namespace ns { class Base {}; }\nclass Derived : public ns::Base {};\n");
let extends: Vec<_> = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.collect();
assert_eq!(
extends.len(),
1,
"qualified base class should produce 1 EXTENDS edge: {:?}",
extends
);
assert!(
extends[0].target.contains("Base"),
"target should contain Base: {}",
extends[0].target
);
}
#[test]
fn class_without_base_has_no_extends_edge() {
let result = extract("class Standalone {};\n");
let extends_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.count();
assert_eq!(
extends_count, 0,
"class without base should have 0 EXTENDS edges"
);
}
#[test]
fn cpp_multifile_call_graph_resolved() {
use crate::resolve::{build_symbol_table, resolve_include, CallResolver, IncludesGraph};
let ext = CppExtractor::new();
let main_src = "#include \"foo.h\"\nint main() { foo(); return 0; }\n";
let foo_h_src = "void foo() {}\n";
let bar_src = "void foo() {}\n";
let main_result = ext.extract(main_src, "/abs/main.cpp", "proj").unwrap();
let foo_h_result = ext.extract(foo_h_src, "/abs/foo.h", "proj").unwrap();
let bar_result = ext.extract(bar_src, "/abs/bar.cpp", "proj").unwrap();
let results = vec![main_result, foo_h_result, bar_result];
let all_files: Vec<String> = results.iter().map(|r| r.file_path.clone()).collect();
let mut graph = IncludesGraph::new();
for result in &results {
for imp in &result.imports {
if let Some(resolved) =
resolve_include(&imp.source_file, &result.file_path, &all_files)
{
graph.add_include(&result.file_path, &resolved);
}
}
}
assert!(
graph.contains("/abs/main.cpp", "/abs/foo.h"),
"main.cpp should #include foo.h"
);
assert!(
!graph.contains("/abs/main.cpp", "/abs/bar.cpp"),
"main.cpp should NOT #include bar.cpp"
);
let table = build_symbol_table(&results, "proj");
let resolver = CallResolver::new(&table, "proj").with_includes_graph(graph);
let resolved = resolver.resolve_call("/abs/main.cpp", "foo");
assert!(
resolved.is_some(),
"foo() call should resolve to foo.h's foo"
);
let (qn, _confidence, _tier) = resolved.unwrap();
assert!(
qn.contains("foo.h"),
"should resolve to foo.h's foo, got: {qn}"
);
assert!(
!qn.contains("bar.cpp"),
"should NOT resolve to bar.cpp's foo, got: {qn}"
);
}
#[test]
fn class_extends_template_type_creates_extends_edge() {
let result = extract(
"namespace std { template<typename T> class vector {}; }\nclass Derived : public std::vector<int> {};\n",
);
let extends: Vec<_> = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Extends)
.collect();
assert_eq!(
extends.len(),
1,
"template base class should produce 1 EXTENDS edge: {:?}",
extends
);
assert!(
extends[0].target.contains("vector"),
"target should contain vector: {}",
extends[0].target
);
}
#[test]
fn comment_only_source_returns_empty() {
let result = extract("// just a comment\n");
assert!(result.is_empty(), "comment-only should produce no nodes");
}
#[test]
fn multiple_namespaces_all_extracted() {
let result = extract("namespace a { void f() {} }\nnamespace b { void g() {} }\n");
let namespaces: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Namespace)
.collect();
assert_eq!(namespaces.len(), 2, "should extract 2 namespaces");
}
#[test]
fn class_with_multiple_methods_all_extracted() {
let result = extract("class Calc { public: int add() { return 0; } int sub() { return 0; } int mul() { return 0; } };\n");
let methods: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Method)
.collect();
assert_eq!(methods.len(), 3, "should extract 3 methods");
}
#[test]
fn nested_class_extracts_both() {
let result = extract("class Outer { public: class Inner {}; };\n");
let classes: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Class)
.collect();
assert_eq!(classes.len(), 2, "should extract outer and inner class");
let names: Vec<_> = classes.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"Outer"));
assert!(names.contains(&"Inner"));
}
#[test]
fn enum_class_extracts_enum_node() {
let result = extract("enum class Color { RED, GREEN, BLUE };\n");
let enums: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Enum)
.collect();
assert_eq!(enums.len(), 1);
assert_eq!(enums[0].name, "Color");
}
#[test]
fn system_include_extracted() {
let result = extract("#include <stdio.h>\n");
assert_eq!(result.imports.len(), 1);
assert_eq!(result.imports[0].source_file, "stdio.h");
}
}