Skip to main content

reflex/
dependency.rs

1//! Dependency tracking and graph analysis
2//!
3//! This module provides functionality for tracking file dependencies (imports/includes)
4//! and analyzing the dependency graph of a codebase.
5//!
6//! # Architecture
7//!
8//! The system uses a "depth-1 storage" approach:
9//! - Only direct dependencies are stored in the database
10//! - Deeper relationships are computed on-demand via graph traversal
11//! - This provides O(n) storage while enabling any-depth queries
12//!
13//! # Example
14//!
15//! ```no_run
16//! use reflex::dependency::DependencyIndex;
17//! use reflex::cache::CacheManager;
18//!
19//! let cache = CacheManager::new(".");
20//! let deps = DependencyIndex::new(cache);
21//!
22//! // Get direct dependencies of a file
23//! let file_deps = deps.get_dependencies(42)?;
24//!
25//! // Get files that import this file (reverse lookup)
26//! let dependents = deps.get_dependents(42)?;
27//!
28//! // Traverse dependency graph to depth 3
29//! let transitive = deps.get_transitive_deps(42, 3)?;
30//! # Ok::<(), anyhow::Error>(())
31//! ```
32
33use anyhow::{Context, Result};
34use rusqlite::Connection;
35use std::collections::{HashMap, HashSet, VecDeque};
36use std::path::PathBuf;
37
38use crate::cache::CacheManager;
39use crate::models::{Dependency, DependencyInfo, ImportType};
40
41/// Manages dependency storage and graph operations
42pub struct DependencyIndex {
43    cache: Option<CacheManager>,
44    db_path: PathBuf,
45}
46
47impl DependencyIndex {
48    /// Create a new dependency index for the given cache
49    pub fn new(cache: CacheManager) -> Self {
50        let db_path = cache.path().join("meta.db");
51        Self { cache: Some(cache), db_path }
52    }
53
54    /// Create a dependency index pointing directly at a database file.
55    ///
56    /// Used by Pulse to run analysis against snapshot databases.
57    pub fn from_db_path(db_path: impl Into<PathBuf>) -> Self {
58        Self { cache: None, db_path: db_path.into() }
59    }
60
61    /// Get a reference to the cache manager.
62    ///
63    /// Panics if this index was created via `from_db_path()`.
64    pub fn get_cache(&self) -> &CacheManager {
65        self.cache.as_ref().expect("DependencyIndex created with from_db_path has no CacheManager")
66    }
67
68    /// Open a database connection to the backing store.
69    fn open_conn(&self) -> Result<Connection> {
70        Connection::open(&self.db_path).context("Failed to open database")
71    }
72
73    /// Insert a dependency into the database
74    ///
75    /// # Arguments
76    ///
77    /// * `file_id` - Source file ID
78    /// * `imported_path` - Import path as written in source
79    /// * `resolved_file_id` - Resolved target file ID (None if external/stdlib)
80    /// * `import_type` - Type of import (internal/external/stdlib)
81    /// * `line_number` - Line where import appears
82    /// * `imported_symbols` - Optional list of imported symbols
83    pub fn insert_dependency(
84        &self,
85        file_id: i64,
86        imported_path: String,
87        resolved_file_id: Option<i64>,
88        import_type: ImportType,
89        line_number: usize,
90        imported_symbols: Option<Vec<String>>,
91    ) -> Result<()> {
92        let conn = self.open_conn()?;
93
94        let import_type_str = match import_type {
95            ImportType::Internal => "internal",
96            ImportType::External => "external",
97            ImportType::Stdlib => "stdlib",
98        };
99
100        let symbols_json = imported_symbols
101            .as_ref()
102            .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
103
104        conn.execute(
105            "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
106             VALUES (?, ?, ?, ?, ?, ?)",
107            rusqlite::params![
108                file_id,
109                imported_path,
110                resolved_file_id,
111                import_type_str,
112                line_number as i64,
113                symbols_json,
114            ],
115        )?;
116
117        Ok(())
118    }
119
120    /// Insert an export into the database
121    ///
122    /// # Arguments
123    ///
124    /// * `file_id` - Source file ID containing the export statement
125    /// * `exported_symbol` - Symbol name being exported (None for wildcard exports)
126    /// * `source_path` - Path where the symbol is re-exported from
127    /// * `resolved_source_id` - Resolved target file ID (None if unresolved)
128    /// * `line_number` - Line where export appears
129    pub fn insert_export(
130        &self,
131        file_id: i64,
132        exported_symbol: Option<String>,
133        source_path: String,
134        resolved_source_id: Option<i64>,
135        line_number: usize,
136    ) -> Result<()> {
137        let conn = self.open_conn()?;
138
139        conn.execute(
140            "INSERT INTO file_exports (file_id, exported_symbol, source_path, resolved_source_id, line_number)
141             VALUES (?, ?, ?, ?, ?)",
142            rusqlite::params![
143                file_id,
144                exported_symbol,
145                source_path,
146                resolved_source_id,
147                line_number as i64,
148            ],
149        )?;
150
151        Ok(())
152    }
153
154    /// Batch insert multiple dependencies in a single transaction
155    ///
156    /// More efficient than individual inserts for bulk operations.
157    pub fn batch_insert_dependencies(&self, dependencies: &[Dependency]) -> Result<()> {
158        if dependencies.is_empty() {
159            return Ok(());
160        }
161
162        let mut conn = self.open_conn()?;
163
164        let tx = conn.transaction()?;
165
166        for dep in dependencies {
167            let import_type_str = match dep.import_type {
168                ImportType::Internal => "internal",
169                ImportType::External => "external",
170                ImportType::Stdlib => "stdlib",
171            };
172
173            let symbols_json = dep
174                .imported_symbols
175                .as_ref()
176                .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
177
178            tx.execute(
179                "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
180                 VALUES (?, ?, ?, ?, ?, ?)",
181                rusqlite::params![
182                    dep.file_id,
183                    dep.imported_path,
184                    dep.resolved_file_id,
185                    import_type_str,
186                    dep.line_number as i64,
187                    symbols_json,
188                ],
189            )?;
190        }
191
192        tx.commit()?;
193        log::debug!("Batch inserted {} dependencies", dependencies.len());
194        Ok(())
195    }
196
197    /// Get all direct dependencies for a file
198    ///
199    /// Returns a list of files/modules that this file imports.
200    pub fn get_dependencies(&self, file_id: i64) -> Result<Vec<Dependency>> {
201        let conn = self.open_conn()?;
202
203        let mut stmt = conn.prepare(
204            "SELECT file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols
205             FROM file_dependencies
206             WHERE file_id = ?
207             ORDER BY line_number",
208        )?;
209
210        let deps = stmt
211            .query_map([file_id], |row| {
212                let import_type_str: String = row.get(3)?;
213                let import_type = match import_type_str.as_str() {
214                    "internal" => ImportType::Internal,
215                    "external" => ImportType::External,
216                    "stdlib" => ImportType::Stdlib,
217                    _ => ImportType::External,
218                };
219
220                let symbols_json: Option<String> = row.get(5)?;
221                let imported_symbols = symbols_json.and_then(|json| {
222                    serde_json::from_str(&json).ok()
223                });
224
225                Ok(Dependency {
226                    file_id: row.get(0)?,
227                    imported_path: row.get(1)?,
228                    resolved_file_id: row.get(2)?,
229                    import_type,
230                    line_number: row.get::<_, i64>(4)? as usize,
231                    imported_symbols,
232                })
233            })?
234            .collect::<Result<Vec<_>, _>>()?;
235
236        Ok(deps)
237    }
238
239    /// Get all files that depend on this file (reverse lookup)
240    ///
241    /// Returns a list of file IDs that import this file.
242    /// Uses `resolved_file_id` column for instant SQL lookup (sub-10ms).
243    pub fn get_dependents(&self, file_id: i64) -> Result<Vec<i64>> {
244        let conn = self.open_conn()?;
245
246        // Pure SQL query on resolved_file_id (instant)
247        let mut stmt = conn.prepare(
248            "SELECT DISTINCT file_id
249             FROM file_dependencies
250             WHERE resolved_file_id = ?
251             ORDER BY file_id"
252        )?;
253
254        let dependents: Vec<i64> = stmt
255            .query_map([file_id], |row| row.get(0))?
256            .collect::<Result<Vec<_>, _>>()?;
257
258        Ok(dependents)
259    }
260
261    /// Get dependencies as DependencyInfo (for API output)
262    ///
263    /// Converts internal Dependency records to simplified DependencyInfo
264    /// suitable for JSON output.
265    pub fn get_dependencies_info(&self, file_id: i64) -> Result<Vec<DependencyInfo>> {
266        let deps = self.get_dependencies(file_id)?;
267
268        let dep_infos = deps
269            .into_iter()
270            .map(|dep| {
271                // Try to get the resolved path (all deps are internal now)
272                let path = if let Some(resolved_id) = dep.resolved_file_id {
273                    // Try to get the actual file path
274                    self.get_file_path(resolved_id).unwrap_or(dep.imported_path)
275                } else {
276                    dep.imported_path
277                };
278
279                DependencyInfo {
280                    path,
281                    line: Some(dep.line_number),
282                    symbols: dep.imported_symbols,
283                }
284            })
285            .collect();
286
287        Ok(dep_infos)
288    }
289
290    /// Get transitive dependencies up to a given depth
291    ///
292    /// Traverses the dependency graph using BFS to find all dependencies
293    /// reachable within the specified depth.
294    /// Uses `resolved_file_id` column for instant SQL lookup (sub-100ms).
295    ///
296    /// # Arguments
297    ///
298    /// * `file_id` - Starting file ID
299    /// * `max_depth` - Maximum traversal depth (0 = only direct deps)
300    ///
301    /// # Returns
302    ///
303    /// HashMap mapping file_id to depth (distance from start file)
304    pub fn get_transitive_deps(&self, file_id: i64, max_depth: usize) -> Result<HashMap<i64, usize>> {
305        let mut visited = HashMap::new();
306        let mut queue = VecDeque::new();
307
308        // Start with the initial file at depth 0
309        queue.push_back((file_id, 0));
310        visited.insert(file_id, 0);
311
312        while let Some((current_id, depth)) = queue.pop_front() {
313            if depth >= max_depth {
314                continue;
315            }
316
317            // Get direct dependencies using resolved_file_id (instant)
318            let deps = self.get_dependencies(current_id)?;
319
320            for dep in deps {
321                // Use resolved_file_id directly (already populated during indexing)
322                if let Some(resolved_id) = dep.resolved_file_id {
323                    // Only visit if we haven't seen it or found a shorter path
324                    if !visited.contains_key(&resolved_id) {
325                        visited.insert(resolved_id, depth + 1);
326                        queue.push_back((resolved_id, depth + 1));
327                    }
328                }
329            }
330        }
331
332        Ok(visited)
333    }
334
335    /// Detect circular dependencies in the entire codebase
336    ///
337    /// Uses depth-first search to find cycles in the dependency graph.
338    /// Uses `resolved_file_id` column for instant SQL lookup (sub-100ms).
339    ///
340    /// Returns a list of cycle paths, where each cycle is represented as
341    /// a vector of file IDs forming the cycle.
342    pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
343        let conn = self.open_conn()?;
344
345        // Build in-memory dependency graph using resolved_file_id (instant)
346        let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
347
348        let mut stmt = conn.prepare(
349            "SELECT file_id, resolved_file_id
350             FROM file_dependencies
351             WHERE resolved_file_id IS NOT NULL"
352        )?;
353
354        let dependencies: Vec<(i64, i64)> = stmt
355            .query_map([], |row| {
356                Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
357            })?
358            .collect::<Result<Vec<_>, _>>()?;
359
360        // Build adjacency list directly from resolved IDs
361        for (file_id, target_id) in dependencies {
362            graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
363        }
364
365        // Get all file IDs for traversal
366        let all_files = self.get_all_file_ids()?;
367
368        let mut visited = HashSet::new();
369        let mut rec_stack = HashSet::new();
370        let mut path = Vec::new();
371        let mut cycles = Vec::new();
372
373        for file_id in all_files {
374            if !visited.contains(&file_id) {
375                self.dfs_cycle_detect(
376                    file_id,
377                    &graph,
378                    &mut visited,
379                    &mut rec_stack,
380                    &mut path,
381                    &mut cycles,
382                )?;
383            }
384        }
385
386        Ok(cycles)
387    }
388
389    /// DFS helper for cycle detection using pre-built graph
390    fn dfs_cycle_detect(
391        &self,
392        file_id: i64,
393        graph: &HashMap<i64, Vec<i64>>,
394        visited: &mut HashSet<i64>,
395        rec_stack: &mut HashSet<i64>,
396        path: &mut Vec<i64>,
397        cycles: &mut Vec<Vec<i64>>,
398    ) -> Result<()> {
399        visited.insert(file_id);
400        rec_stack.insert(file_id);
401        path.push(file_id);
402
403        // Get dependencies from the pre-built graph
404        if let Some(dependencies) = graph.get(&file_id) {
405            for &target_id in dependencies {
406                if !visited.contains(&target_id) {
407                    self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
408                } else if rec_stack.contains(&target_id) {
409                    // Found a cycle! Extract it from path
410                    if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
411                        let cycle = path[cycle_start..].to_vec();
412                        cycles.push(cycle);
413                    }
414                }
415            }
416        }
417
418        path.pop();
419        rec_stack.remove(&file_id);
420
421        Ok(())
422    }
423
424    /// Get file paths for a list of file IDs
425    ///
426    /// Useful for converting file ID results to human-readable paths.
427    pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
428        let conn = self.open_conn()?;
429
430        let mut paths = HashMap::new();
431
432        for &file_id in file_ids {
433            if let Ok(path) = conn.query_row(
434                "SELECT path FROM files WHERE id = ?",
435                [file_id],
436                |row| row.get::<_, String>(0),
437            ) {
438                paths.insert(file_id, path);
439            }
440        }
441
442        Ok(paths)
443    }
444
445    /// Get file path for a single file ID
446    fn get_file_path(&self, file_id: i64) -> Result<String> {
447        let conn = self.open_conn()?;
448
449        let path = conn.query_row(
450            "SELECT path FROM files WHERE id = ?",
451            [file_id],
452            |row| row.get::<_, String>(0),
453        )?;
454
455        Ok(path)
456    }
457
458    /// Get all file IDs in the database
459    fn get_all_file_ids(&self) -> Result<Vec<i64>> {
460        let conn = self.open_conn()?;
461
462        let mut stmt = conn.prepare("SELECT id FROM files")?;
463        let file_ids = stmt
464            .query_map([], |row| row.get(0))?
465            .collect::<Result<Vec<_>, _>>()?;
466
467        Ok(file_ids)
468    }
469
470    /// Find hotspots (most imported files)
471    ///
472    /// Returns a list of (file_id, count) tuples sorted by import count descending.
473    ///
474    /// Uses `resolved_file_id` column for instant SQL aggregation (sub-100ms).
475    ///
476    /// # Arguments
477    ///
478    /// * `limit` - Maximum number of hotspots to return (None = all)
479    /// * `min_dependents` - Minimum number of imports required to be a hotspot (default: 2)
480    pub fn find_hotspots(&self, limit: Option<usize>, min_dependents: usize) -> Result<Vec<(i64, usize)>> {
481        let conn = self.open_conn()?;
482
483        // Pure SQL aggregation on resolved_file_id (instant)
484        let mut stmt = conn.prepare(
485            "SELECT resolved_file_id, COUNT(*) as count
486             FROM file_dependencies
487             WHERE resolved_file_id IS NOT NULL
488             GROUP BY resolved_file_id
489             ORDER BY count DESC"
490        )?;
491
492        // Get all hotspots and filter by minimum dependent count
493        let mut hotspots: Vec<(i64, usize)> = stmt
494            .query_map([], |row| {
495                Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
496            })?
497            .collect::<Result<Vec<_>, _>>()?
498            .into_iter()
499            .filter(|(_, count)| *count >= min_dependents)
500            .collect();
501
502        // Apply limit if specified
503        if let Some(lim) = limit {
504            hotspots.truncate(lim);
505        }
506
507        Ok(hotspots)
508    }
509
510    /// Find unused files (files with no incoming dependencies)
511    ///
512    /// Files that are never imported are potential candidates for deletion.
513    /// Uses `resolved_file_id` column for instant SQL lookup (sub-10ms).
514    ///
515    /// **Barrel Export Resolution**: This function now follows barrel export chains
516    /// to detect files that are indirectly imported via re-exports. For example:
517    /// - `WithLabel.vue` exported by `packages/ui/components/index.ts`
518    /// - App imports `@packages/ui/components` (resolves to index.ts)
519    /// - This function follows the export chain and marks `WithLabel.vue` as used
520    pub fn find_unused_files(&self) -> Result<Vec<i64>> {
521        let conn = self.open_conn()?;
522
523        // Build set of used files by following barrel export chains
524        let mut used_files = HashSet::new();
525
526        // Step 1: Get all files directly referenced in resolved_file_id
527        let mut stmt = conn.prepare(
528            "SELECT DISTINCT resolved_file_id
529             FROM file_dependencies
530             WHERE resolved_file_id IS NOT NULL"
531        )?;
532
533        let direct_imports: Vec<i64> = stmt
534            .query_map([], |row| row.get(0))?
535            .collect::<Result<Vec<_>, _>>()?;
536
537        used_files.extend(&direct_imports);
538
539        // Step 2: For each direct import, follow barrel export chains
540        for file_id in direct_imports {
541            // Resolve through barrel exports to find all indirectly used files
542            let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
543            used_files.extend(barrel_chain);
544        }
545
546        // Step 3: Get all files NOT in the used set
547        let mut stmt = conn.prepare("SELECT id FROM files ORDER BY id")?;
548        let all_files: Vec<i64> = stmt
549            .query_map([], |row| row.get(0))?
550            .collect::<Result<Vec<_>, _>>()?;
551
552        let unused: Vec<i64> = all_files
553            .into_iter()
554            .filter(|id| !used_files.contains(id))
555            .collect();
556
557        Ok(unused)
558    }
559
560    /// Resolve barrel export chains to find all files transitively exported from a given file
561    ///
562    /// Given a barrel file (e.g., `index.ts` that re-exports from other files), this function
563    /// follows the export chain to find all source files that are transitively exported.
564    ///
565    /// # Example
566    ///
567    /// If `packages/ui/components/index.ts` contains:
568    /// ```typescript
569    /// export { default as WithLabel } from './WithLabel.vue';
570    /// export { default as Button } from './Button.vue';
571    /// ```
572    ///
573    /// Then calling this with the file_id of `index.ts` will return the file IDs of
574    /// `WithLabel.vue` and `Button.vue`.
575    ///
576    /// # Arguments
577    ///
578    /// * `barrel_file_id` - File ID of the barrel file to start from
579    ///
580    /// # Returns
581    ///
582    /// Vec of file IDs that are transitively exported (includes the barrel file itself)
583    pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
584        let conn = self.open_conn()?;
585
586        let mut resolved_files = Vec::new();
587        let mut visited = HashSet::new();
588        let mut queue = VecDeque::new();
589
590        // Start with the barrel file itself
591        queue.push_back(barrel_file_id);
592        visited.insert(barrel_file_id);
593
594        while let Some(current_id) = queue.pop_front() {
595            resolved_files.push(current_id);
596
597            // Get all exports from this file
598            let mut stmt = conn.prepare(
599                "SELECT resolved_source_id
600                 FROM file_exports
601                 WHERE file_id = ? AND resolved_source_id IS NOT NULL"
602            )?;
603
604            let exported_files: Vec<i64> = stmt
605                .query_map([current_id], |row| row.get(0))?
606                .collect::<Result<Vec<_>, _>>()?;
607
608            // Follow each exported file
609            for exported_id in exported_files {
610                if !visited.contains(&exported_id) {
611                    visited.insert(exported_id);
612                    queue.push_back(exported_id);
613                }
614            }
615        }
616
617        Ok(resolved_files)
618    }
619
620    /// Find disconnected components (islands) in the dependency graph
621    ///
622    /// An "island" is a connected component - a group of files that depend on each
623    /// other (directly or transitively) but have no dependencies to files outside
624    /// the group.
625    ///
626    /// This is useful for identifying:
627    /// - Independent subsystems that could be extracted as separate modules
628    /// - Unreachable code clusters that might be dead code
629    /// - Microservice boundaries in a monolith
630    ///
631    /// Returns a list of islands, where each island is a vector of file IDs.
632    /// Islands are sorted by size (largest first).
633    pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
634        let conn = self.open_conn()?;
635
636        // Build undirected dependency graph (A imports B => edge A-B and B-A)
637        let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
638
639        let mut stmt = conn.prepare(
640            "SELECT file_id, resolved_file_id
641             FROM file_dependencies
642             WHERE resolved_file_id IS NOT NULL"
643        )?;
644
645        let dependencies: Vec<(i64, i64)> = stmt
646            .query_map([], |row| {
647                Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
648            })?
649            .collect::<Result<Vec<_>, _>>()?;
650
651        // Build adjacency list (undirected) directly from resolved IDs
652        for (file_id, target_id) in dependencies {
653            // Add edge in both directions for undirected graph
654            graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
655            graph.entry(target_id).or_insert_with(Vec::new).push(file_id);
656        }
657
658        // Get all file IDs (including isolated files with no dependencies)
659        let all_files = self.get_all_file_ids()?;
660
661        // Ensure all files are in the graph (even if they have no edges)
662        for file_id in &all_files {
663            graph.entry(*file_id).or_insert_with(Vec::new);
664        }
665
666        // Find connected components using DFS
667        let mut visited = HashSet::new();
668        let mut islands = Vec::new();
669
670        for &file_id in &all_files {
671            if !visited.contains(&file_id) {
672                let mut island = Vec::new();
673                self.dfs_island(&file_id, &graph, &mut visited, &mut island);
674                islands.push(island);
675            }
676        }
677
678        // Sort islands by size (largest first)
679        islands.sort_by(|a, b| b.len().cmp(&a.len()));
680
681        log::info!("Found {} islands (connected components)", islands.len());
682
683        Ok(islands)
684    }
685
686    /// DFS helper for finding connected components (islands)
687    fn dfs_island(
688        &self,
689        file_id: &i64,
690        graph: &HashMap<i64, Vec<i64>>,
691        visited: &mut HashSet<i64>,
692        island: &mut Vec<i64>,
693    ) {
694        visited.insert(*file_id);
695        island.push(*file_id);
696
697        if let Some(neighbors) = graph.get(file_id) {
698            for &neighbor in neighbors {
699                if !visited.contains(&neighbor) {
700                    self.dfs_island(&neighbor, graph, visited, island);
701                }
702            }
703        }
704    }
705
706    /// Build a cache of imported_path → file_id mappings for efficient lookup
707    ///
708    /// This method queries all unique imported_path values from the database
709    /// and resolves each one to a file_id using fuzzy matching. The resulting
710    /// cache enables O(1) lookups instead of repeated database queries.
711    ///
712    /// This is used internally by graph analysis operations (hotspots, circular
713    /// dependencies, reverse lookups, etc.) to avoid O(N*M*K) query complexity.
714    ///
715    /// # Performance
716    ///
717    /// Building the cache requires O(N*M) queries where:
718    /// - N = number of unique imported_path values (~1,000-5,000)
719    /// - M = average number of path variants tried per path (~10)
720    ///
721    /// However, this is done ONCE upfront, enabling O(1) lookups for all
722    /// subsequent operations. Without caching, each operation would make
723    /// 10,000-100,000+ queries.
724    ///
725    /// # Returns
726    ///
727    /// HashMap mapping imported_path to resolved file_id (only includes
728    /// successfully resolved paths; external/unresolved paths are omitted)
729    fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
730        let conn = self.open_conn()?;
731
732        // Get all unique imported_path values (single query)
733        let mut stmt = conn.prepare(
734            "SELECT DISTINCT imported_path FROM file_dependencies"
735        )?;
736
737        let imported_paths: Vec<String> = stmt
738            .query_map([], |row| row.get(0))?
739            .collect::<Result<Vec<_>, _>>()?;
740
741        let total_paths = imported_paths.len();
742        log::info!("Building resolution cache for {} unique imported paths", total_paths);
743
744        // Resolve each imported_path once
745        let mut cache = HashMap::new();
746
747        for imported_path in imported_paths {
748            if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
749                cache.insert(imported_path, file_id);
750            }
751        }
752
753        log::info!(
754            "Resolution cache built: {} resolved, {} unresolved",
755            cache.len(),
756            total_paths - cache.len()
757        );
758
759        Ok(cache)
760    }
761
762    /// Clear all dependencies for a file (used during incremental reindexing)
763    pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
764        let conn = self.open_conn()?;
765
766        conn.execute(
767            "DELETE FROM file_dependencies WHERE file_id = ?",
768            [file_id],
769        )?;
770
771        Ok(())
772    }
773
774    /// Resolve an imported path to a file ID using fuzzy matching
775    ///
776    /// This method converts an import path (e.g., namespace, module path) to various
777    /// file path variants and tries to find a matching file using fuzzy path matching.
778    ///
779    /// # Arguments
780    ///
781    /// * `imported_path` - The import path as stored in the database
782    ///   (e.g., "Rcm\\Http\\Controllers\\Controller", "crate::models", etc.)
783    ///
784    /// # Returns
785    ///
786    /// `Some(file_id)` if exactly one matching file is found, `None` otherwise
787    ///
788    /// # Examples
789    ///
790    /// - `Rcm\\Http\\Controllers\\Controller` → finds `services/php/rcm-backend/app/Http/Controllers/Controller.php`
791    /// - `crate::models` → finds `src/models.rs`
792    pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
793        let path_variants = generate_path_variants(imported_path);
794
795        for variant in &path_variants {
796            if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
797                log::trace!("Resolved '{}' → '{}' (file_id: {})", imported_path, variant, file_id);
798                return Ok(Some(file_id));
799            }
800        }
801
802        Ok(None)
803    }
804
805    /// Get file ID by path with fuzzy matching support
806    ///
807    /// Supports various path formats:
808    /// - Exact paths: `services/php/app/Http/Controllers/FooController.php`
809    /// - Relative paths: `./services/php/app/Http/Controllers/FooController.php`
810    /// - Path fragments: `Controllers/FooController.php` or `FooController.php`
811    /// - Absolute paths: `/home/user/project/services/php/.../FooController.php`
812    ///
813    /// Returns None if no matches found.
814    /// Returns error if multiple matches found (ambiguous path fragment).
815    pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
816        let conn = self.open_conn()?;
817
818        // Normalize path: strip ./ prefix, ../ prefix, and convert absolute to relative
819        let normalized_path = normalize_path_for_lookup(path);
820
821        // Try exact match first (fast path)
822        match conn.query_row(
823            "SELECT id FROM files WHERE path = ?",
824            [&normalized_path],
825            |row| row.get::<_, i64>(0),
826        ) {
827            Ok(id) => return Ok(Some(id)),
828            Err(rusqlite::Error::QueryReturnedNoRows) => {
829                // No exact match, try suffix match
830            }
831            Err(e) => return Err(e.into()),
832        }
833
834        // Try suffix match: find all files whose path ends with the normalized_path
835        let mut stmt = conn.prepare(
836            "SELECT id, path FROM files WHERE path LIKE '%' || ?"
837        )?;
838
839        let matches: Vec<(i64, String)> = stmt
840            .query_map([&normalized_path], |row| {
841                Ok((row.get(0)?, row.get(1)?))
842            })?
843            .collect::<Result<Vec<_>, _>>()?;
844
845        match matches.len() {
846            0 => Ok(None),
847            1 => Ok(Some(matches[0].0)),
848            _ => {
849                // Multiple matches - return error with suggestions
850                let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
851                anyhow::bail!(
852                    "Ambiguous path '{}' matches multiple files:\n  {}\n\nPlease be more specific.",
853                    path,
854                    paths.join("\n  ")
855                );
856            }
857        }
858    }
859
860    /// Get dependency resolution statistics grouped by language
861    ///
862    /// Returns statistics showing how many internal dependencies are resolved vs unresolved
863    /// for each language in the project.
864    ///
865    /// # Returns
866    ///
867    /// A vector of tuples: (language, total_deps, resolved_deps, resolution_rate)
868    pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
869        let conn = self.open_conn()?;
870
871        let mut stmt = conn.prepare(
872            "SELECT
873                CASE
874                    WHEN f.path LIKE '%.py' THEN 'Python'
875                    WHEN f.path LIKE '%.go' THEN 'Go'
876                    WHEN f.path LIKE '%.ts' THEN 'TypeScript'
877                    WHEN f.path LIKE '%.rs' THEN 'Rust'
878                    WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
879                    WHEN f.path LIKE '%.php' THEN 'PHP'
880                    WHEN f.path LIKE '%.java' THEN 'Java'
881                    WHEN f.path LIKE '%.kt' THEN 'Kotlin'
882                    WHEN f.path LIKE '%.rb' THEN 'Ruby'
883                    WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
884                    WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
885                    WHEN f.path LIKE '%.cs' THEN 'C#'
886                    WHEN f.path LIKE '%.zig' THEN 'Zig'
887                    ELSE 'Other'
888                END as language,
889                COUNT(*) as total,
890                SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
891            FROM file_dependencies d
892            JOIN files f ON d.file_id = f.id
893            WHERE d.import_type = 'internal'
894            GROUP BY language
895            ORDER BY language",
896        )?;
897
898        let mut stats = Vec::new();
899
900        let rows = stmt.query_map([], |row| {
901            let language: String = row.get(0)?;
902            let total: i64 = row.get(1)?;
903            let resolved: i64 = row.get(2)?;
904            let rate = if total > 0 {
905                (resolved as f64 / total as f64) * 100.0
906            } else {
907                0.0
908            };
909
910            Ok((language, total as usize, resolved as usize, rate))
911        })?;
912
913        for row in rows {
914            stats.push(row?);
915        }
916
917        Ok(stats)
918    }
919
920    /// Get all internal dependencies with their resolution status
921    ///
922    /// Returns detailed information about each internal dependency including source file,
923    /// imported path, and whether it was successfully resolved.
924    ///
925    /// # Returns
926    ///
927    /// A vector of tuples: (source_file, imported_path, resolved_file_path)
928    /// where resolved_file_path is None if the dependency couldn't be resolved.
929    pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
930        let conn = self.open_conn()?;
931
932        let mut stmt = conn.prepare(
933            "SELECT
934                f.path,
935                d.imported_path,
936                f2.path as resolved_path
937            FROM file_dependencies d
938            JOIN files f ON d.file_id = f.id
939            LEFT JOIN files f2 ON d.resolved_file_id = f2.id
940            WHERE d.import_type = 'internal'
941            ORDER BY f.path",
942        )?;
943
944        let mut deps = Vec::new();
945
946        let rows = stmt.query_map([], |row| {
947            Ok((
948                row.get::<_, String>(0)?,
949                row.get::<_, String>(1)?,
950                row.get::<_, Option<String>>(2)?,
951            ))
952        })?;
953
954        for row in rows {
955            deps.push(row?);
956        }
957
958        Ok(deps)
959    }
960
961    /// Get total count of dependencies by type (for debugging)
962    pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
963        let conn = self.open_conn()?;
964
965        let mut stmt = conn.prepare(
966            "SELECT import_type, COUNT(*) as count
967             FROM file_dependencies
968             GROUP BY import_type
969             ORDER BY import_type",
970        )?;
971
972        let mut counts = Vec::new();
973
974        let rows = stmt.query_map([], |row| {
975            Ok((
976                row.get::<_, String>(0)?,
977                row.get::<_, i64>(1)? as usize,
978            ))
979        })?;
980
981        for row in rows {
982            counts.push(row?);
983        }
984
985        Ok(counts)
986    }
987}
988
989/// Generate path variants for an import path
990///
991/// Converts a namespace/import path to multiple file path variants for fuzzy matching.
992/// Tries progressively shorter paths to handle custom PSR-4 mappings.
993///
994/// Examples:
995/// - `Rcm\\Http\\Controllers\\Controller` →
996///   - `Rcm/Http/Controllers/Controller.php`
997///   - `Http/Controllers/Controller.php`
998///   - `Controllers/Controller.php`
999///   - `Controller.php`
1000fn generate_path_variants(import_path: &str) -> Vec<String> {
1001    // Convert namespace separators to path separators
1002    let path = import_path.replace('\\', "/").replace("::", "/");
1003
1004    // Remove quotes if present (some languages quote import paths)
1005    let path = path.trim_matches('"').trim_matches('\'');
1006
1007    // Split into components
1008    let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1009
1010    if components.is_empty() {
1011        return vec![];
1012    }
1013
1014    let mut variants = Vec::new();
1015
1016    // Generate progressively shorter paths
1017    // E.g., for "Rcm/Http/Controllers/Controller":
1018    // 1. Rcm/Http/Controllers/Controller.php (full path)
1019    // 2. Http/Controllers/Controller.php (without first component)
1020    // 3. Controllers/Controller.php (without first two)
1021    // 4. Controller.php (just the class name)
1022    for start_idx in 0..components.len() {
1023        let suffix = components[start_idx..].join("/");
1024
1025        // Try with .php extension (most common)
1026        if !suffix.ends_with(".php") {
1027            variants.push(format!("{}.php", suffix));
1028        } else {
1029            variants.push(suffix.clone());
1030        }
1031
1032        // Also try without extension (for languages that don't use extensions in imports)
1033        if !suffix.contains('.') {
1034            // Try common extensions
1035            variants.push(format!("{}.rs", suffix));
1036            variants.push(format!("{}.ts", suffix));
1037            variants.push(format!("{}.js", suffix));
1038            variants.push(format!("{}.py", suffix));
1039        }
1040    }
1041
1042    variants
1043}
1044
1045/// Normalize a path for fuzzy lookup
1046///
1047/// Strips common prefixes that might differ between query and database:
1048/// - `./` and `../` prefixes
1049/// - Absolute paths (converts to relative by taking only the path component)
1050///
1051/// Examples:
1052/// - `./services/foo.php` → `services/foo.php`
1053/// - `/home/user/project/services/foo.php` → `services/foo.php` (just filename portion)
1054/// - `GetCaseByBatchNumberController.php` → `GetCaseByBatchNumberController.php`
1055fn normalize_path_for_lookup(path: &str) -> String {
1056    // Strip ./ and ../ prefixes
1057    let mut normalized = path.trim_start_matches("./").to_string();
1058    if normalized.starts_with("../") {
1059        normalized = normalized.trim_start_matches("../").to_string();
1060    }
1061
1062    // If it's an absolute path, extract the relevant portion
1063    // This handles cases like `/home/user/Code/project/services/php/...`
1064    // We want to extract just `services/php/...` part
1065    if normalized.starts_with('/') || normalized.starts_with('\\') {
1066        // Common project markers (ordered by priority)
1067        let markers = ["services", "src", "app", "lib", "packages", "modules"];
1068
1069        let mut found_marker = false;
1070        for marker in &markers {
1071            if let Some(idx) = normalized.find(marker) {
1072                normalized = normalized[idx..].to_string();
1073                found_marker = true;
1074                break;
1075            }
1076        }
1077
1078        // If no marker found, just use the filename
1079        if !found_marker {
1080            use std::path::Path;
1081            let path_obj = Path::new(&normalized);
1082            if let Some(filename) = path_obj.file_name() {
1083                normalized = filename.to_string_lossy().to_string();
1084            }
1085        }
1086    }
1087
1088    normalized
1089}
1090
1091/// Resolve a Rust import path to an absolute file path
1092///
1093/// This function handles Rust-specific path resolution rules:
1094/// - `crate::` - Starts from crate root (src/lib.rs or src/main.rs)
1095/// - `super::` - Goes up one module level
1096/// - `self::` - Stays in current module
1097/// - `mod name` - Looks for name.rs or name/mod.rs
1098/// - External crates - Returns None
1099///
1100/// # Arguments
1101///
1102/// * `import_path` - The import path as written in source (e.g., "crate::models::Language")
1103/// * `current_file` - Path to the file containing the import (e.g., "src/query.rs")
1104/// * `project_root` - Root directory of the project
1105///
1106/// # Returns
1107///
1108/// `Some(path)` if the import resolves to a project file, `None` if it's external/stdlib
1109pub fn resolve_rust_import(
1110    import_path: &str,
1111    current_file: &str,
1112    project_root: &std::path::Path,
1113) -> Option<String> {
1114    use std::path::{Path, PathBuf};
1115
1116    // External crates and stdlib - don't resolve
1117    if !import_path.starts_with("crate::")
1118        && !import_path.starts_with("super::")
1119        && !import_path.starts_with("self::")
1120    {
1121        return None;
1122    }
1123
1124    let current_path = Path::new(current_file);
1125    let mut resolved_path: Option<PathBuf> = None;
1126
1127    if import_path.starts_with("crate::") {
1128        // Start from crate root (src/lib.rs or src/main.rs)
1129        let crate_root = if project_root.join("src/lib.rs").exists() {
1130            project_root.join("src")
1131        } else if project_root.join("src/main.rs").exists() {
1132            project_root.join("src")
1133        } else {
1134            // Fallback to src/ directory
1135            project_root.join("src")
1136        };
1137
1138        let path_parts: Vec<&str> = import_path
1139            .strip_prefix("crate::")
1140            .unwrap()
1141            .split("::")
1142            .collect();
1143
1144        resolved_path = resolve_module_path(&crate_root, &path_parts);
1145    } else if import_path.starts_with("super::") {
1146        // Go up one directory from current file's parent (the current module's parent)
1147        if let Some(current_dir) = current_path.parent() {
1148            if let Some(parent_dir) = current_dir.parent() {
1149                let path_parts: Vec<&str> = import_path
1150                    .strip_prefix("super::")
1151                    .unwrap()
1152                    .split("::")
1153                    .collect();
1154
1155                resolved_path = resolve_module_path(parent_dir, &path_parts);
1156            }
1157        }
1158    } else if import_path.starts_with("self::") {
1159        // Stay in current directory
1160        if let Some(current_dir) = current_path.parent() {
1161            let path_parts: Vec<&str> = import_path
1162                .strip_prefix("self::")
1163                .unwrap()
1164                .split("::")
1165                .collect();
1166
1167            resolved_path = resolve_module_path(current_dir, &path_parts);
1168        }
1169    }
1170
1171    // Convert to string and make relative to project root
1172    resolved_path.and_then(|p| {
1173        p.strip_prefix(project_root)
1174            .ok()
1175            .map(|rel| rel.to_string_lossy().to_string())
1176    })
1177}
1178
1179/// Resolve a module path given a starting directory and path components
1180///
1181/// Handles Rust's module system rules:
1182/// - `foo` → check foo.rs or foo/mod.rs
1183/// - `foo::bar` → check foo/bar.rs or foo/bar/mod.rs
1184fn resolve_module_path(start_dir: &std::path::Path, components: &[&str]) -> Option<std::path::PathBuf> {
1185
1186    if components.is_empty() {
1187        return None;
1188    }
1189
1190    let mut current = start_dir.to_path_buf();
1191
1192    // For all components except the last, they must be directories
1193    for &component in &components[..components.len() - 1] {
1194        // Try as a directory with mod.rs
1195        let dir_path = current.join(component);
1196        let mod_file = dir_path.join("mod.rs");
1197
1198        if mod_file.exists() {
1199            current = dir_path;
1200        } else {
1201            // Component must be a directory for nested paths
1202            return None;
1203        }
1204    }
1205
1206    // For the last component, try both file.rs and file/mod.rs
1207    let last_component = components.last().unwrap();
1208
1209    // Try as a single file
1210    let file_path = current.join(format!("{}.rs", last_component));
1211    if file_path.exists() {
1212        return Some(file_path);
1213    }
1214
1215    // Try as a directory with mod.rs
1216    let dir_path = current.join(last_component);
1217    let mod_file = dir_path.join("mod.rs");
1218    if mod_file.exists() {
1219        return Some(mod_file);
1220    }
1221
1222    None
1223}
1224
1225/// Resolve a `mod` declaration to a file path
1226///
1227/// For `mod parser;`, this checks for:
1228/// - `parser.rs` (sibling file)
1229/// - `parser/mod.rs` (directory module)
1230pub fn resolve_rust_mod_declaration(
1231    mod_name: &str,
1232    current_file: &str,
1233    _project_root: &std::path::Path,
1234) -> Option<String> {
1235    use std::path::Path;
1236
1237    let current_path = Path::new(current_file);
1238    let current_dir = current_path.parent()?;
1239
1240    // Try sibling file
1241    let sibling = current_dir.join(format!("{}.rs", mod_name));
1242    if sibling.exists() {
1243        return Some(sibling.to_string_lossy().to_string());
1244    }
1245
1246    // Try directory module
1247    let dir_mod = current_dir.join(mod_name).join("mod.rs");
1248    if dir_mod.exists() {
1249        return Some(dir_mod.to_string_lossy().to_string());
1250    }
1251
1252    None
1253}
1254
1255/// Resolve a PHP import path to a file path
1256///
1257/// This function handles PHP-specific namespace-to-file mapping:
1258/// - Converts backslash-separated namespaces to forward-slash paths
1259/// - Handles PSR-4 autoloading conventions
1260/// - Filters out external vendor namespaces (returns None for non-project code)
1261///
1262/// # Arguments
1263///
1264/// * `import_path` - PHP namespace path (e.g., "App\\Http\\Controllers\\UserController")
1265/// * `current_file` - Not used for PHP (PHP uses absolute namespaces)
1266/// * `project_root` - Root directory of the project
1267///
1268/// # Returns
1269///
1270/// `Some(path)` if the import resolves to a project file, `None` if it's external/stdlib
1271///
1272/// # Examples
1273///
1274/// - `App\\Http\\Controllers\\FooController` → `app/Http/Controllers/FooController.php`
1275/// - `App\\Models\\User` → `app/Models/User.php`
1276/// - `Illuminate\\Database\\Migration` → `None` (external vendor namespace)
1277pub fn resolve_php_import(
1278    import_path: &str,
1279    _current_file: &str,
1280    project_root: &std::path::Path,
1281) -> Option<String> {
1282    // External vendor namespaces (Laravel, Symfony, etc.) - don't resolve
1283    const VENDOR_NAMESPACES: &[&str] = &[
1284        "Illuminate\\", "Symfony\\", "Laravel\\", "Psr\\",
1285        "Doctrine\\", "Monolog\\", "PHPUnit\\", "Carbon\\",
1286        "GuzzleHttp\\", "Composer\\", "Predis\\", "League\\"
1287    ];
1288
1289    // Check if this is a vendor namespace
1290    for vendor_ns in VENDOR_NAMESPACES {
1291        if import_path.starts_with(vendor_ns) {
1292            return None;
1293        }
1294    }
1295
1296    // Convert namespace to file path
1297    // PHP namespaces use backslashes: App\Http\Controllers\FooController
1298    // Files use forward slashes: app/Http/Controllers/FooController.php
1299    let file_path = import_path.replace('\\', "/");
1300
1301    // Try common PSR-4 mappings (lowercase first component)
1302    // App\... → app/...
1303    // Database\... → database/...
1304    let path_candidates = vec![
1305        // Try with lowercase first component (PSR-4 standard)
1306        {
1307            let parts: Vec<&str> = file_path.split('/').collect();
1308            if let Some(first) = parts.first() {
1309                let mut result = vec![first.to_lowercase()];
1310                result.extend(parts[1..].iter().map(|s| s.to_string()));
1311                result.join("/") + ".php"
1312            } else {
1313                file_path.clone() + ".php"
1314            }
1315        },
1316        // Try exact path (some projects use exact case)
1317        file_path.clone() + ".php",
1318        // Try all lowercase (legacy projects)
1319        file_path.to_lowercase() + ".php",
1320    ];
1321
1322    // Check each candidate path
1323    for candidate in &path_candidates {
1324        let full_path = project_root.join(candidate);
1325        if full_path.exists() {
1326            // Return relative path
1327            return Some(candidate.clone());
1328        }
1329    }
1330
1331    // If no file found, return None (likely external or not yet created)
1332    None
1333}
1334
1335#[cfg(test)]
1336mod tests {
1337    use super::*;
1338    use tempfile::TempDir;
1339
1340    fn setup_test_cache() -> (TempDir, CacheManager) {
1341        let temp = TempDir::new().unwrap();
1342        let cache = CacheManager::new(temp.path());
1343        cache.init().unwrap();
1344
1345        // Add some test files
1346        cache.update_file("src/main.rs", "rust", 100).unwrap();
1347        cache.update_file("src/lib.rs", "rust", 50).unwrap();
1348        cache.update_file("src/utils.rs", "rust", 30).unwrap();
1349
1350        (temp, cache)
1351    }
1352
1353    #[test]
1354    fn test_insert_and_get_dependencies() {
1355        let (_temp, cache) = setup_test_cache();
1356        let deps_index = DependencyIndex::new(cache);
1357
1358        // Get file IDs
1359        let main_id = 1i64;
1360        let lib_id = 2i64;
1361
1362        // Insert a dependency: main.rs imports lib.rs
1363        deps_index
1364            .insert_dependency(
1365                main_id,
1366                "crate::lib".to_string(),
1367                Some(lib_id),
1368                ImportType::Internal,
1369                5,
1370                None,
1371            )
1372            .unwrap();
1373
1374        // Retrieve dependencies
1375        let deps = deps_index.get_dependencies(main_id).unwrap();
1376        assert_eq!(deps.len(), 1);
1377        assert_eq!(deps[0].imported_path, "crate::lib");
1378        assert_eq!(deps[0].resolved_file_id, Some(lib_id));
1379        assert_eq!(deps[0].import_type, ImportType::Internal);
1380    }
1381
1382    #[test]
1383    fn test_reverse_lookup() {
1384        let (_temp, cache) = setup_test_cache();
1385        let deps_index = DependencyIndex::new(cache);
1386
1387        let main_id = 1i64;
1388        let lib_id = 2i64;
1389        let utils_id = 3i64;
1390
1391        // main.rs imports lib.rs
1392        deps_index
1393            .insert_dependency(
1394                main_id,
1395                "crate::lib".to_string(),
1396                Some(lib_id),
1397                ImportType::Internal,
1398                5,
1399                None,
1400            )
1401            .unwrap();
1402
1403        // utils.rs also imports lib.rs
1404        deps_index
1405            .insert_dependency(
1406                utils_id,
1407                "crate::lib".to_string(),
1408                Some(lib_id),
1409                ImportType::Internal,
1410                3,
1411                None,
1412            )
1413            .unwrap();
1414
1415        // Get files that import lib.rs
1416        let dependents = deps_index.get_dependents(lib_id).unwrap();
1417        assert_eq!(dependents.len(), 2);
1418        assert!(dependents.contains(&main_id));
1419        assert!(dependents.contains(&utils_id));
1420    }
1421
1422    #[test]
1423    fn test_transitive_dependencies() {
1424        let (_temp, cache) = setup_test_cache();
1425        let deps_index = DependencyIndex::new(cache);
1426
1427        let file1 = 1i64;
1428        let file2 = 2i64;
1429        let file3 = 3i64;
1430
1431        // file1 → file2 → file3
1432        deps_index
1433            .insert_dependency(
1434                file1,
1435                "file2".to_string(),
1436                Some(file2),
1437                ImportType::Internal,
1438                1,
1439                None,
1440            )
1441            .unwrap();
1442
1443        deps_index
1444            .insert_dependency(
1445                file2,
1446                "file3".to_string(),
1447                Some(file3),
1448                ImportType::Internal,
1449                1,
1450                None,
1451            )
1452            .unwrap();
1453
1454        // Get transitive deps at depth 2
1455        let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
1456
1457        // Should include file1 (depth 0), file2 (depth 1), file3 (depth 2)
1458        assert_eq!(transitive.len(), 3);
1459        assert_eq!(transitive.get(&file1), Some(&0));
1460        assert_eq!(transitive.get(&file2), Some(&1));
1461        assert_eq!(transitive.get(&file3), Some(&2));
1462    }
1463
1464    #[test]
1465    fn test_batch_insert() {
1466        let (_temp, cache) = setup_test_cache();
1467        let deps_index = DependencyIndex::new(cache);
1468
1469        let deps = vec![
1470            Dependency {
1471                file_id: 1,
1472                imported_path: "std::collections".to_string(),
1473                resolved_file_id: None,
1474                import_type: ImportType::Stdlib,
1475                line_number: 1,
1476                imported_symbols: Some(vec!["HashMap".to_string()]),
1477            },
1478            Dependency {
1479                file_id: 1,
1480                imported_path: "crate::lib".to_string(),
1481                resolved_file_id: Some(2),
1482                import_type: ImportType::Internal,
1483                line_number: 2,
1484                imported_symbols: None,
1485            },
1486        ];
1487
1488        deps_index.batch_insert_dependencies(&deps).unwrap();
1489
1490        let retrieved = deps_index.get_dependencies(1).unwrap();
1491        assert_eq!(retrieved.len(), 2);
1492    }
1493
1494    #[test]
1495    fn test_clear_dependencies() {
1496        let (_temp, cache) = setup_test_cache();
1497        let deps_index = DependencyIndex::new(cache);
1498
1499        // Insert dependencies
1500        deps_index
1501            .insert_dependency(
1502                1,
1503                "crate::lib".to_string(),
1504                Some(2),
1505                ImportType::Internal,
1506                1,
1507                None,
1508            )
1509            .unwrap();
1510
1511        // Verify they exist
1512        assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
1513
1514        // Clear them
1515        deps_index.clear_dependencies(1).unwrap();
1516
1517        // Verify they're gone
1518        assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
1519    }
1520
1521    #[test]
1522    fn test_resolve_rust_import_crate() {
1523        use std::fs;
1524        use tempfile::TempDir;
1525
1526        let temp = TempDir::new().unwrap();
1527        let project_root = temp.path();
1528
1529        // Create directory structure
1530        fs::create_dir_all(project_root.join("src")).unwrap();
1531        fs::write(project_root.join("src/lib.rs"), "").unwrap();
1532        fs::write(project_root.join("src/models.rs"), "").unwrap();
1533
1534        // Test crate:: resolution
1535        let resolved = resolve_rust_import(
1536            "crate::models",
1537            "src/query.rs",
1538            project_root,
1539        );
1540
1541        assert_eq!(resolved, Some("src/models.rs".to_string()));
1542    }
1543
1544    #[test]
1545    fn test_resolve_rust_import_super() {
1546        use std::fs;
1547        use tempfile::TempDir;
1548
1549        let temp = TempDir::new().unwrap();
1550        let project_root = temp.path();
1551
1552        // Create directory structure: src/parsers/rust.rs needs to import src/models.rs
1553        fs::create_dir_all(project_root.join("src/parsers")).unwrap();
1554        fs::write(project_root.join("src/models.rs"), "").unwrap();
1555        fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
1556
1557        // Test super:: resolution from parsers/rust.rs
1558        // Use absolute path for current_file
1559        let current_file = project_root.join("src/parsers/rust.rs");
1560        let resolved = resolve_rust_import(
1561            "super::models",
1562            &current_file.to_string_lossy(),
1563            project_root,
1564        );
1565
1566        assert_eq!(resolved, Some("src/models.rs".to_string()));
1567    }
1568
1569    #[test]
1570    fn test_resolve_rust_import_external() {
1571        use tempfile::TempDir;
1572
1573        let temp = TempDir::new().unwrap();
1574        let project_root = temp.path();
1575
1576        // External crates should return None
1577        let resolved = resolve_rust_import(
1578            "serde::Serialize",
1579            "src/models.rs",
1580            project_root,
1581        );
1582
1583        assert_eq!(resolved, None);
1584
1585        // Stdlib should return None
1586        let resolved = resolve_rust_import(
1587            "std::collections::HashMap",
1588            "src/models.rs",
1589            project_root,
1590        );
1591
1592        assert_eq!(resolved, None);
1593    }
1594
1595    #[test]
1596    fn test_resolve_rust_mod_declaration() {
1597        use std::fs;
1598        use tempfile::TempDir;
1599
1600        let temp = TempDir::new().unwrap();
1601        let project_root = temp.path();
1602
1603        // Create directory structure
1604        fs::create_dir_all(project_root.join("src")).unwrap();
1605        fs::write(project_root.join("src/lib.rs"), "").unwrap();
1606        fs::write(project_root.join("src/parser.rs"), "").unwrap();
1607
1608        // Test mod declaration resolution
1609        let resolved = resolve_rust_mod_declaration(
1610            "parser",
1611            &project_root.join("src/lib.rs").to_string_lossy(),
1612            project_root,
1613        );
1614
1615        assert!(resolved.is_some());
1616        assert!(resolved.unwrap().ends_with("src/parser.rs"));
1617    }
1618
1619    #[test]
1620    fn test_resolve_rust_import_nested() {
1621        use std::fs;
1622        use tempfile::TempDir;
1623
1624        let temp = TempDir::new().unwrap();
1625        let project_root = temp.path();
1626
1627        // Create directory structure: src/models/language.rs
1628        fs::create_dir_all(project_root.join("src/models")).unwrap();
1629        fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
1630        fs::write(project_root.join("src/models/language.rs"), "").unwrap();
1631
1632        // Test nested module resolution
1633        let resolved = resolve_rust_import(
1634            "crate::models::language",
1635            "src/query.rs",
1636            project_root,
1637        );
1638
1639        assert_eq!(resolved, Some("src/models/language.rs".to_string()));
1640    }
1641}