1use 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
41pub struct DependencyIndex {
43 cache: Option<CacheManager>,
44 db_path: PathBuf,
45}
46
47impl DependencyIndex {
48 pub fn new(cache: CacheManager) -> Self {
50 let db_path = cache.path().join("meta.db");
51 Self {
52 cache: Some(cache),
53 db_path,
54 }
55 }
56
57 pub fn from_db_path(db_path: impl Into<PathBuf>) -> Self {
61 Self {
62 cache: None,
63 db_path: db_path.into(),
64 }
65 }
66
67 pub fn get_cache(&self) -> &CacheManager {
71 self.cache
72 .as_ref()
73 .expect("DependencyIndex created with from_db_path has no CacheManager")
74 }
75
76 fn open_conn(&self) -> Result<Connection> {
78 Connection::open(&self.db_path).context("Failed to open database")
79 }
80
81 pub fn insert_dependency(
92 &self,
93 file_id: i64,
94 imported_path: String,
95 resolved_file_id: Option<i64>,
96 import_type: ImportType,
97 line_number: usize,
98 imported_symbols: Option<Vec<String>>,
99 ) -> Result<()> {
100 let conn = self.open_conn()?;
101
102 let import_type_str = match import_type {
103 ImportType::Internal => "internal",
104 ImportType::External => "external",
105 ImportType::Stdlib => "stdlib",
106 ImportType::ModDecl => "mod_decl",
107 };
108
109 let symbols_json = imported_symbols
110 .as_ref()
111 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
112
113 conn.execute(
114 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
115 VALUES (?, ?, ?, ?, ?, ?)",
116 rusqlite::params![
117 file_id,
118 imported_path,
119 resolved_file_id,
120 import_type_str,
121 line_number as i64,
122 symbols_json,
123 ],
124 )?;
125
126 Ok(())
127 }
128
129 pub fn insert_export(
139 &self,
140 file_id: i64,
141 exported_symbol: Option<String>,
142 source_path: String,
143 resolved_source_id: Option<i64>,
144 line_number: usize,
145 ) -> Result<()> {
146 let conn = self.open_conn()?;
147
148 conn.execute(
149 "INSERT INTO file_exports (file_id, exported_symbol, source_path, resolved_source_id, line_number)
150 VALUES (?, ?, ?, ?, ?)",
151 rusqlite::params![
152 file_id,
153 exported_symbol,
154 source_path,
155 resolved_source_id,
156 line_number as i64,
157 ],
158 )?;
159
160 Ok(())
161 }
162
163 pub fn batch_insert_dependencies(&self, dependencies: &[Dependency]) -> Result<()> {
167 if dependencies.is_empty() {
168 return Ok(());
169 }
170
171 let mut conn = self.open_conn()?;
172
173 let tx = conn.transaction()?;
174
175 for dep in dependencies {
176 let import_type_str = match dep.import_type {
177 ImportType::Internal => "internal",
178 ImportType::External => "external",
179 ImportType::Stdlib => "stdlib",
180 ImportType::ModDecl => "mod_decl",
181 };
182
183 let symbols_json = dep
184 .imported_symbols
185 .as_ref()
186 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
187
188 tx.execute(
189 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
190 VALUES (?, ?, ?, ?, ?, ?)",
191 rusqlite::params![
192 dep.file_id,
193 dep.imported_path,
194 dep.resolved_file_id,
195 import_type_str,
196 dep.line_number as i64,
197 symbols_json,
198 ],
199 )?;
200 }
201
202 tx.commit()?;
203 log::debug!("Batch inserted {} dependencies", dependencies.len());
204 Ok(())
205 }
206
207 pub fn get_dependencies(&self, file_id: i64) -> Result<Vec<Dependency>> {
211 let conn = self.open_conn()?;
212
213 let mut stmt = conn.prepare(
214 "SELECT file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols
215 FROM file_dependencies
216 WHERE file_id = ?
217 ORDER BY line_number",
218 )?;
219
220 let deps = stmt
221 .query_map([file_id], |row| {
222 let import_type_str: String = row.get(3)?;
223 let import_type = match import_type_str.as_str() {
224 "internal" => ImportType::Internal,
225 "external" => ImportType::External,
226 "stdlib" => ImportType::Stdlib,
227 "mod_decl" => ImportType::ModDecl,
228 _ => ImportType::External,
229 };
230
231 let symbols_json: Option<String> = row.get(5)?;
232 let imported_symbols =
233 symbols_json.and_then(|json| serde_json::from_str(&json).ok());
234
235 Ok(Dependency {
236 file_id: row.get(0)?,
237 imported_path: row.get(1)?,
238 resolved_file_id: row.get(2)?,
239 import_type,
240 line_number: row.get::<_, i64>(4)? as usize,
241 imported_symbols,
242 })
243 })?
244 .collect::<Result<Vec<_>, _>>()?;
245
246 Ok(deps)
247 }
248
249 pub fn get_dependents(&self, file_id: i64) -> Result<Vec<i64>> {
254 let conn = self.open_conn()?;
255
256 let mut stmt = conn.prepare(
258 "SELECT DISTINCT file_id
259 FROM file_dependencies
260 WHERE resolved_file_id = ?
261 ORDER BY file_id",
262 )?;
263
264 let dependents: Vec<i64> = stmt
265 .query_map([file_id], |row| row.get(0))?
266 .collect::<Result<Vec<_>, _>>()?;
267
268 Ok(dependents)
269 }
270
271 pub fn get_dependencies_info(&self, file_id: i64) -> Result<Vec<DependencyInfo>> {
276 let deps = self.get_dependencies(file_id)?;
277
278 let dep_infos = deps
279 .into_iter()
280 .map(|dep| {
281 let path = if let Some(resolved_id) = dep.resolved_file_id {
283 self.get_file_path(resolved_id).unwrap_or(dep.imported_path)
285 } else {
286 dep.imported_path
287 };
288
289 DependencyInfo {
290 path,
291 line: Some(dep.line_number),
292 symbols: dep.imported_symbols,
293 }
294 })
295 .collect();
296
297 Ok(dep_infos)
298 }
299
300 pub fn get_transitive_deps(
315 &self,
316 file_id: i64,
317 max_depth: usize,
318 ) -> Result<HashMap<i64, usize>> {
319 let mut visited = HashMap::new();
320 let mut queue = VecDeque::new();
321
322 queue.push_back((file_id, 0));
324 visited.insert(file_id, 0);
325
326 while let Some((current_id, depth)) = queue.pop_front() {
327 if depth >= max_depth {
328 continue;
329 }
330
331 let deps = self.get_dependencies(current_id)?;
333
334 for dep in deps {
335 if let Some(resolved_id) = dep.resolved_file_id {
337 if !visited.contains_key(&resolved_id) {
339 visited.insert(resolved_id, depth + 1);
340 queue.push_back((resolved_id, depth + 1));
341 }
342 }
343 }
344 }
345
346 Ok(visited)
347 }
348
349 pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
357 let conn = self.open_conn()?;
358
359 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
361
362 let mut stmt = conn.prepare(
365 "SELECT file_id, resolved_file_id
366 FROM file_dependencies
367 WHERE resolved_file_id IS NOT NULL
368 AND import_type != 'mod_decl'",
369 )?;
370
371 let dependencies: Vec<(i64, i64)> = stmt
372 .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?
373 .collect::<Result<Vec<_>, _>>()?;
374
375 for (file_id, target_id) in dependencies {
377 graph
378 .entry(file_id)
379 .or_insert_with(Vec::new)
380 .push(target_id);
381 }
382
383 let all_files = self.get_all_file_ids()?;
385
386 let mut visited = HashSet::new();
387 let mut rec_stack = HashSet::new();
388 let mut path = Vec::new();
389 let mut cycles = Vec::new();
390
391 for file_id in all_files {
392 if !visited.contains(&file_id) {
393 self.dfs_cycle_detect(
394 file_id,
395 &graph,
396 &mut visited,
397 &mut rec_stack,
398 &mut path,
399 &mut cycles,
400 )?;
401 }
402 }
403
404 Ok(cycles)
405 }
406
407 fn dfs_cycle_detect(
409 &self,
410 file_id: i64,
411 graph: &HashMap<i64, Vec<i64>>,
412 visited: &mut HashSet<i64>,
413 rec_stack: &mut HashSet<i64>,
414 path: &mut Vec<i64>,
415 cycles: &mut Vec<Vec<i64>>,
416 ) -> Result<()> {
417 visited.insert(file_id);
418 rec_stack.insert(file_id);
419 path.push(file_id);
420
421 if let Some(dependencies) = graph.get(&file_id) {
423 for &target_id in dependencies {
424 if !visited.contains(&target_id) {
425 self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
426 } else if rec_stack.contains(&target_id) {
427 if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
429 let cycle = path[cycle_start..].to_vec();
430 cycles.push(cycle);
431 }
432 }
433 }
434 }
435
436 path.pop();
437 rec_stack.remove(&file_id);
438
439 Ok(())
440 }
441
442 pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
446 let conn = self.open_conn()?;
447
448 let mut paths = HashMap::new();
449
450 for &file_id in file_ids {
451 if let Ok(path) =
452 conn.query_row("SELECT path FROM files WHERE id = ?", [file_id], |row| {
453 row.get::<_, String>(0)
454 })
455 {
456 paths.insert(file_id, path);
457 }
458 }
459
460 Ok(paths)
461 }
462
463 fn get_file_path(&self, file_id: i64) -> Result<String> {
465 let conn = self.open_conn()?;
466
467 let path = conn.query_row("SELECT path FROM files WHERE id = ?", [file_id], |row| {
468 row.get::<_, String>(0)
469 })?;
470
471 Ok(path)
472 }
473
474 fn get_all_file_ids(&self) -> Result<Vec<i64>> {
476 let conn = self.open_conn()?;
477
478 let mut stmt = conn.prepare("SELECT id FROM files")?;
479 let file_ids = stmt
480 .query_map([], |row| row.get(0))?
481 .collect::<Result<Vec<_>, _>>()?;
482
483 Ok(file_ids)
484 }
485
486 pub fn find_hotspots(
497 &self,
498 limit: Option<usize>,
499 min_dependents: usize,
500 ) -> Result<Vec<(i64, usize)>> {
501 let conn = self.open_conn()?;
502
503 let mut stmt = conn.prepare(
505 "SELECT resolved_file_id, COUNT(*) as count
506 FROM file_dependencies
507 WHERE resolved_file_id IS NOT NULL
508 GROUP BY resolved_file_id
509 ORDER BY count DESC",
510 )?;
511
512 let mut hotspots: Vec<(i64, usize)> = stmt
514 .query_map([], |row| {
515 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
516 })?
517 .collect::<Result<Vec<_>, _>>()?
518 .into_iter()
519 .filter(|(_, count)| *count >= min_dependents)
520 .collect();
521
522 if let Some(lim) = limit {
524 hotspots.truncate(lim);
525 }
526
527 Ok(hotspots)
528 }
529
530 pub fn find_unused_files(&self) -> Result<Vec<i64>> {
541 let conn = self.open_conn()?;
542
543 let mut used_files = HashSet::new();
545
546 let mut stmt = conn.prepare(
548 "SELECT DISTINCT resolved_file_id
549 FROM file_dependencies
550 WHERE resolved_file_id IS NOT NULL",
551 )?;
552
553 let direct_imports: Vec<i64> = stmt
554 .query_map([], |row| row.get(0))?
555 .collect::<Result<Vec<_>, _>>()?;
556
557 used_files.extend(&direct_imports);
558
559 for file_id in direct_imports {
561 let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
563 used_files.extend(barrel_chain);
564 }
565
566 let mut stmt = conn.prepare("SELECT id, path FROM files ORDER BY id")?;
569 let all_files: Vec<(i64, String)> = stmt
570 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
571 .collect::<Result<Vec<_>, _>>()?;
572
573 let unused: Vec<i64> = all_files
574 .into_iter()
575 .filter(|(id, path)| !used_files.contains(id) && !is_entry_point(path))
576 .map(|(id, _)| id)
577 .collect();
578
579 Ok(unused)
580 }
581
582 pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
606 let conn = self.open_conn()?;
607
608 let mut resolved_files = Vec::new();
609 let mut visited = HashSet::new();
610 let mut queue = VecDeque::new();
611
612 queue.push_back(barrel_file_id);
614 visited.insert(barrel_file_id);
615
616 while let Some(current_id) = queue.pop_front() {
617 resolved_files.push(current_id);
618
619 let mut stmt = conn.prepare(
621 "SELECT resolved_source_id
622 FROM file_exports
623 WHERE file_id = ? AND resolved_source_id IS NOT NULL",
624 )?;
625
626 let exported_files: Vec<i64> = stmt
627 .query_map([current_id], |row| row.get(0))?
628 .collect::<Result<Vec<_>, _>>()?;
629
630 for exported_id in exported_files {
632 if !visited.contains(&exported_id) {
633 visited.insert(exported_id);
634 queue.push_back(exported_id);
635 }
636 }
637 }
638
639 Ok(resolved_files)
640 }
641
642 pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
656 let conn = self.open_conn()?;
657
658 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
660
661 let mut stmt = conn.prepare(
662 "SELECT file_id, resolved_file_id
663 FROM file_dependencies
664 WHERE resolved_file_id IS NOT NULL",
665 )?;
666
667 let dependencies: Vec<(i64, i64)> = stmt
668 .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?
669 .collect::<Result<Vec<_>, _>>()?;
670
671 for (file_id, target_id) in dependencies {
673 graph
675 .entry(file_id)
676 .or_insert_with(Vec::new)
677 .push(target_id);
678 graph
679 .entry(target_id)
680 .or_insert_with(Vec::new)
681 .push(file_id);
682 }
683
684 let all_files = self.get_all_file_ids()?;
686
687 for file_id in &all_files {
689 graph.entry(*file_id).or_insert_with(Vec::new);
690 }
691
692 let mut visited = HashSet::new();
694 let mut islands = Vec::new();
695
696 for &file_id in &all_files {
697 if !visited.contains(&file_id) {
698 let mut island = Vec::new();
699 self.dfs_island(&file_id, &graph, &mut visited, &mut island);
700 islands.push(island);
701 }
702 }
703
704 islands.sort_by(|a, b| b.len().cmp(&a.len()));
706
707 log::info!("Found {} islands (connected components)", islands.len());
708
709 Ok(islands)
710 }
711
712 fn dfs_island(
714 &self,
715 file_id: &i64,
716 graph: &HashMap<i64, Vec<i64>>,
717 visited: &mut HashSet<i64>,
718 island: &mut Vec<i64>,
719 ) {
720 visited.insert(*file_id);
721 island.push(*file_id);
722
723 if let Some(neighbors) = graph.get(file_id) {
724 for &neighbor in neighbors {
725 if !visited.contains(&neighbor) {
726 self.dfs_island(&neighbor, graph, visited, island);
727 }
728 }
729 }
730 }
731
732 fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
756 let conn = self.open_conn()?;
757
758 let mut stmt = conn.prepare("SELECT DISTINCT imported_path FROM file_dependencies")?;
760
761 let imported_paths: Vec<String> = stmt
762 .query_map([], |row| row.get(0))?
763 .collect::<Result<Vec<_>, _>>()?;
764
765 let total_paths = imported_paths.len();
766 log::info!(
767 "Building resolution cache for {} unique imported paths",
768 total_paths
769 );
770
771 let mut cache = HashMap::new();
773
774 for imported_path in imported_paths {
775 if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
776 cache.insert(imported_path, file_id);
777 }
778 }
779
780 log::info!(
781 "Resolution cache built: {} resolved, {} unresolved",
782 cache.len(),
783 total_paths - cache.len()
784 );
785
786 Ok(cache)
787 }
788
789 pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
791 let conn = self.open_conn()?;
792
793 conn.execute("DELETE FROM file_dependencies WHERE file_id = ?", [file_id])?;
794
795 Ok(())
796 }
797
798 pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
817 let path_variants = generate_path_variants(imported_path);
818
819 for variant in &path_variants {
820 if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
821 log::trace!(
822 "Resolved '{}' → '{}' (file_id: {})",
823 imported_path,
824 variant,
825 file_id
826 );
827 return Ok(Some(file_id));
828 }
829 }
830
831 Ok(None)
832 }
833
834 pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
845 let conn = self.open_conn()?;
846
847 let normalized_path = normalize_path_for_lookup(path);
849
850 match conn.query_row(
852 "SELECT id FROM files WHERE path = ?",
853 [&normalized_path],
854 |row| row.get::<_, i64>(0),
855 ) {
856 Ok(id) => return Ok(Some(id)),
857 Err(rusqlite::Error::QueryReturnedNoRows) => {
858 }
860 Err(e) => return Err(e.into()),
861 }
862
863 let mut stmt = conn.prepare("SELECT id, path FROM files WHERE path LIKE '%' || ?")?;
865
866 let matches: Vec<(i64, String)> = stmt
867 .query_map([&normalized_path], |row| Ok((row.get(0)?, row.get(1)?)))?
868 .collect::<Result<Vec<_>, _>>()?;
869
870 match matches.len() {
871 0 => Ok(None),
872 1 => Ok(Some(matches[0].0)),
873 _ => {
874 let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
876 anyhow::bail!(
877 "Ambiguous path '{}' matches multiple files:\n {}\n\nPlease be more specific.",
878 path,
879 paths.join("\n ")
880 );
881 }
882 }
883 }
884
885 pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
894 let conn = self.open_conn()?;
895
896 let mut stmt = conn.prepare(
897 "SELECT
898 CASE
899 WHEN f.path LIKE '%.py' THEN 'Python'
900 WHEN f.path LIKE '%.go' THEN 'Go'
901 WHEN f.path LIKE '%.ts' THEN 'TypeScript'
902 WHEN f.path LIKE '%.rs' THEN 'Rust'
903 WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
904 WHEN f.path LIKE '%.php' THEN 'PHP'
905 WHEN f.path LIKE '%.java' THEN 'Java'
906 WHEN f.path LIKE '%.kt' THEN 'Kotlin'
907 WHEN f.path LIKE '%.rb' THEN 'Ruby'
908 WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
909 WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
910 WHEN f.path LIKE '%.cs' THEN 'C#'
911 WHEN f.path LIKE '%.zig' THEN 'Zig'
912 ELSE 'Other'
913 END as language,
914 COUNT(*) as total,
915 SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
916 FROM file_dependencies d
917 JOIN files f ON d.file_id = f.id
918 WHERE d.import_type = 'internal'
919 GROUP BY language
920 ORDER BY language",
921 )?;
922
923 let mut stats = Vec::new();
924
925 let rows = stmt.query_map([], |row| {
926 let language: String = row.get(0)?;
927 let total: i64 = row.get(1)?;
928 let resolved: i64 = row.get(2)?;
929 let rate = if total > 0 {
930 (resolved as f64 / total as f64) * 100.0
931 } else {
932 0.0
933 };
934
935 Ok((language, total as usize, resolved as usize, rate))
936 })?;
937
938 for row in rows {
939 stats.push(row?);
940 }
941
942 Ok(stats)
943 }
944
945 pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
955 let conn = self.open_conn()?;
956
957 let mut stmt = conn.prepare(
958 "SELECT
959 f.path,
960 d.imported_path,
961 f2.path as resolved_path
962 FROM file_dependencies d
963 JOIN files f ON d.file_id = f.id
964 LEFT JOIN files f2 ON d.resolved_file_id = f2.id
965 WHERE d.import_type = 'internal'
966 ORDER BY f.path",
967 )?;
968
969 let mut deps = Vec::new();
970
971 let rows = stmt.query_map([], |row| {
972 Ok((
973 row.get::<_, String>(0)?,
974 row.get::<_, String>(1)?,
975 row.get::<_, Option<String>>(2)?,
976 ))
977 })?;
978
979 for row in rows {
980 deps.push(row?);
981 }
982
983 Ok(deps)
984 }
985
986 pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
988 let conn = self.open_conn()?;
989
990 let mut stmt = conn.prepare(
991 "SELECT import_type, COUNT(*) as count
992 FROM file_dependencies
993 GROUP BY import_type
994 ORDER BY import_type",
995 )?;
996
997 let mut counts = Vec::new();
998
999 let rows = stmt.query_map([], |row| {
1000 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
1001 })?;
1002
1003 for row in rows {
1004 counts.push(row?);
1005 }
1006
1007 Ok(counts)
1008 }
1009}
1010
1011fn is_entry_point(path: &str) -> bool {
1016 let p = path.replace('\\', "/");
1017 let p = p.as_str();
1018
1019 if matches!(
1021 p,
1022 "src/lib.rs" | "src/main.rs" | "build.rs" | "lib.rs" | "main.rs"
1023 ) {
1024 return true;
1025 }
1026
1027 if p.starts_with("tests/") || p.starts_with("benches/") || p.starts_with("examples/") {
1029 return true;
1030 }
1031
1032 let filename = p.rsplit('/').next().unwrap_or(p);
1034 if filename.starts_with("test_")
1035 || filename.ends_with("_test.rs")
1036 || filename.ends_with("_spec.rs")
1037 {
1038 return true;
1039 }
1040
1041 false
1042}
1043
1044fn generate_path_variants(import_path: &str) -> Vec<String> {
1056 let path = import_path.replace('\\', "/").replace("::", "/");
1058
1059 let path = path.trim_matches('"').trim_matches('\'');
1061
1062 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1064
1065 if components.is_empty() {
1066 return vec![];
1067 }
1068
1069 let mut variants = Vec::new();
1070
1071 for start_idx in 0..components.len() {
1078 let suffix = components[start_idx..].join("/");
1079
1080 if !suffix.ends_with(".php") {
1082 variants.push(format!("{}.php", suffix));
1083 } else {
1084 variants.push(suffix.clone());
1085 }
1086
1087 if !suffix.contains('.') {
1089 variants.push(format!("{}.rs", suffix));
1091 variants.push(format!("{}.ts", suffix));
1092 variants.push(format!("{}.js", suffix));
1093 variants.push(format!("{}.py", suffix));
1094 }
1095 }
1096
1097 variants
1098}
1099
1100fn normalize_path_for_lookup(path: &str) -> String {
1111 let mut normalized = path.trim_start_matches("./").to_string();
1113 if normalized.starts_with("../") {
1114 normalized = normalized.trim_start_matches("../").to_string();
1115 }
1116
1117 if normalized.starts_with('/') || normalized.starts_with('\\') {
1121 let markers = ["services", "src", "app", "lib", "packages", "modules"];
1123
1124 let mut found_marker = false;
1125 for marker in &markers {
1126 if let Some(idx) = normalized.find(marker) {
1127 normalized = normalized[idx..].to_string();
1128 found_marker = true;
1129 break;
1130 }
1131 }
1132
1133 if !found_marker {
1135 use std::path::Path;
1136 let path_obj = Path::new(&normalized);
1137 if let Some(filename) = path_obj.file_name() {
1138 normalized = filename.to_string_lossy().to_string();
1139 }
1140 }
1141 }
1142
1143 normalized
1144}
1145
1146pub fn resolve_rust_import(
1165 import_path: &str,
1166 current_file: &str,
1167 project_root: &std::path::Path,
1168) -> Option<String> {
1169 use std::path::{Path, PathBuf};
1170
1171 if !import_path.starts_with("crate::")
1173 && !import_path.starts_with("super::")
1174 && !import_path.starts_with("self::")
1175 {
1176 return None;
1177 }
1178
1179 let current_path = Path::new(current_file);
1180 let mut resolved_path: Option<PathBuf> = None;
1181
1182 if import_path.starts_with("crate::") {
1183 let crate_root = if project_root.join("src/lib.rs").exists() {
1185 project_root.join("src")
1186 } else if project_root.join("src/main.rs").exists() {
1187 project_root.join("src")
1188 } else {
1189 project_root.join("src")
1191 };
1192
1193 let path_parts: Vec<&str> = import_path
1194 .strip_prefix("crate::")
1195 .unwrap()
1196 .split("::")
1197 .collect();
1198
1199 resolved_path = resolve_module_path(&crate_root, &path_parts);
1200 } else if import_path.starts_with("super::") {
1201 if let Some(current_dir) = current_path.parent() {
1203 if let Some(parent_dir) = current_dir.parent() {
1204 let path_parts: Vec<&str> = import_path
1205 .strip_prefix("super::")
1206 .unwrap()
1207 .split("::")
1208 .collect();
1209
1210 resolved_path = resolve_module_path(parent_dir, &path_parts);
1211 }
1212 }
1213 } else if import_path.starts_with("self::") {
1214 if let Some(current_dir) = current_path.parent() {
1216 let path_parts: Vec<&str> = import_path
1217 .strip_prefix("self::")
1218 .unwrap()
1219 .split("::")
1220 .collect();
1221
1222 resolved_path = resolve_module_path(current_dir, &path_parts);
1223 }
1224 }
1225
1226 resolved_path.and_then(|p| {
1228 p.strip_prefix(project_root)
1229 .ok()
1230 .map(|rel| rel.to_string_lossy().to_string())
1231 })
1232}
1233
1234fn resolve_module_path(
1240 start_dir: &std::path::Path,
1241 components: &[&str],
1242) -> Option<std::path::PathBuf> {
1243 if components.is_empty() {
1244 return None;
1245 }
1246
1247 let mut current = start_dir.to_path_buf();
1248
1249 for &component in &components[..components.len() - 1] {
1251 let dir_path = current.join(component);
1253 let mod_file = dir_path.join("mod.rs");
1254
1255 if mod_file.exists() {
1256 current = dir_path;
1257 } else {
1258 return None;
1260 }
1261 }
1262
1263 let last_component = components.last().unwrap();
1265
1266 let file_path = current.join(format!("{}.rs", last_component));
1268 if file_path.exists() {
1269 return Some(file_path);
1270 }
1271
1272 let dir_path = current.join(last_component);
1274 let mod_file = dir_path.join("mod.rs");
1275 if mod_file.exists() {
1276 return Some(mod_file);
1277 }
1278
1279 None
1280}
1281
1282pub fn resolve_rust_mod_declaration(
1288 mod_name: &str,
1289 current_file: &str,
1290 _project_root: &std::path::Path,
1291) -> Option<String> {
1292 use std::path::Path;
1293
1294 let current_path = Path::new(current_file);
1295 let current_dir = current_path.parent()?;
1296
1297 let sibling = current_dir.join(format!("{}.rs", mod_name));
1299 if sibling.exists() {
1300 return Some(sibling.to_string_lossy().to_string());
1301 }
1302
1303 let dir_mod = current_dir.join(mod_name).join("mod.rs");
1305 if dir_mod.exists() {
1306 return Some(dir_mod.to_string_lossy().to_string());
1307 }
1308
1309 None
1310}
1311
1312pub fn resolve_php_import(
1335 import_path: &str,
1336 _current_file: &str,
1337 project_root: &std::path::Path,
1338) -> Option<String> {
1339 const VENDOR_NAMESPACES: &[&str] = &[
1341 "Illuminate\\",
1342 "Symfony\\",
1343 "Laravel\\",
1344 "Psr\\",
1345 "Doctrine\\",
1346 "Monolog\\",
1347 "PHPUnit\\",
1348 "Carbon\\",
1349 "GuzzleHttp\\",
1350 "Composer\\",
1351 "Predis\\",
1352 "League\\",
1353 ];
1354
1355 for vendor_ns in VENDOR_NAMESPACES {
1357 if import_path.starts_with(vendor_ns) {
1358 return None;
1359 }
1360 }
1361
1362 let file_path = import_path.replace('\\', "/");
1366
1367 let path_candidates = vec![
1371 {
1373 let parts: Vec<&str> = file_path.split('/').collect();
1374 if let Some(first) = parts.first() {
1375 let mut result = vec![first.to_lowercase()];
1376 result.extend(parts[1..].iter().map(|s| s.to_string()));
1377 result.join("/") + ".php"
1378 } else {
1379 file_path.clone() + ".php"
1380 }
1381 },
1382 file_path.clone() + ".php",
1384 file_path.to_lowercase() + ".php",
1386 ];
1387
1388 for candidate in &path_candidates {
1390 let full_path = project_root.join(candidate);
1391 if full_path.exists() {
1392 return Some(candidate.clone());
1394 }
1395 }
1396
1397 None
1399}
1400
1401#[cfg(test)]
1402mod tests {
1403 use super::*;
1404 use tempfile::TempDir;
1405
1406 fn setup_test_cache() -> (TempDir, CacheManager) {
1407 let temp = TempDir::new().unwrap();
1408 let cache = CacheManager::new(temp.path());
1409 cache.init().unwrap();
1410
1411 cache.update_file("src/main.rs", "rust", 100).unwrap();
1413 cache.update_file("src/lib.rs", "rust", 50).unwrap();
1414 cache.update_file("src/utils.rs", "rust", 30).unwrap();
1415
1416 (temp, cache)
1417 }
1418
1419 #[test]
1420 fn test_insert_and_get_dependencies() {
1421 let (_temp, cache) = setup_test_cache();
1422 let deps_index = DependencyIndex::new(cache);
1423
1424 let main_id = 1i64;
1426 let lib_id = 2i64;
1427
1428 deps_index
1430 .insert_dependency(
1431 main_id,
1432 "crate::lib".to_string(),
1433 Some(lib_id),
1434 ImportType::Internal,
1435 5,
1436 None,
1437 )
1438 .unwrap();
1439
1440 let deps = deps_index.get_dependencies(main_id).unwrap();
1442 assert_eq!(deps.len(), 1);
1443 assert_eq!(deps[0].imported_path, "crate::lib");
1444 assert_eq!(deps[0].resolved_file_id, Some(lib_id));
1445 assert_eq!(deps[0].import_type, ImportType::Internal);
1446 }
1447
1448 #[test]
1449 fn test_reverse_lookup() {
1450 let (_temp, cache) = setup_test_cache();
1451 let deps_index = DependencyIndex::new(cache);
1452
1453 let main_id = 1i64;
1454 let lib_id = 2i64;
1455 let utils_id = 3i64;
1456
1457 deps_index
1459 .insert_dependency(
1460 main_id,
1461 "crate::lib".to_string(),
1462 Some(lib_id),
1463 ImportType::Internal,
1464 5,
1465 None,
1466 )
1467 .unwrap();
1468
1469 deps_index
1471 .insert_dependency(
1472 utils_id,
1473 "crate::lib".to_string(),
1474 Some(lib_id),
1475 ImportType::Internal,
1476 3,
1477 None,
1478 )
1479 .unwrap();
1480
1481 let dependents = deps_index.get_dependents(lib_id).unwrap();
1483 assert_eq!(dependents.len(), 2);
1484 assert!(dependents.contains(&main_id));
1485 assert!(dependents.contains(&utils_id));
1486 }
1487
1488 #[test]
1489 fn test_transitive_dependencies() {
1490 let (_temp, cache) = setup_test_cache();
1491 let deps_index = DependencyIndex::new(cache);
1492
1493 let file1 = 1i64;
1494 let file2 = 2i64;
1495 let file3 = 3i64;
1496
1497 deps_index
1499 .insert_dependency(
1500 file1,
1501 "file2".to_string(),
1502 Some(file2),
1503 ImportType::Internal,
1504 1,
1505 None,
1506 )
1507 .unwrap();
1508
1509 deps_index
1510 .insert_dependency(
1511 file2,
1512 "file3".to_string(),
1513 Some(file3),
1514 ImportType::Internal,
1515 1,
1516 None,
1517 )
1518 .unwrap();
1519
1520 let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
1522
1523 assert_eq!(transitive.len(), 3);
1525 assert_eq!(transitive.get(&file1), Some(&0));
1526 assert_eq!(transitive.get(&file2), Some(&1));
1527 assert_eq!(transitive.get(&file3), Some(&2));
1528 }
1529
1530 #[test]
1531 fn test_batch_insert() {
1532 let (_temp, cache) = setup_test_cache();
1533 let deps_index = DependencyIndex::new(cache);
1534
1535 let deps = vec![
1536 Dependency {
1537 file_id: 1,
1538 imported_path: "std::collections".to_string(),
1539 resolved_file_id: None,
1540 import_type: ImportType::Stdlib,
1541 line_number: 1,
1542 imported_symbols: Some(vec!["HashMap".to_string()]),
1543 },
1544 Dependency {
1545 file_id: 1,
1546 imported_path: "crate::lib".to_string(),
1547 resolved_file_id: Some(2),
1548 import_type: ImportType::Internal,
1549 line_number: 2,
1550 imported_symbols: None,
1551 },
1552 ];
1553
1554 deps_index.batch_insert_dependencies(&deps).unwrap();
1555
1556 let retrieved = deps_index.get_dependencies(1).unwrap();
1557 assert_eq!(retrieved.len(), 2);
1558 }
1559
1560 #[test]
1561 fn test_clear_dependencies() {
1562 let (_temp, cache) = setup_test_cache();
1563 let deps_index = DependencyIndex::new(cache);
1564
1565 deps_index
1567 .insert_dependency(
1568 1,
1569 "crate::lib".to_string(),
1570 Some(2),
1571 ImportType::Internal,
1572 1,
1573 None,
1574 )
1575 .unwrap();
1576
1577 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
1579
1580 deps_index.clear_dependencies(1).unwrap();
1582
1583 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
1585 }
1586
1587 #[test]
1588 fn test_resolve_rust_import_crate() {
1589 use std::fs;
1590 use tempfile::TempDir;
1591
1592 let temp = TempDir::new().unwrap();
1593 let project_root = temp.path();
1594
1595 fs::create_dir_all(project_root.join("src")).unwrap();
1597 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1598 fs::write(project_root.join("src/models.rs"), "").unwrap();
1599
1600 let resolved = resolve_rust_import("crate::models", "src/query.rs", project_root);
1602
1603 assert_eq!(resolved, Some("src/models.rs".to_string()));
1604 }
1605
1606 #[test]
1607 fn test_resolve_rust_import_super() {
1608 use std::fs;
1609 use tempfile::TempDir;
1610
1611 let temp = TempDir::new().unwrap();
1612 let project_root = temp.path();
1613
1614 fs::create_dir_all(project_root.join("src/parsers")).unwrap();
1616 fs::write(project_root.join("src/models.rs"), "").unwrap();
1617 fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
1618
1619 let current_file = project_root.join("src/parsers/rust.rs");
1622 let resolved = resolve_rust_import(
1623 "super::models",
1624 ¤t_file.to_string_lossy(),
1625 project_root,
1626 );
1627
1628 assert_eq!(resolved, Some("src/models.rs".to_string()));
1629 }
1630
1631 #[test]
1632 fn test_resolve_rust_import_external() {
1633 use tempfile::TempDir;
1634
1635 let temp = TempDir::new().unwrap();
1636 let project_root = temp.path();
1637
1638 let resolved = resolve_rust_import("serde::Serialize", "src/models.rs", project_root);
1640
1641 assert_eq!(resolved, None);
1642
1643 let resolved =
1645 resolve_rust_import("std::collections::HashMap", "src/models.rs", project_root);
1646
1647 assert_eq!(resolved, None);
1648 }
1649
1650 #[test]
1651 fn test_resolve_rust_mod_declaration() {
1652 use std::fs;
1653 use tempfile::TempDir;
1654
1655 let temp = TempDir::new().unwrap();
1656 let project_root = temp.path();
1657
1658 fs::create_dir_all(project_root.join("src")).unwrap();
1660 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1661 fs::write(project_root.join("src/parser.rs"), "").unwrap();
1662
1663 let resolved = resolve_rust_mod_declaration(
1665 "parser",
1666 &project_root.join("src/lib.rs").to_string_lossy(),
1667 project_root,
1668 );
1669
1670 assert!(resolved.is_some());
1671 assert!(resolved.unwrap().ends_with("src/parser.rs"));
1672 }
1673
1674 #[test]
1675 fn test_resolve_rust_import_nested() {
1676 use std::fs;
1677 use tempfile::TempDir;
1678
1679 let temp = TempDir::new().unwrap();
1680 let project_root = temp.path();
1681
1682 fs::create_dir_all(project_root.join("src/models")).unwrap();
1684 fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
1685 fs::write(project_root.join("src/models/language.rs"), "").unwrap();
1686
1687 let resolved = resolve_rust_import("crate::models::language", "src/query.rs", project_root);
1689
1690 assert_eq!(resolved, Some("src/models/language.rs".to_string()));
1691 }
1692}