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 { cache: Some(cache), db_path }
52 }
53
54 pub fn from_db_path(db_path: impl Into<PathBuf>) -> Self {
58 Self { cache: None, db_path: db_path.into() }
59 }
60
61 pub fn get_cache(&self) -> &CacheManager {
65 self.cache.as_ref().expect("DependencyIndex created with from_db_path has no CacheManager")
66 }
67
68 fn open_conn(&self) -> Result<Connection> {
70 Connection::open(&self.db_path).context("Failed to open database")
71 }
72
73 pub fn insert_dependency(
84 &self,
85 file_id: i64,
86 imported_path: String,
87 resolved_file_id: Option<i64>,
88 import_type: ImportType,
89 line_number: usize,
90 imported_symbols: Option<Vec<String>>,
91 ) -> Result<()> {
92 let conn = self.open_conn()?;
93
94 let import_type_str = match import_type {
95 ImportType::Internal => "internal",
96 ImportType::External => "external",
97 ImportType::Stdlib => "stdlib",
98 ImportType::ModDecl => "mod_decl",
99 };
100
101 let symbols_json = imported_symbols
102 .as_ref()
103 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
104
105 conn.execute(
106 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
107 VALUES (?, ?, ?, ?, ?, ?)",
108 rusqlite::params![
109 file_id,
110 imported_path,
111 resolved_file_id,
112 import_type_str,
113 line_number as i64,
114 symbols_json,
115 ],
116 )?;
117
118 Ok(())
119 }
120
121 pub fn insert_export(
131 &self,
132 file_id: i64,
133 exported_symbol: Option<String>,
134 source_path: String,
135 resolved_source_id: Option<i64>,
136 line_number: usize,
137 ) -> Result<()> {
138 let conn = self.open_conn()?;
139
140 conn.execute(
141 "INSERT INTO file_exports (file_id, exported_symbol, source_path, resolved_source_id, line_number)
142 VALUES (?, ?, ?, ?, ?)",
143 rusqlite::params![
144 file_id,
145 exported_symbol,
146 source_path,
147 resolved_source_id,
148 line_number as i64,
149 ],
150 )?;
151
152 Ok(())
153 }
154
155 pub fn batch_insert_dependencies(&self, dependencies: &[Dependency]) -> Result<()> {
159 if dependencies.is_empty() {
160 return Ok(());
161 }
162
163 let mut conn = self.open_conn()?;
164
165 let tx = conn.transaction()?;
166
167 for dep in dependencies {
168 let import_type_str = match dep.import_type {
169 ImportType::Internal => "internal",
170 ImportType::External => "external",
171 ImportType::Stdlib => "stdlib",
172 ImportType::ModDecl => "mod_decl",
173 };
174
175 let symbols_json = dep
176 .imported_symbols
177 .as_ref()
178 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
179
180 tx.execute(
181 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
182 VALUES (?, ?, ?, ?, ?, ?)",
183 rusqlite::params![
184 dep.file_id,
185 dep.imported_path,
186 dep.resolved_file_id,
187 import_type_str,
188 dep.line_number as i64,
189 symbols_json,
190 ],
191 )?;
192 }
193
194 tx.commit()?;
195 log::debug!("Batch inserted {} dependencies", dependencies.len());
196 Ok(())
197 }
198
199 pub fn get_dependencies(&self, file_id: i64) -> Result<Vec<Dependency>> {
203 let conn = self.open_conn()?;
204
205 let mut stmt = conn.prepare(
206 "SELECT file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols
207 FROM file_dependencies
208 WHERE file_id = ?
209 ORDER BY line_number",
210 )?;
211
212 let deps = stmt
213 .query_map([file_id], |row| {
214 let import_type_str: String = row.get(3)?;
215 let import_type = match import_type_str.as_str() {
216 "internal" => ImportType::Internal,
217 "external" => ImportType::External,
218 "stdlib" => ImportType::Stdlib,
219 "mod_decl" => ImportType::ModDecl,
220 _ => ImportType::External,
221 };
222
223 let symbols_json: Option<String> = row.get(5)?;
224 let imported_symbols = symbols_json.and_then(|json| {
225 serde_json::from_str(&json).ok()
226 });
227
228 Ok(Dependency {
229 file_id: row.get(0)?,
230 imported_path: row.get(1)?,
231 resolved_file_id: row.get(2)?,
232 import_type,
233 line_number: row.get::<_, i64>(4)? as usize,
234 imported_symbols,
235 })
236 })?
237 .collect::<Result<Vec<_>, _>>()?;
238
239 Ok(deps)
240 }
241
242 pub fn get_dependents(&self, file_id: i64) -> Result<Vec<i64>> {
247 let conn = self.open_conn()?;
248
249 let mut stmt = conn.prepare(
251 "SELECT DISTINCT file_id
252 FROM file_dependencies
253 WHERE resolved_file_id = ?
254 ORDER BY file_id"
255 )?;
256
257 let dependents: Vec<i64> = stmt
258 .query_map([file_id], |row| row.get(0))?
259 .collect::<Result<Vec<_>, _>>()?;
260
261 Ok(dependents)
262 }
263
264 pub fn get_dependencies_info(&self, file_id: i64) -> Result<Vec<DependencyInfo>> {
269 let deps = self.get_dependencies(file_id)?;
270
271 let dep_infos = deps
272 .into_iter()
273 .map(|dep| {
274 let path = if let Some(resolved_id) = dep.resolved_file_id {
276 self.get_file_path(resolved_id).unwrap_or(dep.imported_path)
278 } else {
279 dep.imported_path
280 };
281
282 DependencyInfo {
283 path,
284 line: Some(dep.line_number),
285 symbols: dep.imported_symbols,
286 }
287 })
288 .collect();
289
290 Ok(dep_infos)
291 }
292
293 pub fn get_transitive_deps(&self, file_id: i64, max_depth: usize) -> Result<HashMap<i64, usize>> {
308 let mut visited = HashMap::new();
309 let mut queue = VecDeque::new();
310
311 queue.push_back((file_id, 0));
313 visited.insert(file_id, 0);
314
315 while let Some((current_id, depth)) = queue.pop_front() {
316 if depth >= max_depth {
317 continue;
318 }
319
320 let deps = self.get_dependencies(current_id)?;
322
323 for dep in deps {
324 if let Some(resolved_id) = dep.resolved_file_id {
326 if !visited.contains_key(&resolved_id) {
328 visited.insert(resolved_id, depth + 1);
329 queue.push_back((resolved_id, depth + 1));
330 }
331 }
332 }
333 }
334
335 Ok(visited)
336 }
337
338 pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
346 let conn = self.open_conn()?;
347
348 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
350
351 let mut stmt = conn.prepare(
354 "SELECT file_id, resolved_file_id
355 FROM file_dependencies
356 WHERE resolved_file_id IS NOT NULL
357 AND import_type != 'mod_decl'"
358 )?;
359
360 let dependencies: Vec<(i64, i64)> = stmt
361 .query_map([], |row| {
362 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
363 })?
364 .collect::<Result<Vec<_>, _>>()?;
365
366 for (file_id, target_id) in dependencies {
368 graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
369 }
370
371 let all_files = self.get_all_file_ids()?;
373
374 let mut visited = HashSet::new();
375 let mut rec_stack = HashSet::new();
376 let mut path = Vec::new();
377 let mut cycles = Vec::new();
378
379 for file_id in all_files {
380 if !visited.contains(&file_id) {
381 self.dfs_cycle_detect(
382 file_id,
383 &graph,
384 &mut visited,
385 &mut rec_stack,
386 &mut path,
387 &mut cycles,
388 )?;
389 }
390 }
391
392 Ok(cycles)
393 }
394
395 fn dfs_cycle_detect(
397 &self,
398 file_id: i64,
399 graph: &HashMap<i64, Vec<i64>>,
400 visited: &mut HashSet<i64>,
401 rec_stack: &mut HashSet<i64>,
402 path: &mut Vec<i64>,
403 cycles: &mut Vec<Vec<i64>>,
404 ) -> Result<()> {
405 visited.insert(file_id);
406 rec_stack.insert(file_id);
407 path.push(file_id);
408
409 if let Some(dependencies) = graph.get(&file_id) {
411 for &target_id in dependencies {
412 if !visited.contains(&target_id) {
413 self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
414 } else if rec_stack.contains(&target_id) {
415 if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
417 let cycle = path[cycle_start..].to_vec();
418 cycles.push(cycle);
419 }
420 }
421 }
422 }
423
424 path.pop();
425 rec_stack.remove(&file_id);
426
427 Ok(())
428 }
429
430 pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
434 let conn = self.open_conn()?;
435
436 let mut paths = HashMap::new();
437
438 for &file_id in file_ids {
439 if let Ok(path) = conn.query_row(
440 "SELECT path FROM files WHERE id = ?",
441 [file_id],
442 |row| row.get::<_, String>(0),
443 ) {
444 paths.insert(file_id, path);
445 }
446 }
447
448 Ok(paths)
449 }
450
451 fn get_file_path(&self, file_id: i64) -> Result<String> {
453 let conn = self.open_conn()?;
454
455 let path = conn.query_row(
456 "SELECT path FROM files WHERE id = ?",
457 [file_id],
458 |row| row.get::<_, String>(0),
459 )?;
460
461 Ok(path)
462 }
463
464 fn get_all_file_ids(&self) -> Result<Vec<i64>> {
466 let conn = self.open_conn()?;
467
468 let mut stmt = conn.prepare("SELECT id FROM files")?;
469 let file_ids = stmt
470 .query_map([], |row| row.get(0))?
471 .collect::<Result<Vec<_>, _>>()?;
472
473 Ok(file_ids)
474 }
475
476 pub fn find_hotspots(&self, limit: Option<usize>, min_dependents: usize) -> Result<Vec<(i64, usize)>> {
487 let conn = self.open_conn()?;
488
489 let mut stmt = conn.prepare(
491 "SELECT resolved_file_id, COUNT(*) as count
492 FROM file_dependencies
493 WHERE resolved_file_id IS NOT NULL
494 GROUP BY resolved_file_id
495 ORDER BY count DESC"
496 )?;
497
498 let mut hotspots: Vec<(i64, usize)> = stmt
500 .query_map([], |row| {
501 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
502 })?
503 .collect::<Result<Vec<_>, _>>()?
504 .into_iter()
505 .filter(|(_, count)| *count >= min_dependents)
506 .collect();
507
508 if let Some(lim) = limit {
510 hotspots.truncate(lim);
511 }
512
513 Ok(hotspots)
514 }
515
516 pub fn find_unused_files(&self) -> Result<Vec<i64>> {
527 let conn = self.open_conn()?;
528
529 let mut used_files = HashSet::new();
531
532 let mut stmt = conn.prepare(
534 "SELECT DISTINCT resolved_file_id
535 FROM file_dependencies
536 WHERE resolved_file_id IS NOT NULL"
537 )?;
538
539 let direct_imports: Vec<i64> = stmt
540 .query_map([], |row| row.get(0))?
541 .collect::<Result<Vec<_>, _>>()?;
542
543 used_files.extend(&direct_imports);
544
545 for file_id in direct_imports {
547 let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
549 used_files.extend(barrel_chain);
550 }
551
552 let mut stmt = conn.prepare("SELECT id, path FROM files ORDER BY id")?;
555 let all_files: Vec<(i64, String)> = stmt
556 .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
557 .collect::<Result<Vec<_>, _>>()?;
558
559 let unused: Vec<i64> = all_files
560 .into_iter()
561 .filter(|(id, path)| !used_files.contains(id) && !is_entry_point(path))
562 .map(|(id, _)| id)
563 .collect();
564
565 Ok(unused)
566 }
567
568 pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
592 let conn = self.open_conn()?;
593
594 let mut resolved_files = Vec::new();
595 let mut visited = HashSet::new();
596 let mut queue = VecDeque::new();
597
598 queue.push_back(barrel_file_id);
600 visited.insert(barrel_file_id);
601
602 while let Some(current_id) = queue.pop_front() {
603 resolved_files.push(current_id);
604
605 let mut stmt = conn.prepare(
607 "SELECT resolved_source_id
608 FROM file_exports
609 WHERE file_id = ? AND resolved_source_id IS NOT NULL"
610 )?;
611
612 let exported_files: Vec<i64> = stmt
613 .query_map([current_id], |row| row.get(0))?
614 .collect::<Result<Vec<_>, _>>()?;
615
616 for exported_id in exported_files {
618 if !visited.contains(&exported_id) {
619 visited.insert(exported_id);
620 queue.push_back(exported_id);
621 }
622 }
623 }
624
625 Ok(resolved_files)
626 }
627
628 pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
642 let conn = self.open_conn()?;
643
644 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
646
647 let mut stmt = conn.prepare(
648 "SELECT file_id, resolved_file_id
649 FROM file_dependencies
650 WHERE resolved_file_id IS NOT NULL"
651 )?;
652
653 let dependencies: Vec<(i64, i64)> = stmt
654 .query_map([], |row| {
655 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
656 })?
657 .collect::<Result<Vec<_>, _>>()?;
658
659 for (file_id, target_id) in dependencies {
661 graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
663 graph.entry(target_id).or_insert_with(Vec::new).push(file_id);
664 }
665
666 let all_files = self.get_all_file_ids()?;
668
669 for file_id in &all_files {
671 graph.entry(*file_id).or_insert_with(Vec::new);
672 }
673
674 let mut visited = HashSet::new();
676 let mut islands = Vec::new();
677
678 for &file_id in &all_files {
679 if !visited.contains(&file_id) {
680 let mut island = Vec::new();
681 self.dfs_island(&file_id, &graph, &mut visited, &mut island);
682 islands.push(island);
683 }
684 }
685
686 islands.sort_by(|a, b| b.len().cmp(&a.len()));
688
689 log::info!("Found {} islands (connected components)", islands.len());
690
691 Ok(islands)
692 }
693
694 fn dfs_island(
696 &self,
697 file_id: &i64,
698 graph: &HashMap<i64, Vec<i64>>,
699 visited: &mut HashSet<i64>,
700 island: &mut Vec<i64>,
701 ) {
702 visited.insert(*file_id);
703 island.push(*file_id);
704
705 if let Some(neighbors) = graph.get(file_id) {
706 for &neighbor in neighbors {
707 if !visited.contains(&neighbor) {
708 self.dfs_island(&neighbor, graph, visited, island);
709 }
710 }
711 }
712 }
713
714 fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
738 let conn = self.open_conn()?;
739
740 let mut stmt = conn.prepare(
742 "SELECT DISTINCT imported_path FROM file_dependencies"
743 )?;
744
745 let imported_paths: Vec<String> = stmt
746 .query_map([], |row| row.get(0))?
747 .collect::<Result<Vec<_>, _>>()?;
748
749 let total_paths = imported_paths.len();
750 log::info!("Building resolution cache for {} unique imported paths", total_paths);
751
752 let mut cache = HashMap::new();
754
755 for imported_path in imported_paths {
756 if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
757 cache.insert(imported_path, file_id);
758 }
759 }
760
761 log::info!(
762 "Resolution cache built: {} resolved, {} unresolved",
763 cache.len(),
764 total_paths - cache.len()
765 );
766
767 Ok(cache)
768 }
769
770 pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
772 let conn = self.open_conn()?;
773
774 conn.execute(
775 "DELETE FROM file_dependencies WHERE file_id = ?",
776 [file_id],
777 )?;
778
779 Ok(())
780 }
781
782 pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
801 let path_variants = generate_path_variants(imported_path);
802
803 for variant in &path_variants {
804 if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
805 log::trace!("Resolved '{}' → '{}' (file_id: {})", imported_path, variant, file_id);
806 return Ok(Some(file_id));
807 }
808 }
809
810 Ok(None)
811 }
812
813 pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
824 let conn = self.open_conn()?;
825
826 let normalized_path = normalize_path_for_lookup(path);
828
829 match conn.query_row(
831 "SELECT id FROM files WHERE path = ?",
832 [&normalized_path],
833 |row| row.get::<_, i64>(0),
834 ) {
835 Ok(id) => return Ok(Some(id)),
836 Err(rusqlite::Error::QueryReturnedNoRows) => {
837 }
839 Err(e) => return Err(e.into()),
840 }
841
842 let mut stmt = conn.prepare(
844 "SELECT id, path FROM files WHERE path LIKE '%' || ?"
845 )?;
846
847 let matches: Vec<(i64, String)> = stmt
848 .query_map([&normalized_path], |row| {
849 Ok((row.get(0)?, row.get(1)?))
850 })?
851 .collect::<Result<Vec<_>, _>>()?;
852
853 match matches.len() {
854 0 => Ok(None),
855 1 => Ok(Some(matches[0].0)),
856 _ => {
857 let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
859 anyhow::bail!(
860 "Ambiguous path '{}' matches multiple files:\n {}\n\nPlease be more specific.",
861 path,
862 paths.join("\n ")
863 );
864 }
865 }
866 }
867
868 pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
877 let conn = self.open_conn()?;
878
879 let mut stmt = conn.prepare(
880 "SELECT
881 CASE
882 WHEN f.path LIKE '%.py' THEN 'Python'
883 WHEN f.path LIKE '%.go' THEN 'Go'
884 WHEN f.path LIKE '%.ts' THEN 'TypeScript'
885 WHEN f.path LIKE '%.rs' THEN 'Rust'
886 WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
887 WHEN f.path LIKE '%.php' THEN 'PHP'
888 WHEN f.path LIKE '%.java' THEN 'Java'
889 WHEN f.path LIKE '%.kt' THEN 'Kotlin'
890 WHEN f.path LIKE '%.rb' THEN 'Ruby'
891 WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
892 WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
893 WHEN f.path LIKE '%.cs' THEN 'C#'
894 WHEN f.path LIKE '%.zig' THEN 'Zig'
895 ELSE 'Other'
896 END as language,
897 COUNT(*) as total,
898 SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
899 FROM file_dependencies d
900 JOIN files f ON d.file_id = f.id
901 WHERE d.import_type = 'internal'
902 GROUP BY language
903 ORDER BY language",
904 )?;
905
906 let mut stats = Vec::new();
907
908 let rows = stmt.query_map([], |row| {
909 let language: String = row.get(0)?;
910 let total: i64 = row.get(1)?;
911 let resolved: i64 = row.get(2)?;
912 let rate = if total > 0 {
913 (resolved as f64 / total as f64) * 100.0
914 } else {
915 0.0
916 };
917
918 Ok((language, total as usize, resolved as usize, rate))
919 })?;
920
921 for row in rows {
922 stats.push(row?);
923 }
924
925 Ok(stats)
926 }
927
928 pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
938 let conn = self.open_conn()?;
939
940 let mut stmt = conn.prepare(
941 "SELECT
942 f.path,
943 d.imported_path,
944 f2.path as resolved_path
945 FROM file_dependencies d
946 JOIN files f ON d.file_id = f.id
947 LEFT JOIN files f2 ON d.resolved_file_id = f2.id
948 WHERE d.import_type = 'internal'
949 ORDER BY f.path",
950 )?;
951
952 let mut deps = Vec::new();
953
954 let rows = stmt.query_map([], |row| {
955 Ok((
956 row.get::<_, String>(0)?,
957 row.get::<_, String>(1)?,
958 row.get::<_, Option<String>>(2)?,
959 ))
960 })?;
961
962 for row in rows {
963 deps.push(row?);
964 }
965
966 Ok(deps)
967 }
968
969 pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
971 let conn = self.open_conn()?;
972
973 let mut stmt = conn.prepare(
974 "SELECT import_type, COUNT(*) as count
975 FROM file_dependencies
976 GROUP BY import_type
977 ORDER BY import_type",
978 )?;
979
980 let mut counts = Vec::new();
981
982 let rows = stmt.query_map([], |row| {
983 Ok((
984 row.get::<_, String>(0)?,
985 row.get::<_, i64>(1)? as usize,
986 ))
987 })?;
988
989 for row in rows {
990 counts.push(row?);
991 }
992
993 Ok(counts)
994 }
995}
996
997fn is_entry_point(path: &str) -> bool {
1002 let p = path.replace('\\', "/");
1003 let p = p.as_str();
1004
1005 if matches!(p, "src/lib.rs" | "src/main.rs" | "build.rs" | "lib.rs" | "main.rs") {
1007 return true;
1008 }
1009
1010 if p.starts_with("tests/") || p.starts_with("benches/") || p.starts_with("examples/") {
1012 return true;
1013 }
1014
1015 let filename = p.rsplit('/').next().unwrap_or(p);
1017 if filename.starts_with("test_")
1018 || filename.ends_with("_test.rs")
1019 || filename.ends_with("_spec.rs")
1020 {
1021 return true;
1022 }
1023
1024 false
1025}
1026
1027fn generate_path_variants(import_path: &str) -> Vec<String> {
1039 let path = import_path.replace('\\', "/").replace("::", "/");
1041
1042 let path = path.trim_matches('"').trim_matches('\'');
1044
1045 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1047
1048 if components.is_empty() {
1049 return vec![];
1050 }
1051
1052 let mut variants = Vec::new();
1053
1054 for start_idx in 0..components.len() {
1061 let suffix = components[start_idx..].join("/");
1062
1063 if !suffix.ends_with(".php") {
1065 variants.push(format!("{}.php", suffix));
1066 } else {
1067 variants.push(suffix.clone());
1068 }
1069
1070 if !suffix.contains('.') {
1072 variants.push(format!("{}.rs", suffix));
1074 variants.push(format!("{}.ts", suffix));
1075 variants.push(format!("{}.js", suffix));
1076 variants.push(format!("{}.py", suffix));
1077 }
1078 }
1079
1080 variants
1081}
1082
1083fn normalize_path_for_lookup(path: &str) -> String {
1094 let mut normalized = path.trim_start_matches("./").to_string();
1096 if normalized.starts_with("../") {
1097 normalized = normalized.trim_start_matches("../").to_string();
1098 }
1099
1100 if normalized.starts_with('/') || normalized.starts_with('\\') {
1104 let markers = ["services", "src", "app", "lib", "packages", "modules"];
1106
1107 let mut found_marker = false;
1108 for marker in &markers {
1109 if let Some(idx) = normalized.find(marker) {
1110 normalized = normalized[idx..].to_string();
1111 found_marker = true;
1112 break;
1113 }
1114 }
1115
1116 if !found_marker {
1118 use std::path::Path;
1119 let path_obj = Path::new(&normalized);
1120 if let Some(filename) = path_obj.file_name() {
1121 normalized = filename.to_string_lossy().to_string();
1122 }
1123 }
1124 }
1125
1126 normalized
1127}
1128
1129pub fn resolve_rust_import(
1148 import_path: &str,
1149 current_file: &str,
1150 project_root: &std::path::Path,
1151) -> Option<String> {
1152 use std::path::{Path, PathBuf};
1153
1154 if !import_path.starts_with("crate::")
1156 && !import_path.starts_with("super::")
1157 && !import_path.starts_with("self::")
1158 {
1159 return None;
1160 }
1161
1162 let current_path = Path::new(current_file);
1163 let mut resolved_path: Option<PathBuf> = None;
1164
1165 if import_path.starts_with("crate::") {
1166 let crate_root = if project_root.join("src/lib.rs").exists() {
1168 project_root.join("src")
1169 } else if project_root.join("src/main.rs").exists() {
1170 project_root.join("src")
1171 } else {
1172 project_root.join("src")
1174 };
1175
1176 let path_parts: Vec<&str> = import_path
1177 .strip_prefix("crate::")
1178 .unwrap()
1179 .split("::")
1180 .collect();
1181
1182 resolved_path = resolve_module_path(&crate_root, &path_parts);
1183 } else if import_path.starts_with("super::") {
1184 if let Some(current_dir) = current_path.parent() {
1186 if let Some(parent_dir) = current_dir.parent() {
1187 let path_parts: Vec<&str> = import_path
1188 .strip_prefix("super::")
1189 .unwrap()
1190 .split("::")
1191 .collect();
1192
1193 resolved_path = resolve_module_path(parent_dir, &path_parts);
1194 }
1195 }
1196 } else if import_path.starts_with("self::") {
1197 if let Some(current_dir) = current_path.parent() {
1199 let path_parts: Vec<&str> = import_path
1200 .strip_prefix("self::")
1201 .unwrap()
1202 .split("::")
1203 .collect();
1204
1205 resolved_path = resolve_module_path(current_dir, &path_parts);
1206 }
1207 }
1208
1209 resolved_path.and_then(|p| {
1211 p.strip_prefix(project_root)
1212 .ok()
1213 .map(|rel| rel.to_string_lossy().to_string())
1214 })
1215}
1216
1217fn resolve_module_path(start_dir: &std::path::Path, components: &[&str]) -> Option<std::path::PathBuf> {
1223
1224 if components.is_empty() {
1225 return None;
1226 }
1227
1228 let mut current = start_dir.to_path_buf();
1229
1230 for &component in &components[..components.len() - 1] {
1232 let dir_path = current.join(component);
1234 let mod_file = dir_path.join("mod.rs");
1235
1236 if mod_file.exists() {
1237 current = dir_path;
1238 } else {
1239 return None;
1241 }
1242 }
1243
1244 let last_component = components.last().unwrap();
1246
1247 let file_path = current.join(format!("{}.rs", last_component));
1249 if file_path.exists() {
1250 return Some(file_path);
1251 }
1252
1253 let dir_path = current.join(last_component);
1255 let mod_file = dir_path.join("mod.rs");
1256 if mod_file.exists() {
1257 return Some(mod_file);
1258 }
1259
1260 None
1261}
1262
1263pub fn resolve_rust_mod_declaration(
1269 mod_name: &str,
1270 current_file: &str,
1271 _project_root: &std::path::Path,
1272) -> Option<String> {
1273 use std::path::Path;
1274
1275 let current_path = Path::new(current_file);
1276 let current_dir = current_path.parent()?;
1277
1278 let sibling = current_dir.join(format!("{}.rs", mod_name));
1280 if sibling.exists() {
1281 return Some(sibling.to_string_lossy().to_string());
1282 }
1283
1284 let dir_mod = current_dir.join(mod_name).join("mod.rs");
1286 if dir_mod.exists() {
1287 return Some(dir_mod.to_string_lossy().to_string());
1288 }
1289
1290 None
1291}
1292
1293pub fn resolve_php_import(
1316 import_path: &str,
1317 _current_file: &str,
1318 project_root: &std::path::Path,
1319) -> Option<String> {
1320 const VENDOR_NAMESPACES: &[&str] = &[
1322 "Illuminate\\", "Symfony\\", "Laravel\\", "Psr\\",
1323 "Doctrine\\", "Monolog\\", "PHPUnit\\", "Carbon\\",
1324 "GuzzleHttp\\", "Composer\\", "Predis\\", "League\\"
1325 ];
1326
1327 for vendor_ns in VENDOR_NAMESPACES {
1329 if import_path.starts_with(vendor_ns) {
1330 return None;
1331 }
1332 }
1333
1334 let file_path = import_path.replace('\\', "/");
1338
1339 let path_candidates = vec![
1343 {
1345 let parts: Vec<&str> = file_path.split('/').collect();
1346 if let Some(first) = parts.first() {
1347 let mut result = vec![first.to_lowercase()];
1348 result.extend(parts[1..].iter().map(|s| s.to_string()));
1349 result.join("/") + ".php"
1350 } else {
1351 file_path.clone() + ".php"
1352 }
1353 },
1354 file_path.clone() + ".php",
1356 file_path.to_lowercase() + ".php",
1358 ];
1359
1360 for candidate in &path_candidates {
1362 let full_path = project_root.join(candidate);
1363 if full_path.exists() {
1364 return Some(candidate.clone());
1366 }
1367 }
1368
1369 None
1371}
1372
1373#[cfg(test)]
1374mod tests {
1375 use super::*;
1376 use tempfile::TempDir;
1377
1378 fn setup_test_cache() -> (TempDir, CacheManager) {
1379 let temp = TempDir::new().unwrap();
1380 let cache = CacheManager::new(temp.path());
1381 cache.init().unwrap();
1382
1383 cache.update_file("src/main.rs", "rust", 100).unwrap();
1385 cache.update_file("src/lib.rs", "rust", 50).unwrap();
1386 cache.update_file("src/utils.rs", "rust", 30).unwrap();
1387
1388 (temp, cache)
1389 }
1390
1391 #[test]
1392 fn test_insert_and_get_dependencies() {
1393 let (_temp, cache) = setup_test_cache();
1394 let deps_index = DependencyIndex::new(cache);
1395
1396 let main_id = 1i64;
1398 let lib_id = 2i64;
1399
1400 deps_index
1402 .insert_dependency(
1403 main_id,
1404 "crate::lib".to_string(),
1405 Some(lib_id),
1406 ImportType::Internal,
1407 5,
1408 None,
1409 )
1410 .unwrap();
1411
1412 let deps = deps_index.get_dependencies(main_id).unwrap();
1414 assert_eq!(deps.len(), 1);
1415 assert_eq!(deps[0].imported_path, "crate::lib");
1416 assert_eq!(deps[0].resolved_file_id, Some(lib_id));
1417 assert_eq!(deps[0].import_type, ImportType::Internal);
1418 }
1419
1420 #[test]
1421 fn test_reverse_lookup() {
1422 let (_temp, cache) = setup_test_cache();
1423 let deps_index = DependencyIndex::new(cache);
1424
1425 let main_id = 1i64;
1426 let lib_id = 2i64;
1427 let utils_id = 3i64;
1428
1429 deps_index
1431 .insert_dependency(
1432 main_id,
1433 "crate::lib".to_string(),
1434 Some(lib_id),
1435 ImportType::Internal,
1436 5,
1437 None,
1438 )
1439 .unwrap();
1440
1441 deps_index
1443 .insert_dependency(
1444 utils_id,
1445 "crate::lib".to_string(),
1446 Some(lib_id),
1447 ImportType::Internal,
1448 3,
1449 None,
1450 )
1451 .unwrap();
1452
1453 let dependents = deps_index.get_dependents(lib_id).unwrap();
1455 assert_eq!(dependents.len(), 2);
1456 assert!(dependents.contains(&main_id));
1457 assert!(dependents.contains(&utils_id));
1458 }
1459
1460 #[test]
1461 fn test_transitive_dependencies() {
1462 let (_temp, cache) = setup_test_cache();
1463 let deps_index = DependencyIndex::new(cache);
1464
1465 let file1 = 1i64;
1466 let file2 = 2i64;
1467 let file3 = 3i64;
1468
1469 deps_index
1471 .insert_dependency(
1472 file1,
1473 "file2".to_string(),
1474 Some(file2),
1475 ImportType::Internal,
1476 1,
1477 None,
1478 )
1479 .unwrap();
1480
1481 deps_index
1482 .insert_dependency(
1483 file2,
1484 "file3".to_string(),
1485 Some(file3),
1486 ImportType::Internal,
1487 1,
1488 None,
1489 )
1490 .unwrap();
1491
1492 let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
1494
1495 assert_eq!(transitive.len(), 3);
1497 assert_eq!(transitive.get(&file1), Some(&0));
1498 assert_eq!(transitive.get(&file2), Some(&1));
1499 assert_eq!(transitive.get(&file3), Some(&2));
1500 }
1501
1502 #[test]
1503 fn test_batch_insert() {
1504 let (_temp, cache) = setup_test_cache();
1505 let deps_index = DependencyIndex::new(cache);
1506
1507 let deps = vec![
1508 Dependency {
1509 file_id: 1,
1510 imported_path: "std::collections".to_string(),
1511 resolved_file_id: None,
1512 import_type: ImportType::Stdlib,
1513 line_number: 1,
1514 imported_symbols: Some(vec!["HashMap".to_string()]),
1515 },
1516 Dependency {
1517 file_id: 1,
1518 imported_path: "crate::lib".to_string(),
1519 resolved_file_id: Some(2),
1520 import_type: ImportType::Internal,
1521 line_number: 2,
1522 imported_symbols: None,
1523 },
1524 ];
1525
1526 deps_index.batch_insert_dependencies(&deps).unwrap();
1527
1528 let retrieved = deps_index.get_dependencies(1).unwrap();
1529 assert_eq!(retrieved.len(), 2);
1530 }
1531
1532 #[test]
1533 fn test_clear_dependencies() {
1534 let (_temp, cache) = setup_test_cache();
1535 let deps_index = DependencyIndex::new(cache);
1536
1537 deps_index
1539 .insert_dependency(
1540 1,
1541 "crate::lib".to_string(),
1542 Some(2),
1543 ImportType::Internal,
1544 1,
1545 None,
1546 )
1547 .unwrap();
1548
1549 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
1551
1552 deps_index.clear_dependencies(1).unwrap();
1554
1555 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
1557 }
1558
1559 #[test]
1560 fn test_resolve_rust_import_crate() {
1561 use std::fs;
1562 use tempfile::TempDir;
1563
1564 let temp = TempDir::new().unwrap();
1565 let project_root = temp.path();
1566
1567 fs::create_dir_all(project_root.join("src")).unwrap();
1569 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1570 fs::write(project_root.join("src/models.rs"), "").unwrap();
1571
1572 let resolved = resolve_rust_import(
1574 "crate::models",
1575 "src/query.rs",
1576 project_root,
1577 );
1578
1579 assert_eq!(resolved, Some("src/models.rs".to_string()));
1580 }
1581
1582 #[test]
1583 fn test_resolve_rust_import_super() {
1584 use std::fs;
1585 use tempfile::TempDir;
1586
1587 let temp = TempDir::new().unwrap();
1588 let project_root = temp.path();
1589
1590 fs::create_dir_all(project_root.join("src/parsers")).unwrap();
1592 fs::write(project_root.join("src/models.rs"), "").unwrap();
1593 fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
1594
1595 let current_file = project_root.join("src/parsers/rust.rs");
1598 let resolved = resolve_rust_import(
1599 "super::models",
1600 ¤t_file.to_string_lossy(),
1601 project_root,
1602 );
1603
1604 assert_eq!(resolved, Some("src/models.rs".to_string()));
1605 }
1606
1607 #[test]
1608 fn test_resolve_rust_import_external() {
1609 use tempfile::TempDir;
1610
1611 let temp = TempDir::new().unwrap();
1612 let project_root = temp.path();
1613
1614 let resolved = resolve_rust_import(
1616 "serde::Serialize",
1617 "src/models.rs",
1618 project_root,
1619 );
1620
1621 assert_eq!(resolved, None);
1622
1623 let resolved = resolve_rust_import(
1625 "std::collections::HashMap",
1626 "src/models.rs",
1627 project_root,
1628 );
1629
1630 assert_eq!(resolved, None);
1631 }
1632
1633 #[test]
1634 fn test_resolve_rust_mod_declaration() {
1635 use std::fs;
1636 use tempfile::TempDir;
1637
1638 let temp = TempDir::new().unwrap();
1639 let project_root = temp.path();
1640
1641 fs::create_dir_all(project_root.join("src")).unwrap();
1643 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1644 fs::write(project_root.join("src/parser.rs"), "").unwrap();
1645
1646 let resolved = resolve_rust_mod_declaration(
1648 "parser",
1649 &project_root.join("src/lib.rs").to_string_lossy(),
1650 project_root,
1651 );
1652
1653 assert!(resolved.is_some());
1654 assert!(resolved.unwrap().ends_with("src/parser.rs"));
1655 }
1656
1657 #[test]
1658 fn test_resolve_rust_import_nested() {
1659 use std::fs;
1660 use tempfile::TempDir;
1661
1662 let temp = TempDir::new().unwrap();
1663 let project_root = temp.path();
1664
1665 fs::create_dir_all(project_root.join("src/models")).unwrap();
1667 fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
1668 fs::write(project_root.join("src/models/language.rs"), "").unwrap();
1669
1670 let resolved = resolve_rust_import(
1672 "crate::models::language",
1673 "src/query.rs",
1674 project_root,
1675 );
1676
1677 assert_eq!(resolved, Some("src/models/language.rs".to_string()));
1678 }
1679}