use crate::parser::get_parser_for_extension;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use walkdir::WalkDir;
lazy_static::lazy_static! {
static ref FUNC_CALL_PATTERN: regex::Regex =
regex::Regex::new(r"(\w+)\s*\(").unwrap();
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallGraph {
pub nodes: HashMap<String, CallNode>,
pub edges: Vec<CallEdge>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallNode {
pub function_name: String,
pub file_path: String,
pub line: usize,
pub is_recursive: bool,
pub call_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallEdge {
pub caller: String,
pub callee: String,
pub call_site_line: usize,
pub is_direct: bool,
}
impl CallGraph {
pub fn new() -> Self {
Self {
nodes: HashMap::new(),
edges: Vec::new(),
}
}
pub fn add_node(&mut self, node: CallNode) {
self.nodes.insert(node.function_name.clone(), node);
}
pub fn add_edge(
&mut self,
caller: String,
callee: String,
call_site_line: usize,
is_direct: bool,
) {
self.edges.push(CallEdge {
caller,
callee,
call_site_line,
is_direct,
});
}
pub fn has_edge(&self, caller: &str, callee: &str) -> bool {
self.edges
.iter()
.any(|e| e.caller == caller && e.callee == callee)
}
pub fn get_callers(&self, function: &str) -> Vec<String> {
self.edges
.iter()
.filter(|e| e.callee == function)
.map(|e| e.caller.clone())
.collect()
}
pub fn get_callees(&self, function: &str) -> Vec<String> {
self.edges
.iter()
.filter(|e| e.caller == function)
.map(|e| e.callee.clone())
.collect()
}
pub fn find_recursive_functions(&self) -> Vec<String> {
let mut recursive = Vec::new();
for (func_name, _) in &self.nodes {
if self.is_recursive(func_name) {
recursive.push(func_name.clone());
}
}
recursive
}
fn is_recursive(&self, function: &str) -> bool {
let mut visited = HashSet::new();
let mut stack = vec![function.to_string()];
while let Some(current) = stack.pop() {
if current == function && !visited.is_empty() {
return true;
}
if visited.insert(current.clone()) {
for callee in self.get_callees(¤t) {
stack.push(callee);
}
}
}
false
}
pub fn find_dead_functions(&self) -> Vec<String> {
let mut called_functions = HashSet::new();
for edge in &self.edges {
called_functions.insert(edge.callee.clone());
}
self.nodes
.keys()
.filter(|func| !called_functions.contains(*func) && *func != "main")
.cloned()
.collect()
}
pub fn calculate_call_depth(&self, function: &str) -> usize {
let mut max_depth = 0;
let mut visited = HashSet::new();
self.calculate_depth_recursive(function, 0, &mut visited, &mut max_depth);
max_depth
}
fn calculate_depth_recursive(
&self,
function: &str,
depth: usize,
visited: &mut HashSet<String>,
max_depth: &mut usize,
) {
if visited.contains(function) {
return;
}
visited.insert(function.to_string());
*max_depth = (*max_depth).max(depth);
for callee in self.get_callees(function) {
self.calculate_depth_recursive(&callee, depth + 1, visited, max_depth);
}
visited.remove(function);
}
pub fn find_call_chains(&self, from: &str, to: &str) -> Vec<Vec<String>> {
let mut chains = Vec::new();
let mut current_path = vec![from.to_string()];
let mut visited = HashSet::new();
self.find_chains_recursive(from, to, &mut current_path, &mut visited, &mut chains);
chains
}
fn find_chains_recursive(
&self,
current: &str,
target: &str,
path: &mut Vec<String>,
visited: &mut HashSet<String>,
chains: &mut Vec<Vec<String>>,
) {
if current == target {
chains.push(path.clone());
return;
}
if visited.contains(current) {
return;
}
visited.insert(current.to_string());
for callee in self.get_callees(current) {
path.push(callee.clone());
self.find_chains_recursive(&callee, target, path, visited, chains);
path.pop();
}
visited.remove(current);
}
pub fn to_svg(&self) -> String {
use crate::svg_export::*;
let mut engine = LayoutEngine::new();
let dead_funcs = self.find_dead_functions();
for (func_name, node) in &self.nodes {
let (fill, stroke) = if node.is_recursive {
("#4a1a1a", "#f87171")
} else if dead_funcs.contains(func_name) {
("#1e293b", "#475569")
} else {
("#1e3a5f", "#3b82f6")
};
let detail = format!(
"file: {}:{}{}",
node.file_path,
node.line,
if node.is_recursive { " | recursive" } else { "" }
);
engine.nodes.push(SvgNode {
id: func_name.clone(),
label: node.function_name.clone(),
x: 0.0,
y: 0.0,
width: NODE_WIDTH,
height: NODE_HEIGHT,
fill: fill.to_string(),
stroke: stroke.to_string(),
text_color: "#e2e8f0".to_string(),
detail: Some(detail),
});
}
for (i, edge) in self.edges.iter().enumerate() {
engine.edges.push(SvgEdge {
id: format!("e-{}", i),
source: edge.caller.clone(),
target: edge.callee.clone(),
label: if edge.is_direct { None } else { Some("indirect".to_string()) },
color: "#64748b".to_string(),
});
}
engine.layout();
engine.to_svg()
}
pub fn to_dot(&self) -> String {
let mut dot = String::from("digraph CallGraph {\n");
dot.push_str(" rankdir=LR;\n");
dot.push_str(" node [shape=box];\n\n");
for (func_name, node) in &self.nodes {
let color = if node.is_recursive {
"lightcoral"
} else if self.get_callers(func_name).is_empty() {
"lightgreen"
} else {
"lightblue"
};
dot.push_str(&format!(
" \"{}\" [label=\"{}\\n({}:{})\", fillcolor={}, style=filled];\n",
func_name, func_name, node.file_path, node.line, color
));
}
dot.push('\n');
for edge in &self.edges {
let style = if edge.is_direct {
""
} else {
" [style=dashed]"
};
dot.push_str(&format!(
" \"{}\" -> \"{}\"{};\n",
edge.caller, edge.callee, style
));
}
dot.push_str("}\n");
dot
}
}
pub fn build_call_graph(
path: &Path,
extensions: Option<&[String]>,
exclude: Option<&[String]>,
) -> Result<CallGraph, Box<dyn std::error::Error>> {
let mut graph = CallGraph::new();
let mut function_definitions: HashMap<String, (String, usize)> = HashMap::new();
let walker = WalkDir::new(path)
.into_iter()
.filter_entry(|e| {
if let Some(name) = e.file_name().to_str() {
if let Some(exclude_dirs) = exclude {
for exclude_dir in exclude_dirs {
if name == exclude_dir {
return false;
}
}
}
}
true
})
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file());
let files: Vec<_> = walker
.filter(|entry| {
let file_path = entry.path();
if let Some(exts) = extensions {
if let Some(ext) = file_path.extension().and_then(|s| s.to_str()) {
exts.iter().any(|e| e == ext)
} else {
false
}
} else {
true
}
})
.collect();
for entry in &files {
let file_path = entry.path();
let content = std::fs::read_to_string(file_path)?;
let ext = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
if let Some(parser) = get_parser_for_extension(ext) {
if let Ok(analysis) = parser.parse_content(&content) {
for func in &analysis.functions {
function_definitions.insert(
func.name.clone(),
(file_path.to_string_lossy().to_string(), func.line),
);
let node = CallNode {
function_name: func.name.clone(),
file_path: file_path.to_string_lossy().to_string(),
line: func.line,
is_recursive: false,
call_count: 0,
};
graph.add_node(node);
}
if !analysis.functions.is_empty() {
continue;
}
}
}
extract_functions_loose(&content, &file_path, ext, &mut function_definitions, &mut graph);
}
for entry in &files {
let file_path = entry.path();
let content = std::fs::read_to_string(file_path)?;
let ext = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
if let Some(parser) = get_parser_for_extension(ext) {
if let Ok(analysis) = parser.parse_content(&content) {
for func in &analysis.functions {
for (line_num, line) in content.lines().enumerate() {
if line_num + 1 >= func.line {
for cap in FUNC_CALL_PATTERN.captures_iter(line) {
if let Some(callee_match) = cap.get(1) {
let callee = callee_match.as_str().to_string();
if function_definitions.contains_key(&callee) {
graph.add_edge(
func.name.clone(),
callee,
line_num + 1,
true,
);
}
}
}
}
}
}
if !analysis.functions.is_empty() {
continue;
}
}
}
extract_calls_loose(&content, &file_path, ext, &function_definitions, &mut graph);
}
graph.edges.retain(|e| {
if e.caller == e.callee {
if let Some((_, def_line)) = function_definitions.get(&e.caller) {
return e.call_site_line != *def_line;
}
}
true
});
for func_name in graph.nodes.keys().cloned().collect::<Vec<_>>() {
if graph.is_recursive(&func_name) {
if let Some(node) = graph.nodes.get_mut(&func_name) {
node.is_recursive = true;
}
}
}
Ok(graph)
}
fn extract_functions_loose(
content: &str,
file_path: &Path,
ext: &str,
function_definitions: &mut HashMap<String, (String, usize)>,
graph: &mut CallGraph,
) {
let patterns = match ext {
"c" | "cpp" | "cxx" | "cc" | "h" | "hpp" => vec![
r"\b(?:\w+\s+)+?(\w+)\s*\([^)]*\)\s*\{",
],
"java" | "cs" => vec![
r"\b(?:public|private|protected|static|final|abstract|override|virtual|internal|async)?\s*(?:<[^>]+>\s*)?(?:\w+\s+)*(?:\w+)\s+(\w+)\s*\([^)]*\)\s*(?:\{|;)",
],
"kt" => vec![
r"\bfun\s+(\w+)\s*\(",
],
"rb" | "cr" => vec![
r"\bdef\s+(?:self\.)?(\w+)",
],
"php" => vec![
r"\bfunction\s+(\w+)",
],
"js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => vec![
r"\bfunction\s+(\w+)",
r"\b(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(",
r"\b(?:const|let|var)\s+(\w+)\s*=\s*\([^)]*\)\s*=>",
r"^\s+(?:public|private|protected|static|readonly|async)?\s*(\w+)\s*\([^)]*\)[^{]*\{",
],
"go" => vec![
r"\bfunc\s+(?:\([^)]*\)\s+)?(\w+)",
],
"swift" => vec![
r"\bfunc\s+(\w+)",
],
"lua" => vec![
r"\bfunction\s+(?:\w+[:\.])?(\w+)",
],
"sh" | "bash" | "zsh" => vec![
r"^(\w+)\s*\(\)\s*\{",
],
"ex" | "exs" => vec![
r"\bdef\s+(?:\w+\.)?(\w+)",
],
"hs" => vec![
r"^(\w+)\s*(?:::[^=]+)?\s*$",
r"^(\w+)\s*(?:\w+\s+)*=",
],
_ => vec![
r"(?:fn|def|function|func)\s+(\w+)",
],
};
for (line_num, line) in content.lines().enumerate() {
for pattern in &patterns {
if let Ok(re) = regex::Regex::new(pattern) {
if let Some(caps) = re.captures(line) {
if let Some(func_name) = caps.get(1) {
let name = func_name.as_str().to_string();
if is_likely_keyword(&name) {
continue;
}
function_definitions.insert(
name.clone(),
(file_path.to_string_lossy().to_string(), line_num + 1),
);
graph.add_node(CallNode {
function_name: name,
file_path: file_path.to_string_lossy().to_string(),
line: line_num + 1,
is_recursive: false,
call_count: 0,
});
}
}
}
}
}
}
fn extract_calls_loose(
content: &str,
_file_path: &Path,
ext: &str,
function_definitions: &HashMap<String, (String, usize)>,
graph: &mut CallGraph,
) {
let func_def_patterns = match ext {
"c" | "cpp" | "cxx" | "cc" | "h" | "hpp" | "java" | "kt" | "cs" | "swift" | "php" | "rb" | "cr" | "lua" | "sh" | "bash" | "zsh" | "ex" | "exs" => vec![
r"\b(?:fn|def|function|func|(?:\w+\s+)*\w+)\s+(\w+)\s*\([^)]*\)\s*\{",
r"\bdef\s+(?:self\.)?(\w+)",
r"\bfunction\s+(?:\w+[:\.])?(\w+)",
r"^(\w+)\s*\(\)\s*\{",
],
"go" => vec![
r"\bfunc\s+(?:\([^)]*\)\s+)?(\w+)",
],
"js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => vec![
r"\bfunction\s+(\w+)",
r"\b(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(",
r"\b(?:const|let|var)\s+(\w+)\s*=\s*\([^)]*\)\s*=>",
r"^\s+(?:public|private|protected|static|readonly|async)?\s*(\w+)\s*\([^)]*\)[^{]*\{",
],
_ => vec![r"(?:fn|def|function|func)\s+(\w+)"],
};
let call_re = regex::Regex::new(r"(\w+(?:\.\w+)*?)\s*\(").unwrap();
let shell_call_re = if matches!(ext, "sh" | "bash" | "zsh") {
Some(regex::Regex::new(r"^\s+(\w+)").unwrap())
} else {
None
};
let shell_builtins: &[&str] = &["echo", "printf", "test", "[", "[[", "cd", "pwd", "exit", "return", "source", ".", "trap", "shift", "unset", "export", "local", "readonly", "declare", "typeset"];
let mut scope_stack: Vec<String> = Vec::new();
let mut brace_depth = 0i32;
for (line_num, line) in content.lines().enumerate() {
let mut entered_func = None;
for pat in &func_def_patterns {
if let Ok(re) = regex::Regex::new(pat) {
if let Some(caps) = re.captures(line) {
if let Some(name) = caps.get(1) {
let n = name.as_str().to_string();
if !is_likely_keyword(&n) {
entered_func = Some(n);
}
}
}
}
}
if let Some(func_name) = entered_func {
if line.contains('{') {
scope_stack.push(func_name);
brace_depth = 1; } else if scope_stack.is_empty() {
scope_stack.push(func_name);
}
}
for ch in line.chars() {
match ch {
'{' => brace_depth += 1,
'}' => {
brace_depth -= 1;
if brace_depth <= 0 && !scope_stack.is_empty() {
scope_stack.pop();
brace_depth = 0;
}
}
_ => {}
}
}
if matches!(ext, "py" | "rb" | "cr" | "ex" | "exs") {
if line.trim().is_empty() && !scope_stack.is_empty() {
scope_stack.pop();
}
}
if let Some(caller) = scope_stack.last() {
for cap in call_re.captures_iter(line) {
if let Some(callee_match) = cap.get(1) {
let raw = callee_match.as_str();
let callee = raw.rsplit('.').next().unwrap_or(raw).to_string();
if function_definitions.contains_key(&callee) {
graph.add_edge(caller.clone(), callee, line_num + 1, true);
}
}
}
if let Some(ref sh_re) = shell_call_re {
if let Some(caps) = sh_re.captures(line) {
if let Some(name) = caps.get(1) {
let callee = name.as_str().to_string();
if !shell_builtins.contains(&callee.as_str())
&& function_definitions.contains_key(&callee)
{
graph.add_edge(caller.clone(), callee, line_num + 1, true);
}
}
}
}
}
}
}
fn is_likely_keyword(name: &str) -> bool {
let keywords: &[&str] = &[
"if", "else", "while", "for", "switch", "case", "return", "break", "continue",
"try", "catch", "finally", "with", "new", "delete", "typeof", "instanceof",
"void", "null", "true", "false", "this", "super", "class", "interface",
"struct", "enum", "union", "typedef", "namespace", "module", "package",
"data", "type", "newtype", "instance", "where", "let", "in", "of", "deriving",
"import", "export", "from", "as", "in", "of", "await", "yield", "throw",
"sizeof", "alignof", "offsetof", "static_assert", "decltype",
"public", "private", "protected", "internal", "static", "const", "final",
"abstract", "virtual", "override", "synchronized", "transient", "volatile",
"strictfp", "native", "default", "extends", "implements", "throws",
"where", "select", "from_keyword", "into", "group", "orderby", "join",
"let_keyword", "on", "equals", "by", "ascending", "descending",
];
keywords.contains(&name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_call_graph_creation() {
let graph = CallGraph::new();
assert_eq!(graph.nodes.len(), 0);
assert_eq!(graph.edges.len(), 0);
}
#[test]
fn test_add_node() {
let mut graph = CallGraph::new();
let node = CallNode {
function_name: "test".to_string(),
file_path: "test.rs".to_string(),
line: 1,
is_recursive: false,
call_count: 0,
};
graph.add_node(node);
assert_eq!(graph.nodes.len(), 1);
}
#[test]
fn test_get_callees() {
let mut graph = CallGraph::new();
graph.add_node(CallNode {
function_name: "main".to_string(),
file_path: "test.rs".to_string(),
line: 1,
is_recursive: false,
call_count: 0,
});
graph.add_node(CallNode {
function_name: "helper".to_string(),
file_path: "test.rs".to_string(),
line: 5,
is_recursive: false,
call_count: 0,
});
graph.add_edge("main".to_string(), "helper".to_string(), 2, true);
let callees = graph.get_callees("main");
assert_eq!(callees.len(), 1);
assert_eq!(callees[0], "helper");
}
#[test]
fn test_find_dead_functions() {
let mut graph = CallGraph::new();
graph.add_node(CallNode {
function_name: "main".to_string(),
file_path: "test.rs".to_string(),
line: 1,
is_recursive: false,
call_count: 0,
});
graph.add_node(CallNode {
function_name: "unused".to_string(),
file_path: "test.rs".to_string(),
line: 10,
is_recursive: false,
call_count: 0,
});
let dead = graph.find_dead_functions();
assert!(dead.contains(&"unused".to_string()));
}
#[test]
fn test_loose_c_function_detection() {
let code = r#"
int calculate(int a, int b) {
return add(a, b);
}
static void helper() {
printf("hello");
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(
code,
Path::new("test.c"),
"c",
&mut defs,
&mut graph,
);
assert!(graph.nodes.contains_key("calculate"));
assert!(graph.nodes.contains_key("helper"));
}
#[test]
fn test_loose_java_function_detection() {
let code = r#"
public class Foo {
public static void main(String[] args) {
helper();
}
private int helper() {
return 42;
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(
code,
Path::new("Test.java"),
"java",
&mut defs,
&mut graph,
);
assert!(graph.nodes.contains_key("main"));
assert!(graph.nodes.contains_key("helper"));
}
#[test]
fn test_loose_call_detection_with_scope() {
let code = r#"
fn main() {
helper();
}
fn helper() {
util();
}
fn util() {
println!("ok");
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("test.rs"), "rs", &defs, &mut graph);
assert!(graph.has_edge("main", "helper"));
assert!(graph.has_edge("helper", "util"));
}
#[test]
fn test_cpp_function_detection() {
let code = r#"
class Calculator {
public:
int add(int a, int b) {
return a + b;
}
};
void main() {
Calculator calc;
calc.add(1, 2);
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.cpp"), "cpp", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("add"));
assert!(graph.nodes.contains_key("main"));
}
#[test]
fn test_javascript_function_detection() {
let code = r#"
function greet(name) {
return `Hello, ${name}`;
}
const farewell = function(name) {
return `Goodbye, ${name}`;
};
class Person {
sayHi() {
greet(this.name);
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.js"), "js", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("greet"));
assert!(graph.nodes.contains_key("farewell"));
assert!(graph.nodes.contains_key("sayHi"));
}
#[test]
fn test_python_function_detection() {
let code = r#"
def calculate(x, y):
return x + y
def main():
result = calculate(1, 2)
print(result)
class Calculator:
def multiply(self, a, b):
return a * b
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.py"), "py", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("calculate"));
assert!(graph.nodes.contains_key("main"));
assert!(graph.nodes.contains_key("multiply"));
}
#[test]
fn test_go_function_detection() {
let code = r#"
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
fmt.Println(add(1, 2))
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("main.go"), "go", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("add"));
assert!(graph.nodes.contains_key("main"));
}
#[test]
fn test_ruby_function_detection() {
let code = r#"
def greet(name)
puts "Hello, #{name}"
end
class Person
def self.create
new
end
def say_hello
greet("world")
end
end
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.rb"), "rb", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("greet"));
assert!(graph.nodes.contains_key("create"));
assert!(graph.nodes.contains_key("say_hello"));
}
#[test]
fn test_php_function_detection() {
let code = r#"
<?php
function calculate($a, $b) {
return $a + $b;
}
class Calculator {
public function multiply($a, $b) {
return $a * $b;
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.php"), "php", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("calculate"));
assert!(graph.nodes.contains_key("multiply"));
}
#[test]
fn test_swift_function_detection() {
let code = r#"
func greet(name: String) -> String {
return "Hello, \(name)"
}
class Person {
func sayHi() {
greet(name: "world")
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.swift"), "swift", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("greet"));
assert!(graph.nodes.contains_key("sayHi"));
}
#[test]
fn test_lua_function_detection() {
let code = r#"
function add(a, b)
return a + b
end
local function subtract(a, b)
return a - b
end
obj = {}
function obj:multiply(a, b)
return a * b
end
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.lua"), "lua", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("add"));
assert!(graph.nodes.contains_key("subtract"));
assert!(graph.nodes.contains_key("multiply"));
}
#[test]
fn test_shell_function_detection() {
let code = r#"
#!/bin/bash
greet() {
echo "Hello, $1"
}
helper() {
greet "world"
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.sh"), "sh", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("greet"));
assert!(graph.nodes.contains_key("helper"));
}
#[test]
fn test_elixir_function_detection() {
let code = r#"
defmodule Math do
def add(a, b) do
a + b
end
def subtract(a, b) do
add(a, -b)
end
end
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("math.ex"), "ex", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("add"));
assert!(graph.nodes.contains_key("subtract"));
}
#[test]
fn test_recursive_function_detection() {
let mut graph = CallGraph::new();
graph.add_node(CallNode {
function_name: "factorial".to_string(),
file_path: "test.rs".to_string(),
line: 1,
is_recursive: false,
call_count: 0,
});
graph.add_edge("factorial".to_string(), "factorial".to_string(), 2, true);
assert!(graph.is_recursive("factorial"));
}
#[test]
fn test_method_chain_call_extraction() {
let code = r#"
fn process() {
let result = data.transform().filter();
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
defs.insert("transform".to_string(), ("lib.rs".to_string(), 1));
defs.insert("filter".to_string(), ("lib.rs".to_string(), 2));
extract_functions_loose(code, Path::new("test.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("test.rs"), "rs", &defs, &mut graph);
assert!(graph.has_edge("process", "transform"));
assert!(graph.has_edge("process", "filter"));
}
#[test]
fn test_cs_function_detection() {
let code = r#"
using System;
class Program {
static int Add(int a, int b) {
return a + b;
}
static void Main() {
Console.WriteLine(Add(1, 2));
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("Program.cs"), "cs", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("Add"));
assert!(graph.nodes.contains_key("Main"));
}
#[test]
fn test_kotlin_function_detection() {
let code = r#"
fun add(a: Int, b: Int): Int {
return a + b
}
class Calculator {
fun multiply(a: Int, b: Int): Int {
return a * b
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("Calculator.kt"), "kt", &mut defs, &mut graph);
assert!(graph.nodes.contains_key("add"));
assert!(graph.nodes.contains_key("multiply"));
}
#[test]
fn test_nested_function_calls() {
let code = r#"
fn outer() {
inner();
}
fn inner() {
deep();
}
fn deep() {
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("test.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("test.rs"), "rs", &defs, &mut graph);
assert!(graph.has_edge("outer", "inner"));
assert!(graph.has_edge("inner", "deep"));
}
#[test]
fn test_typescript_class_method_detection() {
let code = r#"
class Greeter {
greet(name: string): string {
return `Hello, ${name}`;
}
private farewell(): void {
this.greet("all");
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("greeter.ts"), "ts", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("greeter.ts"), "ts", &defs, &mut graph);
assert!(graph.nodes.contains_key("greet"), "should detect TS method greet");
assert!(graph.nodes.contains_key("farewell"), "should detect TS method farewell");
assert!(graph.has_edge("farewell", "greet"), "farewell should call greet");
}
#[test]
fn test_rust_loose_function_detection() {
let code = r#"
pub fn calculate(x: i32, y: i32) -> i32 {
add(x, y)
}
fn add(a: i32, b: i32) -> i32 {
a + b
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("math.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("math.rs"), "rs", &defs, &mut graph);
assert!(graph.nodes.contains_key("calculate"), "should detect Rust fn calculate");
assert!(graph.nodes.contains_key("add"), "should detect Rust fn add");
assert!(graph.has_edge("calculate", "add"), "calculate should call add");
}
#[test]
fn test_go_receiver_method_detection() {
let code = r#"
package main
func (r *Rect) Area() int {
return r.width * r.height
}
func main() {
r := &Rect{}
r.Area()
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("shapes.go"), "go", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("shapes.go"), "go", &defs, &mut graph);
assert!(graph.nodes.contains_key("Area"), "should detect Go receiver method Area");
assert!(graph.nodes.contains_key("main"), "should detect Go main");
}
#[test]
fn test_php_class_method_detection() {
let code = r#"
<?php
class Calculator {
public function add($a, $b) {
return $a + $b;
}
private function helper() {
$this->add(1, 2);
}
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("calc.php"), "php", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("calc.php"), "php", &defs, &mut graph);
assert!(graph.nodes.contains_key("add"), "should detect PHP method add");
assert!(graph.nodes.contains_key("helper"), "should detect PHP method helper");
assert!(graph.has_edge("helper", "add"), "helper should call add");
}
#[test]
fn test_empty_code_no_panic() {
let code = "";
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("empty.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("empty.rs"), "rs", &defs, &mut graph);
assert_eq!(graph.nodes.len(), 0);
assert_eq!(graph.edges.len(), 0);
}
#[test]
fn test_malformed_code_graceful() {
let code = r#"
fn broken( {
{{{
call_me(
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("broken.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("broken.rs"), "rs", &defs, &mut graph);
}
#[test]
fn test_deeply_nested_brace_scope() {
let code = r#"
fn outer() {
if true {
if true {
if true {
helper();
}
}
}
}
fn helper() {}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("nested.rs"), "rs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("nested.rs"), "rs", &defs, &mut graph);
assert!(graph.has_edge("outer", "helper"), "should detect call in deep nesting");
}
#[test]
fn test_haskell_function_detection() {
let code = r#"
add :: Int -> Int -> Int
add a b = a + b
main = do
print (add 1 2)
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("math.hs"), "hs", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("math.hs"), "hs", &defs, &mut graph);
assert!(graph.nodes.contains_key("add"), "should detect Haskell function add");
}
#[test]
fn test_zig_function_detection() {
let code = r#"
const std = @import("std");
fn add(a: i32, b: i32) i32 {
return a + b;
}
pub fn main() void {
const result = add(1, 2);
}
"#;
let mut graph = CallGraph::new();
let mut defs = HashMap::new();
extract_functions_loose(code, Path::new("math.zig"), "zig", &mut defs, &mut graph);
extract_calls_loose(code, Path::new("math.zig"), "zig", &defs, &mut graph);
assert!(graph.nodes.contains_key("add"), "should detect Zig fn add");
assert!(graph.nodes.contains_key("main"), "should detect Zig fn main");
assert!(graph.has_edge("main", "add"), "main should call add");
}
}