infiniloom_engine/index/
query.rs

1//! Call graph query API for analyzing symbol relationships
2//!
3//! This module provides high-level functions for querying call relationships,
4//! dependencies, and references between symbols in a codebase. Perfect for
5//! impact analysis, refactoring support, and understanding code structure.
6//!
7//! # Quick Start
8//!
9//! ```rust
10//! use infiniloom_engine::index::{IndexBuilder, query};
11//!
12//! // Build index for your repository
13//! let mut builder = IndexBuilder::new();
14//! builder.index_directory("/path/to/repo")?;
15//! let (index, graph) = builder.build();
16//!
17//! // Find a symbol by name
18//! let symbols = query::find_symbol(&index, "process_payment");
19//! for symbol in symbols {
20//!     println!("Found: {} in {} at line {}",
21//!         symbol.name, symbol.file, symbol.line);
22//! }
23//! # Ok::<(), Box<dyn std::error::Error>>(())
24//! ```
25//!
26//! # Finding Symbols
27//!
28//! Search for symbols by name across the entire codebase:
29//!
30//! ```rust
31//! use infiniloom_engine::index::query;
32//!
33//! # let (index, _graph) = setup_test_index();
34//! // Find all symbols with matching name
35//! let symbols = query::find_symbol(&index, "authenticate");
36//!
37//! for symbol in symbols {
38//!     println!("{} {} in {}:{}",
39//!         symbol.kind,      // "function", "method", etc.
40//!         symbol.name,      // "authenticate"
41//!         symbol.file,      // "src/auth.rs"
42//!         symbol.line       // 42
43//!     );
44//!
45//!     if let Some(sig) = &symbol.signature {
46//!         println!("  Signature: {}", sig);
47//!     }
48//! }
49//! ```
50//!
51//! # Querying Callers (Who Calls This?)
52//!
53//! Find all functions/methods that call a specific symbol:
54//!
55//! ```rust
56//! use infiniloom_engine::index::query;
57//!
58//! # let (index, graph) = setup_test_index();
59//! // Find who calls "validate_token"
60//! let callers = query::get_callers_by_name(&index, &graph, "validate_token")?;
61//!
62//! println!("Functions that call validate_token:");
63//! for caller in callers {
64//!     println!("  - {} in {}:{}",
65//!         caller.name,      // "check_auth"
66//!         caller.file,      // "src/middleware.rs"
67//!         caller.line       // 23
68//!     );
69//! }
70//! # Ok::<(), Box<dyn std::error::Error>>(())
71//! ```
72//!
73//! # Querying Callees (What Does This Call?)
74//!
75//! Find all functions/methods called by a specific symbol:
76//!
77//! ```rust
78//! use infiniloom_engine::index::query;
79//!
80//! # let (index, graph) = setup_test_index();
81//! // Find what "process_order" calls
82//! let callees = query::get_callees_by_name(&index, &graph, "process_order")?;
83//!
84//! println!("Functions called by process_order:");
85//! for callee in callees {
86//!     println!("  → {} ({})", callee.name, callee.kind);
87//!     println!("    Defined in {}:{}", callee.file, callee.line);
88//! }
89//! # Ok::<(), Box<dyn std::error::Error>>(())
90//! ```
91//!
92//! # Analyzing References (Calls, Imports, Inheritance)
93//!
94//! Get all references to a symbol (calls, imports, inheritance, implementations):
95//!
96//! ```rust
97//! use infiniloom_engine::index::query;
98//!
99//! # let (index, graph) = setup_test_index();
100//! // Find all references to "Database" class
101//! let references = query::get_references_by_name(&index, &graph, "Database")?;
102//!
103//! for reference in references {
104//!     match reference.kind.as_str() {
105//!         "call" => println!("Called by: {}", reference.symbol.name),
106//!         "import" => println!("Imported in: {}", reference.symbol.file),
107//!         "inherit" => println!("Inherited by: {}", reference.symbol.name),
108//!         "implement" => println!("Implemented by: {}", reference.symbol.name),
109//!         _ => {}
110//!     }
111//! }
112//! # Ok::<(), Box<dyn std::error::Error>>(())
113//! ```
114//!
115//! # Complete Call Graph
116//!
117//! Get the entire call graph for visualization or analysis:
118//!
119//! ```rust
120//! use infiniloom_engine::index::query;
121//!
122//! # let (index, graph) = setup_test_index();
123//! // Get complete call graph
124//! let call_graph = query::get_call_graph(&index, &graph);
125//!
126//! println!("Call Graph Summary:");
127//! println!("  Nodes (symbols): {}", call_graph.stats.total_nodes);
128//! println!("  Edges (calls): {}", call_graph.stats.total_edges);
129//! println!("  Entry points: {}", call_graph.stats.entry_points);
130//!
131//! // Analyze specific edges
132//! for edge in call_graph.edges.iter().take(5) {
133//!     println!("{} → {} ({}:{})",
134//!         edge.caller,
135//!         edge.callee,
136//!         edge.file,
137//!         edge.line
138//!     );
139//! }
140//! ```
141//!
142//! # Filtered Call Graph (Large Codebases)
143//!
144//! For large repositories, filter the call graph to manageable size:
145//!
146//! ```rust
147//! use infiniloom_engine::index::query;
148//!
149//! # let (index, graph) = setup_test_index();
150//! // Get top 100 most important symbols, up to 500 edges
151//! let call_graph = query::get_call_graph_filtered(&index, &graph, 100, 500);
152//!
153//! println!("Filtered Call Graph:");
154//! println!("  Nodes: {} (limited to 100)", call_graph.stats.total_nodes);
155//! println!("  Edges: {} (limited to 500)", call_graph.stats.total_edges);
156//!
157//! // Most important symbols are included first
158//! for node in call_graph.nodes.iter().take(10) {
159//!     println!("Top symbol: {} ({}) in {}",
160//!         node.name, node.kind, node.file);
161//! }
162//! ```
163//!
164//! # Symbol ID-Based Queries
165//!
166//! Use symbol IDs for faster lookup when you already know the ID:
167//!
168//! ```rust
169//! use infiniloom_engine::index::query;
170//!
171//! # let (index, graph) = setup_test_index();
172//! # let symbol_id = 42;
173//! // Direct lookup by symbol ID (faster than name-based lookup)
174//! let callers = query::get_callers_by_id(&index, &graph, symbol_id)?;
175//! let callees = query::get_callees_by_id(&index, &graph, symbol_id)?;
176//!
177//! println!("Symbol {} has {} callers and {} callees",
178//!     symbol_id, callers.len(), callees.len());
179//! # Ok::<(), Box<dyn std::error::Error>>(())
180//! ```
181//!
182//! # Impact Analysis Example
183//!
184//! Practical example: Analyze impact of changing a function:
185//!
186//! ```rust
187//! use infiniloom_engine::index::{IndexBuilder, query};
188//!
189//! # fn analyze_impact() -> Result<(), Box<dyn std::error::Error>> {
190//! // Build index
191//! let mut builder = IndexBuilder::new();
192//! builder.index_directory("/path/to/repo")?;
193//! let (index, graph) = builder.build();
194//!
195//! // Function we want to change
196//! let target = "calculate_price";
197//!
198//! // Find direct callers
199//! let direct_callers = query::get_callers_by_name(&index, &graph, target)?;
200//! println!("Direct impact: {} functions call {}",
201//!     direct_callers.len(), target);
202//!
203//! // Find transitive callers (who calls the callers?)
204//! let mut affected = std::collections::HashSet::new();
205//! affected.extend(direct_callers.iter().map(|s| s.id));
206//!
207//! for caller in &direct_callers {
208//!     let transitive = query::get_callers_by_id(&index, &graph, caller.id)?;
209//!     affected.extend(transitive.iter().map(|s| s.id));
210//! }
211//!
212//! println!("Total impact: {} functions affected", affected.len());
213//!
214//! // Find what the target calls (dependencies to consider)
215//! let dependencies = query::get_callees_by_name(&index, &graph, target)?;
216//! println!("Dependencies: {} functions called by {}",
217//!     dependencies.len(), target);
218//!
219//! # Ok(())
220//! # }
221//! ```
222//!
223//! # Performance Characteristics
224//!
225//! - **`find_symbol()`**: O(1) hash lookup, very fast
226//! - **`get_callers_by_name()`**: O(name_lookup + E) where E = number of edges
227//! - **`get_callees_by_name()`**: O(name_lookup + E) where E = number of edges
228//! - **`get_callers_by_id()`**: O(E) - faster than name-based lookup
229//! - **`get_callees_by_id()`**: O(E) - faster than name-based lookup
230//! - **`get_call_graph()`**: O(N + E) where N = nodes, E = edges
231//! - **`get_call_graph_filtered()`**: O(N log N + E) - sorts nodes by importance
232//!
233//! # Deduplication
234//!
235//! All query functions automatically deduplicate results:
236//! - Multiple definitions of the same symbol (overloads, multiple files) are merged
237//! - Results are sorted by file path and line number for consistency
238//!
239//! # Error Handling
240//!
241//! Functions return `Result<Vec<SymbolInfo>, String>` where:
242//! - **Ok(vec)**: Successful query (vec may be empty if no results)
243//! - **Err(msg)**: Symbol not found in index (only for direct ID lookups)
244//!
245//! Name-based queries always succeed, returning empty Vec if symbol not found.
246//!
247//! # Thread Safety
248//!
249//! All query functions are thread-safe and can be called concurrently:
250//! - `SymbolIndex` and `DepGraph` are immutable after construction
251//! - No internal locks or shared mutable state
252//! - Safe to query from multiple threads simultaneously
253
254use super::types::{DepGraph, IndexSymbol, IndexSymbolKind, SymbolIndex, Visibility};
255use serde::Serialize;
256
257#[cfg(test)]
258fn setup_test_index() -> (SymbolIndex, DepGraph) {
259    // Test helper - returns empty index/graph
260    (SymbolIndex::default(), DepGraph::default())
261}
262
263/// Information about a symbol, returned from call graph queries
264#[derive(Debug, Clone, Serialize)]
265pub struct SymbolInfo {
266    /// Symbol ID
267    pub id: u32,
268    /// Symbol name
269    pub name: String,
270    /// Symbol kind (function, class, method, etc.)
271    pub kind: String,
272    /// File path containing the symbol
273    pub file: String,
274    /// Start line number
275    pub line: u32,
276    /// End line number
277    pub end_line: u32,
278    /// Function/method signature
279    pub signature: Option<String>,
280    /// Visibility (public, private, etc.)
281    pub visibility: String,
282}
283
284/// A reference location in the codebase
285#[derive(Debug, Clone, Serialize)]
286pub struct ReferenceInfo {
287    /// Symbol making the reference
288    pub symbol: SymbolInfo,
289    /// Reference kind (call, import, inherit, implement)
290    pub kind: String,
291}
292
293/// An edge in the call graph
294#[derive(Debug, Clone, Serialize)]
295pub struct CallGraphEdge {
296    /// Caller symbol ID
297    pub caller_id: u32,
298    /// Callee symbol ID
299    pub callee_id: u32,
300    /// Caller symbol name
301    pub caller: String,
302    /// Callee symbol name
303    pub callee: String,
304    /// File containing the call site
305    pub file: String,
306    /// Line number of the call
307    pub line: u32,
308}
309
310/// Complete call graph with nodes and edges
311#[derive(Debug, Clone, Serialize)]
312pub struct CallGraph {
313    /// All symbols (nodes)
314    pub nodes: Vec<SymbolInfo>,
315    /// Call relationships (edges)
316    pub edges: Vec<CallGraphEdge>,
317    /// Summary statistics
318    pub stats: CallGraphStats,
319}
320
321/// Call graph statistics
322#[derive(Debug, Clone, Serialize)]
323pub struct CallGraphStats {
324    /// Total number of symbols
325    pub total_symbols: usize,
326    /// Total number of call edges
327    pub total_calls: usize,
328    /// Number of functions/methods
329    pub functions: usize,
330    /// Number of classes/structs
331    pub classes: usize,
332}
333
334impl SymbolInfo {
335    /// Create SymbolInfo from an IndexSymbol
336    pub fn from_index_symbol(sym: &IndexSymbol, index: &SymbolIndex) -> Self {
337        let file_path = index
338            .get_file_by_id(sym.file_id.as_u32())
339            .map(|f| f.path.clone())
340            .unwrap_or_else(|| "<unknown>".to_owned());
341
342        Self {
343            id: sym.id.as_u32(),
344            name: sym.name.clone(),
345            kind: format_symbol_kind(sym.kind),
346            file: file_path,
347            line: sym.span.start_line,
348            end_line: sym.span.end_line,
349            signature: sym.signature.clone(),
350            visibility: format_visibility(sym.visibility),
351        }
352    }
353}
354
355/// Find a symbol by name and return its info
356///
357/// Deduplicates results by file path and line number to avoid returning
358/// the same symbol multiple times (e.g., export + declaration).
359pub fn find_symbol(index: &SymbolIndex, name: &str) -> Vec<SymbolInfo> {
360    let mut results: Vec<SymbolInfo> = index
361        .find_symbols(name)
362        .into_iter()
363        .map(|sym| SymbolInfo::from_index_symbol(sym, index))
364        .collect();
365
366    // Deduplicate by (file, line) to avoid returning export+declaration as separate entries
367    results.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
368    results.dedup_by(|a, b| a.file == b.file && a.line == b.line);
369
370    results
371}
372
373/// Get all callers of a symbol by name
374///
375/// Returns symbols that call any symbol with the given name.
376pub fn get_callers_by_name(index: &SymbolIndex, graph: &DepGraph, name: &str) -> Vec<SymbolInfo> {
377    let mut callers = Vec::new();
378
379    // Find all symbols with this name
380    for sym in index.find_symbols(name) {
381        let symbol_id = sym.id.as_u32();
382
383        // Get callers from the dependency graph
384        for caller_id in graph.get_callers(symbol_id) {
385            if let Some(caller_sym) = index.get_symbol(caller_id) {
386                callers.push(SymbolInfo::from_index_symbol(caller_sym, index));
387            }
388        }
389    }
390
391    // Deduplicate by symbol ID
392    callers.sort_by_key(|s| s.id);
393    callers.dedup_by_key(|s| s.id);
394
395    callers
396}
397
398/// Get all callees of a symbol by name
399///
400/// Returns symbols that are called by any symbol with the given name.
401pub fn get_callees_by_name(index: &SymbolIndex, graph: &DepGraph, name: &str) -> Vec<SymbolInfo> {
402    let mut callees = Vec::new();
403
404    // Find all symbols with this name
405    for sym in index.find_symbols(name) {
406        let symbol_id = sym.id.as_u32();
407
408        // Get callees from the dependency graph
409        for callee_id in graph.get_callees(symbol_id) {
410            if let Some(callee_sym) = index.get_symbol(callee_id) {
411                callees.push(SymbolInfo::from_index_symbol(callee_sym, index));
412            }
413        }
414    }
415
416    // Deduplicate by symbol ID
417    callees.sort_by_key(|s| s.id);
418    callees.dedup_by_key(|s| s.id);
419
420    callees
421}
422
423/// Get all references to a symbol by name
424///
425/// Returns symbols that reference any symbol with the given name
426/// (includes calls, imports, inheritance, and implementations).
427pub fn get_references_by_name(
428    index: &SymbolIndex,
429    graph: &DepGraph,
430    name: &str,
431) -> Vec<ReferenceInfo> {
432    let mut references = Vec::new();
433
434    // Find all symbols with this name
435    for sym in index.find_symbols(name) {
436        let symbol_id = sym.id.as_u32();
437
438        // Get callers (call references)
439        for caller_id in graph.get_callers(symbol_id) {
440            if let Some(caller_sym) = index.get_symbol(caller_id) {
441                references.push(ReferenceInfo {
442                    symbol: SymbolInfo::from_index_symbol(caller_sym, index),
443                    kind: "call".to_owned(),
444                });
445            }
446        }
447
448        // Get referencers (symbol_ref - may include imports/inheritance)
449        for ref_id in graph.get_referencers(symbol_id) {
450            if let Some(ref_sym) = index.get_symbol(ref_id) {
451                // Avoid duplicates with callers
452                if !graph.get_callers(symbol_id).contains(&ref_id) {
453                    references.push(ReferenceInfo {
454                        symbol: SymbolInfo::from_index_symbol(ref_sym, index),
455                        kind: "reference".to_owned(),
456                    });
457                }
458            }
459        }
460    }
461
462    // Deduplicate by symbol ID
463    references.sort_by_key(|r| r.symbol.id);
464    references.dedup_by_key(|r| r.symbol.id);
465
466    references
467}
468
469/// Get the complete call graph
470///
471/// Returns all symbols (nodes) and call relationships (edges).
472/// For large codebases, consider using `get_call_graph_filtered` with limits.
473pub fn get_call_graph(index: &SymbolIndex, graph: &DepGraph) -> CallGraph {
474    get_call_graph_filtered(index, graph, None, None)
475}
476
477/// Get a filtered call graph
478///
479/// Args:
480///   - `max_nodes`: Optional limit on number of symbols returned
481///   - `max_edges`: Optional limit on number of edges returned
482pub fn get_call_graph_filtered(
483    index: &SymbolIndex,
484    graph: &DepGraph,
485    max_nodes: Option<usize>,
486    max_edges: Option<usize>,
487) -> CallGraph {
488    // Bug #5 fix: When only max_edges is specified, limit nodes to those that appear in edges
489    // This ensures users get a small, focused graph rather than all nodes with limited edges
490
491    // First, collect all edges and apply edge limit
492    let mut edges: Vec<CallGraphEdge> = graph
493        .calls
494        .iter()
495        .filter_map(|&(caller_id, callee_id)| {
496            let caller_sym = index.get_symbol(caller_id)?;
497            let callee_sym = index.get_symbol(callee_id)?;
498
499            let file_path = index
500                .get_file_by_id(caller_sym.file_id.as_u32())
501                .map(|f| f.path.clone())
502                .unwrap_or_else(|| "<unknown>".to_owned());
503
504            Some(CallGraphEdge {
505                caller_id,
506                callee_id,
507                caller: caller_sym.name.clone(),
508                callee: callee_sym.name.clone(),
509                file: file_path,
510                line: caller_sym.span.start_line,
511            })
512        })
513        .collect();
514
515    // Apply edge limit first (before node filtering for more intuitive behavior)
516    if let Some(limit) = max_edges {
517        edges.truncate(limit);
518    }
519
520    // Collect node IDs that appear in the (possibly limited) edges
521    let edge_node_ids: std::collections::HashSet<u32> = edges
522        .iter()
523        .flat_map(|e| [e.caller_id, e.callee_id])
524        .collect();
525
526    // Collect nodes - when max_edges is specified without max_nodes, only include nodes from edges
527    let mut nodes: Vec<SymbolInfo> = if max_edges.is_some() && max_nodes.is_none() {
528        // Only include nodes that appear in the limited edges
529        index
530            .symbols
531            .iter()
532            .filter(|sym| edge_node_ids.contains(&sym.id.as_u32()))
533            .map(|sym| SymbolInfo::from_index_symbol(sym, index))
534            .collect()
535    } else {
536        // Include all nodes, then optionally truncate
537        index
538            .symbols
539            .iter()
540            .map(|sym| SymbolInfo::from_index_symbol(sym, index))
541            .collect()
542    };
543
544    // Apply node limit if specified
545    if let Some(limit) = max_nodes {
546        nodes.truncate(limit);
547
548        // When max_nodes is applied, also filter edges to only include those between limited nodes
549        let node_ids: std::collections::HashSet<u32> = nodes.iter().map(|n| n.id).collect();
550        edges.retain(|e| node_ids.contains(&e.caller_id) && node_ids.contains(&e.callee_id));
551    }
552
553    // Calculate statistics
554    let functions = nodes
555        .iter()
556        .filter(|n| n.kind == "function" || n.kind == "method")
557        .count();
558    let classes = nodes
559        .iter()
560        .filter(|n| n.kind == "class" || n.kind == "struct")
561        .count();
562
563    let stats =
564        CallGraphStats { total_symbols: nodes.len(), total_calls: edges.len(), functions, classes };
565
566    CallGraph { nodes, edges, stats }
567}
568
569/// Get callers of a symbol by its ID
570pub fn get_callers_by_id(index: &SymbolIndex, graph: &DepGraph, symbol_id: u32) -> Vec<SymbolInfo> {
571    graph
572        .get_callers(symbol_id)
573        .into_iter()
574        .filter_map(|id| index.get_symbol(id))
575        .map(|sym| SymbolInfo::from_index_symbol(sym, index))
576        .collect()
577}
578
579/// Get callees of a symbol by its ID
580pub fn get_callees_by_id(index: &SymbolIndex, graph: &DepGraph, symbol_id: u32) -> Vec<SymbolInfo> {
581    graph
582        .get_callees(symbol_id)
583        .into_iter()
584        .filter_map(|id| index.get_symbol(id))
585        .map(|sym| SymbolInfo::from_index_symbol(sym, index))
586        .collect()
587}
588
589// Helper functions
590
591fn format_symbol_kind(kind: IndexSymbolKind) -> String {
592    match kind {
593        IndexSymbolKind::Function => "function",
594        IndexSymbolKind::Method => "method",
595        IndexSymbolKind::Class => "class",
596        IndexSymbolKind::Struct => "struct",
597        IndexSymbolKind::Interface => "interface",
598        IndexSymbolKind::Trait => "trait",
599        IndexSymbolKind::Enum => "enum",
600        IndexSymbolKind::Constant => "constant",
601        IndexSymbolKind::Variable => "variable",
602        IndexSymbolKind::Module => "module",
603        IndexSymbolKind::Import => "import",
604        IndexSymbolKind::Export => "export",
605        IndexSymbolKind::TypeAlias => "type_alias",
606        IndexSymbolKind::Macro => "macro",
607    }
608    .to_owned()
609}
610
611fn format_visibility(vis: Visibility) -> String {
612    match vis {
613        Visibility::Public => "public",
614        Visibility::Private => "private",
615        Visibility::Protected => "protected",
616        Visibility::Internal => "internal",
617    }
618    .to_owned()
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use crate::index::types::{FileEntry, FileId, Language, Span, SymbolId};
625
626    fn create_test_index() -> (SymbolIndex, DepGraph) {
627        let mut index = SymbolIndex::default();
628
629        // Add test file
630        index.files.push(FileEntry {
631            id: FileId::new(0),
632            path: "test.py".to_string(),
633            language: Language::Python,
634            symbols: 0..2,
635            imports: vec![],
636            content_hash: [0u8; 32],
637            lines: 25,
638            tokens: 100,
639        });
640
641        // Add test symbols
642        index.symbols.push(IndexSymbol {
643            id: SymbolId::new(0),
644            name: "main".to_string(),
645            kind: IndexSymbolKind::Function,
646            file_id: FileId::new(0),
647            span: Span { start_line: 1, start_col: 0, end_line: 10, end_col: 0 },
648            signature: Some("def main()".to_string()),
649            parent: None,
650            visibility: Visibility::Public,
651            docstring: None,
652        });
653
654        index.symbols.push(IndexSymbol {
655            id: SymbolId::new(1),
656            name: "helper".to_string(),
657            kind: IndexSymbolKind::Function,
658            file_id: FileId::new(0),
659            span: Span { start_line: 12, start_col: 0, end_line: 20, end_col: 0 },
660            signature: Some("def helper()".to_string()),
661            parent: None,
662            visibility: Visibility::Private,
663            docstring: None,
664        });
665
666        // Build name index
667        index.symbols_by_name.insert("main".to_string(), vec![0]);
668        index.symbols_by_name.insert("helper".to_string(), vec![1]);
669
670        // Create dependency graph with call edge: main -> helper
671        let mut graph = DepGraph::new();
672        graph.add_call(0, 1); // main calls helper
673
674        (index, graph)
675    }
676
677    #[test]
678    fn test_find_symbol() {
679        let (index, _graph) = create_test_index();
680
681        let results = find_symbol(&index, "main");
682        assert_eq!(results.len(), 1);
683        assert_eq!(results[0].name, "main");
684        assert_eq!(results[0].kind, "function");
685        assert_eq!(results[0].file, "test.py");
686    }
687
688    #[test]
689    fn test_get_callers() {
690        let (index, graph) = create_test_index();
691
692        // helper is called by main
693        let callers = get_callers_by_name(&index, &graph, "helper");
694        assert_eq!(callers.len(), 1);
695        assert_eq!(callers[0].name, "main");
696    }
697
698    #[test]
699    fn test_get_callees() {
700        let (index, graph) = create_test_index();
701
702        // main calls helper
703        let callees = get_callees_by_name(&index, &graph, "main");
704        assert_eq!(callees.len(), 1);
705        assert_eq!(callees[0].name, "helper");
706    }
707
708    #[test]
709    fn test_get_call_graph() {
710        let (index, graph) = create_test_index();
711
712        let call_graph = get_call_graph(&index, &graph);
713        assert_eq!(call_graph.nodes.len(), 2);
714        assert_eq!(call_graph.edges.len(), 1);
715        assert_eq!(call_graph.stats.functions, 2);
716
717        // Check edge
718        assert_eq!(call_graph.edges[0].caller, "main");
719        assert_eq!(call_graph.edges[0].callee, "helper");
720    }
721}