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