1use crate::parser::get_parser_for_extension;
6use serde::{Deserialize, Serialize};
7use std::collections::{HashMap, HashSet};
8use std::path::Path;
9use walkdir::WalkDir;
10
11lazy_static::lazy_static! {
12 static ref FUNC_CALL_PATTERN: regex::Regex =
13 regex::Regex::new(r"(\w+)\s*\(").unwrap();
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct CallGraph {
18 pub nodes: HashMap<String, CallNode>,
19 pub edges: Vec<CallEdge>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct CallNode {
24 pub function_name: String,
25 pub file_path: String,
26 pub line: usize,
27 pub is_recursive: bool,
28 pub call_count: usize,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct CallEdge {
33 pub caller: String,
34 pub callee: String,
35 pub call_site_line: usize,
36 pub is_direct: bool,
37}
38
39impl Default for CallGraph {
40 fn default() -> Self {
41 Self::new()
42 }
43}
44
45impl CallGraph {
46 pub fn new() -> Self {
47 Self {
48 nodes: HashMap::new(),
49 edges: Vec::new(),
50 }
51 }
52
53 pub fn add_node(&mut self, node: CallNode) {
54 self.nodes.insert(node.function_name.clone(), node);
55 }
56
57 pub fn add_edge(
58 &mut self,
59 caller: String,
60 callee: String,
61 call_site_line: usize,
62 is_direct: bool,
63 ) {
64 self.edges.push(CallEdge {
65 caller,
66 callee,
67 call_site_line,
68 is_direct,
69 });
70 }
71
72 pub fn has_edge(&self, caller: &str, callee: &str) -> bool {
73 self.edges
74 .iter()
75 .any(|e| e.caller == caller && e.callee == callee)
76 }
77
78 pub fn get_callers(&self, function: &str) -> Vec<String> {
79 self.edges
80 .iter()
81 .filter(|e| e.callee == function)
82 .map(|e| e.caller.clone())
83 .collect()
84 }
85
86 pub fn get_callees(&self, function: &str) -> Vec<String> {
87 self.edges
88 .iter()
89 .filter(|e| e.caller == function)
90 .map(|e| e.callee.clone())
91 .collect()
92 }
93
94 pub fn find_recursive_functions(&self) -> Vec<String> {
95 let mut recursive = Vec::new();
96
97 for func_name in self.nodes.keys() {
98 if self.is_recursive(func_name) {
99 recursive.push(func_name.clone());
100 }
101 }
102
103 recursive
104 }
105
106 fn is_recursive(&self, function: &str) -> bool {
107 let mut visited = HashSet::new();
108 let mut stack = vec![function.to_string()];
109
110 while let Some(current) = stack.pop() {
111 if current == function && !visited.is_empty() {
112 return true;
113 }
114
115 if visited.insert(current.clone()) {
116 for callee in self.get_callees(¤t) {
117 stack.push(callee);
118 }
119 }
120 }
121
122 false
123 }
124
125 pub fn find_dead_functions(&self) -> Vec<String> {
126 let mut called_functions = HashSet::new();
127
128 for edge in &self.edges {
129 called_functions.insert(edge.callee.clone());
130 }
131
132 self.nodes
133 .keys()
134 .filter(|func| !called_functions.contains(*func) && *func != "main")
135 .cloned()
136 .collect()
137 }
138
139 pub fn calculate_call_depth(&self, function: &str) -> usize {
140 let mut max_depth = 0;
141 let mut visited = HashSet::new();
142 self.calculate_depth_recursive(function, 0, &mut visited, &mut max_depth);
143 max_depth
144 }
145
146 fn calculate_depth_recursive(
147 &self,
148 function: &str,
149 depth: usize,
150 visited: &mut HashSet<String>,
151 max_depth: &mut usize,
152 ) {
153 if visited.contains(function) {
154 return;
155 }
156
157 visited.insert(function.to_string());
158 *max_depth = (*max_depth).max(depth);
159
160 for callee in self.get_callees(function) {
161 self.calculate_depth_recursive(&callee, depth + 1, visited, max_depth);
162 }
163
164 visited.remove(function);
165 }
166
167 pub fn find_call_chains(&self, from: &str, to: &str) -> Vec<Vec<String>> {
168 let mut chains = Vec::new();
169 let mut current_path = vec![from.to_string()];
170 let mut visited = HashSet::new();
171
172 self.find_chains_recursive(from, to, &mut current_path, &mut visited, &mut chains);
173
174 chains
175 }
176
177 fn find_chains_recursive(
178 &self,
179 current: &str,
180 target: &str,
181 path: &mut Vec<String>,
182 visited: &mut HashSet<String>,
183 chains: &mut Vec<Vec<String>>,
184 ) {
185 if current == target {
186 chains.push(path.clone());
187 return;
188 }
189
190 if visited.contains(current) {
191 return;
192 }
193
194 visited.insert(current.to_string());
195
196 for callee in self.get_callees(current) {
197 path.push(callee.clone());
198 self.find_chains_recursive(&callee, target, path, visited, chains);
199 path.pop();
200 }
201
202 visited.remove(current);
203 }
204
205 pub fn to_svg(&self) -> String {
206 use crate::svg_export::*;
207 let mut engine = LayoutEngine::new();
208
209 let dead_funcs = self.find_dead_functions();
210
211 for (func_name, node) in &self.nodes {
212 let (fill, stroke) = if node.is_recursive {
213 ("#4a1a1a", "#f87171")
214 } else if dead_funcs.contains(func_name) {
215 ("#1e293b", "#475569")
216 } else {
217 ("#1e3a5f", "#3b82f6")
218 };
219
220 let detail = format!(
221 "file: {}:{}{}",
222 node.file_path,
223 node.line,
224 if node.is_recursive {
225 " | recursive"
226 } else {
227 ""
228 }
229 );
230
231 engine.nodes.push(SvgNode {
232 id: func_name.clone(),
233 label: node.function_name.clone(),
234 x: 0.0,
235 y: 0.0,
236 width: NODE_WIDTH,
237 height: NODE_HEIGHT,
238 fill: fill.to_string(),
239 stroke: stroke.to_string(),
240 text_color: "#e2e8f0".to_string(),
241 detail: Some(detail),
242 });
243 }
244
245 for (i, edge) in self.edges.iter().enumerate() {
246 engine.edges.push(SvgEdge {
247 id: format!("e-{}", i),
248 source: edge.caller.clone(),
249 target: edge.callee.clone(),
250 label: if edge.is_direct {
251 None
252 } else {
253 Some("indirect".to_string())
254 },
255 color: "#64748b".to_string(),
256 });
257 }
258
259 engine.layout();
260 engine.to_svg()
261 }
262
263 pub fn to_dot(&self) -> String {
264 let mut dot = String::from("digraph CallGraph {\n");
265 dot.push_str(" rankdir=LR;\n");
266 dot.push_str(" node [shape=box];\n\n");
267
268 for (func_name, node) in &self.nodes {
269 let color = if node.is_recursive {
270 "lightcoral"
271 } else if self.get_callers(func_name).is_empty() {
272 "lightgreen"
273 } else {
274 "lightblue"
275 };
276
277 dot.push_str(&format!(
278 " \"{}\" [label=\"{}\\n({}:{})\", fillcolor={}, style=filled];\n",
279 func_name, func_name, node.file_path, node.line, color
280 ));
281 }
282
283 dot.push('\n');
284
285 for edge in &self.edges {
286 let style = if edge.is_direct {
287 ""
288 } else {
289 " [style=dashed]"
290 };
291 dot.push_str(&format!(
292 " \"{}\" -> \"{}\"{};\n",
293 edge.caller, edge.callee, style
294 ));
295 }
296
297 dot.push_str("}\n");
298 dot
299 }
300}
301
302pub fn build_call_graph(
303 path: &Path,
304 extensions: Option<&[String]>,
305 exclude: Option<&[String]>,
306) -> Result<CallGraph, Box<dyn std::error::Error>> {
307 let mut graph = CallGraph::new();
308 let mut function_definitions: HashMap<String, (String, usize)> = HashMap::new();
309
310 let walker = WalkDir::new(path)
311 .into_iter()
312 .filter_entry(|e| {
313 if let Some(name) = e.file_name().to_str()
314 && let Some(exclude_dirs) = exclude
315 {
316 for exclude_dir in exclude_dirs {
317 if name == exclude_dir {
318 return false;
319 }
320 }
321 }
322 true
323 })
324 .filter_map(|e| e.ok())
325 .filter(|e| e.file_type().is_file());
326
327 let files: Vec<_> = walker
328 .filter(|entry| {
329 let file_path = entry.path();
330 if let Some(exts) = extensions {
331 if let Some(ext) = file_path.extension().and_then(|s| s.to_str()) {
332 exts.iter().any(|e| e == ext)
333 } else {
334 false
335 }
336 } else {
337 true
338 }
339 })
340 .collect();
341
342 for entry in &files {
343 let file_path = entry.path();
344 let content = std::fs::read_to_string(file_path)?;
345
346 let ext = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
347
348 if let Some(parser) = get_parser_for_extension(ext)
350 && let Ok(analysis) = parser.parse_content(&content)
351 {
352 for func in &analysis.functions {
353 function_definitions.insert(
354 func.name.clone(),
355 (file_path.to_string_lossy().to_string(), func.line),
356 );
357
358 let node = CallNode {
359 function_name: func.name.clone(),
360 file_path: file_path.to_string_lossy().to_string(),
361 line: func.line,
362 is_recursive: false,
363 call_count: 0,
364 };
365 graph.add_node(node);
366 }
367 if !analysis.functions.is_empty() {
368 continue;
369 }
370 }
371
372 extract_functions_loose(
374 &content,
375 file_path,
376 ext,
377 &mut function_definitions,
378 &mut graph,
379 );
380 }
381
382 for entry in &files {
384 let file_path = entry.path();
385 let content = std::fs::read_to_string(file_path)?;
386 let ext = file_path.extension().and_then(|s| s.to_str()).unwrap_or("");
387
388 if let Some(parser) = get_parser_for_extension(ext)
390 && let Ok(analysis) = parser.parse_content(&content)
391 {
392 for func in &analysis.functions {
393 for (line_num, line) in content.lines().enumerate() {
394 if line_num + 1 >= func.line {
395 for cap in FUNC_CALL_PATTERN.captures_iter(line) {
396 if let Some(callee_match) = cap.get(1) {
397 let callee = callee_match.as_str().to_string();
398
399 if function_definitions.contains_key(&callee) {
400 graph.add_edge(func.name.clone(), callee, line_num + 1, true);
401 }
402 }
403 }
404 }
405 }
406 }
407 if !analysis.functions.is_empty() {
408 continue;
409 }
410 }
411
412 extract_calls_loose(&content, file_path, ext, &function_definitions, &mut graph);
414 }
415
416 graph.edges.retain(|e| {
418 if e.caller == e.callee
419 && let Some((_, def_line)) = function_definitions.get(&e.caller)
420 {
421 return e.call_site_line != *def_line;
422 }
423 true
424 });
425
426 for func_name in graph.nodes.keys().cloned().collect::<Vec<_>>() {
427 if graph.is_recursive(&func_name)
428 && let Some(node) = graph.nodes.get_mut(&func_name)
429 {
430 node.is_recursive = true;
431 }
432 }
433
434 Ok(graph)
435}
436
437fn extract_functions_loose(
440 content: &str,
441 file_path: &Path,
442 ext: &str,
443 function_definitions: &mut HashMap<String, (String, usize)>,
444 graph: &mut CallGraph,
445) {
446 let patterns = match ext {
447 "c" | "cpp" | "cxx" | "cc" | "h" | "hpp" => vec![
449 r"\b(?:\w+\s+)+?(\w+)\s*\([^)]*\)\s*\{",
451 ],
452 "java" | "cs" => vec![
454 r"\b(?:public|private|protected|static|final|abstract|override|virtual|internal|async)?\s*(?:<[^>]+>\s*)?(?:\w+\s+)*(?:\w+)\s+(\w+)\s*\([^)]*\)\s*(?:\{|;)",
455 ],
456 "kt" => vec![r"\bfun\s+(\w+)\s*\("],
457 "rb" | "cr" => vec![r"\bdef\s+(?:self\.)?(\w+)"],
459 "php" => vec![r"\bfunction\s+(\w+)"],
461 "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => vec![
463 r"\bfunction\s+(\w+)",
464 r"\b(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(",
465 r"\b(?:const|let|var)\s+(\w+)\s*=\s*\([^)]*\)\s*=>",
466 r"^\s+(?:public|private|protected|static|readonly|async)?\s*(\w+)\s*\([^)]*\)[^{]*\{",
468 ],
469 "go" => vec![r"\bfunc\s+(?:\([^)]*\)\s+)?(\w+)"],
471 "swift" => vec![r"\bfunc\s+(\w+)"],
473 "lua" => vec![r"\bfunction\s+(?:\w+[:\.])?(\w+)"],
475 "sh" | "bash" | "zsh" => vec![r"^(\w+)\s*\(\)\s*\{"],
477 "ex" | "exs" => vec![r"\bdef\s+(?:\w+\.)?(\w+)"],
479 "hs" => vec![r"^(\w+)\s*(?:::[^=]+)?\s*$", r"^(\w+)\s*(?:\w+\s+)*="],
481 _ => vec![r"(?:fn|def|function|func)\s+(\w+)"],
483 };
484
485 for (line_num, line) in content.lines().enumerate() {
486 for pattern in &patterns {
487 if let Ok(re) = regex::Regex::new(pattern)
488 && let Some(caps) = re.captures(line)
489 && let Some(func_name) = caps.get(1)
490 {
491 let name = func_name.as_str().to_string();
492 if is_likely_keyword(&name) {
493 continue;
494 }
495 function_definitions.insert(
496 name.clone(),
497 (file_path.to_string_lossy().to_string(), line_num + 1),
498 );
499 graph.add_node(CallNode {
500 function_name: name,
501 file_path: file_path.to_string_lossy().to_string(),
502 line: line_num + 1,
503 is_recursive: false,
504 call_count: 0,
505 });
506 }
507 }
508 }
509}
510
511fn extract_calls_loose(
513 content: &str,
514 _file_path: &Path,
515 ext: &str,
516 function_definitions: &HashMap<String, (String, usize)>,
517 graph: &mut CallGraph,
518) {
519 let func_def_patterns = match ext {
520 "c" | "cpp" | "cxx" | "cc" | "h" | "hpp" | "java" | "kt" | "cs" | "swift" | "php"
521 | "rb" | "cr" | "lua" | "sh" | "bash" | "zsh" | "ex" | "exs" => vec![
522 r"\b(?:fn|def|function|func|(?:\w+\s+)*\w+)\s+(\w+)\s*\([^)]*\)\s*\{",
523 r"\bdef\s+(?:self\.)?(\w+)",
524 r"\bfunction\s+(?:\w+[:\.])?(\w+)",
525 r"^(\w+)\s*\(\)\s*\{",
526 ],
527 "go" => vec![r"\bfunc\s+(?:\([^)]*\)\s+)?(\w+)"],
528 "js" | "jsx" | "ts" | "tsx" | "mjs" | "cjs" => vec![
529 r"\bfunction\s+(\w+)",
530 r"\b(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(",
531 r"\b(?:const|let|var)\s+(\w+)\s*=\s*\([^)]*\)\s*=>",
532 r"^\s+(?:public|private|protected|static|readonly|async)?\s*(\w+)\s*\([^)]*\)[^{]*\{",
533 ],
534 _ => vec![r"(?:fn|def|function|func)\s+(\w+)"],
535 };
536
537 let call_re = regex::Regex::new(r"(\w+(?:\.\w+)*?)\s*\(").unwrap();
538
539 let shell_call_re = if matches!(ext, "sh" | "bash" | "zsh") {
541 Some(regex::Regex::new(r"^\s+(\w+)").unwrap())
542 } else {
543 None
544 };
545 let shell_builtins: &[&str] = &[
546 "echo", "printf", "test", "[", "[[", "cd", "pwd", "exit", "return", "source", ".", "trap",
547 "shift", "unset", "export", "local", "readonly", "declare", "typeset",
548 ];
549
550 let mut scope_stack: Vec<String> = Vec::new();
551 let mut brace_depth = 0i32;
552
553 for (line_num, line) in content.lines().enumerate() {
554 let mut entered_func = None;
556 for pat in &func_def_patterns {
557 if let Ok(re) = regex::Regex::new(pat)
558 && let Some(caps) = re.captures(line)
559 && let Some(name) = caps.get(1)
560 {
561 let n = name.as_str().to_string();
562 if !is_likely_keyword(&n) {
563 entered_func = Some(n);
564 }
565 }
566 }
567
568 if let Some(func_name) = entered_func {
569 if line.contains('{') {
571 scope_stack.push(func_name);
572 brace_depth = 1; } else if scope_stack.is_empty() {
574 scope_stack.push(func_name);
576 }
577 }
578
579 for ch in line.chars() {
581 match ch {
582 '{' => brace_depth += 1,
583 '}' => {
584 brace_depth -= 1;
585 if brace_depth <= 0 && !scope_stack.is_empty() {
586 scope_stack.pop();
587 brace_depth = 0;
588 }
589 }
590 _ => {}
591 }
592 }
593
594 if matches!(ext, "py" | "rb" | "cr" | "ex" | "exs")
596 && line.trim().is_empty()
597 && !scope_stack.is_empty()
598 {
599 scope_stack.pop();
600 }
601
602 if let Some(caller) = scope_stack.last() {
604 for cap in call_re.captures_iter(line) {
605 if let Some(callee_match) = cap.get(1) {
606 let raw = callee_match.as_str();
607 let callee = raw.rsplit('.').next().unwrap_or(raw).to_string();
609
610 if function_definitions.contains_key(&callee) {
611 graph.add_edge(caller.clone(), callee, line_num + 1, true);
612 }
613 }
614 }
615
616 if let Some(ref sh_re) = shell_call_re
618 && let Some(caps) = sh_re.captures(line)
619 && let Some(name) = caps.get(1)
620 {
621 let callee = name.as_str().to_string();
622 if !shell_builtins.contains(&callee.as_str())
623 && function_definitions.contains_key(&callee)
624 {
625 graph.add_edge(caller.clone(), callee, line_num + 1, true);
626 }
627 }
628 }
629 }
630}
631
632fn is_likely_keyword(name: &str) -> bool {
633 let keywords: &[&str] = &[
634 "if",
635 "else",
636 "while",
637 "for",
638 "switch",
639 "case",
640 "return",
641 "break",
642 "continue",
643 "try",
644 "catch",
645 "finally",
646 "with",
647 "new",
648 "delete",
649 "typeof",
650 "instanceof",
651 "void",
652 "null",
653 "true",
654 "false",
655 "this",
656 "super",
657 "class",
658 "interface",
659 "struct",
660 "enum",
661 "union",
662 "typedef",
663 "namespace",
664 "module",
665 "package",
666 "data",
667 "type",
668 "newtype",
669 "instance",
670 "where",
671 "let",
672 "in",
673 "of",
674 "deriving",
675 "import",
676 "export",
677 "from",
678 "as",
679 "in",
680 "of",
681 "await",
682 "yield",
683 "throw",
684 "sizeof",
685 "alignof",
686 "offsetof",
687 "static_assert",
688 "decltype",
689 "public",
690 "private",
691 "protected",
692 "internal",
693 "static",
694 "const",
695 "final",
696 "abstract",
697 "virtual",
698 "override",
699 "synchronized",
700 "transient",
701 "volatile",
702 "strictfp",
703 "native",
704 "default",
705 "extends",
706 "implements",
707 "throws",
708 "where",
709 "select",
710 "from_keyword",
711 "into",
712 "group",
713 "orderby",
714 "join",
715 "let_keyword",
716 "on",
717 "equals",
718 "by",
719 "ascending",
720 "descending",
721 ];
722 keywords.contains(&name)
723}
724
725#[cfg(test)]
726mod tests {
727 use super::*;
728
729 #[test]
730 fn test_call_graph_creation() {
731 let graph = CallGraph::new();
732 assert_eq!(graph.nodes.len(), 0);
733 assert_eq!(graph.edges.len(), 0);
734 }
735
736 #[test]
737 fn test_add_node() {
738 let mut graph = CallGraph::new();
739 let node = CallNode {
740 function_name: "test".to_string(),
741 file_path: "test.rs".to_string(),
742 line: 1,
743 is_recursive: false,
744 call_count: 0,
745 };
746 graph.add_node(node);
747 assert_eq!(graph.nodes.len(), 1);
748 }
749
750 #[test]
751 fn test_get_callees() {
752 let mut graph = CallGraph::new();
753
754 graph.add_node(CallNode {
755 function_name: "main".to_string(),
756 file_path: "test.rs".to_string(),
757 line: 1,
758 is_recursive: false,
759 call_count: 0,
760 });
761
762 graph.add_node(CallNode {
763 function_name: "helper".to_string(),
764 file_path: "test.rs".to_string(),
765 line: 5,
766 is_recursive: false,
767 call_count: 0,
768 });
769
770 graph.add_edge("main".to_string(), "helper".to_string(), 2, true);
771
772 let callees = graph.get_callees("main");
773 assert_eq!(callees.len(), 1);
774 assert_eq!(callees[0], "helper");
775 }
776
777 #[test]
778 fn test_find_dead_functions() {
779 let mut graph = CallGraph::new();
780
781 graph.add_node(CallNode {
782 function_name: "main".to_string(),
783 file_path: "test.rs".to_string(),
784 line: 1,
785 is_recursive: false,
786 call_count: 0,
787 });
788
789 graph.add_node(CallNode {
790 function_name: "unused".to_string(),
791 file_path: "test.rs".to_string(),
792 line: 10,
793 is_recursive: false,
794 call_count: 0,
795 });
796
797 let dead = graph.find_dead_functions();
798 assert!(dead.contains(&"unused".to_string()));
799 }
800
801 #[test]
802 fn test_loose_c_function_detection() {
803 let code = r#"
804int calculate(int a, int b) {
805 return add(a, b);
806}
807
808static void helper() {
809 printf("hello");
810}
811"#;
812 let mut graph = CallGraph::new();
813 let mut defs = HashMap::new();
814 extract_functions_loose(code, Path::new("test.c"), "c", &mut defs, &mut graph);
815 assert!(graph.nodes.contains_key("calculate"));
816 assert!(graph.nodes.contains_key("helper"));
817 }
818
819 #[test]
820 fn test_loose_java_function_detection() {
821 let code = r#"
822public class Foo {
823 public static void main(String[] args) {
824 helper();
825 }
826
827 private int helper() {
828 return 42;
829 }
830}
831"#;
832 let mut graph = CallGraph::new();
833 let mut defs = HashMap::new();
834 extract_functions_loose(code, Path::new("Test.java"), "java", &mut defs, &mut graph);
835 assert!(graph.nodes.contains_key("main"));
836 assert!(graph.nodes.contains_key("helper"));
837 }
838
839 #[test]
840 fn test_loose_call_detection_with_scope() {
841 let code = r#"
842fn main() {
843 helper();
844}
845
846fn helper() {
847 util();
848}
849
850fn util() {
851 println!("ok");
852}
853"#;
854 let mut graph = CallGraph::new();
855 let mut defs = HashMap::new();
856 extract_functions_loose(code, Path::new("test.rs"), "rs", &mut defs, &mut graph);
857 extract_calls_loose(code, Path::new("test.rs"), "rs", &defs, &mut graph);
858 assert!(graph.has_edge("main", "helper"));
859 assert!(graph.has_edge("helper", "util"));
860 }
861
862 #[test]
863 fn test_cpp_function_detection() {
864 let code = r#"
865class Calculator {
866public:
867 int add(int a, int b) {
868 return a + b;
869 }
870};
871
872void main() {
873 Calculator calc;
874 calc.add(1, 2);
875}
876"#;
877 let mut graph = CallGraph::new();
878 let mut defs = HashMap::new();
879 extract_functions_loose(code, Path::new("test.cpp"), "cpp", &mut defs, &mut graph);
880 assert!(graph.nodes.contains_key("add"));
881 assert!(graph.nodes.contains_key("main"));
882 }
883
884 #[test]
885 fn test_javascript_function_detection() {
886 let code = r#"
887function greet(name) {
888 return `Hello, ${name}`;
889}
890
891const farewell = function(name) {
892 return `Goodbye, ${name}`;
893};
894
895class Person {
896 sayHi() {
897 greet(this.name);
898 }
899}
900"#;
901 let mut graph = CallGraph::new();
902 let mut defs = HashMap::new();
903 extract_functions_loose(code, Path::new("test.js"), "js", &mut defs, &mut graph);
904 assert!(graph.nodes.contains_key("greet"));
905 assert!(graph.nodes.contains_key("farewell"));
906 assert!(graph.nodes.contains_key("sayHi"));
907 }
908
909 #[test]
910 fn test_python_function_detection() {
911 let code = r#"
912def calculate(x, y):
913 return x + y
914
915def main():
916 result = calculate(1, 2)
917 print(result)
918
919class Calculator:
920 def multiply(self, a, b):
921 return a * b
922"#;
923 let mut graph = CallGraph::new();
924 let mut defs = HashMap::new();
925 extract_functions_loose(code, Path::new("test.py"), "py", &mut defs, &mut graph);
926 assert!(graph.nodes.contains_key("calculate"));
927 assert!(graph.nodes.contains_key("main"));
928 assert!(graph.nodes.contains_key("multiply"));
929 }
930
931 #[test]
932 fn test_go_function_detection() {
933 let code = r#"
934package main
935
936import "fmt"
937
938func add(a, b int) int {
939 return a + b
940}
941
942func main() {
943 fmt.Println(add(1, 2))
944}
945"#;
946 let mut graph = CallGraph::new();
947 let mut defs = HashMap::new();
948 extract_functions_loose(code, Path::new("main.go"), "go", &mut defs, &mut graph);
949 assert!(graph.nodes.contains_key("add"));
950 assert!(graph.nodes.contains_key("main"));
951 }
952
953 #[test]
954 fn test_ruby_function_detection() {
955 let code = r#"
956def greet(name)
957 puts "Hello, #{name}"
958end
959
960class Person
961 def self.create
962 new
963 end
964
965 def say_hello
966 greet("world")
967 end
968end
969"#;
970 let mut graph = CallGraph::new();
971 let mut defs = HashMap::new();
972 extract_functions_loose(code, Path::new("test.rb"), "rb", &mut defs, &mut graph);
973 assert!(graph.nodes.contains_key("greet"));
974 assert!(graph.nodes.contains_key("create"));
975 assert!(graph.nodes.contains_key("say_hello"));
976 }
977
978 #[test]
979 fn test_php_function_detection() {
980 let code = r#"
981<?php
982function calculate($a, $b) {
983 return $a + $b;
984}
985
986class Calculator {
987 public function multiply($a, $b) {
988 return $a * $b;
989 }
990}
991"#;
992 let mut graph = CallGraph::new();
993 let mut defs = HashMap::new();
994 extract_functions_loose(code, Path::new("test.php"), "php", &mut defs, &mut graph);
995 assert!(graph.nodes.contains_key("calculate"));
996 assert!(graph.nodes.contains_key("multiply"));
997 }
998
999 #[test]
1000 fn test_swift_function_detection() {
1001 let code = r#"
1002func greet(name: String) -> String {
1003 return "Hello, \(name)"
1004}
1005
1006class Person {
1007 func sayHi() {
1008 greet(name: "world")
1009 }
1010}
1011"#;
1012 let mut graph = CallGraph::new();
1013 let mut defs = HashMap::new();
1014 extract_functions_loose(
1015 code,
1016 Path::new("test.swift"),
1017 "swift",
1018 &mut defs,
1019 &mut graph,
1020 );
1021 assert!(graph.nodes.contains_key("greet"));
1022 assert!(graph.nodes.contains_key("sayHi"));
1023 }
1024
1025 #[test]
1026 fn test_lua_function_detection() {
1027 let code = r#"
1028function add(a, b)
1029 return a + b
1030end
1031
1032local function subtract(a, b)
1033 return a - b
1034end
1035
1036obj = {}
1037function obj:multiply(a, b)
1038 return a * b
1039end
1040"#;
1041 let mut graph = CallGraph::new();
1042 let mut defs = HashMap::new();
1043 extract_functions_loose(code, Path::new("test.lua"), "lua", &mut defs, &mut graph);
1044 assert!(graph.nodes.contains_key("add"));
1045 assert!(graph.nodes.contains_key("subtract"));
1046 assert!(graph.nodes.contains_key("multiply"));
1047 }
1048
1049 #[test]
1050 fn test_shell_function_detection() {
1051 let code = r#"
1052#!/bin/bash
1053
1054greet() {
1055 echo "Hello, $1"
1056}
1057
1058helper() {
1059 greet "world"
1060}
1061"#;
1062 let mut graph = CallGraph::new();
1063 let mut defs = HashMap::new();
1064 extract_functions_loose(code, Path::new("test.sh"), "sh", &mut defs, &mut graph);
1065 assert!(graph.nodes.contains_key("greet"));
1066 assert!(graph.nodes.contains_key("helper"));
1067 }
1068
1069 #[test]
1070 fn test_elixir_function_detection() {
1071 let code = r#"
1072defmodule Math do
1073 def add(a, b) do
1074 a + b
1075 end
1076
1077 def subtract(a, b) do
1078 add(a, -b)
1079 end
1080end
1081"#;
1082 let mut graph = CallGraph::new();
1083 let mut defs = HashMap::new();
1084 extract_functions_loose(code, Path::new("math.ex"), "ex", &mut defs, &mut graph);
1085 assert!(graph.nodes.contains_key("add"));
1086 assert!(graph.nodes.contains_key("subtract"));
1087 }
1088
1089 #[test]
1090 fn test_recursive_function_detection() {
1091 let mut graph = CallGraph::new();
1092
1093 graph.add_node(CallNode {
1094 function_name: "factorial".to_string(),
1095 file_path: "test.rs".to_string(),
1096 line: 1,
1097 is_recursive: false,
1098 call_count: 0,
1099 });
1100
1101 graph.add_edge("factorial".to_string(), "factorial".to_string(), 2, true);
1102
1103 assert!(graph.is_recursive("factorial"));
1104 }
1105
1106 #[test]
1107 fn test_method_chain_call_extraction() {
1108 let code = r#"
1109fn process() {
1110 let result = data.transform().filter();
1111}
1112"#;
1113 let mut graph = CallGraph::new();
1114 let mut defs = HashMap::new();
1115 defs.insert("transform".to_string(), ("lib.rs".to_string(), 1));
1116 defs.insert("filter".to_string(), ("lib.rs".to_string(), 2));
1117 extract_functions_loose(code, Path::new("test.rs"), "rs", &mut defs, &mut graph);
1118 extract_calls_loose(code, Path::new("test.rs"), "rs", &defs, &mut graph);
1119 assert!(graph.has_edge("process", "transform"));
1120 assert!(graph.has_edge("process", "filter"));
1121 }
1122
1123 #[test]
1124 fn test_cs_function_detection() {
1125 let code = r#"
1126using System;
1127
1128class Program {
1129 static int Add(int a, int b) {
1130 return a + b;
1131 }
1132
1133 static void Main() {
1134 Console.WriteLine(Add(1, 2));
1135 }
1136}
1137"#;
1138 let mut graph = CallGraph::new();
1139 let mut defs = HashMap::new();
1140 extract_functions_loose(code, Path::new("Program.cs"), "cs", &mut defs, &mut graph);
1141 assert!(graph.nodes.contains_key("Add"));
1142 assert!(graph.nodes.contains_key("Main"));
1143 }
1144
1145 #[test]
1146 fn test_kotlin_function_detection() {
1147 let code = r#"
1148fun add(a: Int, b: Int): Int {
1149 return a + b
1150}
1151
1152class Calculator {
1153 fun multiply(a: Int, b: Int): Int {
1154 return a * b
1155 }
1156}
1157"#;
1158 let mut graph = CallGraph::new();
1159 let mut defs = HashMap::new();
1160 extract_functions_loose(
1161 code,
1162 Path::new("Calculator.kt"),
1163 "kt",
1164 &mut defs,
1165 &mut graph,
1166 );
1167 assert!(graph.nodes.contains_key("add"));
1168 assert!(graph.nodes.contains_key("multiply"));
1169 }
1170
1171 #[test]
1172 fn test_nested_function_calls() {
1173 let code = r#"
1174fn outer() {
1175 inner();
1176}
1177
1178fn inner() {
1179 deep();
1180}
1181
1182fn deep() {
1183}
1184"#;
1185 let mut graph = CallGraph::new();
1186 let mut defs = HashMap::new();
1187 extract_functions_loose(code, Path::new("test.rs"), "rs", &mut defs, &mut graph);
1188 extract_calls_loose(code, Path::new("test.rs"), "rs", &defs, &mut graph);
1189 assert!(graph.has_edge("outer", "inner"));
1190 assert!(graph.has_edge("inner", "deep"));
1191 }
1192
1193 #[test]
1194 fn test_typescript_class_method_detection() {
1195 let code = r#"
1196class Greeter {
1197 greet(name: string): string {
1198 return `Hello, ${name}`;
1199 }
1200
1201 private farewell(): void {
1202 this.greet("all");
1203 }
1204}
1205"#;
1206 let mut graph = CallGraph::new();
1207 let mut defs = HashMap::new();
1208 extract_functions_loose(code, Path::new("greeter.ts"), "ts", &mut defs, &mut graph);
1209 extract_calls_loose(code, Path::new("greeter.ts"), "ts", &defs, &mut graph);
1210 assert!(
1211 graph.nodes.contains_key("greet"),
1212 "should detect TS method greet"
1213 );
1214 assert!(
1215 graph.nodes.contains_key("farewell"),
1216 "should detect TS method farewell"
1217 );
1218 assert!(
1219 graph.has_edge("farewell", "greet"),
1220 "farewell should call greet"
1221 );
1222 }
1223
1224 #[test]
1225 fn test_rust_loose_function_detection() {
1226 let code = r#"
1227pub fn calculate(x: i32, y: i32) -> i32 {
1228 add(x, y)
1229}
1230
1231fn add(a: i32, b: i32) -> i32 {
1232 a + b
1233}
1234"#;
1235 let mut graph = CallGraph::new();
1236 let mut defs = HashMap::new();
1237 extract_functions_loose(code, Path::new("math.rs"), "rs", &mut defs, &mut graph);
1238 extract_calls_loose(code, Path::new("math.rs"), "rs", &defs, &mut graph);
1239 assert!(
1240 graph.nodes.contains_key("calculate"),
1241 "should detect Rust fn calculate"
1242 );
1243 assert!(graph.nodes.contains_key("add"), "should detect Rust fn add");
1244 assert!(
1245 graph.has_edge("calculate", "add"),
1246 "calculate should call add"
1247 );
1248 }
1249
1250 #[test]
1251 fn test_go_receiver_method_detection() {
1252 let code = r#"
1253package main
1254
1255func (r *Rect) Area() int {
1256 return r.width * r.height
1257}
1258
1259func main() {
1260 r := &Rect{}
1261 r.Area()
1262}
1263"#;
1264 let mut graph = CallGraph::new();
1265 let mut defs = HashMap::new();
1266 extract_functions_loose(code, Path::new("shapes.go"), "go", &mut defs, &mut graph);
1267 extract_calls_loose(code, Path::new("shapes.go"), "go", &defs, &mut graph);
1268 assert!(
1269 graph.nodes.contains_key("Area"),
1270 "should detect Go receiver method Area"
1271 );
1272 assert!(graph.nodes.contains_key("main"), "should detect Go main");
1273 }
1274
1275 #[test]
1276 fn test_php_class_method_detection() {
1277 let code = r#"
1278<?php
1279class Calculator {
1280 public function add($a, $b) {
1281 return $a + $b;
1282 }
1283
1284 private function helper() {
1285 $this->add(1, 2);
1286 }
1287}
1288"#;
1289 let mut graph = CallGraph::new();
1290 let mut defs = HashMap::new();
1291 extract_functions_loose(code, Path::new("calc.php"), "php", &mut defs, &mut graph);
1292 extract_calls_loose(code, Path::new("calc.php"), "php", &defs, &mut graph);
1293 assert!(
1294 graph.nodes.contains_key("add"),
1295 "should detect PHP method add"
1296 );
1297 assert!(
1298 graph.nodes.contains_key("helper"),
1299 "should detect PHP method helper"
1300 );
1301 assert!(graph.has_edge("helper", "add"), "helper should call add");
1302 }
1303
1304 #[test]
1305 fn test_empty_code_no_panic() {
1306 let code = "";
1307 let mut graph = CallGraph::new();
1308 let mut defs = HashMap::new();
1309 extract_functions_loose(code, Path::new("empty.rs"), "rs", &mut defs, &mut graph);
1310 extract_calls_loose(code, Path::new("empty.rs"), "rs", &defs, &mut graph);
1311 assert_eq!(graph.nodes.len(), 0);
1312 assert_eq!(graph.edges.len(), 0);
1313 }
1314
1315 #[test]
1316 fn test_malformed_code_graceful() {
1317 let code = r#"
1318fn broken( {
1319 {{{
1320 call_me(
1321}
1322"#;
1323 let mut graph = CallGraph::new();
1324 let mut defs = HashMap::new();
1325 extract_functions_loose(code, Path::new("broken.rs"), "rs", &mut defs, &mut graph);
1326 extract_calls_loose(code, Path::new("broken.rs"), "rs", &defs, &mut graph);
1327 }
1329
1330 #[test]
1331 fn test_deeply_nested_brace_scope() {
1332 let code = r#"
1333fn outer() {
1334 if true {
1335 if true {
1336 if true {
1337 helper();
1338 }
1339 }
1340 }
1341}
1342
1343fn helper() {}
1344"#;
1345 let mut graph = CallGraph::new();
1346 let mut defs = HashMap::new();
1347 extract_functions_loose(code, Path::new("nested.rs"), "rs", &mut defs, &mut graph);
1348 extract_calls_loose(code, Path::new("nested.rs"), "rs", &defs, &mut graph);
1349 assert!(
1350 graph.has_edge("outer", "helper"),
1351 "should detect call in deep nesting"
1352 );
1353 }
1354
1355 #[test]
1356 fn test_haskell_function_detection() {
1357 let code = r#"
1358add :: Int -> Int -> Int
1359add a b = a + b
1360
1361main = do
1362 print (add 1 2)
1363"#;
1364 let mut graph = CallGraph::new();
1365 let mut defs = HashMap::new();
1366 extract_functions_loose(code, Path::new("math.hs"), "hs", &mut defs, &mut graph);
1367 extract_calls_loose(code, Path::new("math.hs"), "hs", &defs, &mut graph);
1368 assert!(
1369 graph.nodes.contains_key("add"),
1370 "should detect Haskell function add"
1371 );
1372 }
1373
1374 #[test]
1375 fn test_zig_function_detection() {
1376 let code = r#"
1377const std = @import("std");
1378
1379fn add(a: i32, b: i32) i32 {
1380 return a + b;
1381}
1382
1383pub fn main() void {
1384 const result = add(1, 2);
1385}
1386"#;
1387 let mut graph = CallGraph::new();
1388 let mut defs = HashMap::new();
1389 extract_functions_loose(code, Path::new("math.zig"), "zig", &mut defs, &mut graph);
1390 extract_calls_loose(code, Path::new("math.zig"), "zig", &defs, &mut graph);
1391 assert!(graph.nodes.contains_key("add"), "should detect Zig fn add");
1392 assert!(
1393 graph.nodes.contains_key("main"),
1394 "should detect Zig fn main"
1395 );
1396 assert!(graph.has_edge("main", "add"), "main should call add");
1397 }
1398}