Skip to main content

codesearch/
callgraph.rs

1//! Call Graph Module
2//!
3//! Analyzes function call relationships in code.
4
5use 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(&current) {
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        // Try language-specific parser first
349        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        // Fallback: loose grammar parsing for any language (also supplements empty parser results)
373        extract_functions_loose(
374            &content,
375            file_path,
376            ext,
377            &mut function_definitions,
378            &mut graph,
379        );
380    }
381
382    // Phase 2: detect calls
383    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        // Try language-specific parser first
389        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        // Fallback: loose grammar call detection (also supplements empty parser results)
413        extract_calls_loose(&content, file_path, ext, &function_definitions, &mut graph);
414    }
415
416    // Remove false-positive self-edges caused by function signatures on their definition lines
417    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
437/// Loose grammar function extraction for languages without a dedicated parser.
438/// Covers C/C++, Java, C#, Ruby, PHP, Swift, Kotlin, and more.
439fn 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-style: int foo() | void Foo() | static char *bar()
448        "c" | "cpp" | "cxx" | "cc" | "h" | "hpp" => vec![
449            // Function pointer typedefs excluded by requiring word-char before parens
450            r"\b(?:\w+\s+)+?(\w+)\s*\([^)]*\)\s*\{",
451        ],
452        // Java / C# / Kotlin
453        "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        // Ruby / Crystal
458        "rb" | "cr" => vec![r"\bdef\s+(?:self\.)?(\w+)"],
459        // PHP
460        "php" => vec![r"\bfunction\s+(\w+)"],
461        // JavaScript / TypeScript
462        "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            // class methods: optional modifier, allow return type annotation
467            r"^\s+(?:public|private|protected|static|readonly|async)?\s*(\w+)\s*\([^)]*\)[^{]*\{",
468        ],
469        // Go
470        "go" => vec![r"\bfunc\s+(?:\([^)]*\)\s+)?(\w+)"],
471        // Swift
472        "swift" => vec![r"\bfunc\s+(\w+)"],
473        // Lua
474        "lua" => vec![r"\bfunction\s+(?:\w+[:\.])?(\w+)"],
475        // Shell / Bash
476        "sh" | "bash" | "zsh" => vec![r"^(\w+)\s*\(\)\s*\{"],
477        // Elixir
478        "ex" | "exs" => vec![r"\bdef\s+(?:\w+\.)?(\w+)"],
479        // Haskell
480        "hs" => vec![r"^(\w+)\s*(?:::[^=]+)?\s*$", r"^(\w+)\s*(?:\w+\s+)*="],
481        // Default: the common keywords we already had
482        _ => 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
511/// Loose grammar call extraction with brace-based scope tracking.
512fn 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    // Shell scripts call functions by bare name without parens
540    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        // Detect function entry via loose patterns
555        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            // Push new function scope only when we also see an opening brace on this line
570            if line.contains('{') {
571                scope_stack.push(func_name);
572                brace_depth = 1; // reset depth tracking for this new scope
573            } else if scope_stack.is_empty() {
574                // Languages like Python/Ruby/Elixir without braces
575                scope_stack.push(func_name);
576            }
577        }
578
579        // Track braces for scope
580        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        // For braceless languages, pop function when dedent or blank line
595        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        // Extract calls inside current function scope
603        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                    // Take only the final identifier for method chains: obj.method() -> method
608                    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            // Shell bare-word calls
617            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        // Should not panic; exact node count is not critical
1328    }
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}