use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, EntryPoint, FfiExport, FileFacts, RefRole, Reference, Scope,
ScopeId, ScopeKind, Symbol, SymbolKind, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, collect_call_references,
definition_bindings, field_text, innermost_scope, is_static, make_symbol, node_span, node_text,
one_line_signature, push_binding, push_ref, push_scope, push_type_ref,
};
const CALL_QUERY: &str = r#"
(call_expression
function: (identifier) @callee
)
"#;
pub struct CExtractor;
impl Extractor for CExtractor {
fn lang(&self) -> Language {
Language::C
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::c();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let namespaces = c_namespaces(file);
let defs = collect_symbols(&root, bytes, file, &namespaces);
let def_bindings = definition_bindings(&defs);
let ffi_exports = jni_exports(&defs);
let mut symbols = defs;
symbols.push(super::module_symbol(
Language::C,
&namespaces,
file,
source.len(),
));
let mut references =
collect_call_references(&root, &ts_language, CALL_QUERY, Language::C, bytes, file)?;
collect_type_references(&root, bytes, file, &mut references);
collect_read_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, bytes, &scopes);
bindings.extend(def_bindings);
Ok(FileFacts {
file: file.to_owned(),
lang: Language::C.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports,
})
}
}
fn c_namespaces(file: &str) -> Vec<String> {
let p = file
.strip_suffix(".c")
.or_else(|| file.strip_suffix(".h"))
.unwrap_or(file);
let p = p.strip_prefix("src/").unwrap_or(p);
p.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn jni_exports(symbols: &[Symbol]) -> Vec<FfiExport> {
symbols
.iter()
.filter(|s| s.kind == SymbolKind::Function)
.filter_map(|s| {
crate::ffi::c_name_export_abi(&s.name).map(|abi| FfiExport {
symbol: s.id.clone(),
abi,
export_name: s.name.clone(),
})
})
.collect()
}
fn declarator_name(node: &Node, bytes: &[u8]) -> Option<(String, bool)> {
match node.kind() {
"identifier" | "type_identifier" | "field_identifier" => {
Some((node_text(node, bytes).to_owned(), false))
}
"function_declarator" => {
let inner = node.child_by_field_name("declarator")?;
let (name, _) = declarator_name(&inner, bytes)?;
Some((name, true))
}
_ => {
if let Some(d) = node.child_by_field_name("declarator") {
return declarator_name(&d, bytes);
}
for c in node.children(&mut node.walk()) {
if let Some(r) = declarator_name(&c, bytes) {
return Some(r);
}
}
None
}
}
}
fn find_function_declarator<'tree>(node: &Node<'tree>) -> Option<Node<'tree>> {
if node.kind() == "function_declarator" {
return Some(*node);
}
if let Some(inner) = node.child_by_field_name("declarator") {
return find_function_declarator(&inner);
}
for child in node.children(&mut node.walk()) {
if let Some(found) = find_function_declarator(&child) {
return Some(found);
}
}
None
}
fn collect_symbols(root: &Node, bytes: &[u8], file: &str, namespaces: &[String]) -> Vec<Symbol> {
let mut out = Vec::new();
let ctx = ExtractCtx {
bytes,
file,
lang: Language::C,
};
let push = |out: &mut Vec<Symbol>,
node: &Node,
name: String,
kind: SymbolKind,
visibility: Visibility,
leaf: Descriptor| {
let mut descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
descriptors.push(leaf);
let signature = one_line_signature(node_text(node, ctx.bytes), &['{', ';']);
out.push(make_symbol(
&ctx,
node,
name,
kind,
visibility,
descriptors,
signature,
));
};
for child in root.children(&mut root.walk()) {
match child.kind() {
"function_definition" => {
let vis = if is_static(&child, bytes) {
Visibility::Private
} else {
Visibility::Public
};
let Some(decl) = child.child_by_field_name("declarator") else {
continue;
};
let Some((name, _)) = declarator_name(&decl, bytes) else {
continue;
};
let is_main = name == "main";
push(
&mut out,
&child,
name.clone(),
SymbolKind::Function,
vis,
Descriptor::Method {
name,
disambiguator: String::new(),
},
);
if is_main {
if let Some(s) = out.last_mut() {
s.entry_points.push(EntryPoint::Main);
}
}
}
"declaration" => {
let vis = if is_static(&child, bytes) {
Visibility::Private
} else {
Visibility::Public
};
if let Some(spec) = child.child_by_field_name("type") {
if let Some((agg_kind, agg_name)) = aggregate_type_symbol(&spec, bytes) {
push(
&mut out,
&spec,
agg_name.clone(),
agg_kind,
vis,
Descriptor::Type(agg_name),
);
}
}
let mut cursor = child.walk();
for decl in child.children_by_field_name("declarator", &mut cursor) {
let Some((name, is_function)) = declarator_name(&decl, bytes) else {
continue;
};
if is_function {
push(
&mut out,
&child,
name.clone(),
SymbolKind::Function,
vis,
Descriptor::Method {
name,
disambiguator: String::new(),
},
);
} else {
push(
&mut out,
&child,
name.clone(),
SymbolKind::Static,
vis,
Descriptor::Term(name),
);
}
}
}
"type_definition" => {
if let Some(spec) = child.child_by_field_name("type") {
if let Some((agg_kind, agg_name)) = aggregate_type_symbol(&spec, bytes) {
push(
&mut out,
&spec,
agg_name.clone(),
agg_kind,
Visibility::Public,
Descriptor::Type(agg_name),
);
}
}
let Some(decl) = child.child_by_field_name("declarator") else {
continue;
};
let Some((name, _)) = declarator_name(&decl, bytes) else {
continue;
};
push(
&mut out,
&child,
name.clone(),
SymbolKind::TypeAlias,
Visibility::Public,
Descriptor::Type(name),
);
}
"preproc_def" => {
let Some(name) = field_text(&child, "name", bytes) else {
continue;
};
push(
&mut out,
&child,
name.clone(),
SymbolKind::Const,
Visibility::Public,
Descriptor::Macro(name),
);
}
"preproc_function_def" => {
let Some(name) = field_text(&child, "name", bytes) else {
continue;
};
push(
&mut out,
&child,
name.clone(),
SymbolKind::Function,
Visibility::Public,
Descriptor::Macro(name),
);
}
"struct_specifier" | "union_specifier" | "enum_specifier" => {
if let Some((agg_kind, agg_name)) = aggregate_type_symbol(&child, bytes) {
push(
&mut out,
&child,
agg_name.clone(),
agg_kind,
Visibility::Public,
Descriptor::Type(agg_name),
);
}
}
_ => continue,
}
}
out
}
fn aggregate_type_symbol(spec: &Node, bytes: &[u8]) -> Option<(SymbolKind, String)> {
let kind = match spec.kind() {
"struct_specifier" => SymbolKind::Struct,
"union_specifier" => SymbolKind::Struct,
"enum_specifier" => SymbolKind::Enum,
_ => return None,
};
spec.child_by_field_name("body")?;
let name = field_text(spec, "name", bytes)?;
Some((kind, name))
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"function_definition" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"compound_statement" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"function_definition" => {
if let Some(decl) = node.child_by_field_name("declarator") {
if let Some(fn_decl) = find_function_declarator(&decl) {
if let Some(params) = fn_decl.child_by_field_name("parameters") {
collect_params(¶ms, bytes, scopes, out);
}
}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
"declaration" => {
let mut cursor = node.walk();
for (i, child) in node.children(&mut cursor).enumerate() {
if node.field_name_for_child(i as u32) == Some("declarator") {
if let Some((name, _)) = declarator_name(&child, bytes) {
let intro = child.start_byte();
if innermost_scope(intro, scopes) != Some(0) {
push_binding(out, name, intro, BindingKind::Local, scopes);
}
}
} else {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
_ => {
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
}
}
fn collect_params(params: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
for child in params.children(&mut params.walk()) {
if child.kind() != "parameter_declaration" {
continue;
}
let Some(decl) = child.child_by_field_name("declarator") else {
continue;
};
let Some((name, _)) = declarator_name(&decl, bytes) else {
continue;
};
let intro = decl.start_byte();
push_binding(out, name, intro, BindingKind::Param, scopes);
}
}
fn type_leaf<'tree>(node: &Node<'tree>, bytes: &[u8]) -> Option<(String, Node<'tree>)> {
match node.kind() {
"type_identifier" => Some((node_text(node, bytes).to_owned(), *node)),
"struct_specifier" | "union_specifier" | "enum_specifier" => {
let name_node = node.child_by_field_name("name")?;
Some((node_text(&name_node, bytes).to_owned(), name_node))
}
_ => None,
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"function_definition" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some((name, leaf)) = type_leaf(&type_node, bytes) {
push_type_ref(out, &name, &leaf, file, TypeRefContext::ReturnType);
}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
return; }
"parameter_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some((name, leaf)) = type_leaf(&type_node, bytes) {
push_type_ref(out, &name, &leaf, file, TypeRefContext::ParameterType);
}
}
return;
}
"field_declaration" => {
if let Some(type_node) = node.child_by_field_name("type") {
if let Some((name, leaf)) = type_leaf(&type_node, bytes) {
push_type_ref(out, &name, &leaf, file, TypeRefContext::Field);
}
}
return;
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"call_expression" => parent.child_by_field_name("function").as_ref() == Some(node),
"pointer_declarator"
| "function_declarator"
| "init_declarator"
| "array_declarator"
| "attributed_declarator" => {
parent.child_by_field_name("declarator").as_ref() == Some(node)
}
"parenthesized_declarator" => true,
"declaration" | "type_definition" | "function_definition" => {
parent.child_by_field_name("declarator").as_ref() == Some(node)
}
"parameter_declaration" => parent.child_by_field_name("declarator").as_ref() == Some(node),
"assignment_expression" => parent.child_by_field_name("left").as_ref() == Some(node),
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "assignment_expression" {
if let Some(lhs) = node.child_by_field_name("left") {
if lhs.kind() == "identifier" {
let name = node_text(&lhs, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &lhs, file, RefRole::Write);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::types::{RefRole, TypeRefContext};
#[test]
fn main_function_is_entry_point() {
let facts = CExtractor
.extract("int main(void) { return 0; }", "src/main.c")
.unwrap();
let main = facts.symbols.iter().find(|s| s.name == "main").unwrap();
assert!(
main.entry_points
.iter()
.any(|e| matches!(e, EntryPoint::Main))
);
}
#[test]
fn extracts_defs_with_visibility() {
let src = r#"
#define MAX_LEN 256
int authenticate(const char *tok) { return validate(tok); }
static int helper(void) { return 0; }
struct Session { int id; };
enum Status { OK, FAIL };
typedef struct Session SessionRef;
int global_count;
static int private_count;
"#;
let facts = CExtractor.extract(src, "src/auth/token.c").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let auth = by_name("authenticate").unwrap();
assert_eq!(auth.kind, SymbolKind::Function);
assert_eq!(auth.visibility, Visibility::Public);
assert_eq!(
auth.id.to_scip_string(),
"codegraph . . . auth/token/authenticate()."
);
let helper = by_name("helper").unwrap();
assert_eq!(helper.kind, SymbolKind::Function);
assert_eq!(helper.visibility, Visibility::Private);
assert_eq!(
helper.id.to_scip_string(),
"codegraph . . . auth/token/helper()."
);
let session = by_name("Session").unwrap();
assert_eq!(session.kind, SymbolKind::Struct);
assert_eq!(
session.id.to_scip_string(),
"codegraph . . . auth/token/Session#"
);
let status = by_name("Status").unwrap();
assert_eq!(status.kind, SymbolKind::Enum);
assert_eq!(
status.id.to_scip_string(),
"codegraph . . . auth/token/Status#"
);
let alias = by_name("SessionRef").unwrap();
assert_eq!(alias.kind, SymbolKind::TypeAlias);
assert_eq!(
alias.id.to_scip_string(),
"codegraph . . . auth/token/SessionRef#"
);
let gc = by_name("global_count").unwrap();
assert_eq!(gc.kind, SymbolKind::Static);
assert_eq!(gc.visibility, Visibility::Public);
assert_eq!(
gc.id.to_scip_string(),
"codegraph . . . auth/token/global_count."
);
let pc = by_name("private_count").unwrap();
assert_eq!(pc.kind, SymbolKind::Static);
assert_eq!(pc.visibility, Visibility::Private);
assert_eq!(
pc.id.to_scip_string(),
"codegraph . . . auth/token/private_count."
);
let max = by_name("MAX_LEN").unwrap();
assert_eq!(max.kind, SymbolKind::Const);
assert_eq!(max.visibility, Visibility::Public);
assert_eq!(
max.id.to_scip_string(),
"codegraph . . . auth/token/MAX_LEN!"
);
assert_eq!(facts.lang, "c");
}
#[test]
fn function_macro_and_prototype() {
let src = r#"
#define SQUARE(x) ((x)*(x))
int compute(int n);
"#;
let facts = CExtractor.extract(src, "src/util.h").unwrap();
let by_name = |n: &str| facts.symbols.iter().find(|s| s.name == n).cloned();
let sq = by_name("SQUARE").unwrap();
assert_eq!(sq.kind, SymbolKind::Function);
assert_eq!(sq.id.to_scip_string(), "codegraph . . . util/SQUARE!");
let comp = by_name("compute").unwrap();
assert_eq!(comp.kind, SymbolKind::Function);
assert_eq!(comp.id.to_scip_string(), "codegraph . . . util/compute().");
}
#[test]
fn extracts_call_references() {
let src = r#"
int main(void) {
authenticate("t");
compute(5);
}
"#;
let facts = CExtractor.extract(src, "src/main.c").unwrap();
let names: Vec<&str> = facts.references.iter().map(|r| r.name.as_str()).collect();
assert!(names.contains(&"authenticate"));
assert!(names.contains(&"compute"));
}
#[test]
fn func_params_emit_param_bindings() {
let src = "int add(int a, int b) { return a + b; }\n";
let facts = CExtractor.extract(src, "src/math.c").unwrap();
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let mut param_names: Vec<(&str, ScopeId)> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| (b.name.as_str(), b.scope))
.collect();
param_names.sort_by_key(|(n, _)| *n);
assert_eq!(
param_names,
vec![("a", fn_scope_id), ("b", fn_scope_id)],
"expected Param bindings for a and b, got {param_names:?}"
);
}
#[test]
fn pointer_param_emits_param_binding() {
let src = "int process(int *p) { return *p; }\n";
let facts = CExtractor.extract(src, "src/proc.c").unwrap();
let p = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "p")
.expect("expected a Param binding for 'p'");
assert_eq!(
facts.scopes[p.scope].kind,
ScopeKind::Function,
"pointer param 'p' should be in a Function scope"
);
}
#[test]
fn pointer_return_function_params_collected() {
let src = "char *dup(const char *s) { return 0; }\n";
let facts = CExtractor.extract(src, "src/dup.c").unwrap();
let s = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Param && b.name == "s")
.expect("pointer-return function's param 's' should be collected");
assert_eq!(
facts.scopes[s.scope].kind,
ScopeKind::Function,
"param 's' should be in a Function scope"
);
}
#[test]
fn local_var_decl_emits_local_binding() {
let src = "int f(void) { int x = 0; return x; }\n";
let facts = CExtractor.extract(src, "src/f.c").unwrap();
let x = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "x")
.expect("expected a Local binding for 'x'");
assert_ne!(x.scope, 0, "local 'x' must NOT be in scope 0 (file root)");
}
#[test]
fn multi_declarator_emits_two_locals() {
let src = "int f(void) { int a, b; return a + b; }\n";
let facts = CExtractor.extract(src, "src/f.c").unwrap();
let locals: Vec<&str> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Local)
.map(|b| b.name.as_str())
.collect();
assert!(
locals.contains(&"a"),
"expected Local for 'a', got {locals:?}"
);
assert!(
locals.contains(&"b"),
"expected Local for 'b', got {locals:?}"
);
}
#[test]
fn for_init_var_emits_local() {
let src = "void f(void) { for (int i = 0; i < 10; i++) {} }\n";
let facts = CExtractor.extract(src, "src/f.c").unwrap();
let i = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Local && b.name == "i")
.expect("expected a Local binding for 'i'");
assert_ne!(
i.scope, 0,
"for-init 'i' must NOT be in scope 0 (file root)"
);
}
#[test]
fn file_scope_global_is_not_local_but_is_definition() {
let src = "int global_count;\nvoid f(void) {}\n";
let facts = CExtractor.extract(src, "src/g.c").unwrap();
assert!(
!facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Local && b.name == "global_count"),
"global_count must NOT be a Local binding"
);
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "global_count"),
"global_count must have a Definition binding"
);
}
#[test]
fn nesting_produces_correct_scope_tree() {
let src = "void f(void) { { ; } }\n";
let facts = CExtractor.extract(src, "src/f.c").unwrap();
assert_eq!(facts.scopes.len(), 3, "expected exactly 3 scopes");
assert_eq!(facts.scopes[0].kind, ScopeKind::Module);
let fn_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Function)
.expect("expected a Function scope");
let block_scope_id = facts
.scopes
.iter()
.position(|s| s.kind == ScopeKind::Block)
.expect("expected a Block scope");
assert_eq!(
facts.scopes[block_scope_id].parent,
Some(fn_scope_id),
"Block scope parent should be the Function scope"
);
}
#[test]
fn top_level_non_static_func_emits_definition_binding() {
let src = "int helper(void) { return 0; }\n";
let facts = CExtractor.extract(src, "src/h.c").unwrap();
let b = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Definition && b.name == "helper")
.expect("expected a Definition binding for 'helper'");
assert_eq!(b.scope, 0, "top-level def must bind in scope 0");
}
#[test]
fn static_func_emits_definition_binding_with_private_visibility() {
let src = "static int helper(void) { return 0; }\n";
let facts = CExtractor.extract(src, "src/h.c").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "helper")
.expect("static 'helper' must be emitted as a symbol");
assert_eq!(
sym.visibility,
Visibility::Private,
"static function must have Private visibility"
);
let b = facts
.bindings
.iter()
.find(|b| b.kind == BindingKind::Definition && b.name == "helper")
.expect("static 'helper' must produce a Definition binding");
assert_eq!(b.scope, 0, "Definition binding must be in scope 0");
}
#[test]
fn non_static_func_has_public_visibility() {
let src = "int greet(void) { return 0; }\n";
let facts = CExtractor.extract(src, "src/vis.c").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "greet")
.expect("'greet' must be emitted");
assert_eq!(
sym.visibility,
Visibility::Public,
"non-static function must have Public visibility"
);
}
#[test]
fn static_func_has_private_visibility() {
let src = "static int internal_fn(void) { return 0; }\n";
let facts = CExtractor.extract(src, "src/vis.c").unwrap();
let sym = facts
.symbols
.iter()
.find(|s| s.name == "internal_fn")
.expect("static 'internal_fn' must be emitted");
assert_eq!(
sym.visibility,
Visibility::Private,
"static function must have Private visibility"
);
}
#[test]
fn void_param_skipped() {
let src = "int f(void) { return 0; }\n";
let facts = CExtractor.extract(src, "src/f.c").unwrap();
let params: Vec<&str> = facts
.bindings
.iter()
.filter(|b| b.kind == BindingKind::Param)
.map(|b| b.name.as_str())
.collect();
assert!(
params.is_empty(),
"expected zero Param bindings for (void), got {params:?}"
);
}
#[test]
fn same_file_call_ref_has_non_zero_scope_and_callee_has_definition() {
let src = "int helper(void) { return 0; }\nint caller(void) { return helper(); }\n";
let facts = CExtractor.extract(src, "src/pair.c").unwrap();
assert!(
facts
.bindings
.iter()
.any(|b| b.kind == BindingKind::Definition && b.name == "helper"),
"expected a Definition binding for 'helper'"
);
let call_ref = facts
.references
.iter()
.find(|r| r.role == RefRole::Call && r.name == "helper")
.expect("expected a Call ref for 'helper'");
assert!(
call_ref.scope.is_some() && call_ref.scope != Some(0),
"helper() call ref must be in a non-zero scope, got {:?}",
call_ref.scope
);
}
#[test]
fn read_at_use_not_at_decl() {
let src = "int f(void) { int base = 1; return base; }\n";
let facts = CExtractor.extract(src, "src/r.c").unwrap();
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "base")
.collect();
assert!(
!read_refs.is_empty(),
"expected at least one Read ref for 'base', got none — all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let use_ref = read_refs
.iter()
.find(|r| r.occ.byte > 20)
.expect("Read ref for 'base' should be at the return site (byte > 20)");
assert!(
use_ref.occ.byte > 20,
"Read ref byte {} should be in the return stmt, not the declarator",
use_ref.occ.byte
);
}
#[test]
fn write_ref_emitted_for_assignment() {
let src = "void f(void) { int cnt = 0; cnt = 5; }\n";
let facts = CExtractor.extract(src, "src/w.c").unwrap();
let write_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Write && r.name == "cnt")
.collect();
assert!(
!write_refs.is_empty(),
"expected at least one Write ref for 'cnt', got none — all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
}
#[test]
fn call_not_also_read() {
let src = "void f(void) { helper(); }\n";
let facts = CExtractor.extract(src, "src/nd.c").unwrap();
let call_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Call && r.name == "helper")
.collect();
assert!(!call_refs.is_empty(), "expected a Call ref for 'helper'");
let read_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "helper")
.collect();
assert!(
read_refs.is_empty(),
"helper() must NOT produce a Read ref; got: {read_refs:?}"
);
}
#[test]
fn field_access_ptr_is_read_field_is_not() {
let src = "struct S { int val; };\nint f(struct S *ptr) { return ptr->val; }\n";
let facts = CExtractor.extract(src, "src/fa.c").unwrap();
let ptr_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "ptr")
.collect();
assert!(
!ptr_reads.is_empty(),
"expected a Read ref for 'ptr', got none — all refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role))
.collect::<Vec<_>>()
);
let val_reads: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "val")
.collect();
assert!(
val_reads.is_empty(),
"field 'val' in field_expression must NOT produce a Read ref; got: {val_reads:?}"
);
}
#[test]
fn typeref_param_type_emitted() {
let src = "void f(Config c) {}\n";
let facts = CExtractor.extract(src, "src/tr.c").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.unwrap_or_else(|| {
panic!(
"expected TypeRef for 'Config', got refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role, r.type_ref_ctx))
.collect::<Vec<_>>()
)
});
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::ParameterType),
"expected ParameterType ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn typeref_return_type_emitted() {
let src = "Config make(void) { Config c; return c; }\n";
let facts = CExtractor.extract(src, "src/tr.c").unwrap();
let type_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef && r.name == "Config")
.collect();
let ret_ref = type_refs
.iter()
.find(|r| r.type_ref_ctx == Some(TypeRefContext::ReturnType))
.unwrap_or_else(|| {
panic!(
"expected TypeRef 'Config' with ReturnType, got: {:?}",
type_refs.iter().map(|r| r.type_ref_ctx).collect::<Vec<_>>()
)
});
assert_eq!(ret_ref.type_ref_ctx, Some(TypeRefContext::ReturnType));
}
#[test]
fn typeref_field_type_emitted() {
let src = "struct T { Config conf; };\n";
let facts = CExtractor.extract(src, "src/tr.c").unwrap();
let r = facts
.references
.iter()
.find(|r| r.role == RefRole::TypeRef && r.name == "Config")
.unwrap_or_else(|| {
panic!(
"expected TypeRef for 'Config' field, got refs: {:?}",
facts
.references
.iter()
.map(|r| (&r.name, r.role, r.type_ref_ctx))
.collect::<Vec<_>>()
)
});
assert_eq!(
r.type_ref_ctx,
Some(TypeRefContext::Field),
"expected Field ctx, got {:?}",
r.type_ref_ctx
);
}
#[test]
fn typeref_primitive_param_not_emitted() {
let src = "void f(int n) {}\n";
let facts = CExtractor.extract(src, "src/tr.c").unwrap();
let type_refs: Vec<_> = facts
.references
.iter()
.filter(|r| r.role == RefRole::TypeRef && r.name == "int")
.collect();
assert!(
type_refs.is_empty(),
"primitive 'int' must NOT produce a TypeRef; got: {type_refs:?}"
);
}
}