use std::{
collections::{HashMap, HashSet},
path::Path,
};
use sqry_core::graph::unified::edge::kind::TypeOfContext;
use sqry_core::graph::unified::{GraphBuildHelper, NodeId, StagingGraph};
use sqry_core::graph::{GraphBuilder, GraphBuilderError, GraphResult, Language, Span};
use tree_sitter::{Node, Tree};
use crate::relations::type_extractor::extract_type_names_from_zig_type;
const FILE_MODULE_NAME: &str = "<file_module>";
#[derive(Debug, Clone, Copy)]
pub struct ZigGraphBuilder {
max_scope_depth: usize,
}
impl Default for ZigGraphBuilder {
fn default() -> Self {
Self {
max_scope_depth: 4, }
}
}
impl ZigGraphBuilder {
#[must_use]
pub fn new(max_scope_depth: usize) -> Self {
Self { max_scope_depth }
}
}
impl GraphBuilder for ZigGraphBuilder {
fn build_graph(
&self,
tree: &Tree,
content: &[u8],
file: &Path,
staging: &mut StagingGraph,
) -> GraphResult<()> {
let mut helper = GraphBuildHelper::new(staging, file, Language::Zig);
let ast_graph = ASTGraph::from_tree(tree, content, self.max_scope_depth).map_err(|e| {
GraphBuilderError::ParseError {
span: Span::default(),
reason: e,
}
})?;
for context in ast_graph.contexts() {
let qualified = context.qualified_name();
let span = Span::from_bytes(context.span.0, context.span.1);
let visibility = if context.is_pub {
Some("public")
} else {
Some("private")
};
helper.add_function_with_visibility(&qualified, Some(span), false, false, visibility);
}
for decl in ast_graph.decl_nodes() {
helper.add_type(&decl.name, Some(Span::from_bytes(decl.span.0, decl.span.1)));
}
let module_id = helper.add_module(FILE_MODULE_NAME, None);
for context in ast_graph.contexts() {
if context.is_pub
&& !context.qualified_name.contains('.')
&& let Some(exported_id) = helper.get_node(&context.qualified_name)
{
helper.add_export_edge(module_id, exported_id);
}
}
for decl in ast_graph.decl_nodes() {
if decl.is_pub
&& let Some(exported_id) = helper.get_node(&decl.name)
{
helper.add_export_edge(module_id, exported_id);
}
}
let mut stack = vec![tree.root_node()];
let mut visited = HashSet::new();
while let Some(node) = stack.pop() {
let node_id = node.id();
if !visited.insert(node_id) {
continue;
}
match node.kind() {
"comment" | "line_comment" | "doc_comment" | "string" | "char" | "integer"
| "float" => {
continue;
}
_ => {}
}
if node.kind() == "builtin_function"
&& is_import_builtin(node, content)
&& let Some(module_name) = extract_import_module_name(node, content)
{
let importer_id = if let Some(ctx) = ast_graph.get_callable_context(node.id()) {
helper.get_node(&ctx.qualified_name()).unwrap_or_else(|| {
let span = Span::from_bytes(ctx.span.0, ctx.span.1);
helper.add_function(&ctx.qualified_name(), Some(span), false, false)
})
} else {
module_id
};
let span = Span::from_bytes(node.start_byte(), node.end_byte());
let import_node_id = helper.add_import(&module_name, Some(span));
helper.add_import_edge(importer_id, import_node_id);
}
else if (node.kind() == "call_expression"
|| (node.kind() == "builtin_function" && !is_import_builtin(node, content)))
&& let Some((caller_id, callee_id, argument_count)) =
build_call_edge_ids(&ast_graph, node, content, &mut helper)
{
let call_span = Span::from_bytes(node.start_byte(), node.end_byte());
helper.add_call_edge_full_with_span(
caller_id,
callee_id,
argument_count,
false,
vec![call_span],
);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
process_typeof_edges(tree.root_node(), content, &mut helper)?;
Ok(())
}
fn language(&self) -> Language {
Language::Zig
}
}
fn build_call_edge_ids(
ast_graph: &ASTGraph,
call_node: Node<'_>,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> Option<(NodeId, NodeId, u8)> {
let module_context;
let call_context = if let Some(ctx) = ast_graph.get_callable_context(call_node.id()) {
ctx
} else {
module_context = CallContext {
qualified_name: "<module>".to_string(),
span: (0, content.len()),
is_pub: false,
};
&module_context
};
let (callee_name, arg_count) = extract_call_info(call_node, content);
if callee_name.is_empty() {
return None;
}
let source_id = if helper.has_node(&call_context.qualified_name()) {
helper.get_node(&call_context.qualified_name()).unwrap()
} else {
let span = Span::from_bytes(call_context.span.0, call_context.span.1);
helper.add_function(&call_context.qualified_name(), Some(span), false, false)
};
let target_id = helper.add_function(&callee_name, None, false, false);
let argument_count = u8::try_from(arg_count).unwrap_or(u8::MAX);
Some((source_id, target_id, argument_count))
}
fn extract_call_info(call_node: Node<'_>, content: &[u8]) -> (String, usize) {
let mut function_name = String::new();
let mut arg_count = 0;
let mut in_arguments = false;
let mut found_function_name = false;
let mut cursor = call_node.walk();
for child in call_node.children(&mut cursor) {
match child.kind() {
"builtin_identifier" => {
if !found_function_name {
function_name = child.utf8_text(content).unwrap_or("").to_string();
found_function_name = true;
}
}
"identifier" | "field_expression" | "field_access" => {
if !found_function_name {
function_name = child.utf8_text(content).unwrap_or("").to_string();
found_function_name = true;
} else if in_arguments {
arg_count += 1;
}
}
"arguments" => {
arg_count = count_arguments_in_node(child);
}
"(" => {
in_arguments = true;
}
")" => {
in_arguments = false;
}
"," => {
}
_ => {
if in_arguments {
arg_count += 1;
}
}
}
}
(function_name, arg_count)
}
fn count_arguments_in_node(args_node: Node<'_>) -> usize {
let mut count = 0;
let mut cursor = args_node.walk();
for child in args_node.children(&mut cursor) {
match child.kind() {
"(" | ")" | "," => {
}
_ => {
count += 1;
}
}
}
count
}
fn is_import_builtin(node: Node<'_>, content: &[u8]) -> bool {
if node.kind() != "builtin_function" {
return false;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "builtin_identifier"
&& let Ok(text) = child.utf8_text(content)
&& text == "@import"
{
return true;
}
}
false
}
fn extract_import_module_name(node: Node<'_>, content: &[u8]) -> Option<String> {
if node.kind() != "builtin_function" {
return None;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "arguments" {
let mut args_cursor = child.walk();
for arg_child in child.children(&mut args_cursor) {
if arg_child.kind() == "string"
&& let Ok(text) = arg_child.utf8_text(content)
{
let trimmed = text.trim().trim_matches('"');
if !trimmed.is_empty() {
return Some(trimmed.to_string());
}
}
}
}
}
None
}
#[derive(Debug)]
struct ASTGraph {
contexts: Vec<CallContext>,
node_to_context: HashMap<usize, usize>,
decl_nodes: Vec<DeclNode>,
}
#[derive(Debug, Clone)]
struct DeclNode {
name: String,
span: (usize, usize),
is_pub: bool,
}
impl ASTGraph {
fn from_tree(tree: &Tree, content: &[u8], _max_depth: usize) -> Result<Self, String> {
let mut contexts = Vec::new();
let mut node_to_context = HashMap::new();
let mut decl_nodes = Vec::new();
let root = tree.root_node();
extract_functions_recursive(root, content, &mut contexts, &mut node_to_context, None)?;
extract_declarations_recursive(root, content, &mut decl_nodes, None)?;
Ok(Self {
contexts,
node_to_context,
decl_nodes,
})
}
fn contexts(&self) -> &[CallContext] {
&self.contexts
}
fn decl_nodes(&self) -> &[DeclNode] {
&self.decl_nodes
}
fn get_callable_context(&self, node_id: usize) -> Option<&CallContext> {
self.node_to_context
.get(&node_id)
.and_then(|idx| self.contexts.get(*idx))
}
}
fn extract_functions_recursive(
node: Node<'_>,
content: &[u8],
contexts: &mut Vec<CallContext>,
node_to_context: &mut HashMap<usize, usize>,
parent_name: Option<&str>,
) -> Result<(), String> {
if node.kind() == "function_declaration"
&& let Some(name) = extract_function_name(node, content)
{
let is_pub = has_pub_modifier(node);
let qualified_name = if let Some(parent) = parent_name {
format!("{parent}.{name}")
} else {
name.clone()
};
let context_idx = contexts.len();
contexts.push(CallContext {
qualified_name: qualified_name.clone(),
span: (node.start_byte(), node.end_byte()),
is_pub,
});
map_descendants_to_context(&node, context_idx, node_to_context);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_functions_recursive(
child,
content,
contexts,
node_to_context,
Some(&qualified_name),
)?;
}
return Ok(());
}
if node.kind() == "struct_declaration"
|| node.kind() == "union_declaration"
|| node.kind() == "enum_declaration"
{
let container_name = node.parent().and_then(|parent| {
if parent.kind() == "variable_declaration" {
extract_container_name_from_var_decl(parent, content)
} else {
None
}
});
let qualified_container = if let Some(name) = container_name {
if let Some(parent) = parent_name {
format!("{parent}.{name}")
} else {
name
}
} else {
parent_name.map(String::from).unwrap_or_default()
};
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
let child_parent = if qualified_container.is_empty() {
parent_name
} else {
Some(qualified_container.as_str())
};
extract_functions_recursive(child, content, contexts, node_to_context, child_parent)?;
}
return Ok(());
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_functions_recursive(child, content, contexts, node_to_context, parent_name)?;
}
Ok(())
}
fn extract_declarations_recursive(
node: Node<'_>,
content: &[u8],
decl_nodes: &mut Vec<DeclNode>,
parent_name: Option<&str>,
) -> Result<(), String> {
if parent_name.is_none()
&& node.kind() == "variable_declaration"
&& let Some((name, is_pub)) = extract_var_decl_info(node, content)
&& is_pub
{
decl_nodes.push(DeclNode {
name,
span: (node.start_byte(), node.end_byte()),
is_pub: true,
});
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
extract_declarations_recursive(child, content, decl_nodes, parent_name)?;
}
Ok(())
}
fn extract_var_decl_info(node: Node<'_>, content: &[u8]) -> Option<(String, bool)> {
let is_pub = has_pub_modifier(node);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier"
&& let Ok(name) = child.utf8_text(content)
{
return Some((name.to_string(), is_pub));
}
}
None
}
fn extract_container_name_from_var_decl(node: Node<'_>, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier"
&& let Ok(name) = child.utf8_text(content)
{
return Some(name.to_string());
}
}
None
}
fn extract_function_name(node: Node<'_>, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier"
&& let Ok(name) = child.utf8_text(content)
{
return Some(name.to_string());
}
}
None
}
fn has_pub_modifier(node: Node<'_>) -> bool {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "pub" {
return true;
}
}
false
}
fn map_descendants_to_context(node: &Node, context_idx: usize, map: &mut HashMap<usize, usize>) {
map.insert(node.id(), context_idx);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
map_descendants_to_context(&child, context_idx, map);
}
}
#[derive(Debug, Clone)]
struct CallContext {
qualified_name: String,
span: (usize, usize),
#[allow(dead_code)] is_pub: bool,
}
impl CallContext {
fn qualified_name(&self) -> String {
self.qualified_name.clone()
}
}
fn process_typeof_edges(
root: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let mut stack = vec![root];
let mut visited = HashSet::new();
while let Some(node) = stack.pop() {
let node_id = node.id();
if !visited.insert(node_id) {
continue;
}
match node.kind() {
"variable_declaration" => {
handle_variable_declaration(node, content, helper)?;
}
"function_declaration" => {
handle_function_typeof_edges(node, content, helper)?;
}
"struct_declaration" | "union_declaration" | "enum_declaration" => {
handle_container_fields(node, content, helper)?;
}
_ => {}
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
stack.push(child);
}
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn handle_variable_declaration(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let var_name = extract_variable_name(node, content);
if let Some(name) = var_name {
let type_node =
find_type_annotation_in_var_decl(node).or_else(|| find_type_alias_expression(node));
if let Some(type_node) = type_node {
let var_id = if let Some(id) = helper.get_node(&name) {
id
} else {
let span = Span::from_bytes(node.start_byte(), node.end_byte());
helper.add_variable(&name, Some(span))
};
if let Ok(type_str) = type_node.utf8_text(content) {
let type_id = helper.add_type(type_str.trim(), None);
helper.add_typeof_edge_with_context(
var_id,
type_id,
Some(TypeOfContext::Variable),
None,
Some(&name),
);
}
let type_names = extract_type_names_from_zig_type(type_node, content);
for type_name in type_names {
let type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(var_id, type_id);
}
}
}
Ok(())
}
fn handle_function_typeof_edges(
node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let fn_name = extract_function_name(node, content);
if let Some(name) = fn_name {
if let Some(fn_id) = helper.get_node(&name) {
if let Some(params_node) = find_parameters_node(node) {
let mut param_index = 0;
let mut cursor = params_node.walk();
for child in params_node.children(&mut cursor) {
if child.kind() == "parameter" {
handle_function_parameter(child, content, helper, fn_id, param_index)?;
param_index += 1;
}
}
}
if let Some(return_type_node) = find_function_return_type(node) {
if let Ok(type_str) = return_type_node.utf8_text(content) {
let type_id = helper.add_type(type_str.trim(), None);
helper.add_typeof_edge_with_context(
fn_id,
type_id,
Some(TypeOfContext::Return),
None,
None,
);
}
let type_names = extract_type_names_from_zig_type(return_type_node, content);
for type_name in type_names {
let type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(fn_id, type_id);
}
}
}
}
Ok(())
}
#[allow(clippy::cast_possible_truncation)]
#[allow(clippy::unnecessary_wraps)]
fn handle_function_parameter(
param_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
fn_id: NodeId,
param_index: usize,
) -> GraphResult<()> {
let param_name = extract_parameter_name(param_node, content);
let type_node = find_parameter_type_node(param_node);
if let Some(type_node) = type_node {
if let Ok(type_str) = type_node.utf8_text(content) {
let type_id = helper.add_type(type_str.trim(), None);
helper.add_typeof_edge_with_context(
fn_id,
type_id,
Some(TypeOfContext::Parameter),
Some(param_index as u16),
param_name.as_deref(),
);
}
let type_names = extract_type_names_from_zig_type(type_node, content);
for type_name in type_names {
let type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(fn_id, type_id);
}
}
Ok(())
}
fn handle_container_fields(
container_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
) -> GraphResult<()> {
let container_name = container_node.parent().and_then(|parent| {
if parent.kind() == "variable_declaration" {
extract_container_name_from_var_decl(parent, content)
} else {
None
}
});
if let Some(container_name) = container_name {
let mut cursor = container_node.walk();
for child in container_node.children(&mut cursor) {
if child.kind() == "container_field" {
handle_container_field(child, content, helper, &container_name)?;
}
}
}
Ok(())
}
#[allow(clippy::unnecessary_wraps)]
fn handle_container_field(
field_node: Node,
content: &[u8],
helper: &mut GraphBuildHelper,
container_name: &str,
) -> GraphResult<()> {
let field_name = extract_field_name(field_node, content);
let type_node = find_field_type_node(field_node);
if let (Some(name), Some(type_node)) = (field_name, type_node) {
let qualified_name = format!("{container_name}.{name}");
let field_id = if let Some(id) = helper.get_node(&qualified_name) {
id
} else {
let span = Span::from_bytes(field_node.start_byte(), field_node.end_byte());
helper.add_variable(&qualified_name, Some(span))
};
if let Ok(type_str) = type_node.utf8_text(content) {
let type_id = helper.add_type(type_str.trim(), None);
helper.add_typeof_edge_with_context(
field_id,
type_id,
Some(TypeOfContext::Field),
None,
Some(&name),
);
}
let type_names = extract_type_names_from_zig_type(type_node, content);
for type_name in type_names {
let type_id = helper.add_type(&type_name, None);
helper.add_reference_edge(field_id, type_id);
}
}
Ok(())
}
fn extract_variable_name(node: Node, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
return child.utf8_text(content).ok().map(String::from);
}
}
None
}
fn find_type_annotation_in_var_decl(node: Node) -> Option<Node> {
let mut found_colon = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == ":" {
found_colon = true;
continue;
}
if found_colon && is_type_like_node(child.kind()) {
return Some(child);
}
}
None
}
fn find_type_alias_expression(node: Node) -> Option<Node> {
let mut found_equals = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "=" {
found_equals = true;
continue;
}
if found_equals && is_type_like_node(child.kind()) {
return Some(child);
}
}
None
}
fn find_parameters_node(node: Node) -> Option<Node> {
let mut cursor = node.walk();
node.children(&mut cursor)
.find(|child| child.kind() == "parameters")
}
fn find_function_return_type(node: Node) -> Option<Node> {
let mut found_params = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "parameters" || child.kind() == ")" {
found_params = true;
continue;
}
if found_params && is_type_like_node(child.kind()) && child.kind() != "block" {
return Some(child);
}
}
None
}
fn extract_parameter_name(node: Node, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
return child.utf8_text(content).ok().map(String::from);
}
}
None
}
fn find_parameter_type_node(node: Node) -> Option<Node> {
let mut found_colon = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == ":" {
found_colon = true;
continue;
}
if found_colon && is_type_like_node(child.kind()) {
return Some(child);
}
}
None
}
fn extract_field_name(node: Node, content: &[u8]) -> Option<String> {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == "identifier" {
return child.utf8_text(content).ok().map(String::from);
}
}
None
}
fn find_field_type_node(node: Node) -> Option<Node> {
let mut found_colon = false;
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if child.kind() == ":" {
found_colon = true;
continue;
}
if found_colon && is_type_like_node(child.kind()) {
return Some(child);
}
}
None
}
fn is_type_like_node(kind: &str) -> bool {
matches!(
kind,
"builtin_type"
| "identifier"
| "pointer_type"
| "slice_type"
| "array_type"
| "optional_type"
| "nullable_type"
| "error_union_type"
| "function_type"
| "FnProto"
| "fn_proto"
| "struct_declaration"
| "enum_declaration"
| "union_declaration"
| "call_expression" | "field_expression" | "field_access" )
}
#[cfg(test)]
mod tests {
use super::*;
use sqry_core::graph::unified::build::StagingOp;
use sqry_core::graph::unified::build::test_helpers::*;
use sqry_core::graph::unified::edge::EdgeKind;
use sqry_core::graph::unified::node::NodeKind;
use std::path::Path;
fn parse_zig(source: &str) -> (Tree, Vec<u8>) {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&tree_sitter_zig::LANGUAGE.into())
.expect("Failed to load Zig grammar");
let content = source.as_bytes().to_vec();
let tree = parser.parse(&content, None).expect("Failed to parse");
(tree, content)
}
fn has_display_name(
staging: &StagingGraph,
canonical_name: &str,
expected_display_name: &str,
) -> bool {
staging.operations().iter().any(|op| {
if let StagingOp::AddNode { entry, .. } = op {
staging.resolve_node_canonical_name(entry) == Some(canonical_name)
&& staging
.resolve_node_display_name(Language::Zig, entry)
.as_deref()
== Some(expected_display_name)
} else {
false
}
})
}
fn has_display_edge(
staging: &StagingGraph,
kind_matches: impl Fn(&EdgeKind) -> bool,
expected_source: &str,
expected_target: &str,
) -> bool {
staging.operations().iter().any(|op| {
if let StagingOp::AddEdge {
source,
target,
kind,
..
} = op
{
if !kind_matches(kind) {
return false;
}
let source_display = staging.operations().iter().find_map(|candidate| {
if let StagingOp::AddNode {
entry,
expected_id: Some(node_id),
} = candidate
&& *node_id == *source
{
staging.resolve_node_display_name(Language::Zig, entry)
} else {
None
}
});
let target_display = staging.operations().iter().find_map(|candidate| {
if let StagingOp::AddNode {
entry,
expected_id: Some(node_id),
} = candidate
&& *node_id == *target
{
staging.resolve_node_display_name(Language::Zig, entry)
} else {
None
}
});
source_display.as_deref() == Some(expected_source)
&& target_display.as_deref() == Some(expected_target)
} else {
false
}
})
}
fn assert_has_display_call_edge(staging: &StagingGraph, source: &str, target: &str) {
assert!(
has_display_edge(
staging,
|kind| matches!(kind, EdgeKind::Calls { .. }),
source,
target,
),
"Expected Zig native display call edge {source} -> {target}"
);
}
fn assert_has_display_import_edge(staging: &StagingGraph, source: &str, target: &str) {
assert!(
has_display_edge(
staging,
|kind| matches!(
kind,
EdgeKind::Imports {
alias: _,
is_wildcard: _,
}
),
source,
target,
),
"Expected Zig native display import edge {source} -> {target}"
);
}
#[test]
fn test_extract_top_level_function() {
let source = r"
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "add", NodeKind::Function);
assert_has_export_edge(&staging, FILE_MODULE_NAME, "add");
}
#[test]
fn test_simple_function_call() {
let source = r"
fn helper() void {
return;
}
fn main() void {
helper();
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "helper", NodeKind::Function);
assert_has_node_with_kind(&staging, "main", NodeKind::Function);
assert_has_call_edge(&staging, "main", "helper");
}
#[test]
fn test_qualified_std_call() {
let source = r#"
const std = @import("std");
fn process(data: []const u8) void {
std.debug.print("Data: {any}\n", .{data});
}
"#;
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
assert_has_node_with_kind(&staging, "process", NodeKind::Function);
assert_has_call_edge(&staging, "process", "std::debug::print");
assert_has_display_call_edge(&staging, "process", "std.debug.print");
}
#[test]
fn test_argument_counting_zero_args() {
let source = r"
fn getValue() i32 {
return 42;
}
fn main() void {
const x = getValue();
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_call_edge(&staging, "main", "getValue");
let call_edges = collect_call_edges(&staging);
assert_eq!(call_edges.len(), 1, "Expected exactly one call edge");
}
#[test]
fn test_argument_counting_multiple_args() {
let source = r"
fn calculate(a: i32, b: i32, c: i32) i32 {
return a + b + c;
}
fn main() void {
const result = calculate(1, 2, 3);
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_call_edge(&staging, "main", "calculate");
let call_edges = collect_call_edges(&staging);
assert_eq!(call_edges.len(), 1, "Expected exactly one call edge");
}
#[test]
fn test_nested_function() {
let source = r"
fn outer() void {
fn inner() void {
return;
}
inner();
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "outer", NodeKind::Function);
}
#[test]
fn test_method_call_as_qualified() {
let source = r"
const ArrayList = struct {
fn append(self: *ArrayList, item: i32) void {
// implementation
}
};
fn main() void {
var list: ArrayList = undefined;
list.append(42);
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind_exact(&staging, "ArrayList::append", NodeKind::Function);
assert!(
has_display_name(&staging, "ArrayList::append", "ArrayList.append"),
"Struct methods should display with Zig native dot syntax"
);
assert_has_node_with_kind(&staging, "main", NodeKind::Function);
assert_has_call_edge(&staging, "main", "list::append");
assert_has_display_call_edge(&staging, "main", "list.append");
}
#[test]
fn test_stdlib_qualified_call() {
let source = r#"
const std = @import("std");
fn copyData(dest: []u8, src: []const u8) void {
std.mem.copy(u8, dest, src);
}
"#;
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
assert_has_node_with_kind(&staging, "copyData", NodeKind::Function);
assert_has_call_edge(&staging, "copyData", "std::mem::copy");
assert_has_display_call_edge(&staging, "copyData", "std.mem.copy");
}
#[test]
fn test_private_function_visibility() {
let source = r"
fn privateHelper() void {
return;
}
pub fn publicFunction() void {
privateHelper();
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "privateHelper", NodeKind::Function);
assert_has_node_with_kind(&staging, "publicFunction", NodeKind::Function);
assert_has_export_edge(&staging, FILE_MODULE_NAME, "publicFunction");
let export_edges = collect_export_edges(&staging);
assert_eq!(export_edges.len(), 1, "Expected only one export edge");
}
#[test]
fn test_multiple_calls_in_function() {
let source = r"
fn helper1() void {}
fn helper2() void {}
fn helper3() void {}
fn main() void {
helper1();
helper2();
helper3();
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "helper1", NodeKind::Function);
assert_has_node_with_kind(&staging, "helper2", NodeKind::Function);
assert_has_node_with_kind(&staging, "helper3", NodeKind::Function);
assert_has_node_with_kind(&staging, "main", NodeKind::Function);
assert_has_call_edge(&staging, "main", "helper1");
assert_has_call_edge(&staging, "main", "helper2");
assert_has_call_edge(&staging, "main", "helper3");
let call_edges = collect_call_edges(&staging);
assert_eq!(call_edges.len(), 3, "Expected exactly three call edges");
}
#[test]
fn test_builtin_function_calls() {
let source = r#"
const std = @import("std");
fn useBuiltins(dest: []u8, src: []const u8) void {
@memcpy(dest.ptr, src.ptr, src.len);
const info = @typeInfo(@TypeOf(dest));
}
"#;
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
assert_has_node_with_kind(&staging, "useBuiltins", NodeKind::Function);
assert_has_call_edge(&staging, "useBuiltins", "@memcpy");
assert_has_call_edge(&staging, "useBuiltins", "@typeInfo");
}
#[test]
fn test_struct_methods_with_same_name() {
let source = r"
const ArrayList = struct {
fn init() ArrayList {
return undefined;
}
fn deinit(self: *ArrayList) void {
// cleanup
}
fn append(self: *ArrayList, item: i32) void {
// add item
}
};
const HashMap = struct {
fn init() HashMap {
return undefined;
}
fn deinit(self: *HashMap) void {
// cleanup
}
};
fn main() void {
var list = ArrayList.init();
list.append(42);
list.deinit();
var map = HashMap.init();
map.deinit();
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind_exact(&staging, "ArrayList::init", NodeKind::Function);
assert_has_node_with_kind_exact(&staging, "ArrayList::deinit", NodeKind::Function);
assert_has_node_with_kind_exact(&staging, "ArrayList::append", NodeKind::Function);
assert_has_node_with_kind_exact(&staging, "HashMap::init", NodeKind::Function);
assert_has_node_with_kind_exact(&staging, "HashMap::deinit", NodeKind::Function);
assert_has_node_with_kind(&staging, "main", NodeKind::Function);
assert!(has_display_name(
&staging,
"ArrayList::init",
"ArrayList.init"
));
assert!(has_display_name(
&staging,
"ArrayList::deinit",
"ArrayList.deinit"
));
assert!(has_display_name(
&staging,
"ArrayList::append",
"ArrayList.append"
));
assert!(has_display_name(&staging, "HashMap::init", "HashMap.init"));
assert!(has_display_name(
&staging,
"HashMap::deinit",
"HashMap.deinit"
));
let func_count = count_nodes_by_kind(&staging, NodeKind::Function);
assert!(
func_count >= 6,
"Expected at least 6 functions (5 methods + main), got {func_count}"
);
}
#[test]
fn test_method_call_normalization() {
let source = r"
const ArrayList = struct {
fn init() ArrayList {
return undefined;
}
fn deinit(self: *ArrayList) void {
// cleanup
}
};
fn main() void {
var list = ArrayList.init();
list.deinit(); // This should resolve to ArrayList.deinit, not list.deinit
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind_exact(&staging, "ArrayList::init", NodeKind::Function);
assert_has_node_with_kind_exact(&staging, "ArrayList::deinit", NodeKind::Function);
assert!(has_display_name(
&staging,
"ArrayList::init",
"ArrayList.init"
));
assert!(has_display_name(
&staging,
"ArrayList::deinit",
"ArrayList.deinit"
));
assert_has_node_with_kind(&staging, "main", NodeKind::Function);
assert_has_call_edge(&staging, "main", "ArrayList::init");
assert_has_call_edge(&staging, "main", "list::deinit");
assert_has_display_call_edge(&staging, "main", "ArrayList.init");
assert_has_display_call_edge(&staging, "main", "list.deinit");
}
#[test]
fn test_language_is_zig() {
let source = r"
fn test_function() void {
return;
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "test_function", NodeKind::Function);
assert_eq!(builder.language(), Language::Zig);
}
#[test]
fn test_import_builtin_detection() {
let source = r#"
const std = @import("std");
const other = @import("other.zig");
fn main() void {
std.debug.print("Hello\n", .{});
}
"#;
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "std", NodeKind::Import);
assert_has_node_with_kind_exact(&staging, "other::zig", NodeKind::Import);
assert_has_import_edge(&staging, FILE_MODULE_NAME, "std");
assert_has_import_edge(&staging, FILE_MODULE_NAME, "other::zig");
assert!(has_display_name(&staging, "other::zig", "other.zig"));
assert_has_display_import_edge(&staging, FILE_MODULE_NAME, "other.zig");
assert_has_node_with_kind(&staging, "main", NodeKind::Function);
assert_has_call_edge(&staging, "main", "std::debug::print");
assert_has_display_call_edge(&staging, "main", "std.debug.print");
}
#[test]
fn test_import_in_function() {
let source = r#"
fn loadModule() void {
const module = @import("dynamic.zig");
module.init();
}
"#;
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind_exact(&staging, "dynamic::zig", NodeKind::Import);
assert_has_import_edge(&staging, "loadModule", "dynamic::zig");
assert!(has_display_name(&staging, "dynamic::zig", "dynamic.zig"));
assert_has_display_import_edge(&staging, "loadModule", "dynamic.zig");
assert_has_node_with_kind(&staging, "loadModule", NodeKind::Function);
assert_has_call_edge(&staging, "loadModule", "module::init");
assert_has_display_call_edge(&staging, "loadModule", "module.init");
}
#[test]
fn test_builtin_non_import_still_creates_call() {
let source = r"
fn copyMemory(dest: []u8, src: []const u8) void {
@memcpy(dest.ptr, src.ptr, src.len);
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "copyMemory", NodeKind::Function);
assert_has_call_edge(&staging, "copyMemory", "@memcpy");
let import_edges = collect_import_edges(&staging);
assert_eq!(
import_edges.len(),
0,
"Non-import builtins should not create import edges"
);
}
#[test]
fn test_export_pub_function() {
let source = r"
pub fn add(a: i32, b: i32) i32 {
return a + b;
}
fn privateHelper() i32 {
return 42;
}
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "add", NodeKind::Function);
assert_has_node_with_kind(&staging, "privateHelper", NodeKind::Function);
assert_has_export_edge(&staging, FILE_MODULE_NAME, "add");
let export_edges = collect_export_edges(&staging);
assert_eq!(export_edges.len(), 1, "Expected only one export edge");
}
#[test]
fn test_export_pub_const_type() {
let source = r#"
pub const Point = struct {
x: f32,
y: f32,
pub fn distance(self: Point) f32 {
return @sqrt(self.x * self.x + self.y * self.y);
}
};
const PrivateType = struct {
value: i32,
};
pub const API_VERSION = "1.0.0";
"#;
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_node_with_kind(&staging, "Point", NodeKind::Type);
assert_has_node_with_kind(&staging, "API_VERSION", NodeKind::Type);
assert_has_export_edge(&staging, FILE_MODULE_NAME, "Point");
assert_has_export_edge(&staging, FILE_MODULE_NAME, "API_VERSION");
let export_edges = collect_export_edges(&staging);
assert!(
export_edges.len() >= 2,
"Expected at least two export edges (Point and API_VERSION)"
);
}
#[test]
fn test_export_nested_pub_in_private_container() {
let source = r"
const PrivateContainer = struct {
pub fn publicMethod() i32 {
return 42;
}
pub const PUBLIC_CONST: i32 = 100;
};
pub const PublicContainer = struct {
fn privateMethod() i32 {
return 42;
}
};
";
let (tree, content) = parse_zig(source);
let mut staging = StagingGraph::new();
let builder = ZigGraphBuilder::default();
builder
.build_graph(&tree, &content, Path::new("test.zig"), &mut staging)
.unwrap();
assert_has_export_edge(&staging, FILE_MODULE_NAME, "PublicContainer");
let export_edges = collect_export_edges(&staging);
assert!(
!export_edges.is_empty(),
"Expected at least one export edge (PublicContainer)"
);
}
}