1use anyhow::{Context, Result};
34use rusqlite::Connection;
35use std::collections::{HashMap, HashSet, VecDeque};
36
37use crate::cache::CacheManager;
38use crate::models::{Dependency, DependencyInfo, ImportType};
39
40pub struct DependencyIndex {
42 cache: CacheManager,
43}
44
45impl DependencyIndex {
46 pub fn new(cache: CacheManager) -> Self {
48 Self { cache }
49 }
50
51 pub fn get_cache(&self) -> &CacheManager {
53 &self.cache
54 }
55
56 pub fn insert_dependency(
67 &self,
68 file_id: i64,
69 imported_path: String,
70 resolved_file_id: Option<i64>,
71 import_type: ImportType,
72 line_number: usize,
73 imported_symbols: Option<Vec<String>>,
74 ) -> Result<()> {
75 let db_path = self.cache.path().join("meta.db");
76 let conn = Connection::open(&db_path)
77 .context("Failed to open meta.db for dependency insert")?;
78
79 let import_type_str = match import_type {
80 ImportType::Internal => "internal",
81 ImportType::External => "external",
82 ImportType::Stdlib => "stdlib",
83 };
84
85 let symbols_json = imported_symbols
86 .as_ref()
87 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
88
89 conn.execute(
90 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
91 VALUES (?, ?, ?, ?, ?, ?)",
92 rusqlite::params![
93 file_id,
94 imported_path,
95 resolved_file_id,
96 import_type_str,
97 line_number as i64,
98 symbols_json,
99 ],
100 )?;
101
102 Ok(())
103 }
104
105 pub fn insert_export(
115 &self,
116 file_id: i64,
117 exported_symbol: Option<String>,
118 source_path: String,
119 resolved_source_id: Option<i64>,
120 line_number: usize,
121 ) -> Result<()> {
122 let db_path = self.cache.path().join("meta.db");
123 let conn = Connection::open(&db_path)
124 .context("Failed to open meta.db for export insert")?;
125
126 conn.execute(
127 "INSERT INTO file_exports (file_id, exported_symbol, source_path, resolved_source_id, line_number)
128 VALUES (?, ?, ?, ?, ?)",
129 rusqlite::params![
130 file_id,
131 exported_symbol,
132 source_path,
133 resolved_source_id,
134 line_number as i64,
135 ],
136 )?;
137
138 Ok(())
139 }
140
141 pub fn batch_insert_dependencies(&self, dependencies: &[Dependency]) -> Result<()> {
145 if dependencies.is_empty() {
146 return Ok(());
147 }
148
149 let db_path = self.cache.path().join("meta.db");
150 let mut conn = Connection::open(&db_path)
151 .context("Failed to open meta.db for batch dependency insert")?;
152
153 let tx = conn.transaction()?;
154
155 for dep in dependencies {
156 let import_type_str = match dep.import_type {
157 ImportType::Internal => "internal",
158 ImportType::External => "external",
159 ImportType::Stdlib => "stdlib",
160 };
161
162 let symbols_json = dep
163 .imported_symbols
164 .as_ref()
165 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
166
167 tx.execute(
168 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
169 VALUES (?, ?, ?, ?, ?, ?)",
170 rusqlite::params![
171 dep.file_id,
172 dep.imported_path,
173 dep.resolved_file_id,
174 import_type_str,
175 dep.line_number as i64,
176 symbols_json,
177 ],
178 )?;
179 }
180
181 tx.commit()?;
182 log::debug!("Batch inserted {} dependencies", dependencies.len());
183 Ok(())
184 }
185
186 pub fn get_dependencies(&self, file_id: i64) -> Result<Vec<Dependency>> {
190 let db_path = self.cache.path().join("meta.db");
191 let conn = Connection::open(&db_path)
192 .context("Failed to open meta.db for dependency lookup")?;
193
194 let mut stmt = conn.prepare(
195 "SELECT file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols
196 FROM file_dependencies
197 WHERE file_id = ?
198 ORDER BY line_number",
199 )?;
200
201 let deps = stmt
202 .query_map([file_id], |row| {
203 let import_type_str: String = row.get(3)?;
204 let import_type = match import_type_str.as_str() {
205 "internal" => ImportType::Internal,
206 "external" => ImportType::External,
207 "stdlib" => ImportType::Stdlib,
208 _ => ImportType::External,
209 };
210
211 let symbols_json: Option<String> = row.get(5)?;
212 let imported_symbols = symbols_json.and_then(|json| {
213 serde_json::from_str(&json).ok()
214 });
215
216 Ok(Dependency {
217 file_id: row.get(0)?,
218 imported_path: row.get(1)?,
219 resolved_file_id: row.get(2)?,
220 import_type,
221 line_number: row.get::<_, i64>(4)? as usize,
222 imported_symbols,
223 })
224 })?
225 .collect::<Result<Vec<_>, _>>()?;
226
227 Ok(deps)
228 }
229
230 pub fn get_dependents(&self, file_id: i64) -> Result<Vec<i64>> {
235 let db_path = self.cache.path().join("meta.db");
236 let conn = Connection::open(&db_path)
237 .context("Failed to open meta.db for reverse dependency lookup")?;
238
239 let mut stmt = conn.prepare(
241 "SELECT DISTINCT file_id
242 FROM file_dependencies
243 WHERE resolved_file_id = ?
244 ORDER BY file_id"
245 )?;
246
247 let dependents: Vec<i64> = stmt
248 .query_map([file_id], |row| row.get(0))?
249 .collect::<Result<Vec<_>, _>>()?;
250
251 Ok(dependents)
252 }
253
254 pub fn get_dependencies_info(&self, file_id: i64) -> Result<Vec<DependencyInfo>> {
259 let deps = self.get_dependencies(file_id)?;
260
261 let dep_infos = deps
262 .into_iter()
263 .map(|dep| {
264 let path = if let Some(resolved_id) = dep.resolved_file_id {
266 self.get_file_path(resolved_id).unwrap_or(dep.imported_path)
268 } else {
269 dep.imported_path
270 };
271
272 DependencyInfo {
273 path,
274 line: Some(dep.line_number),
275 symbols: dep.imported_symbols,
276 }
277 })
278 .collect();
279
280 Ok(dep_infos)
281 }
282
283 pub fn get_transitive_deps(&self, file_id: i64, max_depth: usize) -> Result<HashMap<i64, usize>> {
298 let mut visited = HashMap::new();
299 let mut queue = VecDeque::new();
300
301 queue.push_back((file_id, 0));
303 visited.insert(file_id, 0);
304
305 while let Some((current_id, depth)) = queue.pop_front() {
306 if depth >= max_depth {
307 continue;
308 }
309
310 let deps = self.get_dependencies(current_id)?;
312
313 for dep in deps {
314 if let Some(resolved_id) = dep.resolved_file_id {
316 if !visited.contains_key(&resolved_id) {
318 visited.insert(resolved_id, depth + 1);
319 queue.push_back((resolved_id, depth + 1));
320 }
321 }
322 }
323 }
324
325 Ok(visited)
326 }
327
328 pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
336 let db_path = self.cache.path().join("meta.db");
337 let conn = Connection::open(&db_path)
338 .context("Failed to open meta.db for circular dependency analysis")?;
339
340 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
342
343 let mut stmt = conn.prepare(
344 "SELECT file_id, resolved_file_id
345 FROM file_dependencies
346 WHERE resolved_file_id IS NOT NULL"
347 )?;
348
349 let dependencies: Vec<(i64, i64)> = stmt
350 .query_map([], |row| {
351 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
352 })?
353 .collect::<Result<Vec<_>, _>>()?;
354
355 for (file_id, target_id) in dependencies {
357 graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
358 }
359
360 let all_files = self.get_all_file_ids()?;
362
363 let mut visited = HashSet::new();
364 let mut rec_stack = HashSet::new();
365 let mut path = Vec::new();
366 let mut cycles = Vec::new();
367
368 for file_id in all_files {
369 if !visited.contains(&file_id) {
370 self.dfs_cycle_detect(
371 file_id,
372 &graph,
373 &mut visited,
374 &mut rec_stack,
375 &mut path,
376 &mut cycles,
377 )?;
378 }
379 }
380
381 Ok(cycles)
382 }
383
384 fn dfs_cycle_detect(
386 &self,
387 file_id: i64,
388 graph: &HashMap<i64, Vec<i64>>,
389 visited: &mut HashSet<i64>,
390 rec_stack: &mut HashSet<i64>,
391 path: &mut Vec<i64>,
392 cycles: &mut Vec<Vec<i64>>,
393 ) -> Result<()> {
394 visited.insert(file_id);
395 rec_stack.insert(file_id);
396 path.push(file_id);
397
398 if let Some(dependencies) = graph.get(&file_id) {
400 for &target_id in dependencies {
401 if !visited.contains(&target_id) {
402 self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
403 } else if rec_stack.contains(&target_id) {
404 if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
406 let cycle = path[cycle_start..].to_vec();
407 cycles.push(cycle);
408 }
409 }
410 }
411 }
412
413 path.pop();
414 rec_stack.remove(&file_id);
415
416 Ok(())
417 }
418
419 pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
423 let db_path = self.cache.path().join("meta.db");
424 let conn = Connection::open(&db_path)
425 .context("Failed to open meta.db for file path lookup")?;
426
427 let mut paths = HashMap::new();
428
429 for &file_id in file_ids {
430 if let Ok(path) = conn.query_row(
431 "SELECT path FROM files WHERE id = ?",
432 [file_id],
433 |row| row.get::<_, String>(0),
434 ) {
435 paths.insert(file_id, path);
436 }
437 }
438
439 Ok(paths)
440 }
441
442 fn get_file_path(&self, file_id: i64) -> Result<String> {
444 let db_path = self.cache.path().join("meta.db");
445 let conn = Connection::open(&db_path)
446 .context("Failed to open meta.db for file path lookup")?;
447
448 let path = conn.query_row(
449 "SELECT path FROM files WHERE id = ?",
450 [file_id],
451 |row| row.get::<_, String>(0),
452 )?;
453
454 Ok(path)
455 }
456
457 fn get_all_file_ids(&self) -> Result<Vec<i64>> {
459 let db_path = self.cache.path().join("meta.db");
460 let conn = Connection::open(&db_path)
461 .context("Failed to open meta.db for file ID lookup")?;
462
463 let mut stmt = conn.prepare("SELECT id FROM files")?;
464 let file_ids = stmt
465 .query_map([], |row| row.get(0))?
466 .collect::<Result<Vec<_>, _>>()?;
467
468 Ok(file_ids)
469 }
470
471 pub fn find_hotspots(&self, limit: Option<usize>, min_dependents: usize) -> Result<Vec<(i64, usize)>> {
482 let db_path = self.cache.path().join("meta.db");
483 let conn = Connection::open(&db_path)
484 .context("Failed to open meta.db for hotspot analysis")?;
485
486 let mut stmt = conn.prepare(
488 "SELECT resolved_file_id, COUNT(*) as count
489 FROM file_dependencies
490 WHERE resolved_file_id IS NOT NULL
491 GROUP BY resolved_file_id
492 ORDER BY count DESC"
493 )?;
494
495 let mut hotspots: Vec<(i64, usize)> = stmt
497 .query_map([], |row| {
498 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
499 })?
500 .collect::<Result<Vec<_>, _>>()?
501 .into_iter()
502 .filter(|(_, count)| *count >= min_dependents)
503 .collect();
504
505 if let Some(lim) = limit {
507 hotspots.truncate(lim);
508 }
509
510 Ok(hotspots)
511 }
512
513 pub fn find_unused_files(&self) -> Result<Vec<i64>> {
524 let db_path = self.cache.path().join("meta.db");
525 let conn = Connection::open(&db_path)
526 .context("Failed to open meta.db for unused files analysis")?;
527
528 let mut used_files = HashSet::new();
530
531 let mut stmt = conn.prepare(
533 "SELECT DISTINCT resolved_file_id
534 FROM file_dependencies
535 WHERE resolved_file_id IS NOT NULL"
536 )?;
537
538 let direct_imports: Vec<i64> = stmt
539 .query_map([], |row| row.get(0))?
540 .collect::<Result<Vec<_>, _>>()?;
541
542 used_files.extend(&direct_imports);
543
544 for file_id in direct_imports {
546 let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
548 used_files.extend(barrel_chain);
549 }
550
551 let mut stmt = conn.prepare("SELECT id FROM files ORDER BY id")?;
553 let all_files: Vec<i64> = stmt
554 .query_map([], |row| row.get(0))?
555 .collect::<Result<Vec<_>, _>>()?;
556
557 let unused: Vec<i64> = all_files
558 .into_iter()
559 .filter(|id| !used_files.contains(id))
560 .collect();
561
562 Ok(unused)
563 }
564
565 pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
589 let db_path = self.cache.path().join("meta.db");
590 let conn = Connection::open(&db_path)
591 .context("Failed to open meta.db for barrel export resolution")?;
592
593 let mut resolved_files = Vec::new();
594 let mut visited = HashSet::new();
595 let mut queue = VecDeque::new();
596
597 queue.push_back(barrel_file_id);
599 visited.insert(barrel_file_id);
600
601 while let Some(current_id) = queue.pop_front() {
602 resolved_files.push(current_id);
603
604 let mut stmt = conn.prepare(
606 "SELECT resolved_source_id
607 FROM file_exports
608 WHERE file_id = ? AND resolved_source_id IS NOT NULL"
609 )?;
610
611 let exported_files: Vec<i64> = stmt
612 .query_map([current_id], |row| row.get(0))?
613 .collect::<Result<Vec<_>, _>>()?;
614
615 for exported_id in exported_files {
617 if !visited.contains(&exported_id) {
618 visited.insert(exported_id);
619 queue.push_back(exported_id);
620 }
621 }
622 }
623
624 Ok(resolved_files)
625 }
626
627 pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
641 let db_path = self.cache.path().join("meta.db");
642 let conn = Connection::open(&db_path)
643 .context("Failed to open meta.db for island analysis")?;
644
645 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
647
648 let mut stmt = conn.prepare(
649 "SELECT file_id, resolved_file_id
650 FROM file_dependencies
651 WHERE resolved_file_id IS NOT NULL"
652 )?;
653
654 let dependencies: Vec<(i64, i64)> = stmt
655 .query_map([], |row| {
656 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
657 })?
658 .collect::<Result<Vec<_>, _>>()?;
659
660 for (file_id, target_id) in dependencies {
662 graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
664 graph.entry(target_id).or_insert_with(Vec::new).push(file_id);
665 }
666
667 let all_files = self.get_all_file_ids()?;
669
670 for file_id in &all_files {
672 graph.entry(*file_id).or_insert_with(Vec::new);
673 }
674
675 let mut visited = HashSet::new();
677 let mut islands = Vec::new();
678
679 for &file_id in &all_files {
680 if !visited.contains(&file_id) {
681 let mut island = Vec::new();
682 self.dfs_island(&file_id, &graph, &mut visited, &mut island);
683 islands.push(island);
684 }
685 }
686
687 islands.sort_by(|a, b| b.len().cmp(&a.len()));
689
690 log::info!("Found {} islands (connected components)", islands.len());
691
692 Ok(islands)
693 }
694
695 fn dfs_island(
697 &self,
698 file_id: &i64,
699 graph: &HashMap<i64, Vec<i64>>,
700 visited: &mut HashSet<i64>,
701 island: &mut Vec<i64>,
702 ) {
703 visited.insert(*file_id);
704 island.push(*file_id);
705
706 if let Some(neighbors) = graph.get(file_id) {
707 for &neighbor in neighbors {
708 if !visited.contains(&neighbor) {
709 self.dfs_island(&neighbor, graph, visited, island);
710 }
711 }
712 }
713 }
714
715 fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
739 let db_path = self.cache.path().join("meta.db");
740 let conn = Connection::open(&db_path)
741 .context("Failed to open meta.db for building resolution cache")?;
742
743 let mut stmt = conn.prepare(
745 "SELECT DISTINCT imported_path FROM file_dependencies"
746 )?;
747
748 let imported_paths: Vec<String> = stmt
749 .query_map([], |row| row.get(0))?
750 .collect::<Result<Vec<_>, _>>()?;
751
752 let total_paths = imported_paths.len();
753 log::info!("Building resolution cache for {} unique imported paths", total_paths);
754
755 let mut cache = HashMap::new();
757
758 for imported_path in imported_paths {
759 if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
760 cache.insert(imported_path, file_id);
761 }
762 }
763
764 log::info!(
765 "Resolution cache built: {} resolved, {} unresolved",
766 cache.len(),
767 total_paths - cache.len()
768 );
769
770 Ok(cache)
771 }
772
773 pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
775 let db_path = self.cache.path().join("meta.db");
776 let conn = Connection::open(&db_path)
777 .context("Failed to open meta.db for dependency clearing")?;
778
779 conn.execute(
780 "DELETE FROM file_dependencies WHERE file_id = ?",
781 [file_id],
782 )?;
783
784 Ok(())
785 }
786
787 pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
806 let path_variants = generate_path_variants(imported_path);
807
808 for variant in &path_variants {
809 if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
810 log::trace!("Resolved '{}' → '{}' (file_id: {})", imported_path, variant, file_id);
811 return Ok(Some(file_id));
812 }
813 }
814
815 Ok(None)
816 }
817
818 pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
829 let db_path = self.cache.path().join("meta.db");
830 let conn = Connection::open(&db_path)
831 .context("Failed to open meta.db for file ID lookup")?;
832
833 let normalized_path = normalize_path_for_lookup(path);
835
836 match conn.query_row(
838 "SELECT id FROM files WHERE path = ?",
839 [&normalized_path],
840 |row| row.get::<_, i64>(0),
841 ) {
842 Ok(id) => return Ok(Some(id)),
843 Err(rusqlite::Error::QueryReturnedNoRows) => {
844 }
846 Err(e) => return Err(e.into()),
847 }
848
849 let mut stmt = conn.prepare(
851 "SELECT id, path FROM files WHERE path LIKE '%' || ?"
852 )?;
853
854 let matches: Vec<(i64, String)> = stmt
855 .query_map([&normalized_path], |row| {
856 Ok((row.get(0)?, row.get(1)?))
857 })?
858 .collect::<Result<Vec<_>, _>>()?;
859
860 match matches.len() {
861 0 => Ok(None),
862 1 => Ok(Some(matches[0].0)),
863 _ => {
864 let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
866 anyhow::bail!(
867 "Ambiguous path '{}' matches multiple files:\n {}\n\nPlease be more specific.",
868 path,
869 paths.join("\n ")
870 );
871 }
872 }
873 }
874
875 pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
884 let db_path = self.cache.path().join("meta.db");
885 let conn = Connection::open(&db_path)
886 .context("Failed to open meta.db for resolution stats")?;
887
888 let mut stmt = conn.prepare(
889 "SELECT
890 CASE
891 WHEN f.path LIKE '%.py' THEN 'Python'
892 WHEN f.path LIKE '%.go' THEN 'Go'
893 WHEN f.path LIKE '%.ts' THEN 'TypeScript'
894 WHEN f.path LIKE '%.rs' THEN 'Rust'
895 WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
896 WHEN f.path LIKE '%.php' THEN 'PHP'
897 WHEN f.path LIKE '%.java' THEN 'Java'
898 WHEN f.path LIKE '%.kt' THEN 'Kotlin'
899 WHEN f.path LIKE '%.rb' THEN 'Ruby'
900 WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
901 WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
902 WHEN f.path LIKE '%.cs' THEN 'C#'
903 WHEN f.path LIKE '%.zig' THEN 'Zig'
904 ELSE 'Other'
905 END as language,
906 COUNT(*) as total,
907 SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
908 FROM file_dependencies d
909 JOIN files f ON d.file_id = f.id
910 WHERE d.import_type = 'internal'
911 GROUP BY language
912 ORDER BY language",
913 )?;
914
915 let mut stats = Vec::new();
916
917 let rows = stmt.query_map([], |row| {
918 let language: String = row.get(0)?;
919 let total: i64 = row.get(1)?;
920 let resolved: i64 = row.get(2)?;
921 let rate = if total > 0 {
922 (resolved as f64 / total as f64) * 100.0
923 } else {
924 0.0
925 };
926
927 Ok((language, total as usize, resolved as usize, rate))
928 })?;
929
930 for row in rows {
931 stats.push(row?);
932 }
933
934 Ok(stats)
935 }
936
937 pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
947 let db_path = self.cache.path().join("meta.db");
948 let conn = Connection::open(&db_path)
949 .context("Failed to open meta.db for internal dependencies")?;
950
951 let mut stmt = conn.prepare(
952 "SELECT
953 f.path,
954 d.imported_path,
955 f2.path as resolved_path
956 FROM file_dependencies d
957 JOIN files f ON d.file_id = f.id
958 LEFT JOIN files f2 ON d.resolved_file_id = f2.id
959 WHERE d.import_type = 'internal'
960 ORDER BY f.path",
961 )?;
962
963 let mut deps = Vec::new();
964
965 let rows = stmt.query_map([], |row| {
966 Ok((
967 row.get::<_, String>(0)?,
968 row.get::<_, String>(1)?,
969 row.get::<_, Option<String>>(2)?,
970 ))
971 })?;
972
973 for row in rows {
974 deps.push(row?);
975 }
976
977 Ok(deps)
978 }
979
980 pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
982 let db_path = self.cache.path().join("meta.db");
983 let conn = Connection::open(&db_path)
984 .context("Failed to open meta.db for dependency count")?;
985
986 let mut stmt = conn.prepare(
987 "SELECT import_type, COUNT(*) as count
988 FROM file_dependencies
989 GROUP BY import_type
990 ORDER BY import_type",
991 )?;
992
993 let mut counts = Vec::new();
994
995 let rows = stmt.query_map([], |row| {
996 Ok((
997 row.get::<_, String>(0)?,
998 row.get::<_, i64>(1)? as usize,
999 ))
1000 })?;
1001
1002 for row in rows {
1003 counts.push(row?);
1004 }
1005
1006 Ok(counts)
1007 }
1008}
1009
1010fn generate_path_variants(import_path: &str) -> Vec<String> {
1022 let path = import_path.replace('\\', "/").replace("::", "/");
1024
1025 let path = path.trim_matches('"').trim_matches('\'');
1027
1028 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1030
1031 if components.is_empty() {
1032 return vec![];
1033 }
1034
1035 let mut variants = Vec::new();
1036
1037 for start_idx in 0..components.len() {
1044 let suffix = components[start_idx..].join("/");
1045
1046 if !suffix.ends_with(".php") {
1048 variants.push(format!("{}.php", suffix));
1049 } else {
1050 variants.push(suffix.clone());
1051 }
1052
1053 if !suffix.contains('.') {
1055 variants.push(format!("{}.rs", suffix));
1057 variants.push(format!("{}.ts", suffix));
1058 variants.push(format!("{}.js", suffix));
1059 variants.push(format!("{}.py", suffix));
1060 }
1061 }
1062
1063 variants
1064}
1065
1066fn normalize_path_for_lookup(path: &str) -> String {
1077 let mut normalized = path.trim_start_matches("./").to_string();
1079 if normalized.starts_with("../") {
1080 normalized = normalized.trim_start_matches("../").to_string();
1081 }
1082
1083 if normalized.starts_with('/') || normalized.starts_with('\\') {
1087 let markers = ["services", "src", "app", "lib", "packages", "modules"];
1089
1090 let mut found_marker = false;
1091 for marker in &markers {
1092 if let Some(idx) = normalized.find(marker) {
1093 normalized = normalized[idx..].to_string();
1094 found_marker = true;
1095 break;
1096 }
1097 }
1098
1099 if !found_marker {
1101 use std::path::Path;
1102 let path_obj = Path::new(&normalized);
1103 if let Some(filename) = path_obj.file_name() {
1104 normalized = filename.to_string_lossy().to_string();
1105 }
1106 }
1107 }
1108
1109 normalized
1110}
1111
1112pub fn resolve_rust_import(
1131 import_path: &str,
1132 current_file: &str,
1133 project_root: &std::path::Path,
1134) -> Option<String> {
1135 use std::path::{Path, PathBuf};
1136
1137 if !import_path.starts_with("crate::")
1139 && !import_path.starts_with("super::")
1140 && !import_path.starts_with("self::")
1141 {
1142 return None;
1143 }
1144
1145 let current_path = Path::new(current_file);
1146 let mut resolved_path: Option<PathBuf> = None;
1147
1148 if import_path.starts_with("crate::") {
1149 let crate_root = if project_root.join("src/lib.rs").exists() {
1151 project_root.join("src")
1152 } else if project_root.join("src/main.rs").exists() {
1153 project_root.join("src")
1154 } else {
1155 project_root.join("src")
1157 };
1158
1159 let path_parts: Vec<&str> = import_path
1160 .strip_prefix("crate::")
1161 .unwrap()
1162 .split("::")
1163 .collect();
1164
1165 resolved_path = resolve_module_path(&crate_root, &path_parts);
1166 } else if import_path.starts_with("super::") {
1167 if let Some(current_dir) = current_path.parent() {
1169 if let Some(parent_dir) = current_dir.parent() {
1170 let path_parts: Vec<&str> = import_path
1171 .strip_prefix("super::")
1172 .unwrap()
1173 .split("::")
1174 .collect();
1175
1176 resolved_path = resolve_module_path(parent_dir, &path_parts);
1177 }
1178 }
1179 } else if import_path.starts_with("self::") {
1180 if let Some(current_dir) = current_path.parent() {
1182 let path_parts: Vec<&str> = import_path
1183 .strip_prefix("self::")
1184 .unwrap()
1185 .split("::")
1186 .collect();
1187
1188 resolved_path = resolve_module_path(current_dir, &path_parts);
1189 }
1190 }
1191
1192 resolved_path.and_then(|p| {
1194 p.strip_prefix(project_root)
1195 .ok()
1196 .map(|rel| rel.to_string_lossy().to_string())
1197 })
1198}
1199
1200fn resolve_module_path(start_dir: &std::path::Path, components: &[&str]) -> Option<std::path::PathBuf> {
1206
1207 if components.is_empty() {
1208 return None;
1209 }
1210
1211 let mut current = start_dir.to_path_buf();
1212
1213 for &component in &components[..components.len() - 1] {
1215 let dir_path = current.join(component);
1217 let mod_file = dir_path.join("mod.rs");
1218
1219 if mod_file.exists() {
1220 current = dir_path;
1221 } else {
1222 return None;
1224 }
1225 }
1226
1227 let last_component = components.last().unwrap();
1229
1230 let file_path = current.join(format!("{}.rs", last_component));
1232 if file_path.exists() {
1233 return Some(file_path);
1234 }
1235
1236 let dir_path = current.join(last_component);
1238 let mod_file = dir_path.join("mod.rs");
1239 if mod_file.exists() {
1240 return Some(mod_file);
1241 }
1242
1243 None
1244}
1245
1246pub fn resolve_rust_mod_declaration(
1252 mod_name: &str,
1253 current_file: &str,
1254 _project_root: &std::path::Path,
1255) -> Option<String> {
1256 use std::path::Path;
1257
1258 let current_path = Path::new(current_file);
1259 let current_dir = current_path.parent()?;
1260
1261 let sibling = current_dir.join(format!("{}.rs", mod_name));
1263 if sibling.exists() {
1264 return Some(sibling.to_string_lossy().to_string());
1265 }
1266
1267 let dir_mod = current_dir.join(mod_name).join("mod.rs");
1269 if dir_mod.exists() {
1270 return Some(dir_mod.to_string_lossy().to_string());
1271 }
1272
1273 None
1274}
1275
1276pub fn resolve_php_import(
1299 import_path: &str,
1300 _current_file: &str,
1301 project_root: &std::path::Path,
1302) -> Option<String> {
1303 const VENDOR_NAMESPACES: &[&str] = &[
1305 "Illuminate\\", "Symfony\\", "Laravel\\", "Psr\\",
1306 "Doctrine\\", "Monolog\\", "PHPUnit\\", "Carbon\\",
1307 "GuzzleHttp\\", "Composer\\", "Predis\\", "League\\"
1308 ];
1309
1310 for vendor_ns in VENDOR_NAMESPACES {
1312 if import_path.starts_with(vendor_ns) {
1313 return None;
1314 }
1315 }
1316
1317 let file_path = import_path.replace('\\', "/");
1321
1322 let path_candidates = vec![
1326 {
1328 let parts: Vec<&str> = file_path.split('/').collect();
1329 if let Some(first) = parts.first() {
1330 let mut result = vec![first.to_lowercase()];
1331 result.extend(parts[1..].iter().map(|s| s.to_string()));
1332 result.join("/") + ".php"
1333 } else {
1334 file_path.clone() + ".php"
1335 }
1336 },
1337 file_path.clone() + ".php",
1339 file_path.to_lowercase() + ".php",
1341 ];
1342
1343 for candidate in &path_candidates {
1345 let full_path = project_root.join(candidate);
1346 if full_path.exists() {
1347 return Some(candidate.clone());
1349 }
1350 }
1351
1352 None
1354}
1355
1356#[cfg(test)]
1357mod tests {
1358 use super::*;
1359 use tempfile::TempDir;
1360
1361 fn setup_test_cache() -> (TempDir, CacheManager) {
1362 let temp = TempDir::new().unwrap();
1363 let cache = CacheManager::new(temp.path());
1364 cache.init().unwrap();
1365
1366 cache.update_file("src/main.rs", "rust", 100).unwrap();
1368 cache.update_file("src/lib.rs", "rust", 50).unwrap();
1369 cache.update_file("src/utils.rs", "rust", 30).unwrap();
1370
1371 (temp, cache)
1372 }
1373
1374 #[test]
1375 fn test_insert_and_get_dependencies() {
1376 let (_temp, cache) = setup_test_cache();
1377 let deps_index = DependencyIndex::new(cache);
1378
1379 let main_id = 1i64;
1381 let lib_id = 2i64;
1382
1383 deps_index
1385 .insert_dependency(
1386 main_id,
1387 "crate::lib".to_string(),
1388 Some(lib_id),
1389 ImportType::Internal,
1390 5,
1391 None,
1392 )
1393 .unwrap();
1394
1395 let deps = deps_index.get_dependencies(main_id).unwrap();
1397 assert_eq!(deps.len(), 1);
1398 assert_eq!(deps[0].imported_path, "crate::lib");
1399 assert_eq!(deps[0].resolved_file_id, Some(lib_id));
1400 assert_eq!(deps[0].import_type, ImportType::Internal);
1401 }
1402
1403 #[test]
1404 fn test_reverse_lookup() {
1405 let (_temp, cache) = setup_test_cache();
1406 let deps_index = DependencyIndex::new(cache);
1407
1408 let main_id = 1i64;
1409 let lib_id = 2i64;
1410 let utils_id = 3i64;
1411
1412 deps_index
1414 .insert_dependency(
1415 main_id,
1416 "crate::lib".to_string(),
1417 Some(lib_id),
1418 ImportType::Internal,
1419 5,
1420 None,
1421 )
1422 .unwrap();
1423
1424 deps_index
1426 .insert_dependency(
1427 utils_id,
1428 "crate::lib".to_string(),
1429 Some(lib_id),
1430 ImportType::Internal,
1431 3,
1432 None,
1433 )
1434 .unwrap();
1435
1436 let dependents = deps_index.get_dependents(lib_id).unwrap();
1438 assert_eq!(dependents.len(), 2);
1439 assert!(dependents.contains(&main_id));
1440 assert!(dependents.contains(&utils_id));
1441 }
1442
1443 #[test]
1444 fn test_transitive_dependencies() {
1445 let (_temp, cache) = setup_test_cache();
1446 let deps_index = DependencyIndex::new(cache);
1447
1448 let file1 = 1i64;
1449 let file2 = 2i64;
1450 let file3 = 3i64;
1451
1452 deps_index
1454 .insert_dependency(
1455 file1,
1456 "file2".to_string(),
1457 Some(file2),
1458 ImportType::Internal,
1459 1,
1460 None,
1461 )
1462 .unwrap();
1463
1464 deps_index
1465 .insert_dependency(
1466 file2,
1467 "file3".to_string(),
1468 Some(file3),
1469 ImportType::Internal,
1470 1,
1471 None,
1472 )
1473 .unwrap();
1474
1475 let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
1477
1478 assert_eq!(transitive.len(), 3);
1480 assert_eq!(transitive.get(&file1), Some(&0));
1481 assert_eq!(transitive.get(&file2), Some(&1));
1482 assert_eq!(transitive.get(&file3), Some(&2));
1483 }
1484
1485 #[test]
1486 fn test_batch_insert() {
1487 let (_temp, cache) = setup_test_cache();
1488 let deps_index = DependencyIndex::new(cache);
1489
1490 let deps = vec![
1491 Dependency {
1492 file_id: 1,
1493 imported_path: "std::collections".to_string(),
1494 resolved_file_id: None,
1495 import_type: ImportType::Stdlib,
1496 line_number: 1,
1497 imported_symbols: Some(vec!["HashMap".to_string()]),
1498 },
1499 Dependency {
1500 file_id: 1,
1501 imported_path: "crate::lib".to_string(),
1502 resolved_file_id: Some(2),
1503 import_type: ImportType::Internal,
1504 line_number: 2,
1505 imported_symbols: None,
1506 },
1507 ];
1508
1509 deps_index.batch_insert_dependencies(&deps).unwrap();
1510
1511 let retrieved = deps_index.get_dependencies(1).unwrap();
1512 assert_eq!(retrieved.len(), 2);
1513 }
1514
1515 #[test]
1516 fn test_clear_dependencies() {
1517 let (_temp, cache) = setup_test_cache();
1518 let deps_index = DependencyIndex::new(cache);
1519
1520 deps_index
1522 .insert_dependency(
1523 1,
1524 "crate::lib".to_string(),
1525 Some(2),
1526 ImportType::Internal,
1527 1,
1528 None,
1529 )
1530 .unwrap();
1531
1532 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
1534
1535 deps_index.clear_dependencies(1).unwrap();
1537
1538 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
1540 }
1541
1542 #[test]
1543 fn test_resolve_rust_import_crate() {
1544 use std::fs;
1545 use tempfile::TempDir;
1546
1547 let temp = TempDir::new().unwrap();
1548 let project_root = temp.path();
1549
1550 fs::create_dir_all(project_root.join("src")).unwrap();
1552 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1553 fs::write(project_root.join("src/models.rs"), "").unwrap();
1554
1555 let resolved = resolve_rust_import(
1557 "crate::models",
1558 "src/query.rs",
1559 project_root,
1560 );
1561
1562 assert_eq!(resolved, Some("src/models.rs".to_string()));
1563 }
1564
1565 #[test]
1566 fn test_resolve_rust_import_super() {
1567 use std::fs;
1568 use tempfile::TempDir;
1569
1570 let temp = TempDir::new().unwrap();
1571 let project_root = temp.path();
1572
1573 fs::create_dir_all(project_root.join("src/parsers")).unwrap();
1575 fs::write(project_root.join("src/models.rs"), "").unwrap();
1576 fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
1577
1578 let current_file = project_root.join("src/parsers/rust.rs");
1581 let resolved = resolve_rust_import(
1582 "super::models",
1583 ¤t_file.to_string_lossy(),
1584 project_root,
1585 );
1586
1587 assert_eq!(resolved, Some("src/models.rs".to_string()));
1588 }
1589
1590 #[test]
1591 fn test_resolve_rust_import_external() {
1592 use tempfile::TempDir;
1593
1594 let temp = TempDir::new().unwrap();
1595 let project_root = temp.path();
1596
1597 let resolved = resolve_rust_import(
1599 "serde::Serialize",
1600 "src/models.rs",
1601 project_root,
1602 );
1603
1604 assert_eq!(resolved, None);
1605
1606 let resolved = resolve_rust_import(
1608 "std::collections::HashMap",
1609 "src/models.rs",
1610 project_root,
1611 );
1612
1613 assert_eq!(resolved, None);
1614 }
1615
1616 #[test]
1617 fn test_resolve_rust_mod_declaration() {
1618 use std::fs;
1619 use tempfile::TempDir;
1620
1621 let temp = TempDir::new().unwrap();
1622 let project_root = temp.path();
1623
1624 fs::create_dir_all(project_root.join("src")).unwrap();
1626 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1627 fs::write(project_root.join("src/parser.rs"), "").unwrap();
1628
1629 let resolved = resolve_rust_mod_declaration(
1631 "parser",
1632 &project_root.join("src/lib.rs").to_string_lossy(),
1633 project_root,
1634 );
1635
1636 assert!(resolved.is_some());
1637 assert!(resolved.unwrap().ends_with("src/parser.rs"));
1638 }
1639
1640 #[test]
1641 fn test_resolve_rust_import_nested() {
1642 use std::fs;
1643 use tempfile::TempDir;
1644
1645 let temp = TempDir::new().unwrap();
1646 let project_root = temp.path();
1647
1648 fs::create_dir_all(project_root.join("src/models")).unwrap();
1650 fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
1651 fs::write(project_root.join("src/models/language.rs"), "").unwrap();
1652
1653 let resolved = resolve_rust_import(
1655 "crate::models::language",
1656 "src/query.rs",
1657 project_root,
1658 );
1659
1660 assert_eq!(resolved, Some("src/models/language.rs".to_string()));
1661 }
1662}