Skip to main content

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}