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 let std::collections::hash_map::Entry::Vacant(e) = visited.entry(resolved_id)
339 {
340 e.insert(depth + 1);
341 queue.push_back((resolved_id, depth + 1));
342 }
343 }
344 }
345 }
346
347 Ok(visited)
348 }
349
350 pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
358 let conn = self.open_conn()?;
359
360 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
362
363 let mut stmt = conn.prepare(
366 "SELECT file_id, resolved_file_id
367 FROM file_dependencies
368 WHERE resolved_file_id IS NOT NULL
369 AND import_type != 'mod_decl'",
370 )?;
371
372 let dependencies: Vec<(i64, i64)> = stmt
373 .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?
374 .collect::<Result<Vec<_>, _>>()?;
375
376 for (file_id, target_id) in dependencies {
378 graph.entry(file_id).or_default().push(target_id);
379 }
380
381 let all_files = self.get_all_file_ids()?;
383
384 let mut visited = HashSet::new();
385 let mut rec_stack = HashSet::new();
386 let mut path = Vec::new();
387 let mut cycles = Vec::new();
388
389 for file_id in all_files {
390 if !visited.contains(&file_id) {
391 self.dfs_cycle_detect(
392 file_id,
393 &graph,
394 &mut visited,
395 &mut rec_stack,
396 &mut path,
397 &mut cycles,
398 )?;
399 }
400 }
401
402 Ok(cycles)
403 }
404
405 fn dfs_cycle_detect(
407 &self,
408 file_id: i64,
409 graph: &HashMap<i64, Vec<i64>>,
410 visited: &mut HashSet<i64>,
411 rec_stack: &mut HashSet<i64>,
412 path: &mut Vec<i64>,
413 cycles: &mut Vec<Vec<i64>>,
414 ) -> Result<()> {
415 visited.insert(file_id);
416 rec_stack.insert(file_id);
417 path.push(file_id);
418
419 if let Some(dependencies) = graph.get(&file_id) {
421 for &target_id in dependencies {
422 if !visited.contains(&target_id) {
423 self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
424 } else if rec_stack.contains(&target_id) {
425 if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
427 let cycle = path[cycle_start..].to_vec();
428 cycles.push(cycle);
429 }
430 }
431 }
432 }
433
434 path.pop();
435 rec_stack.remove(&file_id);
436
437 Ok(())
438 }
439
440 pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
444 let conn = self.open_conn()?;
445
446 let mut paths = HashMap::new();
447
448 for &file_id in file_ids {
449 if let Ok(path) =
450 conn.query_row("SELECT path FROM files WHERE id = ?", [file_id], |row| {
451 row.get::<_, String>(0)
452 })
453 {
454 paths.insert(file_id, path);
455 }
456 }
457
458 Ok(paths)
459 }
460
461 fn get_file_path(&self, file_id: i64) -> Result<String> {
463 let conn = self.open_conn()?;
464
465 let path = conn.query_row("SELECT path FROM files WHERE id = ?", [file_id], |row| {
466 row.get::<_, String>(0)
467 })?;
468
469 Ok(path)
470 }
471
472 fn get_all_file_ids(&self) -> Result<Vec<i64>> {
474 let conn = self.open_conn()?;
475
476 let mut stmt = conn.prepare("SELECT id FROM files")?;
477 let file_ids = stmt
478 .query_map([], |row| row.get(0))?
479 .collect::<Result<Vec<_>, _>>()?;
480
481 Ok(file_ids)
482 }
483
484 pub fn find_hotspots(
495 &self,
496 limit: Option<usize>,
497 min_dependents: usize,
498 ) -> Result<Vec<(i64, usize)>> {
499 let conn = self.open_conn()?;
500
501 let mut stmt = conn.prepare(
503 "SELECT resolved_file_id, COUNT(*) as count
504 FROM file_dependencies
505 WHERE resolved_file_id IS NOT NULL
506 GROUP BY resolved_file_id
507 ORDER BY count DESC",
508 )?;
509
510 let mut hotspots: Vec<(i64, usize)> = stmt
512 .query_map([], |row| {
513 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
514 })?
515 .collect::<Result<Vec<_>, _>>()?
516 .into_iter()
517 .filter(|(_, count)| *count >= min_dependents)
518 .collect();
519
520 if let Some(lim) = limit {
522 hotspots.truncate(lim);
523 }
524
525 Ok(hotspots)
526 }
527
528 pub fn find_unused_files(&self) -> Result<Vec<i64>> {
539 let conn = self.open_conn()?;
540
541 let mut used_files = HashSet::new();
543
544 let mut stmt = conn.prepare(
546 "SELECT DISTINCT resolved_file_id
547 FROM file_dependencies
548 WHERE resolved_file_id IS NOT NULL",
549 )?;
550
551 let direct_imports: Vec<i64> = stmt
552 .query_map([], |row| row.get(0))?
553 .collect::<Result<Vec<_>, _>>()?;
554
555 used_files.extend(&direct_imports);
556
557 for file_id in direct_imports {
559 let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
561 used_files.extend(barrel_chain);
562 }
563
564 let mut stmt = conn.prepare("SELECT id, path FROM files ORDER BY id")?;
567 let all_files: Vec<(i64, String)> = stmt
568 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
569 .collect::<Result<Vec<_>, _>>()?;
570
571 let unused: Vec<i64> = all_files
572 .into_iter()
573 .filter(|(id, path)| !used_files.contains(id) && !is_entry_point(path))
574 .map(|(id, _)| id)
575 .collect();
576
577 Ok(unused)
578 }
579
580 pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
604 let conn = self.open_conn()?;
605
606 let mut resolved_files = Vec::new();
607 let mut visited = HashSet::new();
608 let mut queue = VecDeque::new();
609
610 queue.push_back(barrel_file_id);
612 visited.insert(barrel_file_id);
613
614 while let Some(current_id) = queue.pop_front() {
615 resolved_files.push(current_id);
616
617 let mut stmt = conn.prepare(
619 "SELECT resolved_source_id
620 FROM file_exports
621 WHERE file_id = ? AND resolved_source_id IS NOT NULL",
622 )?;
623
624 let exported_files: Vec<i64> = stmt
625 .query_map([current_id], |row| row.get(0))?
626 .collect::<Result<Vec<_>, _>>()?;
627
628 for exported_id in exported_files {
630 if !visited.contains(&exported_id) {
631 visited.insert(exported_id);
632 queue.push_back(exported_id);
633 }
634 }
635 }
636
637 Ok(resolved_files)
638 }
639
640 pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
654 let conn = self.open_conn()?;
655
656 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
658
659 let mut stmt = conn.prepare(
660 "SELECT file_id, resolved_file_id
661 FROM file_dependencies
662 WHERE resolved_file_id IS NOT NULL",
663 )?;
664
665 let dependencies: Vec<(i64, i64)> = stmt
666 .query_map([], |row| Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?)))?
667 .collect::<Result<Vec<_>, _>>()?;
668
669 for (file_id, target_id) in dependencies {
671 graph.entry(file_id).or_default().push(target_id);
673 graph.entry(target_id).or_default().push(file_id);
674 }
675
676 let all_files = self.get_all_file_ids()?;
678
679 for file_id in &all_files {
681 graph.entry(*file_id).or_default();
682 }
683
684 let mut visited = HashSet::new();
686 let mut islands = Vec::new();
687
688 for &file_id in &all_files {
689 if !visited.contains(&file_id) {
690 let mut island = Vec::new();
691 self.dfs_island(&file_id, &graph, &mut visited, &mut island);
692 islands.push(island);
693 }
694 }
695
696 islands.sort_by_key(|a: &Vec<_>| std::cmp::Reverse(a.len()));
698
699 log::info!("Found {} islands (connected components)", islands.len());
700
701 Ok(islands)
702 }
703
704 fn dfs_island(
706 &self,
707 file_id: &i64,
708 graph: &HashMap<i64, Vec<i64>>,
709 visited: &mut HashSet<i64>,
710 island: &mut Vec<i64>,
711 ) {
712 visited.insert(*file_id);
713 island.push(*file_id);
714
715 if let Some(neighbors) = graph.get(file_id) {
716 for &neighbor in neighbors {
717 if !visited.contains(&neighbor) {
718 self.dfs_island(&neighbor, graph, visited, island);
719 }
720 }
721 }
722 }
723
724 #[allow(dead_code)]
748 fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
749 let conn = self.open_conn()?;
750
751 let mut stmt = conn.prepare("SELECT DISTINCT imported_path FROM file_dependencies")?;
753
754 let imported_paths: Vec<String> = stmt
755 .query_map([], |row| row.get(0))?
756 .collect::<Result<Vec<_>, _>>()?;
757
758 let total_paths = imported_paths.len();
759 log::info!(
760 "Building resolution cache for {} unique imported paths",
761 total_paths
762 );
763
764 let mut cache = HashMap::new();
766
767 for imported_path in imported_paths {
768 if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
769 cache.insert(imported_path, file_id);
770 }
771 }
772
773 log::info!(
774 "Resolution cache built: {} resolved, {} unresolved",
775 cache.len(),
776 total_paths - cache.len()
777 );
778
779 Ok(cache)
780 }
781
782 pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
784 let conn = self.open_conn()?;
785
786 conn.execute("DELETE FROM file_dependencies WHERE file_id = ?", [file_id])?;
787
788 Ok(())
789 }
790
791 pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
810 let path_variants = generate_path_variants(imported_path);
811
812 for variant in &path_variants {
813 if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
814 log::trace!(
815 "Resolved '{}' → '{}' (file_id: {})",
816 imported_path,
817 variant,
818 file_id
819 );
820 return Ok(Some(file_id));
821 }
822 }
823
824 Ok(None)
825 }
826
827 pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
838 let conn = self.open_conn()?;
839
840 let normalized_path = normalize_path_for_lookup(path);
842
843 match conn.query_row(
845 "SELECT id FROM files WHERE path = ?",
846 [&normalized_path],
847 |row| row.get::<_, i64>(0),
848 ) {
849 Ok(id) => return Ok(Some(id)),
850 Err(rusqlite::Error::QueryReturnedNoRows) => {
851 }
853 Err(e) => return Err(e.into()),
854 }
855
856 let mut stmt = conn.prepare("SELECT id, path FROM files WHERE path LIKE '%' || ?")?;
858
859 let matches: Vec<(i64, String)> = stmt
860 .query_map([&normalized_path], |row| Ok((row.get(0)?, row.get(1)?)))?
861 .collect::<Result<Vec<_>, _>>()?;
862
863 match matches.len() {
864 0 => Ok(None),
865 1 => Ok(Some(matches[0].0)),
866 _ => {
867 let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
869 anyhow::bail!(
870 "Ambiguous path '{}' matches multiple files:\n {}\n\nPlease be more specific.",
871 path,
872 paths.join("\n ")
873 );
874 }
875 }
876 }
877
878 pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
887 let conn = self.open_conn()?;
888
889 let mut stmt = conn.prepare(
890 "SELECT
891 CASE
892 WHEN f.path LIKE '%.py' THEN 'Python'
893 WHEN f.path LIKE '%.go' THEN 'Go'
894 WHEN f.path LIKE '%.ts' THEN 'TypeScript'
895 WHEN f.path LIKE '%.rs' THEN 'Rust'
896 WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
897 WHEN f.path LIKE '%.php' THEN 'PHP'
898 WHEN f.path LIKE '%.java' THEN 'Java'
899 WHEN f.path LIKE '%.kt' THEN 'Kotlin'
900 WHEN f.path LIKE '%.rb' THEN 'Ruby'
901 WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
902 WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
903 WHEN f.path LIKE '%.cs' THEN 'C#'
904 WHEN f.path LIKE '%.zig' THEN 'Zig'
905 ELSE 'Other'
906 END as language,
907 COUNT(*) as total,
908 SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
909 FROM file_dependencies d
910 JOIN files f ON d.file_id = f.id
911 WHERE d.import_type = 'internal'
912 GROUP BY language
913 ORDER BY language",
914 )?;
915
916 let mut stats = Vec::new();
917
918 let rows = stmt.query_map([], |row| {
919 let language: String = row.get(0)?;
920 let total: i64 = row.get(1)?;
921 let resolved: i64 = row.get(2)?;
922 let rate = if total > 0 {
923 (resolved as f64 / total as f64) * 100.0
924 } else {
925 0.0
926 };
927
928 Ok((language, total as usize, resolved as usize, rate))
929 })?;
930
931 for row in rows {
932 stats.push(row?);
933 }
934
935 Ok(stats)
936 }
937
938 pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
948 let conn = self.open_conn()?;
949
950 let mut stmt = conn.prepare(
951 "SELECT
952 f.path,
953 d.imported_path,
954 f2.path as resolved_path
955 FROM file_dependencies d
956 JOIN files f ON d.file_id = f.id
957 LEFT JOIN files f2 ON d.resolved_file_id = f2.id
958 WHERE d.import_type = 'internal'
959 ORDER BY f.path",
960 )?;
961
962 let mut deps = Vec::new();
963
964 let rows = stmt.query_map([], |row| {
965 Ok((
966 row.get::<_, String>(0)?,
967 row.get::<_, String>(1)?,
968 row.get::<_, Option<String>>(2)?,
969 ))
970 })?;
971
972 for row in rows {
973 deps.push(row?);
974 }
975
976 Ok(deps)
977 }
978
979 pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
981 let conn = self.open_conn()?;
982
983 let mut stmt = conn.prepare(
984 "SELECT import_type, COUNT(*) as count
985 FROM file_dependencies
986 GROUP BY import_type
987 ORDER BY import_type",
988 )?;
989
990 let mut counts = Vec::new();
991
992 let rows = stmt.query_map([], |row| {
993 Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)? as usize))
994 })?;
995
996 for row in rows {
997 counts.push(row?);
998 }
999
1000 Ok(counts)
1001 }
1002}
1003
1004fn is_entry_point(path: &str) -> bool {
1009 let p = path.replace('\\', "/");
1010 let p = p.as_str();
1011
1012 if matches!(
1014 p,
1015 "src/lib.rs" | "src/main.rs" | "build.rs" | "lib.rs" | "main.rs"
1016 ) {
1017 return true;
1018 }
1019
1020 if p.starts_with("tests/") || p.starts_with("benches/") || p.starts_with("examples/") {
1022 return true;
1023 }
1024
1025 let filename = p.rsplit('/').next().unwrap_or(p);
1027 if filename.starts_with("test_")
1028 || filename.ends_with("_test.rs")
1029 || filename.ends_with("_spec.rs")
1030 {
1031 return true;
1032 }
1033
1034 false
1035}
1036
1037fn generate_path_variants(import_path: &str) -> Vec<String> {
1049 let path = import_path.replace('\\', "/").replace("::", "/");
1051
1052 let path = path.trim_matches('"').trim_matches('\'');
1054
1055 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1057
1058 if components.is_empty() {
1059 return vec![];
1060 }
1061
1062 let mut variants = Vec::new();
1063
1064 for start_idx in 0..components.len() {
1071 let suffix = components[start_idx..].join("/");
1072
1073 if !suffix.ends_with(".php") {
1075 variants.push(format!("{}.php", suffix));
1076 } else {
1077 variants.push(suffix.clone());
1078 }
1079
1080 if !suffix.contains('.') {
1082 variants.push(format!("{}.rs", suffix));
1084 variants.push(format!("{}.ts", suffix));
1085 variants.push(format!("{}.js", suffix));
1086 variants.push(format!("{}.py", suffix));
1087 }
1088 }
1089
1090 variants
1091}
1092
1093fn normalize_path_for_lookup(path: &str) -> String {
1104 let mut normalized = path.trim_start_matches("./").to_string();
1106 if normalized.starts_with("../") {
1107 normalized = normalized.trim_start_matches("../").to_string();
1108 }
1109
1110 if normalized.starts_with('/') || normalized.starts_with('\\') {
1114 let markers = ["services", "src", "app", "lib", "packages", "modules"];
1116
1117 let mut found_marker = false;
1118 for marker in &markers {
1119 if let Some(idx) = normalized.find(marker) {
1120 normalized = normalized[idx..].to_string();
1121 found_marker = true;
1122 break;
1123 }
1124 }
1125
1126 if !found_marker {
1128 use std::path::Path;
1129 let path_obj = Path::new(&normalized);
1130 if let Some(filename) = path_obj.file_name() {
1131 normalized = filename.to_string_lossy().to_string();
1132 }
1133 }
1134 }
1135
1136 normalized
1137}
1138
1139pub fn resolve_rust_import(
1158 import_path: &str,
1159 current_file: &str,
1160 project_root: &std::path::Path,
1161) -> Option<String> {
1162 use std::path::{Path, PathBuf};
1163
1164 if !import_path.starts_with("crate::")
1166 && !import_path.starts_with("super::")
1167 && !import_path.starts_with("self::")
1168 {
1169 return None;
1170 }
1171
1172 let current_path = Path::new(current_file);
1173 let mut resolved_path: Option<PathBuf> = None;
1174
1175 if import_path.starts_with("crate::") {
1176 let crate_root = if project_root.join("src/lib.rs").exists()
1178 || project_root.join("src/main.rs").exists()
1179 {
1180 project_root.join("src")
1181 } else {
1182 project_root.join("src")
1184 };
1185
1186 let path_parts: Vec<&str> = import_path
1187 .strip_prefix("crate::")
1188 .unwrap()
1189 .split("::")
1190 .collect();
1191
1192 resolved_path = resolve_module_path(&crate_root, &path_parts);
1193 } else if import_path.starts_with("super::") {
1194 if let Some(current_dir) = current_path.parent()
1196 && let Some(parent_dir) = current_dir.parent()
1197 {
1198 let path_parts: Vec<&str> = import_path
1199 .strip_prefix("super::")
1200 .unwrap()
1201 .split("::")
1202 .collect();
1203
1204 resolved_path = resolve_module_path(parent_dir, &path_parts);
1205 }
1206 } else if import_path.starts_with("self::") {
1207 if let Some(current_dir) = current_path.parent() {
1209 let path_parts: Vec<&str> = import_path
1210 .strip_prefix("self::")
1211 .unwrap()
1212 .split("::")
1213 .collect();
1214
1215 resolved_path = resolve_module_path(current_dir, &path_parts);
1216 }
1217 }
1218
1219 resolved_path.and_then(|p| {
1222 p.strip_prefix(project_root)
1223 .ok()
1224 .map(|rel| rel.to_string_lossy().replace('\\', "/"))
1225 })
1226}
1227
1228fn resolve_module_path(
1234 start_dir: &std::path::Path,
1235 components: &[&str],
1236) -> Option<std::path::PathBuf> {
1237 if components.is_empty() {
1238 return None;
1239 }
1240
1241 let mut current = start_dir.to_path_buf();
1242
1243 for &component in &components[..components.len() - 1] {
1245 let dir_path = current.join(component);
1247 let mod_file = dir_path.join("mod.rs");
1248
1249 if mod_file.exists() {
1250 current = dir_path;
1251 } else {
1252 return None;
1254 }
1255 }
1256
1257 let last_component = components.last().unwrap();
1259
1260 let file_path = current.join(format!("{}.rs", last_component));
1262 if file_path.exists() {
1263 return Some(file_path);
1264 }
1265
1266 let dir_path = current.join(last_component);
1268 let mod_file = dir_path.join("mod.rs");
1269 if mod_file.exists() {
1270 return Some(mod_file);
1271 }
1272
1273 None
1274}
1275
1276pub fn resolve_rust_mod_declaration(
1282 mod_name: &str,
1283 current_file: &str,
1284 _project_root: &std::path::Path,
1285) -> Option<String> {
1286 use std::path::Path;
1287
1288 let current_path = Path::new(current_file);
1289 let current_dir = current_path.parent()?;
1290
1291 let sibling = current_dir.join(format!("{}.rs", mod_name));
1293 if sibling.exists() {
1294 return Some(sibling.to_string_lossy().replace('\\', "/"));
1295 }
1296
1297 let dir_mod = current_dir.join(mod_name).join("mod.rs");
1299 if dir_mod.exists() {
1300 return Some(dir_mod.to_string_lossy().replace('\\', "/"));
1301 }
1302
1303 None
1304}
1305
1306pub fn resolve_php_import(
1329 import_path: &str,
1330 _current_file: &str,
1331 project_root: &std::path::Path,
1332) -> Option<String> {
1333 const VENDOR_NAMESPACES: &[&str] = &[
1335 "Illuminate\\",
1336 "Symfony\\",
1337 "Laravel\\",
1338 "Psr\\",
1339 "Doctrine\\",
1340 "Monolog\\",
1341 "PHPUnit\\",
1342 "Carbon\\",
1343 "GuzzleHttp\\",
1344 "Composer\\",
1345 "Predis\\",
1346 "League\\",
1347 ];
1348
1349 for vendor_ns in VENDOR_NAMESPACES {
1351 if import_path.starts_with(vendor_ns) {
1352 return None;
1353 }
1354 }
1355
1356 let file_path = import_path.replace('\\', "/");
1360
1361 let path_candidates = vec![
1365 {
1367 let parts: Vec<&str> = file_path.split('/').collect();
1368 if let Some(first) = parts.first() {
1369 let mut result = vec![first.to_lowercase()];
1370 result.extend(parts[1..].iter().map(|s| s.to_string()));
1371 result.join("/") + ".php"
1372 } else {
1373 file_path.clone() + ".php"
1374 }
1375 },
1376 file_path.clone() + ".php",
1378 file_path.to_lowercase() + ".php",
1380 ];
1381
1382 for candidate in &path_candidates {
1384 let full_path = project_root.join(candidate);
1385 if full_path.exists() {
1386 return Some(candidate.clone());
1388 }
1389 }
1390
1391 None
1393}
1394
1395#[cfg(test)]
1396mod tests {
1397 use super::*;
1398 use tempfile::TempDir;
1399
1400 fn setup_test_cache() -> (TempDir, CacheManager) {
1401 let temp = TempDir::new().unwrap();
1402 let cache = CacheManager::new(temp.path());
1403 cache.init().unwrap();
1404
1405 cache.update_file("src/main.rs", "rust", 100).unwrap();
1407 cache.update_file("src/lib.rs", "rust", 50).unwrap();
1408 cache.update_file("src/utils.rs", "rust", 30).unwrap();
1409
1410 (temp, cache)
1411 }
1412
1413 #[test]
1414 fn test_insert_and_get_dependencies() {
1415 let (_temp, cache) = setup_test_cache();
1416 let deps_index = DependencyIndex::new(cache);
1417
1418 let main_id = 1i64;
1420 let lib_id = 2i64;
1421
1422 deps_index
1424 .insert_dependency(
1425 main_id,
1426 "crate::lib".to_string(),
1427 Some(lib_id),
1428 ImportType::Internal,
1429 5,
1430 None,
1431 )
1432 .unwrap();
1433
1434 let deps = deps_index.get_dependencies(main_id).unwrap();
1436 assert_eq!(deps.len(), 1);
1437 assert_eq!(deps[0].imported_path, "crate::lib");
1438 assert_eq!(deps[0].resolved_file_id, Some(lib_id));
1439 assert_eq!(deps[0].import_type, ImportType::Internal);
1440 }
1441
1442 #[test]
1443 fn test_reverse_lookup() {
1444 let (_temp, cache) = setup_test_cache();
1445 let deps_index = DependencyIndex::new(cache);
1446
1447 let main_id = 1i64;
1448 let lib_id = 2i64;
1449 let utils_id = 3i64;
1450
1451 deps_index
1453 .insert_dependency(
1454 main_id,
1455 "crate::lib".to_string(),
1456 Some(lib_id),
1457 ImportType::Internal,
1458 5,
1459 None,
1460 )
1461 .unwrap();
1462
1463 deps_index
1465 .insert_dependency(
1466 utils_id,
1467 "crate::lib".to_string(),
1468 Some(lib_id),
1469 ImportType::Internal,
1470 3,
1471 None,
1472 )
1473 .unwrap();
1474
1475 let dependents = deps_index.get_dependents(lib_id).unwrap();
1477 assert_eq!(dependents.len(), 2);
1478 assert!(dependents.contains(&main_id));
1479 assert!(dependents.contains(&utils_id));
1480 }
1481
1482 #[test]
1483 fn test_transitive_dependencies() {
1484 let (_temp, cache) = setup_test_cache();
1485 let deps_index = DependencyIndex::new(cache);
1486
1487 let file1 = 1i64;
1488 let file2 = 2i64;
1489 let file3 = 3i64;
1490
1491 deps_index
1493 .insert_dependency(
1494 file1,
1495 "file2".to_string(),
1496 Some(file2),
1497 ImportType::Internal,
1498 1,
1499 None,
1500 )
1501 .unwrap();
1502
1503 deps_index
1504 .insert_dependency(
1505 file2,
1506 "file3".to_string(),
1507 Some(file3),
1508 ImportType::Internal,
1509 1,
1510 None,
1511 )
1512 .unwrap();
1513
1514 let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
1516
1517 assert_eq!(transitive.len(), 3);
1519 assert_eq!(transitive.get(&file1), Some(&0));
1520 assert_eq!(transitive.get(&file2), Some(&1));
1521 assert_eq!(transitive.get(&file3), Some(&2));
1522 }
1523
1524 #[test]
1525 fn test_batch_insert() {
1526 let (_temp, cache) = setup_test_cache();
1527 let deps_index = DependencyIndex::new(cache);
1528
1529 let deps = vec![
1530 Dependency {
1531 file_id: 1,
1532 imported_path: "std::collections".to_string(),
1533 resolved_file_id: None,
1534 import_type: ImportType::Stdlib,
1535 line_number: 1,
1536 imported_symbols: Some(vec!["HashMap".to_string()]),
1537 },
1538 Dependency {
1539 file_id: 1,
1540 imported_path: "crate::lib".to_string(),
1541 resolved_file_id: Some(2),
1542 import_type: ImportType::Internal,
1543 line_number: 2,
1544 imported_symbols: None,
1545 },
1546 ];
1547
1548 deps_index.batch_insert_dependencies(&deps).unwrap();
1549
1550 let retrieved = deps_index.get_dependencies(1).unwrap();
1551 assert_eq!(retrieved.len(), 2);
1552 }
1553
1554 #[test]
1555 fn test_clear_dependencies() {
1556 let (_temp, cache) = setup_test_cache();
1557 let deps_index = DependencyIndex::new(cache);
1558
1559 deps_index
1561 .insert_dependency(
1562 1,
1563 "crate::lib".to_string(),
1564 Some(2),
1565 ImportType::Internal,
1566 1,
1567 None,
1568 )
1569 .unwrap();
1570
1571 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
1573
1574 deps_index.clear_dependencies(1).unwrap();
1576
1577 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
1579 }
1580
1581 #[test]
1582 fn test_resolve_rust_import_crate() {
1583 use std::fs;
1584 use tempfile::TempDir;
1585
1586 let temp = TempDir::new().unwrap();
1587 let project_root = temp.path();
1588
1589 fs::create_dir_all(project_root.join("src")).unwrap();
1591 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1592 fs::write(project_root.join("src/models.rs"), "").unwrap();
1593
1594 let resolved = resolve_rust_import("crate::models", "src/query.rs", project_root);
1596
1597 assert_eq!(resolved, Some("src/models.rs".to_string()));
1598 }
1599
1600 #[test]
1601 fn test_resolve_rust_import_super() {
1602 use std::fs;
1603 use tempfile::TempDir;
1604
1605 let temp = TempDir::new().unwrap();
1606 let project_root = temp.path();
1607
1608 fs::create_dir_all(project_root.join("src/parsers")).unwrap();
1610 fs::write(project_root.join("src/models.rs"), "").unwrap();
1611 fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
1612
1613 let current_file = project_root.join("src/parsers/rust.rs");
1616 let resolved = resolve_rust_import(
1617 "super::models",
1618 ¤t_file.to_string_lossy(),
1619 project_root,
1620 );
1621
1622 assert_eq!(resolved, Some("src/models.rs".to_string()));
1623 }
1624
1625 #[test]
1626 fn test_resolve_rust_import_external() {
1627 use tempfile::TempDir;
1628
1629 let temp = TempDir::new().unwrap();
1630 let project_root = temp.path();
1631
1632 let resolved = resolve_rust_import("serde::Serialize", "src/models.rs", project_root);
1634
1635 assert_eq!(resolved, None);
1636
1637 let resolved =
1639 resolve_rust_import("std::collections::HashMap", "src/models.rs", project_root);
1640
1641 assert_eq!(resolved, None);
1642 }
1643
1644 #[test]
1645 fn test_resolve_rust_mod_declaration() {
1646 use std::fs;
1647 use tempfile::TempDir;
1648
1649 let temp = TempDir::new().unwrap();
1650 let project_root = temp.path();
1651
1652 fs::create_dir_all(project_root.join("src")).unwrap();
1654 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1655 fs::write(project_root.join("src/parser.rs"), "").unwrap();
1656
1657 let resolved = resolve_rust_mod_declaration(
1659 "parser",
1660 &project_root.join("src/lib.rs").to_string_lossy(),
1661 project_root,
1662 );
1663
1664 assert!(resolved.is_some());
1665 assert!(resolved.unwrap().ends_with("src/parser.rs"));
1666 }
1667
1668 #[test]
1669 fn test_resolve_rust_import_nested() {
1670 use std::fs;
1671 use tempfile::TempDir;
1672
1673 let temp = TempDir::new().unwrap();
1674 let project_root = temp.path();
1675
1676 fs::create_dir_all(project_root.join("src/models")).unwrap();
1678 fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
1679 fs::write(project_root.join("src/models/language.rs"), "").unwrap();
1680
1681 let resolved = resolve_rust_import("crate::models::language", "src/query.rs", project_root);
1683
1684 assert_eq!(resolved, Some("src/models/language.rs".to_string()));
1685 }
1686}