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 };
99
100 let symbols_json = imported_symbols
101 .as_ref()
102 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
103
104 conn.execute(
105 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
106 VALUES (?, ?, ?, ?, ?, ?)",
107 rusqlite::params![
108 file_id,
109 imported_path,
110 resolved_file_id,
111 import_type_str,
112 line_number as i64,
113 symbols_json,
114 ],
115 )?;
116
117 Ok(())
118 }
119
120 pub fn insert_export(
130 &self,
131 file_id: i64,
132 exported_symbol: Option<String>,
133 source_path: String,
134 resolved_source_id: Option<i64>,
135 line_number: usize,
136 ) -> Result<()> {
137 let conn = self.open_conn()?;
138
139 conn.execute(
140 "INSERT INTO file_exports (file_id, exported_symbol, source_path, resolved_source_id, line_number)
141 VALUES (?, ?, ?, ?, ?)",
142 rusqlite::params![
143 file_id,
144 exported_symbol,
145 source_path,
146 resolved_source_id,
147 line_number as i64,
148 ],
149 )?;
150
151 Ok(())
152 }
153
154 pub fn batch_insert_dependencies(&self, dependencies: &[Dependency]) -> Result<()> {
158 if dependencies.is_empty() {
159 return Ok(());
160 }
161
162 let mut conn = self.open_conn()?;
163
164 let tx = conn.transaction()?;
165
166 for dep in dependencies {
167 let import_type_str = match dep.import_type {
168 ImportType::Internal => "internal",
169 ImportType::External => "external",
170 ImportType::Stdlib => "stdlib",
171 };
172
173 let symbols_json = dep
174 .imported_symbols
175 .as_ref()
176 .map(|syms| serde_json::to_string(syms).unwrap_or_else(|_| "[]".to_string()));
177
178 tx.execute(
179 "INSERT INTO file_dependencies (file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols)
180 VALUES (?, ?, ?, ?, ?, ?)",
181 rusqlite::params![
182 dep.file_id,
183 dep.imported_path,
184 dep.resolved_file_id,
185 import_type_str,
186 dep.line_number as i64,
187 symbols_json,
188 ],
189 )?;
190 }
191
192 tx.commit()?;
193 log::debug!("Batch inserted {} dependencies", dependencies.len());
194 Ok(())
195 }
196
197 pub fn get_dependencies(&self, file_id: i64) -> Result<Vec<Dependency>> {
201 let conn = self.open_conn()?;
202
203 let mut stmt = conn.prepare(
204 "SELECT file_id, imported_path, resolved_file_id, import_type, line_number, imported_symbols
205 FROM file_dependencies
206 WHERE file_id = ?
207 ORDER BY line_number",
208 )?;
209
210 let deps = stmt
211 .query_map([file_id], |row| {
212 let import_type_str: String = row.get(3)?;
213 let import_type = match import_type_str.as_str() {
214 "internal" => ImportType::Internal,
215 "external" => ImportType::External,
216 "stdlib" => ImportType::Stdlib,
217 _ => ImportType::External,
218 };
219
220 let symbols_json: Option<String> = row.get(5)?;
221 let imported_symbols = symbols_json.and_then(|json| {
222 serde_json::from_str(&json).ok()
223 });
224
225 Ok(Dependency {
226 file_id: row.get(0)?,
227 imported_path: row.get(1)?,
228 resolved_file_id: row.get(2)?,
229 import_type,
230 line_number: row.get::<_, i64>(4)? as usize,
231 imported_symbols,
232 })
233 })?
234 .collect::<Result<Vec<_>, _>>()?;
235
236 Ok(deps)
237 }
238
239 pub fn get_dependents(&self, file_id: i64) -> Result<Vec<i64>> {
244 let conn = self.open_conn()?;
245
246 let mut stmt = conn.prepare(
248 "SELECT DISTINCT file_id
249 FROM file_dependencies
250 WHERE resolved_file_id = ?
251 ORDER BY file_id"
252 )?;
253
254 let dependents: Vec<i64> = stmt
255 .query_map([file_id], |row| row.get(0))?
256 .collect::<Result<Vec<_>, _>>()?;
257
258 Ok(dependents)
259 }
260
261 pub fn get_dependencies_info(&self, file_id: i64) -> Result<Vec<DependencyInfo>> {
266 let deps = self.get_dependencies(file_id)?;
267
268 let dep_infos = deps
269 .into_iter()
270 .map(|dep| {
271 let path = if let Some(resolved_id) = dep.resolved_file_id {
273 self.get_file_path(resolved_id).unwrap_or(dep.imported_path)
275 } else {
276 dep.imported_path
277 };
278
279 DependencyInfo {
280 path,
281 line: Some(dep.line_number),
282 symbols: dep.imported_symbols,
283 }
284 })
285 .collect();
286
287 Ok(dep_infos)
288 }
289
290 pub fn get_transitive_deps(&self, file_id: i64, max_depth: usize) -> Result<HashMap<i64, usize>> {
305 let mut visited = HashMap::new();
306 let mut queue = VecDeque::new();
307
308 queue.push_back((file_id, 0));
310 visited.insert(file_id, 0);
311
312 while let Some((current_id, depth)) = queue.pop_front() {
313 if depth >= max_depth {
314 continue;
315 }
316
317 let deps = self.get_dependencies(current_id)?;
319
320 for dep in deps {
321 if let Some(resolved_id) = dep.resolved_file_id {
323 if !visited.contains_key(&resolved_id) {
325 visited.insert(resolved_id, depth + 1);
326 queue.push_back((resolved_id, depth + 1));
327 }
328 }
329 }
330 }
331
332 Ok(visited)
333 }
334
335 pub fn detect_circular_dependencies(&self) -> Result<Vec<Vec<i64>>> {
343 let conn = self.open_conn()?;
344
345 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
347
348 let mut stmt = conn.prepare(
349 "SELECT file_id, resolved_file_id
350 FROM file_dependencies
351 WHERE resolved_file_id IS NOT NULL"
352 )?;
353
354 let dependencies: Vec<(i64, i64)> = stmt
355 .query_map([], |row| {
356 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
357 })?
358 .collect::<Result<Vec<_>, _>>()?;
359
360 for (file_id, target_id) in dependencies {
362 graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
363 }
364
365 let all_files = self.get_all_file_ids()?;
367
368 let mut visited = HashSet::new();
369 let mut rec_stack = HashSet::new();
370 let mut path = Vec::new();
371 let mut cycles = Vec::new();
372
373 for file_id in all_files {
374 if !visited.contains(&file_id) {
375 self.dfs_cycle_detect(
376 file_id,
377 &graph,
378 &mut visited,
379 &mut rec_stack,
380 &mut path,
381 &mut cycles,
382 )?;
383 }
384 }
385
386 Ok(cycles)
387 }
388
389 fn dfs_cycle_detect(
391 &self,
392 file_id: i64,
393 graph: &HashMap<i64, Vec<i64>>,
394 visited: &mut HashSet<i64>,
395 rec_stack: &mut HashSet<i64>,
396 path: &mut Vec<i64>,
397 cycles: &mut Vec<Vec<i64>>,
398 ) -> Result<()> {
399 visited.insert(file_id);
400 rec_stack.insert(file_id);
401 path.push(file_id);
402
403 if let Some(dependencies) = graph.get(&file_id) {
405 for &target_id in dependencies {
406 if !visited.contains(&target_id) {
407 self.dfs_cycle_detect(target_id, graph, visited, rec_stack, path, cycles)?;
408 } else if rec_stack.contains(&target_id) {
409 if let Some(cycle_start) = path.iter().position(|&id| id == target_id) {
411 let cycle = path[cycle_start..].to_vec();
412 cycles.push(cycle);
413 }
414 }
415 }
416 }
417
418 path.pop();
419 rec_stack.remove(&file_id);
420
421 Ok(())
422 }
423
424 pub fn get_file_paths(&self, file_ids: &[i64]) -> Result<HashMap<i64, String>> {
428 let conn = self.open_conn()?;
429
430 let mut paths = HashMap::new();
431
432 for &file_id in file_ids {
433 if let Ok(path) = conn.query_row(
434 "SELECT path FROM files WHERE id = ?",
435 [file_id],
436 |row| row.get::<_, String>(0),
437 ) {
438 paths.insert(file_id, path);
439 }
440 }
441
442 Ok(paths)
443 }
444
445 fn get_file_path(&self, file_id: i64) -> Result<String> {
447 let conn = self.open_conn()?;
448
449 let path = conn.query_row(
450 "SELECT path FROM files WHERE id = ?",
451 [file_id],
452 |row| row.get::<_, String>(0),
453 )?;
454
455 Ok(path)
456 }
457
458 fn get_all_file_ids(&self) -> Result<Vec<i64>> {
460 let conn = self.open_conn()?;
461
462 let mut stmt = conn.prepare("SELECT id FROM files")?;
463 let file_ids = stmt
464 .query_map([], |row| row.get(0))?
465 .collect::<Result<Vec<_>, _>>()?;
466
467 Ok(file_ids)
468 }
469
470 pub fn find_hotspots(&self, limit: Option<usize>, min_dependents: usize) -> Result<Vec<(i64, usize)>> {
481 let conn = self.open_conn()?;
482
483 let mut stmt = conn.prepare(
485 "SELECT resolved_file_id, COUNT(*) as count
486 FROM file_dependencies
487 WHERE resolved_file_id IS NOT NULL
488 GROUP BY resolved_file_id
489 ORDER BY count DESC"
490 )?;
491
492 let mut hotspots: Vec<(i64, usize)> = stmt
494 .query_map([], |row| {
495 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)? as usize))
496 })?
497 .collect::<Result<Vec<_>, _>>()?
498 .into_iter()
499 .filter(|(_, count)| *count >= min_dependents)
500 .collect();
501
502 if let Some(lim) = limit {
504 hotspots.truncate(lim);
505 }
506
507 Ok(hotspots)
508 }
509
510 pub fn find_unused_files(&self) -> Result<Vec<i64>> {
521 let conn = self.open_conn()?;
522
523 let mut used_files = HashSet::new();
525
526 let mut stmt = conn.prepare(
528 "SELECT DISTINCT resolved_file_id
529 FROM file_dependencies
530 WHERE resolved_file_id IS NOT NULL"
531 )?;
532
533 let direct_imports: Vec<i64> = stmt
534 .query_map([], |row| row.get(0))?
535 .collect::<Result<Vec<_>, _>>()?;
536
537 used_files.extend(&direct_imports);
538
539 for file_id in direct_imports {
541 let barrel_chain = self.resolve_through_barrel_exports(file_id)?;
543 used_files.extend(barrel_chain);
544 }
545
546 let mut stmt = conn.prepare("SELECT id FROM files ORDER BY id")?;
548 let all_files: Vec<i64> = stmt
549 .query_map([], |row| row.get(0))?
550 .collect::<Result<Vec<_>, _>>()?;
551
552 let unused: Vec<i64> = all_files
553 .into_iter()
554 .filter(|id| !used_files.contains(id))
555 .collect();
556
557 Ok(unused)
558 }
559
560 pub fn resolve_through_barrel_exports(&self, barrel_file_id: i64) -> Result<Vec<i64>> {
584 let conn = self.open_conn()?;
585
586 let mut resolved_files = Vec::new();
587 let mut visited = HashSet::new();
588 let mut queue = VecDeque::new();
589
590 queue.push_back(barrel_file_id);
592 visited.insert(barrel_file_id);
593
594 while let Some(current_id) = queue.pop_front() {
595 resolved_files.push(current_id);
596
597 let mut stmt = conn.prepare(
599 "SELECT resolved_source_id
600 FROM file_exports
601 WHERE file_id = ? AND resolved_source_id IS NOT NULL"
602 )?;
603
604 let exported_files: Vec<i64> = stmt
605 .query_map([current_id], |row| row.get(0))?
606 .collect::<Result<Vec<_>, _>>()?;
607
608 for exported_id in exported_files {
610 if !visited.contains(&exported_id) {
611 visited.insert(exported_id);
612 queue.push_back(exported_id);
613 }
614 }
615 }
616
617 Ok(resolved_files)
618 }
619
620 pub fn find_islands(&self) -> Result<Vec<Vec<i64>>> {
634 let conn = self.open_conn()?;
635
636 let mut graph: HashMap<i64, Vec<i64>> = HashMap::new();
638
639 let mut stmt = conn.prepare(
640 "SELECT file_id, resolved_file_id
641 FROM file_dependencies
642 WHERE resolved_file_id IS NOT NULL"
643 )?;
644
645 let dependencies: Vec<(i64, i64)> = stmt
646 .query_map([], |row| {
647 Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
648 })?
649 .collect::<Result<Vec<_>, _>>()?;
650
651 for (file_id, target_id) in dependencies {
653 graph.entry(file_id).or_insert_with(Vec::new).push(target_id);
655 graph.entry(target_id).or_insert_with(Vec::new).push(file_id);
656 }
657
658 let all_files = self.get_all_file_ids()?;
660
661 for file_id in &all_files {
663 graph.entry(*file_id).or_insert_with(Vec::new);
664 }
665
666 let mut visited = HashSet::new();
668 let mut islands = Vec::new();
669
670 for &file_id in &all_files {
671 if !visited.contains(&file_id) {
672 let mut island = Vec::new();
673 self.dfs_island(&file_id, &graph, &mut visited, &mut island);
674 islands.push(island);
675 }
676 }
677
678 islands.sort_by(|a, b| b.len().cmp(&a.len()));
680
681 log::info!("Found {} islands (connected components)", islands.len());
682
683 Ok(islands)
684 }
685
686 fn dfs_island(
688 &self,
689 file_id: &i64,
690 graph: &HashMap<i64, Vec<i64>>,
691 visited: &mut HashSet<i64>,
692 island: &mut Vec<i64>,
693 ) {
694 visited.insert(*file_id);
695 island.push(*file_id);
696
697 if let Some(neighbors) = graph.get(file_id) {
698 for &neighbor in neighbors {
699 if !visited.contains(&neighbor) {
700 self.dfs_island(&neighbor, graph, visited, island);
701 }
702 }
703 }
704 }
705
706 fn build_resolution_cache(&self) -> Result<HashMap<String, i64>> {
730 let conn = self.open_conn()?;
731
732 let mut stmt = conn.prepare(
734 "SELECT DISTINCT imported_path FROM file_dependencies"
735 )?;
736
737 let imported_paths: Vec<String> = stmt
738 .query_map([], |row| row.get(0))?
739 .collect::<Result<Vec<_>, _>>()?;
740
741 let total_paths = imported_paths.len();
742 log::info!("Building resolution cache for {} unique imported paths", total_paths);
743
744 let mut cache = HashMap::new();
746
747 for imported_path in imported_paths {
748 if let Ok(Some(file_id)) = self.resolve_imported_path_to_file_id(&imported_path) {
749 cache.insert(imported_path, file_id);
750 }
751 }
752
753 log::info!(
754 "Resolution cache built: {} resolved, {} unresolved",
755 cache.len(),
756 total_paths - cache.len()
757 );
758
759 Ok(cache)
760 }
761
762 pub fn clear_dependencies(&self, file_id: i64) -> Result<()> {
764 let conn = self.open_conn()?;
765
766 conn.execute(
767 "DELETE FROM file_dependencies WHERE file_id = ?",
768 [file_id],
769 )?;
770
771 Ok(())
772 }
773
774 pub fn resolve_imported_path_to_file_id(&self, imported_path: &str) -> Result<Option<i64>> {
793 let path_variants = generate_path_variants(imported_path);
794
795 for variant in &path_variants {
796 if let Ok(Some(file_id)) = self.get_file_id_by_path(variant) {
797 log::trace!("Resolved '{}' → '{}' (file_id: {})", imported_path, variant, file_id);
798 return Ok(Some(file_id));
799 }
800 }
801
802 Ok(None)
803 }
804
805 pub fn get_file_id_by_path(&self, path: &str) -> Result<Option<i64>> {
816 let conn = self.open_conn()?;
817
818 let normalized_path = normalize_path_for_lookup(path);
820
821 match conn.query_row(
823 "SELECT id FROM files WHERE path = ?",
824 [&normalized_path],
825 |row| row.get::<_, i64>(0),
826 ) {
827 Ok(id) => return Ok(Some(id)),
828 Err(rusqlite::Error::QueryReturnedNoRows) => {
829 }
831 Err(e) => return Err(e.into()),
832 }
833
834 let mut stmt = conn.prepare(
836 "SELECT id, path FROM files WHERE path LIKE '%' || ?"
837 )?;
838
839 let matches: Vec<(i64, String)> = stmt
840 .query_map([&normalized_path], |row| {
841 Ok((row.get(0)?, row.get(1)?))
842 })?
843 .collect::<Result<Vec<_>, _>>()?;
844
845 match matches.len() {
846 0 => Ok(None),
847 1 => Ok(Some(matches[0].0)),
848 _ => {
849 let paths: Vec<String> = matches.iter().map(|(_, p)| p.clone()).collect();
851 anyhow::bail!(
852 "Ambiguous path '{}' matches multiple files:\n {}\n\nPlease be more specific.",
853 path,
854 paths.join("\n ")
855 );
856 }
857 }
858 }
859
860 pub fn get_resolution_stats(&self) -> Result<Vec<(String, usize, usize, f64)>> {
869 let conn = self.open_conn()?;
870
871 let mut stmt = conn.prepare(
872 "SELECT
873 CASE
874 WHEN f.path LIKE '%.py' THEN 'Python'
875 WHEN f.path LIKE '%.go' THEN 'Go'
876 WHEN f.path LIKE '%.ts' THEN 'TypeScript'
877 WHEN f.path LIKE '%.rs' THEN 'Rust'
878 WHEN f.path LIKE '%.js' OR f.path LIKE '%.jsx' THEN 'JavaScript'
879 WHEN f.path LIKE '%.php' THEN 'PHP'
880 WHEN f.path LIKE '%.java' THEN 'Java'
881 WHEN f.path LIKE '%.kt' THEN 'Kotlin'
882 WHEN f.path LIKE '%.rb' THEN 'Ruby'
883 WHEN f.path LIKE '%.c' OR f.path LIKE '%.h' THEN 'C'
884 WHEN f.path LIKE '%.cpp' OR f.path LIKE '%.cc' OR f.path LIKE '%.hpp' THEN 'C++'
885 WHEN f.path LIKE '%.cs' THEN 'C#'
886 WHEN f.path LIKE '%.zig' THEN 'Zig'
887 ELSE 'Other'
888 END as language,
889 COUNT(*) as total,
890 SUM(CASE WHEN d.resolved_file_id IS NOT NULL THEN 1 ELSE 0 END) as resolved
891 FROM file_dependencies d
892 JOIN files f ON d.file_id = f.id
893 WHERE d.import_type = 'internal'
894 GROUP BY language
895 ORDER BY language",
896 )?;
897
898 let mut stats = Vec::new();
899
900 let rows = stmt.query_map([], |row| {
901 let language: String = row.get(0)?;
902 let total: i64 = row.get(1)?;
903 let resolved: i64 = row.get(2)?;
904 let rate = if total > 0 {
905 (resolved as f64 / total as f64) * 100.0
906 } else {
907 0.0
908 };
909
910 Ok((language, total as usize, resolved as usize, rate))
911 })?;
912
913 for row in rows {
914 stats.push(row?);
915 }
916
917 Ok(stats)
918 }
919
920 pub fn get_all_internal_dependencies(&self) -> Result<Vec<(String, String, Option<String>)>> {
930 let conn = self.open_conn()?;
931
932 let mut stmt = conn.prepare(
933 "SELECT
934 f.path,
935 d.imported_path,
936 f2.path as resolved_path
937 FROM file_dependencies d
938 JOIN files f ON d.file_id = f.id
939 LEFT JOIN files f2 ON d.resolved_file_id = f2.id
940 WHERE d.import_type = 'internal'
941 ORDER BY f.path",
942 )?;
943
944 let mut deps = Vec::new();
945
946 let rows = stmt.query_map([], |row| {
947 Ok((
948 row.get::<_, String>(0)?,
949 row.get::<_, String>(1)?,
950 row.get::<_, Option<String>>(2)?,
951 ))
952 })?;
953
954 for row in rows {
955 deps.push(row?);
956 }
957
958 Ok(deps)
959 }
960
961 pub fn get_dependency_count_by_type(&self) -> Result<Vec<(String, usize)>> {
963 let conn = self.open_conn()?;
964
965 let mut stmt = conn.prepare(
966 "SELECT import_type, COUNT(*) as count
967 FROM file_dependencies
968 GROUP BY import_type
969 ORDER BY import_type",
970 )?;
971
972 let mut counts = Vec::new();
973
974 let rows = stmt.query_map([], |row| {
975 Ok((
976 row.get::<_, String>(0)?,
977 row.get::<_, i64>(1)? as usize,
978 ))
979 })?;
980
981 for row in rows {
982 counts.push(row?);
983 }
984
985 Ok(counts)
986 }
987}
988
989fn generate_path_variants(import_path: &str) -> Vec<String> {
1001 let path = import_path.replace('\\', "/").replace("::", "/");
1003
1004 let path = path.trim_matches('"').trim_matches('\'');
1006
1007 let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
1009
1010 if components.is_empty() {
1011 return vec![];
1012 }
1013
1014 let mut variants = Vec::new();
1015
1016 for start_idx in 0..components.len() {
1023 let suffix = components[start_idx..].join("/");
1024
1025 if !suffix.ends_with(".php") {
1027 variants.push(format!("{}.php", suffix));
1028 } else {
1029 variants.push(suffix.clone());
1030 }
1031
1032 if !suffix.contains('.') {
1034 variants.push(format!("{}.rs", suffix));
1036 variants.push(format!("{}.ts", suffix));
1037 variants.push(format!("{}.js", suffix));
1038 variants.push(format!("{}.py", suffix));
1039 }
1040 }
1041
1042 variants
1043}
1044
1045fn normalize_path_for_lookup(path: &str) -> String {
1056 let mut normalized = path.trim_start_matches("./").to_string();
1058 if normalized.starts_with("../") {
1059 normalized = normalized.trim_start_matches("../").to_string();
1060 }
1061
1062 if normalized.starts_with('/') || normalized.starts_with('\\') {
1066 let markers = ["services", "src", "app", "lib", "packages", "modules"];
1068
1069 let mut found_marker = false;
1070 for marker in &markers {
1071 if let Some(idx) = normalized.find(marker) {
1072 normalized = normalized[idx..].to_string();
1073 found_marker = true;
1074 break;
1075 }
1076 }
1077
1078 if !found_marker {
1080 use std::path::Path;
1081 let path_obj = Path::new(&normalized);
1082 if let Some(filename) = path_obj.file_name() {
1083 normalized = filename.to_string_lossy().to_string();
1084 }
1085 }
1086 }
1087
1088 normalized
1089}
1090
1091pub fn resolve_rust_import(
1110 import_path: &str,
1111 current_file: &str,
1112 project_root: &std::path::Path,
1113) -> Option<String> {
1114 use std::path::{Path, PathBuf};
1115
1116 if !import_path.starts_with("crate::")
1118 && !import_path.starts_with("super::")
1119 && !import_path.starts_with("self::")
1120 {
1121 return None;
1122 }
1123
1124 let current_path = Path::new(current_file);
1125 let mut resolved_path: Option<PathBuf> = None;
1126
1127 if import_path.starts_with("crate::") {
1128 let crate_root = if project_root.join("src/lib.rs").exists() {
1130 project_root.join("src")
1131 } else if project_root.join("src/main.rs").exists() {
1132 project_root.join("src")
1133 } else {
1134 project_root.join("src")
1136 };
1137
1138 let path_parts: Vec<&str> = import_path
1139 .strip_prefix("crate::")
1140 .unwrap()
1141 .split("::")
1142 .collect();
1143
1144 resolved_path = resolve_module_path(&crate_root, &path_parts);
1145 } else if import_path.starts_with("super::") {
1146 if let Some(current_dir) = current_path.parent() {
1148 if let Some(parent_dir) = current_dir.parent() {
1149 let path_parts: Vec<&str> = import_path
1150 .strip_prefix("super::")
1151 .unwrap()
1152 .split("::")
1153 .collect();
1154
1155 resolved_path = resolve_module_path(parent_dir, &path_parts);
1156 }
1157 }
1158 } else if import_path.starts_with("self::") {
1159 if let Some(current_dir) = current_path.parent() {
1161 let path_parts: Vec<&str> = import_path
1162 .strip_prefix("self::")
1163 .unwrap()
1164 .split("::")
1165 .collect();
1166
1167 resolved_path = resolve_module_path(current_dir, &path_parts);
1168 }
1169 }
1170
1171 resolved_path.and_then(|p| {
1173 p.strip_prefix(project_root)
1174 .ok()
1175 .map(|rel| rel.to_string_lossy().to_string())
1176 })
1177}
1178
1179fn resolve_module_path(start_dir: &std::path::Path, components: &[&str]) -> Option<std::path::PathBuf> {
1185
1186 if components.is_empty() {
1187 return None;
1188 }
1189
1190 let mut current = start_dir.to_path_buf();
1191
1192 for &component in &components[..components.len() - 1] {
1194 let dir_path = current.join(component);
1196 let mod_file = dir_path.join("mod.rs");
1197
1198 if mod_file.exists() {
1199 current = dir_path;
1200 } else {
1201 return None;
1203 }
1204 }
1205
1206 let last_component = components.last().unwrap();
1208
1209 let file_path = current.join(format!("{}.rs", last_component));
1211 if file_path.exists() {
1212 return Some(file_path);
1213 }
1214
1215 let dir_path = current.join(last_component);
1217 let mod_file = dir_path.join("mod.rs");
1218 if mod_file.exists() {
1219 return Some(mod_file);
1220 }
1221
1222 None
1223}
1224
1225pub fn resolve_rust_mod_declaration(
1231 mod_name: &str,
1232 current_file: &str,
1233 _project_root: &std::path::Path,
1234) -> Option<String> {
1235 use std::path::Path;
1236
1237 let current_path = Path::new(current_file);
1238 let current_dir = current_path.parent()?;
1239
1240 let sibling = current_dir.join(format!("{}.rs", mod_name));
1242 if sibling.exists() {
1243 return Some(sibling.to_string_lossy().to_string());
1244 }
1245
1246 let dir_mod = current_dir.join(mod_name).join("mod.rs");
1248 if dir_mod.exists() {
1249 return Some(dir_mod.to_string_lossy().to_string());
1250 }
1251
1252 None
1253}
1254
1255pub fn resolve_php_import(
1278 import_path: &str,
1279 _current_file: &str,
1280 project_root: &std::path::Path,
1281) -> Option<String> {
1282 const VENDOR_NAMESPACES: &[&str] = &[
1284 "Illuminate\\", "Symfony\\", "Laravel\\", "Psr\\",
1285 "Doctrine\\", "Monolog\\", "PHPUnit\\", "Carbon\\",
1286 "GuzzleHttp\\", "Composer\\", "Predis\\", "League\\"
1287 ];
1288
1289 for vendor_ns in VENDOR_NAMESPACES {
1291 if import_path.starts_with(vendor_ns) {
1292 return None;
1293 }
1294 }
1295
1296 let file_path = import_path.replace('\\', "/");
1300
1301 let path_candidates = vec![
1305 {
1307 let parts: Vec<&str> = file_path.split('/').collect();
1308 if let Some(first) = parts.first() {
1309 let mut result = vec![first.to_lowercase()];
1310 result.extend(parts[1..].iter().map(|s| s.to_string()));
1311 result.join("/") + ".php"
1312 } else {
1313 file_path.clone() + ".php"
1314 }
1315 },
1316 file_path.clone() + ".php",
1318 file_path.to_lowercase() + ".php",
1320 ];
1321
1322 for candidate in &path_candidates {
1324 let full_path = project_root.join(candidate);
1325 if full_path.exists() {
1326 return Some(candidate.clone());
1328 }
1329 }
1330
1331 None
1333}
1334
1335#[cfg(test)]
1336mod tests {
1337 use super::*;
1338 use tempfile::TempDir;
1339
1340 fn setup_test_cache() -> (TempDir, CacheManager) {
1341 let temp = TempDir::new().unwrap();
1342 let cache = CacheManager::new(temp.path());
1343 cache.init().unwrap();
1344
1345 cache.update_file("src/main.rs", "rust", 100).unwrap();
1347 cache.update_file("src/lib.rs", "rust", 50).unwrap();
1348 cache.update_file("src/utils.rs", "rust", 30).unwrap();
1349
1350 (temp, cache)
1351 }
1352
1353 #[test]
1354 fn test_insert_and_get_dependencies() {
1355 let (_temp, cache) = setup_test_cache();
1356 let deps_index = DependencyIndex::new(cache);
1357
1358 let main_id = 1i64;
1360 let lib_id = 2i64;
1361
1362 deps_index
1364 .insert_dependency(
1365 main_id,
1366 "crate::lib".to_string(),
1367 Some(lib_id),
1368 ImportType::Internal,
1369 5,
1370 None,
1371 )
1372 .unwrap();
1373
1374 let deps = deps_index.get_dependencies(main_id).unwrap();
1376 assert_eq!(deps.len(), 1);
1377 assert_eq!(deps[0].imported_path, "crate::lib");
1378 assert_eq!(deps[0].resolved_file_id, Some(lib_id));
1379 assert_eq!(deps[0].import_type, ImportType::Internal);
1380 }
1381
1382 #[test]
1383 fn test_reverse_lookup() {
1384 let (_temp, cache) = setup_test_cache();
1385 let deps_index = DependencyIndex::new(cache);
1386
1387 let main_id = 1i64;
1388 let lib_id = 2i64;
1389 let utils_id = 3i64;
1390
1391 deps_index
1393 .insert_dependency(
1394 main_id,
1395 "crate::lib".to_string(),
1396 Some(lib_id),
1397 ImportType::Internal,
1398 5,
1399 None,
1400 )
1401 .unwrap();
1402
1403 deps_index
1405 .insert_dependency(
1406 utils_id,
1407 "crate::lib".to_string(),
1408 Some(lib_id),
1409 ImportType::Internal,
1410 3,
1411 None,
1412 )
1413 .unwrap();
1414
1415 let dependents = deps_index.get_dependents(lib_id).unwrap();
1417 assert_eq!(dependents.len(), 2);
1418 assert!(dependents.contains(&main_id));
1419 assert!(dependents.contains(&utils_id));
1420 }
1421
1422 #[test]
1423 fn test_transitive_dependencies() {
1424 let (_temp, cache) = setup_test_cache();
1425 let deps_index = DependencyIndex::new(cache);
1426
1427 let file1 = 1i64;
1428 let file2 = 2i64;
1429 let file3 = 3i64;
1430
1431 deps_index
1433 .insert_dependency(
1434 file1,
1435 "file2".to_string(),
1436 Some(file2),
1437 ImportType::Internal,
1438 1,
1439 None,
1440 )
1441 .unwrap();
1442
1443 deps_index
1444 .insert_dependency(
1445 file2,
1446 "file3".to_string(),
1447 Some(file3),
1448 ImportType::Internal,
1449 1,
1450 None,
1451 )
1452 .unwrap();
1453
1454 let transitive = deps_index.get_transitive_deps(file1, 2).unwrap();
1456
1457 assert_eq!(transitive.len(), 3);
1459 assert_eq!(transitive.get(&file1), Some(&0));
1460 assert_eq!(transitive.get(&file2), Some(&1));
1461 assert_eq!(transitive.get(&file3), Some(&2));
1462 }
1463
1464 #[test]
1465 fn test_batch_insert() {
1466 let (_temp, cache) = setup_test_cache();
1467 let deps_index = DependencyIndex::new(cache);
1468
1469 let deps = vec![
1470 Dependency {
1471 file_id: 1,
1472 imported_path: "std::collections".to_string(),
1473 resolved_file_id: None,
1474 import_type: ImportType::Stdlib,
1475 line_number: 1,
1476 imported_symbols: Some(vec!["HashMap".to_string()]),
1477 },
1478 Dependency {
1479 file_id: 1,
1480 imported_path: "crate::lib".to_string(),
1481 resolved_file_id: Some(2),
1482 import_type: ImportType::Internal,
1483 line_number: 2,
1484 imported_symbols: None,
1485 },
1486 ];
1487
1488 deps_index.batch_insert_dependencies(&deps).unwrap();
1489
1490 let retrieved = deps_index.get_dependencies(1).unwrap();
1491 assert_eq!(retrieved.len(), 2);
1492 }
1493
1494 #[test]
1495 fn test_clear_dependencies() {
1496 let (_temp, cache) = setup_test_cache();
1497 let deps_index = DependencyIndex::new(cache);
1498
1499 deps_index
1501 .insert_dependency(
1502 1,
1503 "crate::lib".to_string(),
1504 Some(2),
1505 ImportType::Internal,
1506 1,
1507 None,
1508 )
1509 .unwrap();
1510
1511 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 1);
1513
1514 deps_index.clear_dependencies(1).unwrap();
1516
1517 assert_eq!(deps_index.get_dependencies(1).unwrap().len(), 0);
1519 }
1520
1521 #[test]
1522 fn test_resolve_rust_import_crate() {
1523 use std::fs;
1524 use tempfile::TempDir;
1525
1526 let temp = TempDir::new().unwrap();
1527 let project_root = temp.path();
1528
1529 fs::create_dir_all(project_root.join("src")).unwrap();
1531 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1532 fs::write(project_root.join("src/models.rs"), "").unwrap();
1533
1534 let resolved = resolve_rust_import(
1536 "crate::models",
1537 "src/query.rs",
1538 project_root,
1539 );
1540
1541 assert_eq!(resolved, Some("src/models.rs".to_string()));
1542 }
1543
1544 #[test]
1545 fn test_resolve_rust_import_super() {
1546 use std::fs;
1547 use tempfile::TempDir;
1548
1549 let temp = TempDir::new().unwrap();
1550 let project_root = temp.path();
1551
1552 fs::create_dir_all(project_root.join("src/parsers")).unwrap();
1554 fs::write(project_root.join("src/models.rs"), "").unwrap();
1555 fs::write(project_root.join("src/parsers/rust.rs"), "").unwrap();
1556
1557 let current_file = project_root.join("src/parsers/rust.rs");
1560 let resolved = resolve_rust_import(
1561 "super::models",
1562 ¤t_file.to_string_lossy(),
1563 project_root,
1564 );
1565
1566 assert_eq!(resolved, Some("src/models.rs".to_string()));
1567 }
1568
1569 #[test]
1570 fn test_resolve_rust_import_external() {
1571 use tempfile::TempDir;
1572
1573 let temp = TempDir::new().unwrap();
1574 let project_root = temp.path();
1575
1576 let resolved = resolve_rust_import(
1578 "serde::Serialize",
1579 "src/models.rs",
1580 project_root,
1581 );
1582
1583 assert_eq!(resolved, None);
1584
1585 let resolved = resolve_rust_import(
1587 "std::collections::HashMap",
1588 "src/models.rs",
1589 project_root,
1590 );
1591
1592 assert_eq!(resolved, None);
1593 }
1594
1595 #[test]
1596 fn test_resolve_rust_mod_declaration() {
1597 use std::fs;
1598 use tempfile::TempDir;
1599
1600 let temp = TempDir::new().unwrap();
1601 let project_root = temp.path();
1602
1603 fs::create_dir_all(project_root.join("src")).unwrap();
1605 fs::write(project_root.join("src/lib.rs"), "").unwrap();
1606 fs::write(project_root.join("src/parser.rs"), "").unwrap();
1607
1608 let resolved = resolve_rust_mod_declaration(
1610 "parser",
1611 &project_root.join("src/lib.rs").to_string_lossy(),
1612 project_root,
1613 );
1614
1615 assert!(resolved.is_some());
1616 assert!(resolved.unwrap().ends_with("src/parser.rs"));
1617 }
1618
1619 #[test]
1620 fn test_resolve_rust_import_nested() {
1621 use std::fs;
1622 use tempfile::TempDir;
1623
1624 let temp = TempDir::new().unwrap();
1625 let project_root = temp.path();
1626
1627 fs::create_dir_all(project_root.join("src/models")).unwrap();
1629 fs::write(project_root.join("src/models/mod.rs"), "").unwrap();
1630 fs::write(project_root.join("src/models/language.rs"), "").unwrap();
1631
1632 let resolved = resolve_rust_import(
1634 "crate::models::language",
1635 "src/query.rs",
1636 project_root,
1637 );
1638
1639 assert_eq!(resolved, Some("src/models/language.rs".to_string()));
1640 }
1641}