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}