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 Default for CallGraph {
fn default() -> Self {
Self::new()
}
}
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.keys() {
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()
&& 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)
&& 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)
&& 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
&& 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)
&& 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)
&& let Some(caps) = re.captures(line)
&& 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)
&& let Some(caps) = re.captures(line)
&& 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")
&& 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
&& let Some(caps) = sh_re.captures(line)
&& 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");
}
}