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 file.ends_with(".go") {
449 let dir = Path::new(file).parent()?.to_string_lossy();
450 if dir == pkg_path || dir.ends_with(pkg_path) {
451 return Some(dir.to_string());
452 }
453 }
454 }
455 None
456}
457
458fn resolve_java(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
463 let source = &imp.source;
464
465 if source.starts_with("java.") || source.starts_with("javax.") || source.starts_with("sun.") {
466 return (None, true);
467 }
468
469 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
470 if parts.len() < 2 {
471 return (None, true);
472 }
473
474 let class_name = parts[0];
475 let package_path = parts[1].replace('.', "/");
476 let file_path = format!("{package_path}/{class_name}.java");
477
478 let search_roots = ["", "src/main/java/", "src/", "app/src/main/java/"];
479 for root in &search_roots {
480 let candidate = format!("{root}{file_path}");
481 if ctx.file_exists(&candidate) {
482 return (Some(candidate), false);
483 }
484 }
485
486 (
487 None,
488 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
489 )
490}
491
492fn resolve_kotlin(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
497 let source = &imp.source;
498
499 if source.starts_with("java.")
500 || source.starts_with("javax.")
501 || source.starts_with("kotlin.")
502 || source.starts_with("kotlinx.")
503 || source.starts_with("android.")
504 || source.starts_with("androidx.")
505 || source.starts_with("org.junit.")
506 || source.starts_with("org.jetbrains.")
507 {
508 return (None, true);
509 }
510
511 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
512 if parts.len() < 2 {
513 return (None, true);
514 }
515
516 let class_name = parts[0];
517 let package_path = parts[1].replace('.', "/");
518
519 let search_roots = [
520 "",
521 "src/main/kotlin/",
522 "src/main/java/",
523 "src/",
524 "app/src/main/kotlin/",
525 "app/src/main/java/",
526 "src/commonMain/kotlin/",
527 ];
528
529 for root in &search_roots {
530 for ext in &["kt", "kts", "java"] {
531 let candidate = format!("{root}{package_path}/{class_name}.{ext}");
532 if ctx.file_exists(&candidate) {
533 return (Some(candidate), false);
534 }
535 }
536 }
537
538 (
539 None,
540 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
541 )
542}
543
544fn resolve_c_like(
549 imp: &ImportInfo,
550 file_path: &str,
551 ctx: &ResolverContext,
552) -> (Option<String>, bool) {
553 let source = imp.source.trim();
554 if source.is_empty() {
555 return (None, true);
556 }
557
558 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
559 let rel = rel.trim_start_matches("./").trim_start_matches('/');
560 let mut candidates: Vec<String> = vec![rel.to_string()];
561 for ext in [".h", ".hpp", ".c", ".cpp"] {
562 if !rel.ends_with(ext) {
563 candidates.push(format!("{rel}{ext}"));
564 }
565 }
566 for prefix in prefixes {
567 for c in candidates.iter() {
568 let p = format!("{prefix}{c}");
569 if ctx.file_exists(&p) {
570 return Some(p);
571 }
572 }
573 }
574 None
575 };
576
577 if source.starts_with('.') {
578 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
579 let dir_prefix = if dir.as_os_str().is_empty() {
580 "".to_string()
581 } else {
582 format!("{}/", dir.to_string_lossy())
583 };
584 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
585 return (Some(found), false);
586 }
587 return (None, false);
588 }
589
590 if ctx.file_exists(source) {
591 return (Some(source.to_string()), false);
592 }
593
594 if let Some(found) = try_prefixes(&["", "include/", "src/"], source) {
595 return (Some(found), false);
596 }
597
598 (None, true)
599}
600
601fn resolve_ruby(
606 imp: &ImportInfo,
607 file_path: &str,
608 ctx: &ResolverContext,
609) -> (Option<String>, bool) {
610 let source = imp.source.trim();
611 if source.is_empty() {
612 return (None, true);
613 }
614 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
615
616 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
617 let mut candidates: Vec<String> = vec![source_rel.to_string()];
618 if !source_rel.ends_with(".rb") {
619 candidates.push(format!("{source_rel}.rb"));
620 }
621 for prefix in prefixes {
622 for c in candidates.iter() {
623 let p = format!("{prefix}{c}");
624 if ctx.file_exists(&p) {
625 return Some(p);
626 }
627 }
628 }
629 None
630 };
631
632 if source.starts_with('.') || source_rel.contains('/') {
633 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
634 let dir_prefix = if dir.as_os_str().is_empty() {
635 "".to_string()
636 } else {
637 format!("{}/", dir.to_string_lossy())
638 };
639 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
640 return (Some(found), false);
641 }
642 if let Some(found) = try_prefixes(&["", "lib/", "src/"]) {
643 return (Some(found), false);
644 }
645 return (None, false);
646 }
647
648 (None, true)
649}
650
651fn resolve_php(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
656 let source = imp.source.trim();
657 if source.is_empty() {
658 return (None, true);
659 }
660 if source.starts_with("http://") || source.starts_with("https://") {
661 return (None, true);
662 }
663 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
664
665 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
666 let mut candidates: Vec<String> = vec![source_rel.to_string()];
667 if !source_rel.ends_with(".php") {
668 candidates.push(format!("{source_rel}.php"));
669 }
670 for prefix in prefixes {
671 for c in candidates.iter() {
672 let p = format!("{prefix}{c}");
673 if ctx.file_exists(&p) {
674 return Some(p);
675 }
676 }
677 }
678 None
679 };
680
681 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
682 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
683 let dir_prefix = if dir.as_os_str().is_empty() {
684 "".to_string()
685 } else {
686 format!("{}/", dir.to_string_lossy())
687 };
688 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
689 return (Some(found), false);
690 }
691 if let Some(found) = try_prefixes(&["", "src/", "lib/"]) {
692 return (Some(found), false);
693 }
694 return (None, false);
695 }
696
697 (None, true)
698}
699
700fn resolve_bash(
705 imp: &ImportInfo,
706 file_path: &str,
707 ctx: &ResolverContext,
708) -> (Option<String>, bool) {
709 let source = imp.source.trim();
710 if source.is_empty() {
711 return (None, true);
712 }
713 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
714
715 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
716 let mut candidates: Vec<String> = vec![source_rel.to_string()];
717 if !source_rel.ends_with(".sh") {
718 candidates.push(format!("{source_rel}.sh"));
719 }
720 for prefix in prefixes {
721 for c in candidates.iter() {
722 let p = format!("{prefix}{c}");
723 if ctx.file_exists(&p) {
724 return Some(p);
725 }
726 }
727 }
728 None
729 };
730
731 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
732 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
733 let dir_prefix = if dir.as_os_str().is_empty() {
734 "".to_string()
735 } else {
736 format!("{}/", dir.to_string_lossy())
737 };
738 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
739 return (Some(found), false);
740 }
741 if let Some(found) = try_prefixes(&["", "scripts/", "bin/"]) {
742 return (Some(found), false);
743 }
744 return (None, false);
745 }
746
747 (None, true)
748}
749
750fn resolve_dart(
755 imp: &ImportInfo,
756 file_path: &str,
757 ctx: &ResolverContext,
758) -> (Option<String>, bool) {
759 let source = imp.source.trim();
760 if source.is_empty() {
761 return (None, true);
762 }
763 if source.starts_with("dart:") {
764 return (None, true);
765 }
766
767 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
768 let rel = rel.trim_start_matches("./").trim_start_matches('/').trim();
769 let mut candidates: Vec<String> = vec![rel.to_string()];
770 if !rel.ends_with(".dart") {
771 candidates.push(format!("{rel}.dart"));
772 }
773 for prefix in prefixes {
774 for c in candidates.iter() {
775 let p = format!("{prefix}{c}");
776 if ctx.file_exists(&p) {
777 return Some(p);
778 }
779 }
780 }
781 None
782 };
783
784 if source.starts_with("package:") {
785 if let Some(pkg) = ctx.dart_package.as_deref() {
786 let prefix = format!("package:{pkg}/");
787 if let Some(rest) = source.strip_prefix(&prefix) {
788 if let Some(found) = try_prefixes(&["lib/", ""], rest) {
789 return (Some(found), false);
790 }
791 return (None, false);
792 }
793 }
794 return (None, true);
795 }
796
797 if source.starts_with('.') || source.starts_with('/') {
798 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
799 let dir_prefix = if dir.as_os_str().is_empty() {
800 "".to_string()
801 } else {
802 format!("{}/", dir.to_string_lossy())
803 };
804 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
805 return (Some(found), false);
806 }
807 if let Some(found) = try_prefixes(&["", "lib/"], source) {
808 return (Some(found), false);
809 }
810 return (None, false);
811 }
812
813 (None, true)
814}
815
816fn resolve_zig(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
821 let source = imp.source.trim();
822 if source.is_empty() {
823 return (None, true);
824 }
825 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
826 if source_rel == "std" {
827 return (None, true);
828 }
829
830 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
831 let mut candidates: Vec<String> = vec![source_rel.to_string()];
832 if !source_rel.ends_with(".zig") {
833 candidates.push(format!("{source_rel}.zig"));
834 }
835 for prefix in prefixes {
836 for c in candidates.iter() {
837 let p = format!("{prefix}{c}");
838 if ctx.file_exists(&p) {
839 return Some(p);
840 }
841 }
842 }
843 None
844 };
845
846 if source.starts_with('.') || source_rel.contains('/') || source_rel.ends_with(".zig") {
847 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
848 let dir_prefix = if dir.as_os_str().is_empty() {
849 "".to_string()
850 } else {
851 format!("{}/", dir.to_string_lossy())
852 };
853 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
854 return (Some(found), false);
855 }
856 if let Some(found) = try_prefixes(&["", "src/"]) {
857 return (Some(found), false);
858 }
859 return (None, false);
860 }
861
862 (None, true)
863}
864
865fn load_tsconfig_paths(root: &Path) -> HashMap<String, String> {
870 let mut paths = HashMap::new();
871
872 let candidates = ["tsconfig.json", "tsconfig.base.json", "jsconfig.json"];
873 for name in &candidates {
874 let tsconfig_path = root.join(name);
875 if let Ok(content) = std::fs::read_to_string(&tsconfig_path) {
876 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
877 if let Some(compiler) = json.get("compilerOptions") {
878 let base_url = compiler
879 .get("baseUrl")
880 .and_then(|v| v.as_str())
881 .unwrap_or(".");
882
883 if let Some(path_map) = compiler.get("paths").and_then(|v| v.as_object()) {
884 for (pattern, targets) in path_map {
885 if let Some(first_target) = targets
886 .as_array()
887 .and_then(|a| a.first())
888 .and_then(|v| v.as_str())
889 {
890 let resolved = if base_url == "." {
891 first_target.to_string()
892 } else {
893 format!("{base_url}/{first_target}")
894 };
895 paths.insert(pattern.clone(), resolved);
896 }
897 }
898 }
899 }
900 }
901 break;
902 }
903 }
904
905 paths
906}
907
908fn load_go_module(root: &Path) -> Option<String> {
909 let go_mod = root.join("go.mod");
910 let content = std::fs::read_to_string(go_mod).ok()?;
911 for line in content.lines() {
912 let trimmed = line.trim();
913 if trimmed.starts_with("module ") {
914 return Some(trimmed.strip_prefix("module ")?.trim().to_string());
915 }
916 }
917 None
918}
919
920fn load_dart_package(root: &Path) -> Option<String> {
921 let pubspec = root.join("pubspec.yaml");
922 let content = std::fs::read_to_string(pubspec).ok()?;
923 for line in content.lines() {
924 let trimmed = line.trim();
925 if let Some(rest) = trimmed.strip_prefix("name:") {
926 let name = rest.trim();
927 if !name.is_empty() {
928 return Some(name.to_string());
929 }
930 }
931 }
932 None
933}
934
935fn normalize_path(path: &Path) -> String {
940 let mut parts: Vec<&str> = Vec::new();
941 for component in path.components() {
942 match component {
943 std::path::Component::ParentDir => {
944 parts.pop();
945 }
946 std::path::Component::CurDir => {}
947 std::path::Component::Normal(s) => {
948 parts.push(s.to_str().unwrap_or(""));
949 }
950 _ => {}
951 }
952 }
953 parts.join("/")
954}
955
956#[cfg(test)]
961mod tests {
962 use super::*;
963 use crate::core::deep_queries::{ImportInfo, ImportKind};
964
965 fn make_ctx(files: &[&str]) -> ResolverContext {
966 ResolverContext {
967 project_root: PathBuf::from("/project"),
968 file_paths: files.iter().map(|s| s.to_string()).collect(),
969 tsconfig_paths: HashMap::new(),
970 go_module: None,
971 dart_package: None,
972 file_set: files.iter().map(|s| s.to_string()).collect(),
973 }
974 }
975
976 fn make_import(source: &str) -> ImportInfo {
977 ImportInfo {
978 source: source.to_string(),
979 names: Vec::new(),
980 kind: ImportKind::Named,
981 line: 1,
982 is_type_only: false,
983 }
984 }
985
986 #[test]
989 fn ts_relative_import() {
990 let ctx = make_ctx(&["src/components/Button.tsx", "src/utils/helpers.ts"]);
991 let imp = make_import("./helpers");
992 let results = resolve_imports(&[imp], "src/utils/index.ts", "ts", &ctx);
993 assert_eq!(
994 results[0].resolved_path.as_deref(),
995 Some("src/utils/helpers.ts")
996 );
997 assert!(!results[0].is_external);
998 }
999
1000 #[test]
1001 fn ts_relative_parent() {
1002 let ctx = make_ctx(&["src/utils.ts", "src/components/Button.tsx"]);
1003 let imp = make_import("../utils");
1004 let results = resolve_imports(&[imp], "src/components/Button.tsx", "ts", &ctx);
1005 assert_eq!(results[0].resolved_path.as_deref(), Some("src/utils.ts"));
1006 }
1007
1008 #[test]
1009 fn ts_index_file() {
1010 let ctx = make_ctx(&["src/components/index.ts", "src/app.ts"]);
1011 let imp = make_import("./components");
1012 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1013 assert_eq!(
1014 results[0].resolved_path.as_deref(),
1015 Some("src/components/index.ts")
1016 );
1017 }
1018
1019 #[test]
1020 fn ts_relative_js_specifier_resolves_to_ts_source() {
1021 let ctx = make_ctx(&["src/b.ts", "src/a.ts"]);
1022 let imp = make_import("./b.js");
1023 let results = resolve_imports(&[imp], "src/a.ts", "ts", &ctx);
1024 assert_eq!(results[0].resolved_path.as_deref(), Some("src/b.ts"));
1025 assert!(!results[0].is_external);
1026 }
1027
1028 #[test]
1029 fn ts_relative_jsx_specifier_resolves_to_tsx_source() {
1030 let ctx = make_ctx(&["src/Button.tsx", "src/App.tsx"]);
1031 let imp = make_import("./Button.jsx");
1032 let results = resolve_imports(&[imp], "src/App.tsx", "tsx", &ctx);
1033 assert_eq!(results[0].resolved_path.as_deref(), Some("src/Button.tsx"));
1034 }
1035
1036 #[test]
1037 fn ts_relative_mjs_specifier_resolves_to_mts_source() {
1038 let ctx = make_ctx(&["src/utils.mts", "src/main.mts"]);
1039 let imp = make_import("./utils.mjs");
1040 let results = resolve_imports(&[imp], "src/main.mts", "ts", &ctx);
1041 assert_eq!(results[0].resolved_path.as_deref(), Some("src/utils.mts"));
1042 }
1043
1044 #[test]
1045 fn ts_relative_js_specifier_falls_back_to_js_file() {
1046 let ctx = make_ctx(&["src/legacy.js", "src/app.ts"]);
1047 let imp = make_import("./legacy.js");
1048 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1049 assert_eq!(results[0].resolved_path.as_deref(), Some("src/legacy.js"));
1050 }
1051
1052 #[test]
1053 fn ts_external_package() {
1054 let ctx = make_ctx(&["src/app.ts"]);
1055 let imp = make_import("react");
1056 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1057 assert!(results[0].is_external);
1058 assert!(results[0].resolved_path.is_none());
1059 }
1060
1061 #[test]
1062 fn ts_tsconfig_paths() {
1063 let mut ctx = make_ctx(&["src/lib/utils/format.ts"]);
1064 ctx.tsconfig_paths
1065 .insert("@utils/*".to_string(), "src/lib/utils/*".to_string());
1066 let imp = make_import("@utils/format");
1067 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
1068 assert_eq!(
1069 results[0].resolved_path.as_deref(),
1070 Some("src/lib/utils/format.ts")
1071 );
1072 assert!(!results[0].is_external);
1073 }
1074
1075 #[test]
1078 fn rust_crate_import() {
1079 let ctx = make_ctx(&["src/core/session.rs", "src/main.rs"]);
1080 let imp = make_import("crate::core::session");
1081 let results = resolve_imports(&[imp], "src/server.rs", "rs", &ctx);
1082 assert_eq!(
1083 results[0].resolved_path.as_deref(),
1084 Some("src/core/session.rs")
1085 );
1086 assert!(!results[0].is_external);
1087 }
1088
1089 #[test]
1090 fn rust_mod_rs() {
1091 let ctx = make_ctx(&["src/core/mod.rs", "src/main.rs"]);
1092 let imp = make_import("crate::core");
1093 let results = resolve_imports(&[imp], "src/main.rs", "rs", &ctx);
1094 assert_eq!(results[0].resolved_path.as_deref(), Some("src/core/mod.rs"));
1095 }
1096
1097 #[test]
1098 fn rust_external_crate() {
1099 let ctx = make_ctx(&["src/main.rs"]);
1100 let imp = make_import("anyhow::Result");
1101 let results = resolve_imports(&[imp], "src/main.rs", "rs", &ctx);
1102 assert!(results[0].is_external);
1103 }
1104
1105 #[test]
1106 fn rust_symbol_in_module() {
1107 let ctx = make_ctx(&["src/core/session.rs"]);
1108 let imp = make_import("crate::core::session::SessionState");
1109 let results = resolve_imports(&[imp], "src/server.rs", "rs", &ctx);
1110 assert_eq!(
1111 results[0].resolved_path.as_deref(),
1112 Some("src/core/session.rs")
1113 );
1114 }
1115
1116 #[test]
1119 fn python_absolute_import() {
1120 let ctx = make_ctx(&["models/user.py", "app.py"]);
1121 let imp = make_import("models.user");
1122 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1123 assert_eq!(results[0].resolved_path.as_deref(), Some("models/user.py"));
1124 }
1125
1126 #[test]
1127 fn python_package_init() {
1128 let ctx = make_ctx(&["utils/__init__.py", "app.py"]);
1129 let imp = make_import("utils");
1130 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1131 assert_eq!(
1132 results[0].resolved_path.as_deref(),
1133 Some("utils/__init__.py")
1134 );
1135 }
1136
1137 #[test]
1138 fn python_relative_import() {
1139 let ctx = make_ctx(&["pkg/utils.py", "pkg/main.py"]);
1140 let imp = make_import(".utils");
1141 let results = resolve_imports(&[imp], "pkg/main.py", "py", &ctx);
1142 assert_eq!(results[0].resolved_path.as_deref(), Some("pkg/utils.py"));
1143 }
1144
1145 #[test]
1146 fn python_stdlib() {
1147 let ctx = make_ctx(&["app.py"]);
1148 let imp = make_import("os");
1149 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1150 assert!(results[0].is_external);
1151 }
1152
1153 #[test]
1156 fn go_internal_package() {
1157 let mut ctx = make_ctx(&["cmd/server/main.go", "internal/auth/auth.go"]);
1158 ctx.go_module = Some("github.com/org/project".to_string());
1159 let imp = make_import("github.com/org/project/internal/auth");
1160 let results = resolve_imports(&[imp], "cmd/server/main.go", "go", &ctx);
1161 assert_eq!(results[0].resolved_path.as_deref(), Some("internal/auth"));
1162 assert!(!results[0].is_external);
1163 }
1164
1165 #[test]
1166 fn go_external_package() {
1167 let ctx = make_ctx(&["main.go"]);
1168 let imp = make_import("fmt");
1169 let results = resolve_imports(&[imp], "main.go", "go", &ctx);
1170 assert!(results[0].is_external);
1171 }
1172
1173 #[test]
1176 fn java_internal_class() {
1177 let ctx = make_ctx(&[
1178 "src/main/java/com/example/service/UserService.java",
1179 "src/main/java/com/example/model/User.java",
1180 ]);
1181 let imp = make_import("com.example.model.User");
1182 let results = resolve_imports(
1183 &[imp],
1184 "src/main/java/com/example/service/UserService.java",
1185 "java",
1186 &ctx,
1187 );
1188 assert_eq!(
1189 results[0].resolved_path.as_deref(),
1190 Some("src/main/java/com/example/model/User.java")
1191 );
1192 assert!(!results[0].is_external);
1193 }
1194
1195 #[test]
1196 fn java_stdlib() {
1197 let ctx = make_ctx(&["Main.java"]);
1198 let imp = make_import("java.util.List");
1199 let results = resolve_imports(&[imp], "Main.java", "java", &ctx);
1200 assert!(results[0].is_external);
1201 }
1202
1203 #[test]
1206 fn empty_imports() {
1207 let ctx = make_ctx(&["src/main.rs"]);
1208 let results = resolve_imports(&[], "src/main.rs", "rs", &ctx);
1209 assert!(results.is_empty());
1210 }
1211
1212 #[test]
1213 fn unsupported_language() {
1214 let ctx = make_ctx(&["main.rb"]);
1215 let imp = make_import("some_module");
1216 let results = resolve_imports(&[imp], "main.rb", "rb", &ctx);
1217 assert!(results[0].is_external);
1218 }
1219
1220 #[test]
1221 fn c_include_resolves_from_include_dir() {
1222 let ctx = make_ctx(&["include/foo/bar.h", "src/main.c"]);
1223 let imp = make_import("foo/bar.h");
1224 let results = resolve_imports(&[imp], "src/main.c", "c", &ctx);
1225 assert_eq!(
1226 results[0].resolved_path.as_deref(),
1227 Some("include/foo/bar.h")
1228 );
1229 assert!(!results[0].is_external);
1230 }
1231
1232 #[test]
1233 fn ruby_require_relative_resolves() {
1234 let ctx = make_ctx(&["lib/utils.rb", "app.rb"]);
1235 let imp = make_import("./lib/utils");
1236 let results = resolve_imports(&[imp], "app.rb", "rb", &ctx);
1237 assert_eq!(results[0].resolved_path.as_deref(), Some("lib/utils.rb"));
1238 assert!(!results[0].is_external);
1239 }
1240
1241 #[test]
1242 fn php_require_resolves() {
1243 let ctx = make_ctx(&["vendor/autoload.php", "index.php"]);
1244 let imp = make_import("./vendor/autoload.php");
1245 let results = resolve_imports(&[imp], "index.php", "php", &ctx);
1246 assert_eq!(
1247 results[0].resolved_path.as_deref(),
1248 Some("vendor/autoload.php")
1249 );
1250 assert!(!results[0].is_external);
1251 }
1252
1253 #[test]
1254 fn bash_source_resolves() {
1255 let ctx = make_ctx(&["scripts/env.sh", "main.sh"]);
1256 let imp = make_import("./scripts/env.sh");
1257 let results = resolve_imports(&[imp], "main.sh", "sh", &ctx);
1258 assert_eq!(results[0].resolved_path.as_deref(), Some("scripts/env.sh"));
1259 assert!(!results[0].is_external);
1260 }
1261
1262 #[test]
1263 fn dart_package_import_resolves_to_lib() {
1264 let mut ctx = make_ctx(&["lib/src/util.dart", "lib/app.dart"]);
1265 ctx.dart_package = Some("myapp".to_string());
1266 let imp = make_import("package:myapp/src/util.dart");
1267 let results = resolve_imports(&[imp], "lib/app.dart", "dart", &ctx);
1268 assert_eq!(
1269 results[0].resolved_path.as_deref(),
1270 Some("lib/src/util.dart")
1271 );
1272 assert!(!results[0].is_external);
1273 }
1274
1275 #[test]
1276 fn kotlin_import_resolves_to_src_main_kotlin() {
1277 let ctx = make_ctx(&[
1278 "src/main/kotlin/com/example/service/UserService.kt",
1279 "src/main/kotlin/com/example/App.kt",
1280 ]);
1281 let imp = make_import("com.example.service.UserService");
1282 let results = resolve_imports(&[imp], "src/main/kotlin/com/example/App.kt", "kt", &ctx);
1283 assert_eq!(
1284 results[0].resolved_path.as_deref(),
1285 Some("src/main/kotlin/com/example/service/UserService.kt")
1286 );
1287 assert!(!results[0].is_external);
1288 }
1289
1290 #[test]
1291 fn kotlin_stdlib_import_is_external() {
1292 let ctx = make_ctx(&["src/main/kotlin/App.kt"]);
1293 let imp = make_import("kotlin.collections.List");
1294 let results = resolve_imports(&[imp], "src/main/kotlin/App.kt", "kt", &ctx);
1295 assert!(results[0].is_external);
1296 }
1297
1298 #[test]
1299 fn kotlin_import_resolves_java_file() {
1300 let ctx = make_ctx(&[
1301 "src/main/java/com/example/LegacyUtil.java",
1302 "src/main/kotlin/com/example/App.kt",
1303 ]);
1304 let imp = make_import("com.example.LegacyUtil");
1305 let results = resolve_imports(&[imp], "src/main/kotlin/com/example/App.kt", "kt", &ctx);
1306 assert_eq!(
1307 results[0].resolved_path.as_deref(),
1308 Some("src/main/java/com/example/LegacyUtil.java")
1309 );
1310 assert!(!results[0].is_external);
1311 }
1312}