1use std::collections::HashMap;
18use std::path::{Path, PathBuf};
19
20use super::deep_queries::ImportInfo;
21
22#[derive(Debug, Clone)]
23pub struct ResolvedImport {
24 pub source: String,
25 pub resolved_path: Option<String>,
26 pub is_external: bool,
27 pub line: usize,
28}
29
30#[derive(Debug)]
31pub struct ResolverContext {
32 pub project_root: PathBuf,
33 pub file_paths: Vec<String>,
34 pub tsconfig_paths: HashMap<String, String>,
35 pub go_module: Option<String>,
36 pub dart_package: Option<String>,
37 file_set: std::collections::HashSet<String>,
38}
39
40impl ResolverContext {
41 pub fn new(project_root: &Path, file_paths: Vec<String>) -> Self {
42 let file_set: std::collections::HashSet<String> = file_paths.iter().cloned().collect();
43
44 let tsconfig_paths = load_tsconfig_paths(project_root);
45 let go_module = load_go_module(project_root);
46 let dart_package = load_dart_package(project_root);
47
48 Self {
49 project_root: project_root.to_path_buf(),
50 file_paths,
51 tsconfig_paths,
52 go_module,
53 dart_package,
54 file_set,
55 }
56 }
57
58 fn file_exists(&self, rel_path: &str) -> bool {
59 self.file_set.contains(rel_path)
60 }
61}
62
63pub fn resolve_imports(
64 imports: &[ImportInfo],
65 file_path: &str,
66 ext: &str,
67 ctx: &ResolverContext,
68) -> Vec<ResolvedImport> {
69 imports
70 .iter()
71 .map(|imp| {
72 let (resolved, is_external) = resolve_one(imp, file_path, ext, ctx);
73 ResolvedImport {
74 source: imp.source.clone(),
75 resolved_path: resolved,
76 is_external,
77 line: imp.line,
78 }
79 })
80 .collect()
81}
82
83fn resolve_one(
84 imp: &ImportInfo,
85 file_path: &str,
86 ext: &str,
87 ctx: &ResolverContext,
88) -> (Option<String>, bool) {
89 match ext {
90 "ts" | "tsx" | "js" | "jsx" => resolve_ts(imp, file_path, ctx),
91 "rs" => resolve_rust(imp, file_path, ctx),
92 "py" => resolve_python(imp, file_path, ctx),
93 "go" => resolve_go(imp, ctx),
94 "java" => resolve_java(imp, ctx),
95 "c" | "h" | "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => {
96 resolve_c_like(imp, file_path, ctx)
97 }
98 "rb" => resolve_ruby(imp, file_path, ctx),
99 "php" => resolve_php(imp, file_path, ctx),
100 "sh" | "bash" => resolve_bash(imp, file_path, ctx),
101 "dart" => resolve_dart(imp, file_path, ctx),
102 "zig" => resolve_zig(imp, file_path, ctx),
103 "kt" | "kts" => resolve_kotlin(imp, ctx),
104 _ => (None, true),
105 }
106}
107
108fn resolve_ts(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
113 let source = &imp.source;
114
115 if source.starts_with('.') {
116 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
117 let resolved = dir.join(source);
118 let normalized = normalize_path(&resolved);
119
120 if let Some(found) = try_ts_with_js_remap(&normalized, ctx) {
121 return (Some(found), false);
122 }
123 return (None, false);
124 }
125
126 if let Some(mapped) = resolve_tsconfig_path(source, ctx) {
127 return (Some(mapped), false);
128 }
129
130 (None, true)
131}
132
133fn try_ts_with_js_remap(base: &str, ctx: &ResolverContext) -> Option<String> {
137 const JS_TO_TS: &[(&str, &[&str])] = &[
138 (".js", &[".ts", ".tsx"]),
139 (".jsx", &[".tsx", ".ts"]),
140 (".mjs", &[".mts"]),
141 (".cjs", &[".cts"]),
142 ];
143
144 for &(js_ext, ts_exts) in JS_TO_TS {
145 if let Some(stem) = base.strip_suffix(js_ext) {
146 for ts_ext in ts_exts {
147 let candidate = format!("{stem}{ts_ext}");
148 if ctx.file_exists(&candidate) {
149 return Some(candidate);
150 }
151 }
152 }
153 }
154
155 try_ts_extensions(base, ctx)
156}
157
158fn try_ts_extensions(base: &str, ctx: &ResolverContext) -> Option<String> {
159 let extensions = [".ts", ".tsx", ".js", ".jsx", ".d.ts"];
160
161 if ctx.file_exists(base) {
162 return Some(base.to_string());
163 }
164
165 for ext in &extensions {
166 let with_ext = format!("{base}{ext}");
167 if ctx.file_exists(&with_ext) {
168 return Some(with_ext);
169 }
170 }
171
172 let index_extensions = ["index.ts", "index.tsx", "index.js", "index.jsx"];
173 for idx in &index_extensions {
174 let index_path = format!("{base}/{idx}");
175 if ctx.file_exists(&index_path) {
176 return Some(index_path);
177 }
178 }
179
180 None
181}
182
183fn resolve_tsconfig_path(source: &str, ctx: &ResolverContext) -> Option<String> {
184 for (pattern, target) in &ctx.tsconfig_paths {
185 let prefix = pattern.trim_end_matches('*');
186 if let Some(remainder) = source.strip_prefix(prefix) {
187 let target_base = target.trim_end_matches('*');
188 let candidate = format!("{target_base}{remainder}");
189 if let Some(found) = try_ts_with_js_remap(&candidate, ctx) {
190 return Some(found);
191 }
192 }
193 }
194 None
195}
196
197fn resolve_rust(
202 imp: &ImportInfo,
203 file_path: &str,
204 ctx: &ResolverContext,
205) -> (Option<String>, bool) {
206 let source = &imp.source;
207
208 if source.starts_with("crate::")
209 || source.starts_with("super::")
210 || source.starts_with("self::")
211 {
212 let cleaned = source.replace("crate::", "").replace("self::", "");
213
214 let resolved = if source.starts_with("super::") {
215 let dir = Path::new(file_path).parent().and_then(|p| p.parent());
216 match dir {
217 Some(d) => {
218 let rest = source.trim_start_matches("super::");
219 d.join(rest.replace("::", "/"))
220 .to_string_lossy()
221 .to_string()
222 }
223 None => cleaned.replace("::", "/"),
224 }
225 } else {
226 cleaned.replace("::", "/")
227 };
228
229 if let Some(found) = try_rust_paths(&resolved, ctx) {
230 return (Some(found), false);
231 }
232 return (None, false);
233 }
234
235 let parts: Vec<&str> = source.split("::").collect();
236 if parts.is_empty() {
237 return (None, true);
238 }
239
240 let is_external = !source.starts_with("crate")
241 && !ctx.file_paths.iter().any(|f| {
242 let stem = Path::new(f)
243 .file_stem()
244 .and_then(|s| s.to_str())
245 .unwrap_or("");
246 stem == parts[0]
247 });
248
249 if is_external {
250 return (None, true);
251 }
252
253 let as_path = source.replace("::", "/");
254 if let Some(found) = try_rust_paths(&as_path, ctx) {
255 return (Some(found), false);
256 }
257
258 (None, is_external)
259}
260
261fn try_rust_paths(base: &str, ctx: &ResolverContext) -> Option<String> {
262 let prefixes = ["", "src/", "rust/src/"];
263 for prefix in &prefixes {
264 let candidate = format!("{prefix}{base}.rs");
265 if ctx.file_exists(&candidate) {
266 return Some(candidate);
267 }
268 let mod_candidate = format!("{prefix}{base}/mod.rs");
269 if ctx.file_exists(&mod_candidate) {
270 return Some(mod_candidate);
271 }
272 }
273
274 let parts: Vec<&str> = base.rsplitn(2, '/').collect();
275 if parts.len() == 2 {
276 let parent = parts[1];
277 for prefix in &prefixes {
278 let candidate = format!("{prefix}{parent}.rs");
279 if ctx.file_exists(&candidate) {
280 return Some(candidate);
281 }
282 }
283 }
284
285 None
286}
287
288fn resolve_python(
293 imp: &ImportInfo,
294 file_path: &str,
295 ctx: &ResolverContext,
296) -> (Option<String>, bool) {
297 let source = &imp.source;
298
299 if source.starts_with('.') {
300 let dot_count = source.chars().take_while(|c| *c == '.').count();
301 let module_part = &source[dot_count..];
302
303 let mut dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
304 for _ in 1..dot_count {
305 dir = dir.parent().unwrap_or(Path::new(""));
306 }
307
308 let as_path = module_part.replace('.', "/");
309 let base = if as_path.is_empty() {
310 dir.to_string_lossy().to_string()
311 } else {
312 format!("{}/{as_path}", dir.display())
313 };
314
315 if let Some(found) = try_python_paths(&base, ctx) {
316 return (Some(found), false);
317 }
318 return (None, false);
319 }
320
321 let as_path = source.replace('.', "/");
322
323 if let Some(found) = try_python_paths(&as_path, ctx) {
324 return (Some(found), false);
325 }
326
327 let is_stdlib = is_python_stdlib(source);
328 (
329 None,
330 is_stdlib || !ctx.file_paths.iter().any(|f| f.contains(&as_path)),
331 )
332}
333
334fn try_python_paths(base: &str, ctx: &ResolverContext) -> Option<String> {
335 let py_file = format!("{base}.py");
336 if ctx.file_exists(&py_file) {
337 return Some(py_file);
338 }
339
340 let init_file = format!("{base}/__init__.py");
341 if ctx.file_exists(&init_file) {
342 return Some(init_file);
343 }
344
345 let prefixes = ["src/", "lib/"];
346 for prefix in &prefixes {
347 let candidate = format!("{prefix}{base}.py");
348 if ctx.file_exists(&candidate) {
349 return Some(candidate);
350 }
351 let init = format!("{prefix}{base}/__init__.py");
352 if ctx.file_exists(&init) {
353 return Some(init);
354 }
355 }
356
357 None
358}
359
360fn is_python_stdlib(module: &str) -> bool {
361 let first = module.split('.').next().unwrap_or(module);
362 matches!(
363 first,
364 "os" | "sys"
365 | "json"
366 | "re"
367 | "math"
368 | "datetime"
369 | "typing"
370 | "collections"
371 | "itertools"
372 | "functools"
373 | "pathlib"
374 | "io"
375 | "abc"
376 | "enum"
377 | "dataclasses"
378 | "logging"
379 | "unittest"
380 | "argparse"
381 | "subprocess"
382 | "threading"
383 | "multiprocessing"
384 | "socket"
385 | "http"
386 | "urllib"
387 | "hashlib"
388 | "hmac"
389 | "secrets"
390 | "time"
391 | "copy"
392 | "pprint"
393 | "textwrap"
394 | "shutil"
395 | "tempfile"
396 | "glob"
397 | "fnmatch"
398 | "contextlib"
399 | "inspect"
400 | "importlib"
401 | "pickle"
402 | "shelve"
403 | "csv"
404 | "configparser"
405 | "struct"
406 | "codecs"
407 | "string"
408 | "difflib"
409 | "ast"
410 | "dis"
411 | "traceback"
412 | "warnings"
413 | "concurrent"
414 | "asyncio"
415 | "signal"
416 | "select"
417 )
418}
419
420fn resolve_go(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
425 let source = &imp.source;
426
427 if let Some(ref go_mod) = ctx.go_module {
428 if source.starts_with(go_mod.as_str()) {
429 let relative = source.strip_prefix(go_mod.as_str()).unwrap_or(source);
430 let relative = relative.trim_start_matches('/');
431
432 if let Some(found) = try_go_package(relative, ctx) {
433 return (Some(found), false);
434 }
435 return (None, false);
436 }
437 }
438
439 if let Some(found) = try_go_package(source, ctx) {
440 return (Some(found), false);
441 }
442
443 (None, true)
444}
445
446fn try_go_package(pkg_path: &str, ctx: &ResolverContext) -> Option<String> {
447 for file in &ctx.file_paths {
448 if Path::new(file.as_str())
449 .extension()
450 .is_some_and(|e| e.eq_ignore_ascii_case("go"))
451 {
452 let dir = Path::new(file).parent()?.to_string_lossy();
453 if dir == pkg_path || dir.ends_with(pkg_path) {
454 return Some(dir.to_string());
455 }
456 }
457 }
458 None
459}
460
461fn resolve_java(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
466 let source = &imp.source;
467
468 if source.starts_with("java.") || source.starts_with("javax.") || source.starts_with("sun.") {
469 return (None, true);
470 }
471
472 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
473 if parts.len() < 2 {
474 return (None, true);
475 }
476
477 let class_name = parts[0];
478 let package_path = parts[1].replace('.', "/");
479 let file_path = format!("{package_path}/{class_name}.java");
480
481 let search_roots = ["", "src/main/java/", "src/", "app/src/main/java/"];
482 for root in &search_roots {
483 let candidate = format!("{root}{file_path}");
484 if ctx.file_exists(&candidate) {
485 return (Some(candidate), false);
486 }
487 }
488
489 (
490 None,
491 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
492 )
493}
494
495fn resolve_kotlin(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
500 let source = &imp.source;
501
502 if source.starts_with("java.")
503 || source.starts_with("javax.")
504 || source.starts_with("kotlin.")
505 || source.starts_with("kotlinx.")
506 || source.starts_with("android.")
507 || source.starts_with("androidx.")
508 || source.starts_with("org.junit.")
509 || source.starts_with("org.jetbrains.")
510 {
511 return (None, true);
512 }
513
514 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
515 if parts.len() < 2 {
516 return (None, true);
517 }
518
519 let class_name = parts[0];
520 let package_path = parts[1].replace('.', "/");
521
522 let search_roots = [
523 "",
524 "src/main/kotlin/",
525 "src/main/java/",
526 "src/",
527 "app/src/main/kotlin/",
528 "app/src/main/java/",
529 "src/commonMain/kotlin/",
530 ];
531
532 for root in &search_roots {
533 for ext in &["kt", "kts", "java"] {
534 let candidate = format!("{root}{package_path}/{class_name}.{ext}");
535 if ctx.file_exists(&candidate) {
536 return (Some(candidate), false);
537 }
538 }
539 }
540
541 (
542 None,
543 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
544 )
545}
546
547fn resolve_c_like(
552 imp: &ImportInfo,
553 file_path: &str,
554 ctx: &ResolverContext,
555) -> (Option<String>, bool) {
556 let source = imp.source.trim();
557 if source.is_empty() {
558 return (None, true);
559 }
560
561 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
562 let rel = rel.trim_start_matches("./").trim_start_matches('/');
563 let mut candidates: Vec<String> = vec![rel.to_string()];
564 for ext in [".h", ".hpp", ".c", ".cpp"] {
565 if !rel.ends_with(ext) {
566 candidates.push(format!("{rel}{ext}"));
567 }
568 }
569 for prefix in prefixes {
570 for c in &candidates {
571 let p = format!("{prefix}{c}");
572 if ctx.file_exists(&p) {
573 return Some(p);
574 }
575 }
576 }
577 None
578 };
579
580 if source.starts_with('.') {
581 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
582 let dir_prefix = if dir.as_os_str().is_empty() {
583 String::new()
584 } else {
585 format!("{}/", dir.to_string_lossy())
586 };
587 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
588 return (Some(found), false);
589 }
590 return (None, false);
591 }
592
593 if ctx.file_exists(source) {
594 return (Some(source.to_string()), false);
595 }
596
597 if let Some(found) = try_prefixes(&["", "include/", "src/"], source) {
598 return (Some(found), false);
599 }
600
601 (None, true)
602}
603
604fn resolve_ruby(
609 imp: &ImportInfo,
610 file_path: &str,
611 ctx: &ResolverContext,
612) -> (Option<String>, bool) {
613 let source = imp.source.trim();
614 if source.is_empty() {
615 return (None, true);
616 }
617 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
618
619 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
620 let mut candidates: Vec<String> = vec![source_rel.to_string()];
621 if !Path::new(source_rel)
622 .extension()
623 .is_some_and(|e| e.eq_ignore_ascii_case("rb"))
624 {
625 candidates.push(format!("{source_rel}.rb"));
626 }
627 for prefix in prefixes {
628 for c in &candidates {
629 let p = format!("{prefix}{c}");
630 if ctx.file_exists(&p) {
631 return Some(p);
632 }
633 }
634 }
635 None
636 };
637
638 if source.starts_with('.') || source_rel.contains('/') {
639 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
640 let dir_prefix = if dir.as_os_str().is_empty() {
641 String::new()
642 } else {
643 format!("{}/", dir.to_string_lossy())
644 };
645 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
646 return (Some(found), false);
647 }
648 if let Some(found) = try_prefixes(&["", "lib/", "src/"]) {
649 return (Some(found), false);
650 }
651 return (None, false);
652 }
653
654 (None, true)
655}
656
657fn resolve_php(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
662 let source = imp.source.trim();
663 if source.is_empty() {
664 return (None, true);
665 }
666 if source.starts_with("http://") || source.starts_with("https://") {
667 return (None, true);
668 }
669 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
670
671 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
672 let mut candidates: Vec<String> = vec![source_rel.to_string()];
673 if !Path::new(source_rel)
674 .extension()
675 .is_some_and(|e| e.eq_ignore_ascii_case("php"))
676 {
677 candidates.push(format!("{source_rel}.php"));
678 }
679 for prefix in prefixes {
680 for c in &candidates {
681 let p = format!("{prefix}{c}");
682 if ctx.file_exists(&p) {
683 return Some(p);
684 }
685 }
686 }
687 None
688 };
689
690 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
691 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
692 let dir_prefix = if dir.as_os_str().is_empty() {
693 String::new()
694 } else {
695 format!("{}/", dir.to_string_lossy())
696 };
697 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
698 return (Some(found), false);
699 }
700 if let Some(found) = try_prefixes(&["", "src/", "lib/"]) {
701 return (Some(found), false);
702 }
703 return (None, false);
704 }
705
706 (None, true)
707}
708
709fn resolve_bash(
714 imp: &ImportInfo,
715 file_path: &str,
716 ctx: &ResolverContext,
717) -> (Option<String>, bool) {
718 let source = imp.source.trim();
719 if source.is_empty() {
720 return (None, true);
721 }
722 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
723
724 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
725 let mut candidates: Vec<String> = vec![source_rel.to_string()];
726 if !Path::new(source_rel)
727 .extension()
728 .is_some_and(|e| e.eq_ignore_ascii_case("sh"))
729 {
730 candidates.push(format!("{source_rel}.sh"));
731 }
732 for prefix in prefixes {
733 for c in &candidates {
734 let p = format!("{prefix}{c}");
735 if ctx.file_exists(&p) {
736 return Some(p);
737 }
738 }
739 }
740 None
741 };
742
743 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
744 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
745 let dir_prefix = if dir.as_os_str().is_empty() {
746 String::new()
747 } else {
748 format!("{}/", dir.to_string_lossy())
749 };
750 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
751 return (Some(found), false);
752 }
753 if let Some(found) = try_prefixes(&["", "scripts/", "bin/"]) {
754 return (Some(found), false);
755 }
756 return (None, false);
757 }
758
759 (None, true)
760}
761
762fn resolve_dart(
767 imp: &ImportInfo,
768 file_path: &str,
769 ctx: &ResolverContext,
770) -> (Option<String>, bool) {
771 let source = imp.source.trim();
772 if source.is_empty() {
773 return (None, true);
774 }
775 if source.starts_with("dart:") {
776 return (None, true);
777 }
778
779 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
780 let rel = rel.trim_start_matches("./").trim_start_matches('/').trim();
781 let mut candidates: Vec<String> = vec![rel.to_string()];
782 if !Path::new(rel)
783 .extension()
784 .is_some_and(|e| e.eq_ignore_ascii_case("dart"))
785 {
786 candidates.push(format!("{rel}.dart"));
787 }
788 for prefix in prefixes {
789 for c in &candidates {
790 let p = format!("{prefix}{c}");
791 if ctx.file_exists(&p) {
792 return Some(p);
793 }
794 }
795 }
796 None
797 };
798
799 if source.starts_with("package:") {
800 if let Some(pkg) = ctx.dart_package.as_deref() {
801 let prefix = format!("package:{pkg}/");
802 if let Some(rest) = source.strip_prefix(&prefix) {
803 if let Some(found) = try_prefixes(&["lib/", ""], rest) {
804 return (Some(found), false);
805 }
806 return (None, false);
807 }
808 }
809 return (None, true);
810 }
811
812 if source.starts_with('.') || source.starts_with('/') {
813 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
814 let dir_prefix = if dir.as_os_str().is_empty() {
815 String::new()
816 } else {
817 format!("{}/", dir.to_string_lossy())
818 };
819 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
820 return (Some(found), false);
821 }
822 if let Some(found) = try_prefixes(&["", "lib/"], source) {
823 return (Some(found), false);
824 }
825 return (None, false);
826 }
827
828 (None, true)
829}
830
831fn resolve_zig(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
836 let source = imp.source.trim();
837 if source.is_empty() {
838 return (None, true);
839 }
840 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
841 if source_rel == "std" {
842 return (None, true);
843 }
844
845 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
846 let mut candidates: Vec<String> = vec![source_rel.to_string()];
847 if !Path::new(source_rel)
848 .extension()
849 .is_some_and(|e| e.eq_ignore_ascii_case("zig"))
850 {
851 candidates.push(format!("{source_rel}.zig"));
852 }
853 for prefix in prefixes {
854 for c in &candidates {
855 let p = format!("{prefix}{c}");
856 if ctx.file_exists(&p) {
857 return Some(p);
858 }
859 }
860 }
861 None
862 };
863
864 if source.starts_with('.')
865 || source_rel.contains('/')
866 || Path::new(source_rel)
867 .extension()
868 .is_some_and(|e| e.eq_ignore_ascii_case("zig"))
869 {
870 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
871 let dir_prefix = if dir.as_os_str().is_empty() {
872 String::new()
873 } else {
874 format!("{}/", dir.to_string_lossy())
875 };
876 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
877 return (Some(found), false);
878 }
879 if let Some(found) = try_prefixes(&["", "src/"]) {
880 return (Some(found), false);
881 }
882 return (None, false);
883 }
884
885 (None, true)
886}
887
888fn load_tsconfig_paths(root: &Path) -> HashMap<String, String> {
893 let mut paths = HashMap::new();
894
895 let candidates = ["tsconfig.json", "tsconfig.base.json", "jsconfig.json"];
896 for name in &candidates {
897 let tsconfig_path = root.join(name);
898 if let Ok(content) = std::fs::read_to_string(&tsconfig_path) {
899 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
900 if let Some(compiler) = json.get("compilerOptions") {
901 let base_url = compiler
902 .get("baseUrl")
903 .and_then(|v| v.as_str())
904 .unwrap_or(".");
905
906 if let Some(path_map) = compiler.get("paths").and_then(|v| v.as_object()) {
907 for (pattern, targets) in path_map {
908 if let Some(first_target) = targets
909 .as_array()
910 .and_then(|a| a.first())
911 .and_then(|v| v.as_str())
912 {
913 let resolved = if base_url == "." {
914 first_target.to_string()
915 } else {
916 format!("{base_url}/{first_target}")
917 };
918 paths.insert(pattern.clone(), resolved);
919 }
920 }
921 }
922 }
923 }
924 break;
925 }
926 }
927
928 paths
929}
930
931fn load_go_module(root: &Path) -> Option<String> {
932 let go_mod = root.join("go.mod");
933 let content = std::fs::read_to_string(go_mod).ok()?;
934 for line in content.lines() {
935 let trimmed = line.trim();
936 if trimmed.starts_with("module ") {
937 return Some(trimmed.strip_prefix("module ")?.trim().to_string());
938 }
939 }
940 None
941}
942
943fn load_dart_package(root: &Path) -> Option<String> {
944 let pubspec = root.join("pubspec.yaml");
945 let content = std::fs::read_to_string(pubspec).ok()?;
946 for line in content.lines() {
947 let trimmed = line.trim();
948 if let Some(rest) = trimmed.strip_prefix("name:") {
949 let name = rest.trim();
950 if !name.is_empty() {
951 return Some(name.to_string());
952 }
953 }
954 }
955 None
956}
957
958fn normalize_path(path: &Path) -> String {
963 let mut parts: Vec<&str> = Vec::new();
964 for component in path.components() {
965 match component {
966 std::path::Component::ParentDir => {
967 parts.pop();
968 }
969 std::path::Component::Normal(s) => {
970 parts.push(s.to_str().unwrap_or(""));
971 }
972 _ => {}
973 }
974 }
975 parts.join("/")
976}
977
978#[cfg(test)]
983mod tests {
984 use super::*;
985 use crate::core::deep_queries::{ImportInfo, ImportKind};
986
987 fn make_ctx(files: &[&str]) -> ResolverContext {
988 ResolverContext {
989 project_root: PathBuf::from("/project"),
990 file_paths: files.iter().map(std::string::ToString::to_string).collect(),
991 tsconfig_paths: HashMap::new(),
992 go_module: None,
993 dart_package: None,
994 file_set: files.iter().map(std::string::ToString::to_string).collect(),
995 }
996 }
997
998 fn make_import(source: &str) -> ImportInfo {
999 ImportInfo {
1000 source: source.to_string(),
1001 names: Vec::new(),
1002 kind: ImportKind::Named,
1003 line: 1,
1004 is_type_only: false,
1005 }
1006 }
1007
1008 #[test]
1011 fn ts_relative_import() {
1012 let ctx = make_ctx(&["src/components/Button.tsx", "src/utils/helpers.ts"]);
1013 let imp = make_import("./helpers");
1014 let results = resolve_imports(&[imp], "src/utils/index.ts", "ts", &ctx);
1015 assert_eq!(
1016 results[0].resolved_path.as_deref(),
1017 Some("src/utils/helpers.ts")
1018 );
1019 assert!(!results[0].is_external);
1020 }
1021
1022 #[test]
1023 fn ts_relative_parent() {
1024 let ctx = make_ctx(&["src/utils.ts", "src/components/Button.tsx"]);
1025 let imp = make_import("../utils");
1026 let results = resolve_imports(&[imp], "src/components/Button.tsx", "ts", &ctx);
1027 assert_eq!(results[0].resolved_path.as_deref(), Some("src/utils.ts"));
1028 }
1029
1030 #[test]
1031 fn ts_index_file() {
1032 let ctx = make_ctx(&["src/components/index.ts", "src/app.ts"]);
1033 let imp = make_import("./components");
1034 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1035 assert_eq!(
1036 results[0].resolved_path.as_deref(),
1037 Some("src/components/index.ts")
1038 );
1039 }
1040
1041 #[test]
1042 fn ts_relative_js_specifier_resolves_to_ts_source() {
1043 let ctx = make_ctx(&["src/b.ts", "src/a.ts"]);
1044 let imp = make_import("./b.js");
1045 let results = resolve_imports(&[imp], "src/a.ts", "ts", &ctx);
1046 assert_eq!(results[0].resolved_path.as_deref(), Some("src/b.ts"));
1047 assert!(!results[0].is_external);
1048 }
1049
1050 #[test]
1051 fn ts_relative_jsx_specifier_resolves_to_tsx_source() {
1052 let ctx = make_ctx(&["src/Button.tsx", "src/App.tsx"]);
1053 let imp = make_import("./Button.jsx");
1054 let results = resolve_imports(&[imp], "src/App.tsx", "tsx", &ctx);
1055 assert_eq!(results[0].resolved_path.as_deref(), Some("src/Button.tsx"));
1056 }
1057
1058 #[test]
1059 fn ts_relative_mjs_specifier_resolves_to_mts_source() {
1060 let ctx = make_ctx(&["src/utils.mts", "src/main.mts"]);
1061 let imp = make_import("./utils.mjs");
1062 let results = resolve_imports(&[imp], "src/main.mts", "ts", &ctx);
1063 assert_eq!(results[0].resolved_path.as_deref(), Some("src/utils.mts"));
1064 }
1065
1066 #[test]
1067 fn ts_relative_js_specifier_falls_back_to_js_file() {
1068 let ctx = make_ctx(&["src/legacy.js", "src/app.ts"]);
1069 let imp = make_import("./legacy.js");
1070 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1071 assert_eq!(results[0].resolved_path.as_deref(), Some("src/legacy.js"));
1072 }
1073
1074 #[test]
1075 fn ts_external_package() {
1076 let ctx = make_ctx(&["src/app.ts"]);
1077 let imp = make_import("react");
1078 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1079 assert!(results[0].is_external);
1080 assert!(results[0].resolved_path.is_none());
1081 }
1082
1083 #[test]
1084 fn ts_tsconfig_paths() {
1085 let mut ctx = make_ctx(&["src/lib/utils/format.ts"]);
1086 ctx.tsconfig_paths
1087 .insert("@utils/*".to_string(), "src/lib/utils/*".to_string());
1088 let imp = make_import("@utils/format");
1089 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1090 assert_eq!(
1091 results[0].resolved_path.as_deref(),
1092 Some("src/lib/utils/format.ts")
1093 );
1094 assert!(!results[0].is_external);
1095 }
1096
1097 #[test]
1100 fn rust_crate_import() {
1101 let ctx = make_ctx(&["src/core/session.rs", "src/main.rs"]);
1102 let imp = make_import("crate::core::session");
1103 let results = resolve_imports(&[imp], "src/server.rs", "rs", &ctx);
1104 assert_eq!(
1105 results[0].resolved_path.as_deref(),
1106 Some("src/core/session.rs")
1107 );
1108 assert!(!results[0].is_external);
1109 }
1110
1111 #[test]
1112 fn rust_mod_rs() {
1113 let ctx = make_ctx(&["src/core/mod.rs", "src/main.rs"]);
1114 let imp = make_import("crate::core");
1115 let results = resolve_imports(&[imp], "src/main.rs", "rs", &ctx);
1116 assert_eq!(results[0].resolved_path.as_deref(), Some("src/core/mod.rs"));
1117 }
1118
1119 #[test]
1120 fn rust_external_crate() {
1121 let ctx = make_ctx(&["src/main.rs"]);
1122 let imp = make_import("anyhow::Result");
1123 let results = resolve_imports(&[imp], "src/main.rs", "rs", &ctx);
1124 assert!(results[0].is_external);
1125 }
1126
1127 #[test]
1128 fn rust_symbol_in_module() {
1129 let ctx = make_ctx(&["src/core/session.rs"]);
1130 let imp = make_import("crate::core::session::SessionState");
1131 let results = resolve_imports(&[imp], "src/server.rs", "rs", &ctx);
1132 assert_eq!(
1133 results[0].resolved_path.as_deref(),
1134 Some("src/core/session.rs")
1135 );
1136 }
1137
1138 #[test]
1141 fn python_absolute_import() {
1142 let ctx = make_ctx(&["models/user.py", "app.py"]);
1143 let imp = make_import("models.user");
1144 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1145 assert_eq!(results[0].resolved_path.as_deref(), Some("models/user.py"));
1146 }
1147
1148 #[test]
1149 fn python_package_init() {
1150 let ctx = make_ctx(&["utils/__init__.py", "app.py"]);
1151 let imp = make_import("utils");
1152 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1153 assert_eq!(
1154 results[0].resolved_path.as_deref(),
1155 Some("utils/__init__.py")
1156 );
1157 }
1158
1159 #[test]
1160 fn python_relative_import() {
1161 let ctx = make_ctx(&["pkg/utils.py", "pkg/main.py"]);
1162 let imp = make_import(".utils");
1163 let results = resolve_imports(&[imp], "pkg/main.py", "py", &ctx);
1164 assert_eq!(results[0].resolved_path.as_deref(), Some("pkg/utils.py"));
1165 }
1166
1167 #[test]
1168 fn python_stdlib() {
1169 let ctx = make_ctx(&["app.py"]);
1170 let imp = make_import("os");
1171 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1172 assert!(results[0].is_external);
1173 }
1174
1175 #[test]
1178 fn go_internal_package() {
1179 let mut ctx = make_ctx(&["cmd/server/main.go", "internal/auth/auth.go"]);
1180 ctx.go_module = Some("github.com/org/project".to_string());
1181 let imp = make_import("github.com/org/project/internal/auth");
1182 let results = resolve_imports(&[imp], "cmd/server/main.go", "go", &ctx);
1183 assert_eq!(results[0].resolved_path.as_deref(), Some("internal/auth"));
1184 assert!(!results[0].is_external);
1185 }
1186
1187 #[test]
1188 fn go_external_package() {
1189 let ctx = make_ctx(&["main.go"]);
1190 let imp = make_import("fmt");
1191 let results = resolve_imports(&[imp], "main.go", "go", &ctx);
1192 assert!(results[0].is_external);
1193 }
1194
1195 #[test]
1198 fn java_internal_class() {
1199 let ctx = make_ctx(&[
1200 "src/main/java/com/example/service/UserService.java",
1201 "src/main/java/com/example/model/User.java",
1202 ]);
1203 let imp = make_import("com.example.model.User");
1204 let results = resolve_imports(
1205 &[imp],
1206 "src/main/java/com/example/service/UserService.java",
1207 "java",
1208 &ctx,
1209 );
1210 assert_eq!(
1211 results[0].resolved_path.as_deref(),
1212 Some("src/main/java/com/example/model/User.java")
1213 );
1214 assert!(!results[0].is_external);
1215 }
1216
1217 #[test]
1218 fn java_stdlib() {
1219 let ctx = make_ctx(&["Main.java"]);
1220 let imp = make_import("java.util.List");
1221 let results = resolve_imports(&[imp], "Main.java", "java", &ctx);
1222 assert!(results[0].is_external);
1223 }
1224
1225 #[test]
1228 fn empty_imports() {
1229 let ctx = make_ctx(&["src/main.rs"]);
1230 let results = resolve_imports(&[], "src/main.rs", "rs", &ctx);
1231 assert!(results.is_empty());
1232 }
1233
1234 #[test]
1235 fn unsupported_language() {
1236 let ctx = make_ctx(&["main.rb"]);
1237 let imp = make_import("some_module");
1238 let results = resolve_imports(&[imp], "main.rb", "rb", &ctx);
1239 assert!(results[0].is_external);
1240 }
1241
1242 #[test]
1243 fn c_include_resolves_from_include_dir() {
1244 let ctx = make_ctx(&["include/foo/bar.h", "src/main.c"]);
1245 let imp = make_import("foo/bar.h");
1246 let results = resolve_imports(&[imp], "src/main.c", "c", &ctx);
1247 assert_eq!(
1248 results[0].resolved_path.as_deref(),
1249 Some("include/foo/bar.h")
1250 );
1251 assert!(!results[0].is_external);
1252 }
1253
1254 #[test]
1255 fn ruby_require_relative_resolves() {
1256 let ctx = make_ctx(&["lib/utils.rb", "app.rb"]);
1257 let imp = make_import("./lib/utils");
1258 let results = resolve_imports(&[imp], "app.rb", "rb", &ctx);
1259 assert_eq!(results[0].resolved_path.as_deref(), Some("lib/utils.rb"));
1260 assert!(!results[0].is_external);
1261 }
1262
1263 #[test]
1264 fn php_require_resolves() {
1265 let ctx = make_ctx(&["vendor/autoload.php", "index.php"]);
1266 let imp = make_import("./vendor/autoload.php");
1267 let results = resolve_imports(&[imp], "index.php", "php", &ctx);
1268 assert_eq!(
1269 results[0].resolved_path.as_deref(),
1270 Some("vendor/autoload.php")
1271 );
1272 assert!(!results[0].is_external);
1273 }
1274
1275 #[test]
1276 fn bash_source_resolves() {
1277 let ctx = make_ctx(&["scripts/env.sh", "main.sh"]);
1278 let imp = make_import("./scripts/env.sh");
1279 let results = resolve_imports(&[imp], "main.sh", "sh", &ctx);
1280 assert_eq!(results[0].resolved_path.as_deref(), Some("scripts/env.sh"));
1281 assert!(!results[0].is_external);
1282 }
1283
1284 #[test]
1285 fn dart_package_import_resolves_to_lib() {
1286 let mut ctx = make_ctx(&["lib/src/util.dart", "lib/app.dart"]);
1287 ctx.dart_package = Some("myapp".to_string());
1288 let imp = make_import("package:myapp/src/util.dart");
1289 let results = resolve_imports(&[imp], "lib/app.dart", "dart", &ctx);
1290 assert_eq!(
1291 results[0].resolved_path.as_deref(),
1292 Some("lib/src/util.dart")
1293 );
1294 assert!(!results[0].is_external);
1295 }
1296
1297 #[test]
1298 fn kotlin_import_resolves_to_src_main_kotlin() {
1299 let ctx = make_ctx(&[
1300 "src/main/kotlin/com/example/service/UserService.kt",
1301 "src/main/kotlin/com/example/App.kt",
1302 ]);
1303 let imp = make_import("com.example.service.UserService");
1304 let results = resolve_imports(&[imp], "src/main/kotlin/com/example/App.kt", "kt", &ctx);
1305 assert_eq!(
1306 results[0].resolved_path.as_deref(),
1307 Some("src/main/kotlin/com/example/service/UserService.kt")
1308 );
1309 assert!(!results[0].is_external);
1310 }
1311
1312 #[test]
1313 fn kotlin_stdlib_import_is_external() {
1314 let ctx = make_ctx(&["src/main/kotlin/App.kt"]);
1315 let imp = make_import("kotlin.collections.List");
1316 let results = resolve_imports(&[imp], "src/main/kotlin/App.kt", "kt", &ctx);
1317 assert!(results[0].is_external);
1318 }
1319
1320 #[test]
1321 fn kotlin_import_resolves_java_file() {
1322 let ctx = make_ctx(&[
1323 "src/main/java/com/example/LegacyUtil.java",
1324 "src/main/kotlin/com/example/App.kt",
1325 ]);
1326 let imp = make_import("com.example.LegacyUtil");
1327 let results = resolve_imports(&[imp], "src/main/kotlin/com/example/App.kt", "kt", &ctx);
1328 assert_eq!(
1329 results[0].resolved_path.as_deref(),
1330 Some("src/main/java/com/example/LegacyUtil.java")
1331 );
1332 assert!(!results[0].is_external);
1333 }
1334}