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_or_else(|| "<unknown>".to_owned(), |f| f.path.clone());
332
333 Self {
334 id: sym.id.as_u32(),
335 name: sym.name.clone(),
336 kind: format_symbol_kind(sym.kind),
337 file: file_path,
338 line: sym.span.start_line,
339 end_line: sym.span.end_line,
340 signature: sym.signature.clone(),
341 visibility: format_visibility(sym.visibility),
342 }
343 }
344}
345
346/// Find a symbol by name and return its info
347///
348/// Deduplicates results by file path and line number to avoid returning
349/// the same symbol multiple times (e.g., export + declaration).
350pub fn find_symbol(index: &SymbolIndex, name: &str) -> Vec<SymbolInfo> {
351 let mut results: Vec<SymbolInfo> = index
352 .find_symbols(name)
353 .into_iter()
354 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
355 .collect();
356
357 // Deduplicate by (file, line) to avoid returning export+declaration as separate entries
358 results.sort_by(|a, b| (&a.file, a.line).cmp(&(&b.file, b.line)));
359 results.dedup_by(|a, b| a.file == b.file && a.line == b.line);
360
361 results
362}
363
364/// Get all callers of a symbol by name
365///
366/// Returns symbols that call any symbol with the given name.
367pub fn get_callers_by_name(index: &SymbolIndex, graph: &DepGraph, name: &str) -> Vec<SymbolInfo> {
368 let mut callers = Vec::new();
369
370 // Find all symbols with this name
371 for sym in index.find_symbols(name) {
372 let symbol_id = sym.id.as_u32();
373
374 // Get callers from the dependency graph
375 for caller_id in graph.get_callers(symbol_id) {
376 if let Some(caller_sym) = index.get_symbol(caller_id) {
377 callers.push(SymbolInfo::from_index_symbol(caller_sym, index));
378 }
379 }
380 }
381
382 // Deduplicate by symbol ID
383 callers.sort_by_key(|s| s.id);
384 callers.dedup_by_key(|s| s.id);
385
386 callers
387}
388
389/// Get all callees of a symbol by name
390///
391/// Returns symbols that are called by any symbol with the given name.
392pub fn get_callees_by_name(index: &SymbolIndex, graph: &DepGraph, name: &str) -> Vec<SymbolInfo> {
393 let mut callees = Vec::new();
394
395 // Find all symbols with this name
396 for sym in index.find_symbols(name) {
397 let symbol_id = sym.id.as_u32();
398
399 // Get callees from the dependency graph
400 for callee_id in graph.get_callees(symbol_id) {
401 if let Some(callee_sym) = index.get_symbol(callee_id) {
402 callees.push(SymbolInfo::from_index_symbol(callee_sym, index));
403 }
404 }
405 }
406
407 // Deduplicate by symbol ID
408 callees.sort_by_key(|s| s.id);
409 callees.dedup_by_key(|s| s.id);
410
411 callees
412}
413
414/// Get all references to a symbol by name
415///
416/// Returns symbols that reference any symbol with the given name
417/// (includes calls, imports, inheritance, and implementations).
418pub fn get_references_by_name(
419 index: &SymbolIndex,
420 graph: &DepGraph,
421 name: &str,
422) -> Vec<ReferenceInfo> {
423 let mut references = Vec::new();
424
425 // Find all symbols with this name
426 for sym in index.find_symbols(name) {
427 let symbol_id = sym.id.as_u32();
428
429 // Get callers (call references)
430 for caller_id in graph.get_callers(symbol_id) {
431 if let Some(caller_sym) = index.get_symbol(caller_id) {
432 references.push(ReferenceInfo {
433 symbol: SymbolInfo::from_index_symbol(caller_sym, index),
434 kind: "call".to_owned(),
435 });
436 }
437 }
438
439 // Get referencers (symbol_ref - may include imports/inheritance)
440 for ref_id in graph.get_referencers(symbol_id) {
441 if let Some(ref_sym) = index.get_symbol(ref_id) {
442 // Avoid duplicates with callers
443 if !graph.get_callers(symbol_id).contains(&ref_id) {
444 references.push(ReferenceInfo {
445 symbol: SymbolInfo::from_index_symbol(ref_sym, index),
446 kind: "reference".to_owned(),
447 });
448 }
449 }
450 }
451 }
452
453 // Deduplicate by symbol ID
454 references.sort_by_key(|r| r.symbol.id);
455 references.dedup_by_key(|r| r.symbol.id);
456
457 references
458}
459
460/// Get the complete call graph
461///
462/// Returns all symbols (nodes) and call relationships (edges).
463/// For large codebases, consider using `get_call_graph_filtered` with limits.
464pub fn get_call_graph(index: &SymbolIndex, graph: &DepGraph) -> CallGraph {
465 get_call_graph_filtered(index, graph, None, None)
466}
467
468/// Get a filtered call graph
469///
470/// Args:
471/// - `max_nodes`: Optional limit on number of symbols returned
472/// - `max_edges`: Optional limit on number of edges returned
473pub fn get_call_graph_filtered(
474 index: &SymbolIndex,
475 graph: &DepGraph,
476 max_nodes: Option<usize>,
477 max_edges: Option<usize>,
478) -> CallGraph {
479 // Bug #5 fix: When only max_edges is specified, limit nodes to those that appear in edges
480 // This ensures users get a small, focused graph rather than all nodes with limited edges
481
482 // First, collect all edges and apply edge limit
483 let mut edges: Vec<CallGraphEdge> = graph
484 .calls
485 .iter()
486 .filter_map(|&(caller_id, callee_id)| {
487 let caller_sym = index.get_symbol(caller_id)?;
488 let callee_sym = index.get_symbol(callee_id)?;
489
490 let file_path = index
491 .get_file_by_id(caller_sym.file_id.as_u32())
492 .map_or_else(|| "<unknown>".to_owned(), |f| f.path.clone());
493
494 Some(CallGraphEdge {
495 caller_id,
496 callee_id,
497 caller: caller_sym.name.clone(),
498 callee: callee_sym.name.clone(),
499 file: file_path,
500 line: caller_sym.span.start_line,
501 })
502 })
503 .collect();
504
505 // Apply edge limit first (before node filtering for more intuitive behavior)
506 if let Some(limit) = max_edges {
507 edges.truncate(limit);
508 }
509
510 // Collect node IDs that appear in the (possibly limited) edges
511 let edge_node_ids: std::collections::HashSet<u32> = edges
512 .iter()
513 .flat_map(|e| [e.caller_id, e.callee_id])
514 .collect();
515
516 // Collect nodes - when max_edges is specified without max_nodes, only include nodes from edges
517 let mut nodes: Vec<SymbolInfo> = if max_edges.is_some() && max_nodes.is_none() {
518 // Only include nodes that appear in the limited edges
519 index
520 .symbols
521 .iter()
522 .filter(|sym| edge_node_ids.contains(&sym.id.as_u32()))
523 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
524 .collect()
525 } else {
526 // Include all nodes, then optionally truncate
527 index
528 .symbols
529 .iter()
530 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
531 .collect()
532 };
533
534 // Apply node limit if specified
535 if let Some(limit) = max_nodes {
536 nodes.truncate(limit);
537
538 // When max_nodes is applied, also filter edges to only include those between limited nodes
539 let node_ids: std::collections::HashSet<u32> = nodes.iter().map(|n| n.id).collect();
540 edges.retain(|e| node_ids.contains(&e.caller_id) && node_ids.contains(&e.callee_id));
541 }
542
543 // Calculate statistics
544 let functions = nodes
545 .iter()
546 .filter(|n| n.kind == "function" || n.kind == "method")
547 .count();
548 let classes = nodes
549 .iter()
550 .filter(|n| n.kind == "class" || n.kind == "struct")
551 .count();
552
553 let stats =
554 CallGraphStats { total_symbols: nodes.len(), total_calls: edges.len(), functions, classes };
555
556 CallGraph { nodes, edges, stats }
557}
558
559/// Get callers of a symbol by its ID
560pub fn get_callers_by_id(index: &SymbolIndex, graph: &DepGraph, symbol_id: u32) -> Vec<SymbolInfo> {
561 graph
562 .get_callers(symbol_id)
563 .into_iter()
564 .filter_map(|id| index.get_symbol(id))
565 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
566 .collect()
567}
568
569/// Get callees of a symbol by its ID
570pub fn get_callees_by_id(index: &SymbolIndex, graph: &DepGraph, symbol_id: u32) -> Vec<SymbolInfo> {
571 graph
572 .get_callees(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/// A cycle in the dependency graph
580#[derive(Debug, Clone, Serialize)]
581pub struct DependencyCycle {
582 /// File IDs forming the cycle (for file-level cycles)
583 pub file_ids: Vec<u32>,
584 /// File paths forming the cycle
585 pub files: Vec<String>,
586 /// Cycle length
587 pub length: usize,
588}
589
590/// Find circular dependencies in file imports
591///
592/// Uses DFS to detect cycles in the file import graph.
593/// Returns all distinct cycles found.
594///
595/// # Example
596///
597/// ```rust,ignore
598/// let cycles = find_circular_dependencies(&index, &graph);
599/// for cycle in cycles {
600/// println!("Cycle: {} -> {}", cycle.files.join(" -> "), cycle.files[0]);
601/// }
602/// ```
603pub fn find_circular_dependencies(index: &SymbolIndex, graph: &DepGraph) -> Vec<DependencyCycle> {
604 use std::collections::HashSet;
605
606 let mut cycles = Vec::new();
607 let mut visited = HashSet::new();
608 let mut rec_stack = HashSet::new();
609 let mut path = Vec::new();
610
611 // Get all file IDs
612 let file_ids: Vec<u32> = index.files.iter().map(|f| f.id.as_u32()).collect();
613
614 fn dfs(
615 node: u32,
616 graph: &DepGraph,
617 index: &SymbolIndex,
618 visited: &mut HashSet<u32>,
619 rec_stack: &mut HashSet<u32>,
620 path: &mut Vec<u32>,
621 cycles: &mut Vec<DependencyCycle>,
622 ) {
623 visited.insert(node);
624 rec_stack.insert(node);
625 path.push(node);
626
627 for &neighbor in graph.imports_adj.get(&node).unwrap_or(&Vec::new()) {
628 if !visited.contains(&neighbor) {
629 dfs(neighbor, graph, index, visited, rec_stack, path, cycles);
630 } else if rec_stack.contains(&neighbor) {
631 // Found a cycle - extract it from the path
632 if let Some(start_idx) = path.iter().position(|&n| n == neighbor) {
633 let cycle_ids: Vec<u32> = path[start_idx..].to_vec();
634 let cycle_files: Vec<String> = cycle_ids
635 .iter()
636 .filter_map(|&id| index.get_file_by_id(id).map(|f| f.path.clone()))
637 .collect();
638
639 if !cycle_files.is_empty() {
640 cycles.push(DependencyCycle {
641 length: cycle_ids.len(),
642 file_ids: cycle_ids,
643 files: cycle_files,
644 });
645 }
646 }
647 }
648 }
649
650 path.pop();
651 rec_stack.remove(&node);
652 }
653
654 for &file_id in &file_ids {
655 if !visited.contains(&file_id) {
656 dfs(file_id, graph, index, &mut visited, &mut rec_stack, &mut path, &mut cycles);
657 }
658 }
659
660 // Deduplicate cycles (same cycle can be found from different starting points)
661 let mut seen_cycles: HashSet<Vec<u32>> = HashSet::new();
662 cycles.retain(|cycle| {
663 // Normalize cycle by rotating to start with smallest ID
664 let mut normalized = cycle.file_ids.clone();
665 if let Some(min_pos) = normalized
666 .iter()
667 .enumerate()
668 .min_by_key(|(_, &id)| id)
669 .map(|(i, _)| i)
670 {
671 normalized.rotate_left(min_pos);
672 }
673 seen_cycles.insert(normalized)
674 });
675
676 cycles
677}
678
679/// Get all exported/public symbols from the index
680///
681/// Returns symbols that are either:
682/// - Explicitly marked as exports (IndexSymbolKind::Export)
683/// - Public visibility functions, classes, etc.
684///
685/// # Example
686///
687/// ```rust,ignore
688/// let exports = get_exported_symbols(&index);
689/// for sym in exports {
690/// println!("Export: {} ({}) in {}", sym.name, sym.kind, sym.file);
691/// }
692/// ```
693pub fn get_exported_symbols(index: &SymbolIndex) -> Vec<SymbolInfo> {
694 index
695 .symbols
696 .iter()
697 .filter(|sym| {
698 // Include explicit exports
699 sym.kind == IndexSymbolKind::Export
700 // Include public functions, classes, structs, traits, enums
701 || (sym.visibility == Visibility::Public
702 && matches!(
703 sym.kind,
704 IndexSymbolKind::Function
705 | IndexSymbolKind::Class
706 | IndexSymbolKind::Struct
707 | IndexSymbolKind::Trait
708 | IndexSymbolKind::Enum
709 | IndexSymbolKind::Interface
710 | IndexSymbolKind::Constant
711 | IndexSymbolKind::TypeAlias
712 ))
713 })
714 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
715 .collect()
716}
717
718/// Get exported symbols filtered by file path
719///
720/// Returns public API symbols from a specific file.
721pub fn get_exported_symbols_in_file(index: &SymbolIndex, file_path: &str) -> Vec<SymbolInfo> {
722 let file_id = match index.file_by_path.get(file_path) {
723 Some(&id) => id,
724 None => return Vec::new(),
725 };
726
727 index
728 .symbols
729 .iter()
730 .filter(|sym| {
731 sym.file_id.as_u32() == file_id
732 && (sym.kind == IndexSymbolKind::Export
733 || (sym.visibility == Visibility::Public
734 && matches!(
735 sym.kind,
736 IndexSymbolKind::Function
737 | IndexSymbolKind::Class
738 | IndexSymbolKind::Struct
739 | IndexSymbolKind::Trait
740 | IndexSymbolKind::Enum
741 | IndexSymbolKind::Interface
742 | IndexSymbolKind::Constant
743 | IndexSymbolKind::TypeAlias
744 )))
745 })
746 .map(|sym| SymbolInfo::from_index_symbol(sym, index))
747 .collect()
748}
749
750// Helper functions
751
752fn format_symbol_kind(kind: IndexSymbolKind) -> String {
753 match kind {
754 IndexSymbolKind::Function => "function",
755 IndexSymbolKind::Method => "method",
756 IndexSymbolKind::Class => "class",
757 IndexSymbolKind::Struct => "struct",
758 IndexSymbolKind::Interface => "interface",
759 IndexSymbolKind::Trait => "trait",
760 IndexSymbolKind::Enum => "enum",
761 IndexSymbolKind::Constant => "constant",
762 IndexSymbolKind::Variable => "variable",
763 IndexSymbolKind::Module => "module",
764 IndexSymbolKind::Import => "import",
765 IndexSymbolKind::Export => "export",
766 IndexSymbolKind::TypeAlias => "type_alias",
767 IndexSymbolKind::Macro => "macro",
768 }
769 .to_owned()
770}
771
772fn format_visibility(vis: Visibility) -> String {
773 match vis {
774 Visibility::Public => "public",
775 Visibility::Private => "private",
776 Visibility::Protected => "protected",
777 Visibility::Internal => "internal",
778 }
779 .to_owned()
780}
781
782#[cfg(test)]
783mod tests {
784 use super::*;
785 use crate::index::types::{FileEntry, FileId, Language, Span, SymbolId};
786
787 fn create_test_index() -> (SymbolIndex, DepGraph) {
788 let mut index = SymbolIndex::default();
789
790 // Add test file
791 index.files.push(FileEntry {
792 id: FileId::new(0),
793 path: "test.py".to_owned(),
794 language: Language::Python,
795 symbols: 0..2,
796 imports: vec![],
797 content_hash: [0u8; 32],
798 lines: 25,
799 tokens: 100,
800 });
801
802 // Add test symbols
803 index.symbols.push(IndexSymbol {
804 id: SymbolId::new(0),
805 name: "main".to_owned(),
806 kind: IndexSymbolKind::Function,
807 file_id: FileId::new(0),
808 span: Span { start_line: 1, start_col: 0, end_line: 10, end_col: 0 },
809 signature: Some("def main()".to_owned()),
810 parent: None,
811 visibility: Visibility::Public,
812 docstring: None,
813 });
814
815 index.symbols.push(IndexSymbol {
816 id: SymbolId::new(1),
817 name: "helper".to_owned(),
818 kind: IndexSymbolKind::Function,
819 file_id: FileId::new(0),
820 span: Span { start_line: 12, start_col: 0, end_line: 20, end_col: 0 },
821 signature: Some("def helper()".to_owned()),
822 parent: None,
823 visibility: Visibility::Private,
824 docstring: None,
825 });
826
827 // Build lookup tables (including file_by_path)
828 index.rebuild_lookups();
829
830 // Create dependency graph with call edge: main -> helper
831 let mut graph = DepGraph::new();
832 graph.add_call(0, 1); // main calls helper
833
834 (index, graph)
835 }
836
837 #[test]
838 fn test_find_symbol() {
839 let (index, _graph) = create_test_index();
840
841 let results = find_symbol(&index, "main");
842 assert_eq!(results.len(), 1);
843 assert_eq!(results[0].name, "main");
844 assert_eq!(results[0].kind, "function");
845 assert_eq!(results[0].file, "test.py");
846 }
847
848 #[test]
849 fn test_get_callers() {
850 let (index, graph) = create_test_index();
851
852 // helper is called by main
853 let callers = get_callers_by_name(&index, &graph, "helper");
854 assert_eq!(callers.len(), 1);
855 assert_eq!(callers[0].name, "main");
856 }
857
858 #[test]
859 fn test_get_callees() {
860 let (index, graph) = create_test_index();
861
862 // main calls helper
863 let callees = get_callees_by_name(&index, &graph, "main");
864 assert_eq!(callees.len(), 1);
865 assert_eq!(callees[0].name, "helper");
866 }
867
868 #[test]
869 fn test_get_call_graph() {
870 let (index, graph) = create_test_index();
871
872 let call_graph = get_call_graph(&index, &graph);
873 assert_eq!(call_graph.nodes.len(), 2);
874 assert_eq!(call_graph.edges.len(), 1);
875 assert_eq!(call_graph.stats.functions, 2);
876
877 // Check edge
878 assert_eq!(call_graph.edges[0].caller, "main");
879 assert_eq!(call_graph.edges[0].callee, "helper");
880 }
881
882 #[test]
883 fn test_find_circular_dependencies_no_cycles() {
884 let (index, graph) = create_test_index();
885
886 // The test index has no file imports, so no cycles
887 let cycles = find_circular_dependencies(&index, &graph);
888 assert!(cycles.is_empty());
889 }
890
891 #[test]
892 fn test_find_circular_dependencies_with_cycle() {
893 let mut index = SymbolIndex::default();
894
895 // Create 3 files: a.py -> b.py -> c.py -> a.py (cycle)
896 index.files.push(FileEntry {
897 id: FileId::new(0),
898 path: "a.py".to_owned(),
899 language: Language::Python,
900 symbols: 0..0,
901 imports: vec![],
902 content_hash: [0u8; 32],
903 lines: 10,
904 tokens: 50,
905 });
906 index.files.push(FileEntry {
907 id: FileId::new(1),
908 path: "b.py".to_owned(),
909 language: Language::Python,
910 symbols: 0..0,
911 imports: vec![],
912 content_hash: [0u8; 32],
913 lines: 10,
914 tokens: 50,
915 });
916 index.files.push(FileEntry {
917 id: FileId::new(2),
918 path: "c.py".to_owned(),
919 language: Language::Python,
920 symbols: 0..0,
921 imports: vec![],
922 content_hash: [0u8; 32],
923 lines: 10,
924 tokens: 50,
925 });
926
927 index.rebuild_lookups();
928
929 let mut graph = DepGraph::new();
930 graph.add_file_import(0, 1); // a -> b
931 graph.add_file_import(1, 2); // b -> c
932 graph.add_file_import(2, 0); // c -> a (creates cycle)
933
934 let cycles = find_circular_dependencies(&index, &graph);
935 assert_eq!(cycles.len(), 1);
936 assert_eq!(cycles[0].length, 3);
937 assert!(cycles[0].files.contains(&"a.py".to_owned()));
938 assert!(cycles[0].files.contains(&"b.py".to_owned()));
939 assert!(cycles[0].files.contains(&"c.py".to_owned()));
940 }
941
942 #[test]
943 fn test_get_exported_symbols() {
944 let (index, _graph) = create_test_index();
945
946 // main is public, helper is private
947 let exports = get_exported_symbols(&index);
948 assert_eq!(exports.len(), 1);
949 assert_eq!(exports[0].name, "main");
950 assert_eq!(exports[0].visibility, "public");
951 }
952
953 #[test]
954 fn test_get_exported_symbols_in_file() {
955 let (index, _graph) = create_test_index();
956
957 let exports = get_exported_symbols_in_file(&index, "test.py");
958 assert_eq!(exports.len(), 1);
959 assert_eq!(exports[0].name, "main");
960
961 // Non-existent file returns empty
962 let no_exports = get_exported_symbols_in_file(&index, "nonexistent.py");
963 assert!(no_exports.is_empty());
964 }
965}