use tree_sitter::Node;
use crate::model::{Edge, EdgeType, Language, Node as ModelNode, NodeLabel};
use crate::resolve::{FqnGenerator, ScopeContext, ScopeResolverRegistry};
use super::dedupe_qn;
use super::error::{ParseError, Result};
use super::extractor::{ExtractResult, Extractor, ImportInfo, ReadInfo, WriteInfo};
use super::parser_factory::ParserFactory;
pub struct CExtractor {
_priv: (),
}
impl CExtractor {
#[must_use]
pub const fn new() -> Self {
Self { _priv: () }
}
}
impl Default for CExtractor {
fn default() -> Self {
Self::new()
}
}
impl Extractor for CExtractor {
fn language(&self) -> Language {
Language::C
}
fn extract(&self, source: &str, file_path: &str, project: &str) -> Result<ExtractResult> {
let mut result = ExtractResult::new(file_path, Language::C);
let mut parser = ParserFactory::create_parser(Language::C)?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ParseError::ParseFailed {
file_path: file_path.to_string(),
})?;
let root = tree.root_node();
let registry = ScopeResolverRegistry::new();
let ctx = VisitContext {
file_path,
project,
current_func: None,
current_parent: None,
resolver: ®istry,
};
for i in 0..root.named_child_count() as u32 {
let child = match root.named_child(i) {
Some(c) => c,
None => continue,
};
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>,
resolver: &'a ScopeResolverRegistry,
}
fn visit_node(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
match node.kind() {
"function_definition" => {
let scope_ctx = ScopeContext {
source,
file_path: ctx.file_path,
project: ctx.project,
current_parent: ctx.current_parent,
};
let scope = ctx
.resolver
.get(Language::C)
.and_then(|r| r.resolve(node, &scope_ctx));
match scope.as_ref().map(|s| s.label) {
Some(NodeLabel::Namespace) | Some(NodeLabel::Class) | Some(NodeLabel::Struct) => {
let scope_name = scope.as_ref().map(|s| s.name.as_str());
let combined = combine_scope(ctx.current_parent, scope_name);
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "compound_statement" {
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: None,
current_parent: combined.as_deref(),
resolver: ctx.resolver,
};
visit_children(child, source, &child_ctx, result);
}
}
}
}
_ => {
let start_line = node.start_position().row as u32 + 1;
let method_parent = ctx.current_parent.map(|p| format!("{p}_L{start_line}"));
extract_function(node, source, ctx, method_parent.as_deref(), result);
let func_name = scope.as_ref().map(|s| s.name.as_str());
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "compound_statement" {
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: func_name,
current_parent: method_parent.as_deref(),
resolver: ctx.resolver,
};
visit_children(child, source, &child_ctx, result);
}
}
}
}
}
}
"declaration" => {
extract_global_var(node, source, ctx, result);
visit_children(node, source, ctx, result);
}
"preproc_include" => {
extract_include(node, source, result);
}
"preproc_def" | "preproc_function_def" => {
extract_macro(node, source, ctx, result);
}
"type_definition" => {
extract_typedef(node, source, ctx, result);
}
"struct_specifier" => {
extract_struct(node, source, ctx, result);
if node.child_by_field_name("body").is_some() {
let scope_ctx = ScopeContext {
source,
file_path: ctx.file_path,
project: ctx.project,
current_parent: ctx.current_parent,
};
let scope = ctx
.resolver
.get(Language::C)
.and_then(|r| r.resolve(node, &scope_ctx));
let struct_name = scope.as_ref().map(|s| s.name.as_str());
let combined = combine_scope(ctx.current_parent, struct_name);
let child_ctx = VisitContext {
file_path: ctx.file_path,
project: ctx.project,
current_func: ctx.current_func,
current_parent: combined.as_deref(),
resolver: ctx.resolver,
};
visit_children(node, source, &child_ctx, result);
}
}
"enum_specifier" => {
extract_enum(node, source, ctx, result);
}
"call_expression" => {
extract_call(node, source, ctx, result);
visit_children(node, source, ctx, result);
}
"init_declarator" => {
if let Some(func) = ctx.current_func {
if let Some(name) = declarator_name(node, source) {
result.writes.push(WriteInfo {
writer_qn: Some(make_qn(
ctx.file_path,
func,
ctx.project,
ctx.current_parent,
)),
var_name: name,
line: node.start_position().row as u32 + 1,
});
}
}
visit_children(node, source, ctx, result);
}
"assignment_expression" => {
if let Some(func) = ctx.current_func {
if let Some(left) = node.child_by_field_name("left") {
if let Some(name) = identifier_text(left, source) {
result.writes.push(WriteInfo {
writer_qn: Some(make_qn(
ctx.file_path,
func,
ctx.project,
ctx.current_parent,
)),
var_name: name,
line: node.start_position().row as u32 + 1,
});
}
}
}
visit_children(node, source, ctx, result);
}
"identifier" => {
if let Some(func) = ctx.current_func {
if is_read_position(node) {
if let Some(name) = node_text(node, source).map(String::from) {
result.reads.push(ReadInfo {
reader_qn: Some(make_qn(
ctx.file_path,
func,
ctx.project,
ctx.current_parent,
)),
var_name: name,
line: node.start_position().row as u32 + 1,
});
}
}
}
visit_children(node, source, ctx, result);
}
"linkage_specification" => {
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<'_>,
parent: Option<&str>,
result: &mut ExtractResult,
) {
let Some(name) = function_name(node, source) else {
return;
};
let start_line = node.start_position().row as u32 + 1;
let end_line = node.end_position().row as u32 + 1;
let signature = declarator_signature(node, source);
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, parent),
start_line,
result,
);
let mut builder = ModelNode::builder(NodeLabel::Function, name.clone(), qn.clone())
.file_path(ctx.file_path)
.start_line(start_line)
.end_line(end_line)
.language(Language::C)
.project(ctx.project)
.is_global(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);
}
fn extract_global_var(
node: Node,
source: &str,
ctx: &VisitContext<'_>,
result: &mut ExtractResult,
) {
let is_top_level = node
.parent()
.map(|p| p.kind() == "translation_unit")
.unwrap_or(false);
if !is_top_level {
return;
}
let mut has_function_decl = false;
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if let Some(fd) = find_function_declarator(child) {
extract_function_declaration(fd, node, source, ctx, result);
has_function_decl = true;
}
}
}
if has_function_decl {
return;
}
let mut i: u32 = 0;
while i < node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "init_declarator" {
if let Some(name) = declarator_name(child, source) {
push_global_var(
&name,
node.start_position().row as u32 + 1,
ctx.file_path,
ctx.project,
result,
);
}
}
}
i += 1;
}
let has_init = (0..node.named_child_count() as u32).any(|i| {
node.named_child(i)
.map(|c| c.kind() == "init_declarator")
.unwrap_or(false)
});
if !has_init {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "identifier" {
if let Some(name) = node_text(child, source).map(String::from) {
push_global_var(
&name,
node.start_position().row as u32 + 1,
ctx.file_path,
ctx.project,
result,
);
}
}
}
}
}
}
fn push_global_var(
name: &str,
line: u32,
file_path: &str,
project: &str,
result: &mut ExtractResult,
) {
let qn = dedupe_qn(make_qn(file_path, name, project, None), line, result);
let model_node = ModelNode::builder(NodeLabel::GlobalVar, name.to_string(), qn.clone())
.file_path(file_path)
.start_line(line)
.language(Language::C)
.project(project)
.is_global(true)
.build();
add_definition_edges(file_path, project, &model_node, result);
result.push_node(model_node);
}
fn extract_function_declaration(
fd_node: Node,
decl_node: Node,
source: &str,
ctx: &VisitContext<'_>,
result: &mut ExtractResult,
) {
let Some(name) = declarator_name(fd_node, source) else {
return;
};
let start_line = decl_node.start_position().row as u32 + 1;
let end_line = decl_node.end_position().row as u32 + 1;
let signature = node_text(fd_node, source).map(String::from);
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
start_line,
result,
);
let mut builder = ModelNode::builder(NodeLabel::Function, name.clone(), qn)
.file_path(ctx.file_path)
.start_line(start_line)
.end_line(end_line)
.language(Language::C)
.project(ctx.project)
.is_global(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);
}
fn find_function_declarator(node: Node) -> Option<Node> {
match node.kind() {
"function_declarator" => Some(node),
"pointer_declarator" | "parenthesized_declarator" => {
let inner = node.child_by_field_name("declarator")?;
find_function_declarator(inner)
}
_ => None,
}
}
fn extract_typedef(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let mut typedef_name: Option<String> = None;
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
if child.kind() == "type_identifier" {
if let Some(name) = node_text(child, source).map(String::from) {
let line = node.start_position().row as u32 + 1;
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
line,
result,
);
let model_node = ModelNode::builder(NodeLabel::Typedef, name.clone(), qn)
.file_path(ctx.file_path)
.start_line(line)
.language(Language::C)
.project(ctx.project)
.is_global(true)
.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
typedef_name = Some(name);
}
}
}
}
if let Some(name) = typedef_name {
for i in 0..node.named_child_count() as u32 {
if let Some(child) = node.named_child(i) {
match child.kind() {
"struct_specifier"
if child.child_by_field_name("name").is_none()
&& child.child_by_field_name("body").is_some() =>
{
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
child.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::Struct, name.clone(), qn)
.file_path(ctx.file_path)
.start_line(child.start_position().row as u32 + 1)
.end_line(child.end_position().row as u32 + 1)
.language(Language::C)
.project(ctx.project)
.is_global(true)
.build();
add_definition_edges(ctx.file_path, ctx.project, &model_node, result);
result.push_node(model_node);
}
"enum_specifier"
if child.child_by_field_name("name").is_none()
&& child.child_by_field_name("body").is_some() =>
{
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
child.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::Enum, name.clone(), qn)
.file_path(ctx.file_path)
.start_line(child.start_position().row as u32 + 1)
.end_line(child.end_position().row as u32 + 1)
.language(Language::C)
.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_struct(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
if node.child_by_field_name("body").is_none() {
return;
}
let Some(name) = node_text(name_node, source).map(String::from) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
node.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::Struct, 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::C)
.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_enum(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
if node.child_by_field_name("body").is_none() {
return;
}
let Some(name) = node_text(name_node, source).map(String::from) else {
return;
};
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
node.start_position().row as u32 + 1,
result,
);
let model_node = ModelNode::builder(NodeLabel::Enum, 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::C)
.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_macro(node: Node, source: &str, ctx: &VisitContext<'_>, result: &mut ExtractResult) {
let Some(name_node) = node.child_by_field_name("name") else {
return;
};
let Some(name) = node_text(name_node, source).map(String::from) else {
return;
};
let start_line = node.start_position().row as u32 + 1;
let end_line = node.end_position().row as u32 + 1;
let signature = node
.child_by_field_name("parameters")
.and_then(|p| node_text(p, source).map(String::from));
let qn = dedupe_qn(
make_qn(ctx.file_path, &name, ctx.project, ctx.current_parent),
start_line,
result,
);
let mut builder = ModelNode::builder(NodeLabel::Macro, name, qn)
.file_path(ctx.file_path)
.start_line(start_line)
.end_line(end_line)
.language(Language::C)
.project(ctx.project)
.is_global(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);
}
fn extract_include(node: Node, source: &str, result: &mut ExtractResult) {
let Some(path_node) = node.child_by_field_name("path") else {
return;
};
let raw = node_text(path_node, source).unwrap_or("");
let cleaned = raw
.trim_start_matches('<')
.trim_end_matches('>')
.trim_start_matches('"')
.trim_end_matches('"')
.to_string();
result.imports.push(ImportInfo {
source_file: cleaned,
imported_names: Vec::new(),
line: node.start_position().row as u32 + 1,
});
}
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(super::extractor::CallInfo {
caller_qn,
callee_name: callee,
line: node.start_position().row as u32 + 1,
args,
});
}
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" => node_text(node, source).map(String::from),
"function_declarator" => {
let inner = node.child_by_field_name("declarator")?;
if inner.kind() == "function_declarator" {
return None;
}
declarator_name(inner, source)
}
"pointer_declarator"
| "array_declarator"
| "parenthesized_declarator"
| "init_declarator" => {
let inner = node.child_by_field_name("declarator")?;
declarator_name(inner, source)
}
_ => None,
}
}
fn declarator_signature(node: Node, source: &str) -> Option<String> {
let declarator = node.child_by_field_name("declarator")?;
node_text(declarator, source).map(String::from)
}
fn callee_name(node: Node, source: &str) -> Option<String> {
match node.kind() {
"identifier" | "type_identifier" => node_text(node, 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)
}
"parenthesized_expression" => {
let inner = node.named_child(0)?;
callee_name(inner, source)
}
_ => None,
}
}
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 identifier_text(node: Node, source: &str) -> Option<String> {
if node.kind() == "identifier" {
node_text(node, source).map(String::from)
} else {
None
}
}
fn is_read_position(node: Node) -> bool {
let Some(parent) = node.parent() else {
return false;
};
match parent.kind() {
"binary_expression"
| "unary_expression"
| "parenthesized_expression"
| "return_statement"
| "argument_list"
| "subscript_expression"
| "conditional_expression" => true,
"call_expression" => !is_at_field(node, parent, "function"),
"assignment_expression" => !is_at_field(node, parent, "left"),
"field_expression" => is_at_field(node, parent, "argument"),
_ => false,
}
}
fn is_at_field(node: Node, parent: Node, field: &str) -> bool {
parent
.child_by_field_name(field)
.is_some_and(|f| f.byte_range() == node.byte_range())
}
fn make_qn(file_path: &str, name: &str, project: &str, parent: Option<&str>) -> String {
FqnGenerator::generate(project, file_path, name, Language::C, parent)
}
fn combine_scope(parent: Option<&str>, child: Option<&str>) -> Option<String> {
match (parent, child) {
(Some(p), Some(c)) => Some(format!("{p}_{c}")),
(None, Some(c)) => Some(c.to_string()),
(Some(p), None) => Some(p.to_string()),
(None, None) => None,
}
}
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;
const C_SOURCE: &str = r#"#include <stdio.h>
#include "myheader.h"
typedef int my_int;
int global_var = 42;
int add(int a, int b) {
return a + b;
}
int main() {
int result = add(1, 2);
printf("hello");
return result;
}
"#;
fn extract(source: &str) -> ExtractResult {
let ext = CExtractor::new();
ext.extract(source, "test.c", "proj")
.expect("extraction should succeed")
}
#[test]
fn language_returns_c() {
assert_eq!(CExtractor::new().language(), Language::C);
}
#[test]
fn default_creates_extractor() {
let ext = CExtractor::default();
assert_eq!(ext.language(), Language::C);
}
#[test]
fn extracts_two_includes() {
let result = extract(C_SOURCE);
assert_eq!(
result.imports.len(),
2,
"should extract 2 #include directives"
);
assert_eq!(result.imports[0].source_file, "stdio.h");
assert_eq!(result.imports[1].source_file, "myheader.h");
assert_eq!(result.imports[0].line, 1);
assert_eq!(result.imports[1].line, 2);
}
#[test]
fn extracts_typedef() {
let result = extract(C_SOURCE);
let typedefs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Typedef)
.collect();
assert_eq!(typedefs.len(), 1, "should extract 1 typedef");
assert_eq!(typedefs[0].name, "my_int");
assert_eq!(typedefs[0].start_line, Some(3));
assert_eq!(typedefs[0].language, Some(Language::C));
assert_eq!(typedefs[0].project, "proj");
assert_eq!(typedefs[0].file_path.as_deref(), Some("test.c"));
assert!(typedefs[0].is_global);
}
#[test]
fn typedef_duplicate_name_disambiguated() {
let src = r#"typedef int flex_uint16_t;
typedef unsigned short int flex_uint16_t;
"#;
let result = extract(src);
let typedefs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Typedef && n.name == "flex_uint16_t")
.collect();
assert_eq!(typedefs.len(), 2, "should extract 2 typedefs");
assert!(
!typedefs[0].qualified_name.contains('#'),
"first typedef should have no disambiguator: {}",
typedefs[0].qualified_name
);
assert!(
typedefs[1].qualified_name.contains("#L"),
"second typedef should have #L disambiguator: {}",
typedefs[1].qualified_name
);
assert_ne!(
typedefs[0].qualified_name, typedefs[1].qualified_name,
"FQNs must differ to avoid collision"
);
}
#[test]
fn extracts_global_var() {
let result = extract(C_SOURCE);
let globals: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::GlobalVar)
.collect();
assert_eq!(globals.len(), 1, "should extract 1 global variable");
assert_eq!(globals[0].name, "global_var");
assert_eq!(globals[0].start_line, Some(4));
assert_eq!(globals[0].language, Some(Language::C));
}
#[test]
fn extracts_functions() {
let result = extract(C_SOURCE);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(funcs.len(), 2, "should extract 2 functions (add, main)");
let names: Vec<_> = funcs.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"add"));
assert!(names.contains(&"main"));
}
#[test]
fn function_has_signature_and_lines() {
let result = extract(C_SOURCE);
let add = result
.nodes
.iter()
.find(|n| n.name == "add")
.expect("add function should exist");
assert_eq!(add.start_line, Some(5));
assert_eq!(add.end_line, Some(7));
assert!(add.signature.is_some(), "function should have a signature");
assert!(add.signature.as_deref().unwrap().contains("add"));
}
#[test]
fn extracts_calls() {
let result = extract(C_SOURCE);
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callees.contains(&"add"), "should extract call to add");
assert!(callees.contains(&"printf"), "should extract call to printf");
}
#[test]
fn call_has_line_and_args() {
let result = extract(C_SOURCE);
let add_call = result
.calls
.iter()
.find(|c| c.callee_name == "add")
.expect("call to add should exist");
assert_eq!(add_call.line, 9);
assert_eq!(add_call.args.len(), 2, "add(1, 2) should have 2 args");
}
#[test]
fn creates_defines_edges() {
let result = extract(C_SOURCE);
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,
"each node should have a DEFINES edge"
);
let contains_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Contains)
.count();
assert_eq!(
contains_count, 0,
"B1 fix: no CONTAINS edges should be emitted"
);
}
#[test]
fn edges_reference_file_and_node_ids() {
let result = extract(C_SOURCE);
for edge in &result.edges {
assert_eq!(edge.source, "test.c", "edge source should be the file path");
assert!(!edge.target.is_empty(), "edge target should be a node id");
assert_eq!(edge.project, "proj");
}
}
#[test]
fn qualified_name_uses_file_path_and_name() {
let result = extract(C_SOURCE);
let add = result.nodes.iter().find(|n| n.name == "add").unwrap();
assert_eq!(add.qualified_name, "proj.test.c.add");
}
#[test]
fn empty_source_returns_empty_result() {
let result = extract("");
assert!(result.nodes.is_empty());
assert!(result.imports.is_empty());
assert!(result.calls.is_empty());
assert!(result.is_empty());
}
#[test]
fn extracts_struct_definition() {
let src = "struct Point { int x; int y; };";
let result = extract(src);
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert_eq!(structs.len(), 1);
assert_eq!(structs[0].name, "Point");
}
#[test]
fn extracts_enum_definition() {
let src = "enum Color { RED, GREEN, BLUE };";
let result = extract(src);
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 struct_without_body_is_not_extracted() {
let src = "struct Point p;";
let result = extract(src);
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert_eq!(
structs.len(),
0,
"struct without body should not be extracted"
);
}
#[test]
fn handles_pointer_function_declarator() {
let src = "int* alloc(int n) { return 0; }";
let result = extract(src);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(funcs.len(), 1);
assert_eq!(funcs[0].name, "alloc");
}
#[test]
fn handles_extern_linkage_block() {
let src = r#"extern "C" {
int c_func(int x);
}"#;
let result = extract(src);
assert_eq!(result.language, Language::C);
}
#[test]
fn multiple_global_vars_in_one_declaration() {
let src = "int a = 1, b = 2, c = 3;";
let result = extract(src);
let globals: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::GlobalVar)
.collect();
assert_eq!(globals.len(), 3, "should extract 3 global variables");
let names: Vec<_> = globals.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"a"));
assert!(names.contains(&"b"));
assert!(names.contains(&"c"));
}
#[test]
fn nested_call_expressions() {
let src = "int main() { printf(format_str(add(1))); }";
let result = extract(src);
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(callees.contains(&"printf"), "should find printf call");
assert!(callees.contains(&"add"), "should find nested add call");
}
#[test]
fn field_expression_call() {
let src = "int main() { obj.method(); }";
let result = extract(src);
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callees.contains(&"method"),
"should extract method name from field expression"
);
}
#[test]
fn result_language_is_c() {
let result = extract(C_SOURCE);
assert_eq!(result.language, Language::C);
assert_eq!(result.file_path, "test.c");
}
#[test]
fn call_in_function_has_dotted_fqn_caller_qn() {
let src = "int caller(void) {\n callee();\n return 0;\n}\n";
let ext = CExtractor::new();
let result = ext
.extract(src, "/tmp/demo/main.c", "proj")
.expect("extraction should succeed");
let call = result
.calls
.iter()
.find(|c| c.callee_name == "callee")
.expect("should find call to callee");
assert_eq!(
call.caller_qn.as_deref(),
Some("proj.tmp.demo.main.c.caller"),
"caller_qn should be the dotted FQN of the enclosing function"
);
let caller_node = result
.nodes
.iter()
.find(|n| n.name == "caller")
.expect("should find caller function node");
assert_eq!(
call.caller_qn.as_deref(),
Some(caller_node.qualified_name.as_str()),
"caller_qn must match the caller function node id"
);
}
#[test]
fn read_in_function_has_dotted_fqn_reader_qn() {
let src = "int caller(int x) {\n return x;\n}\n";
let ext = CExtractor::new();
let result = ext
.extract(src, "/tmp/demo/main.c", "proj")
.expect("extraction should succeed");
let read = result
.reads
.iter()
.find(|r| r.var_name == "x")
.expect("should find a read of x");
assert_eq!(
read.reader_qn.as_deref(),
Some("proj.tmp.demo.main.c.caller"),
"reader_qn should be the dotted FQN of the enclosing function"
);
let caller_node = result
.nodes
.iter()
.find(|n| n.name == "caller")
.expect("should find caller function node");
assert_eq!(
read.reader_qn.as_deref(),
Some(caller_node.qualified_name.as_str()),
"reader_qn must match the caller function node id"
);
}
#[test]
fn write_in_function_init_declarator_has_dotted_fqn_writer_qn() {
let src = "void caller(void) {\n int y = 1;\n}\n";
let ext = CExtractor::new();
let result = ext
.extract(src, "/tmp/demo/main.c", "proj")
.expect("extraction should succeed");
let write = result
.writes
.iter()
.find(|w| w.var_name == "y")
.expect("should find a write of y");
assert_eq!(write.var_name, "y");
assert_eq!(
write.writer_qn.as_deref(),
Some("proj.tmp.demo.main.c.caller"),
"writer_qn should be the dotted FQN of the enclosing function"
);
let caller_node = result
.nodes
.iter()
.find(|n| n.name == "caller")
.expect("should find caller function node");
assert_eq!(
write.writer_qn.as_deref(),
Some(caller_node.qualified_name.as_str()),
"writer_qn must match the caller function node id"
);
}
#[test]
fn write_in_function_assignment_has_dotted_fqn_writer_qn() {
let src = "void caller(void) {\n int y;\n y = 2;\n}\n";
let ext = CExtractor::new();
let result = ext
.extract(src, "/tmp/demo/main.c", "proj")
.expect("extraction should succeed");
let assignment_write = result
.writes
.iter()
.find(|w| w.var_name == "y")
.expect("should find a write of y from assignment");
assert_eq!(
assignment_write.writer_qn.as_deref(),
Some("proj.tmp.demo.main.c.caller"),
"writer_qn should be the dotted FQN of the enclosing function"
);
}
#[test]
fn declaration_position_identifier_not_a_read() {
let src = "void caller(void) {\n int x = 1;\n}\n";
let ext = CExtractor::new();
let result = ext
.extract(src, "/tmp/demo/main.c", "proj")
.expect("extraction should succeed");
let x_reads: Vec<_> = result.reads.iter().filter(|r| r.var_name == "x").collect();
assert!(
x_reads.is_empty(),
"declarator-position x must NOT appear in ReadInfo: {:?}",
x_reads
);
let x_writes: Vec<_> = result.writes.iter().filter(|w| w.var_name == "x").collect();
assert!(
!x_writes.is_empty(),
"declarator-position x SHOULD appear in WriteInfo"
);
}
#[test]
fn top_level_declaration_no_reads_or_writes() {
let src = "int g = 0;\n";
let ext = CExtractor::new();
let result = ext
.extract(src, "/tmp/demo/main.c", "proj")
.expect("extraction should succeed");
assert!(
result.reads.is_empty(),
"top-level declaration must not produce ReadInfo: {:?}",
result.reads
);
assert!(
result.writes.is_empty(),
"top-level declaration must not produce WriteInfo: {:?}",
result.writes
);
}
#[test]
fn cpp_overloaded_methods_get_line_disambiguator() {
let src = "\
class Container {
public:
int* begin() { return data_; }
const int* begin() const { return data_; }
int* end() { return data_ + size_; }
const int* end() const { return data_ + size_; }
private:
int data_[10];
int size_ = 0;
};
";
let result = extract(src);
let ends: Vec<_> = result.nodes.iter().filter(|n| n.name == "end").collect();
assert_eq!(ends.len(), 2, "should extract two `end` methods");
assert_ne!(ends[0].qualified_name, ends[1].qualified_name);
for e in &ends {
assert!(
e.qualified_name.contains("#Container_L"),
"expected #Container_L<line>: {}",
e.qualified_name
);
}
}
#[test]
fn extracts_object_like_macro() {
let src = "#define FOO 1\n";
let result = extract(src);
let macros: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Macro)
.collect();
assert_eq!(macros.len(), 1, "should extract 1 object-like macro");
assert_eq!(macros[0].name, "FOO");
assert_eq!(macros[0].start_line, Some(1));
assert_eq!(macros[0].language, Some(Language::C));
assert_eq!(macros[0].project, "proj");
assert_eq!(macros[0].file_path.as_deref(), Some("test.c"));
assert!(
macros[0].signature.is_none(),
"object-like macro has no parameter signature"
);
}
#[test]
fn extracts_function_like_macro() {
let src = "#define MAX(a, b) ((a) > (b) ? (a) : (b))\n";
let result = extract(src);
let macros: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Macro && n.name == "MAX")
.collect();
assert_eq!(macros.len(), 1, "should extract 1 function-like macro");
assert_eq!(macros[0].start_line, Some(1));
assert!(
macros[0].signature.is_some(),
"function-like macro should expose parameter list as signature"
);
let sig = macros[0].signature.as_deref().unwrap();
assert!(sig.contains('a'), "signature should contain param a: {sig}");
assert!(sig.contains('b'), "signature should contain param b: {sig}");
}
#[test]
fn extracts_mixed_macros_and_functions() {
let src = r#"#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define cJSON_min(a, b) ((a) < (b) ? (a) : (b))
#define cJSON_max(a, b) ((a) > (b) ? (a) : (b))
static void cJSON_skip_whitespace(const char *buffer) {
(void)buffer;
}
"#;
let result = extract(src);
let macros: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Macro)
.collect();
assert_eq!(macros.len(), 4, "should extract all 4 #define macros");
let macro_names: Vec<_> = macros.iter().map(|n| n.name.as_str()).collect();
assert!(macro_names.contains(&"CJSON_VERSION_MAJOR"));
assert!(macro_names.contains(&"CJSON_VERSION_MINOR"));
assert!(macro_names.contains(&"cJSON_min"));
assert!(macro_names.contains(&"cJSON_max"));
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(funcs.len(), 1, "should still extract the function");
assert_eq!(funcs[0].name, "cJSON_skip_whitespace");
}
#[test]
fn macro_invocation_not_extracted_as_function() {
let src = r#"#define API_SUFFIX(a) a##_64
void API_SUFFIX(cblas_caxpy)(int N, const void *X) {
(void)N;
(void)X;
}
void normal_func(int x) {
(void)x;
}
"#;
let result = extract(src);
let api_suffix_funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function && n.name == "API_SUFFIX")
.collect();
assert!(
api_suffix_funcs.is_empty(),
"B9 fix: API_SUFFIX (macro invocation) must NOT be extracted as Function: {:?}",
api_suffix_funcs
);
let normal_funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function && n.name == "normal_func")
.collect();
assert_eq!(
normal_funcs.len(),
1,
"normal function should still be extracted"
);
let macros: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Macro && n.name == "API_SUFFIX")
.collect();
assert_eq!(
macros.len(),
1,
"API_SUFFIX macro definition should be extracted as Macro"
);
}
#[test]
fn macro_has_contains_and_defines_edges() {
let src = "#define FOO 1\n";
let result = extract(src);
let macro_node = result
.nodes
.iter()
.find(|n| n.label == NodeLabel::Macro)
.expect("macro node should exist");
let defines_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Defines && e.target == macro_node.id)
.count();
assert_eq!(defines_count, 1, "macro should have 1 DEFINES edge");
let contains_count = result
.edges
.iter()
.filter(|e| e.edge_type == EdgeType::Contains && e.target == macro_node.id)
.count();
assert_eq!(
contains_count, 0,
"B1 fix: macro should have 0 CONTAINS edges"
);
}
#[test]
fn extracts_function_declaration_in_header() {
let src = "int foo(int x);\n";
let result = extract(src);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(funcs.len(), 1, "should extract 1 function declaration");
assert_eq!(funcs[0].name, "foo");
assert!(
funcs[0].signature.is_some(),
"declaration should have signature"
);
assert_eq!(funcs[0].start_line, Some(1));
}
#[test]
fn function_declaration_does_not_create_global_var() {
let src = "int declare_only(int x);\n";
let result = extract(src);
let globals: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::GlobalVar)
.collect();
assert!(
globals.is_empty(),
"function declaration should not create a global var"
);
}
#[test]
fn pointer_function_declaration_extracted() {
let src = "int *get_ptr(void);\n";
let result = extract(src);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(
funcs.len(),
1,
"pointer function declaration should be extracted"
);
assert_eq!(funcs[0].name, "get_ptr");
}
#[test]
fn multiple_function_declarations_in_header() {
let src = "int add(int a, int b);\nint sub(int a, int b);\n";
let result = extract(src);
let funcs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Function)
.collect();
assert_eq!(funcs.len(), 2, "should extract 2 function declarations");
let names: Vec<_> = funcs.iter().map(|n| n.name.as_str()).collect();
assert!(names.contains(&"add"));
assert!(names.contains(&"sub"));
}
#[test]
fn anonymous_struct_in_typedef_creates_struct_node() {
let src = "typedef struct { int x; int y; } Point;\n";
let result = extract(src);
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert_eq!(
structs.len(),
1,
"anonymous struct should create a Struct node"
);
assert_eq!(
structs[0].name, "Point",
"struct should use the typedef name"
);
let typedefs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Typedef)
.collect();
assert_eq!(typedefs.len(), 1, "typedef should also be extracted");
}
#[test]
fn anonymous_enum_in_typedef_creates_enum_node() {
let src = "typedef enum { RED, GREEN, BLUE } Color;\n";
let result = extract(src);
let enums: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Enum)
.collect();
assert_eq!(enums.len(), 1, "anonymous enum should create an Enum node");
assert_eq!(enums[0].name, "Color", "enum should use the typedef name");
}
#[test]
fn parenthesized_call_expression_extracts_callee() {
let src = "void wrapper(void) { (func)(42); }\n";
let result = extract(src);
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callees.contains(&"func"),
"parenthesized call should extract callee name"
);
}
#[test]
fn call_expression_result_as_callee() {
let src = "void caller(void) { get_fn()(42); }\n";
let result = extract(src);
let callees: Vec<_> = result
.calls
.iter()
.map(|c| c.callee_name.as_str())
.collect();
assert!(
callees.contains(&"get_fn"),
"chained call should extract inner callee"
);
}
#[test]
fn struct_without_name_skipped() {
let src = "struct { int x; };\n";
let result = extract(src);
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert!(
structs.is_empty(),
"unnamed struct without typedef should not be extracted"
);
}
#[test]
fn struct_without_body_skipped() {
let src = "struct Point;\n";
let result = extract(src);
let structs: Vec<_> = result
.nodes
.iter()
.filter(|n| n.label == NodeLabel::Struct)
.collect();
assert!(
structs.is_empty(),
"struct without body should not be extracted"
);
}
}