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 _ => (None, true),
104 }
105}
106
107fn resolve_ts(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
112 let source = &imp.source;
113
114 if source.starts_with('.') {
115 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
116 let resolved = dir.join(source);
117 let normalized = normalize_path(&resolved);
118
119 if let Some(found) = try_ts_extensions(&normalized, ctx) {
120 return (Some(found), false);
121 }
122 return (None, false);
123 }
124
125 if let Some(mapped) = resolve_tsconfig_path(source, ctx) {
126 return (Some(mapped), false);
127 }
128
129 (None, true)
130}
131
132fn try_ts_extensions(base: &str, ctx: &ResolverContext) -> Option<String> {
133 let extensions = [".ts", ".tsx", ".js", ".jsx", ".d.ts"];
134
135 if ctx.file_exists(base) {
136 return Some(base.to_string());
137 }
138
139 for ext in &extensions {
140 let with_ext = format!("{base}{ext}");
141 if ctx.file_exists(&with_ext) {
142 return Some(with_ext);
143 }
144 }
145
146 let index_extensions = ["index.ts", "index.tsx", "index.js", "index.jsx"];
147 for idx in &index_extensions {
148 let index_path = format!("{base}/{idx}");
149 if ctx.file_exists(&index_path) {
150 return Some(index_path);
151 }
152 }
153
154 None
155}
156
157fn resolve_tsconfig_path(source: &str, ctx: &ResolverContext) -> Option<String> {
158 for (pattern, target) in &ctx.tsconfig_paths {
159 let prefix = pattern.trim_end_matches('*');
160 if let Some(remainder) = source.strip_prefix(prefix) {
161 let target_base = target.trim_end_matches('*');
162 let candidate = format!("{target_base}{remainder}");
163 if let Some(found) = try_ts_extensions(&candidate, ctx) {
164 return Some(found);
165 }
166 }
167 }
168 None
169}
170
171fn resolve_rust(
176 imp: &ImportInfo,
177 file_path: &str,
178 ctx: &ResolverContext,
179) -> (Option<String>, bool) {
180 let source = &imp.source;
181
182 if source.starts_with("crate::")
183 || source.starts_with("super::")
184 || source.starts_with("self::")
185 {
186 let cleaned = source.replace("crate::", "").replace("self::", "");
187
188 let resolved = if source.starts_with("super::") {
189 let dir = Path::new(file_path).parent().and_then(|p| p.parent());
190 match dir {
191 Some(d) => {
192 let rest = source.trim_start_matches("super::");
193 d.join(rest.replace("::", "/"))
194 .to_string_lossy()
195 .to_string()
196 }
197 None => cleaned.replace("::", "/"),
198 }
199 } else {
200 cleaned.replace("::", "/")
201 };
202
203 if let Some(found) = try_rust_paths(&resolved, ctx) {
204 return (Some(found), false);
205 }
206 return (None, false);
207 }
208
209 let parts: Vec<&str> = source.split("::").collect();
210 if parts.is_empty() {
211 return (None, true);
212 }
213
214 let is_external = !source.starts_with("crate")
215 && !ctx.file_paths.iter().any(|f| {
216 let stem = Path::new(f)
217 .file_stem()
218 .and_then(|s| s.to_str())
219 .unwrap_or("");
220 stem == parts[0]
221 });
222
223 if is_external {
224 return (None, true);
225 }
226
227 let as_path = source.replace("::", "/");
228 if let Some(found) = try_rust_paths(&as_path, ctx) {
229 return (Some(found), false);
230 }
231
232 (None, is_external)
233}
234
235fn try_rust_paths(base: &str, ctx: &ResolverContext) -> Option<String> {
236 let prefixes = ["", "src/", "rust/src/"];
237 for prefix in &prefixes {
238 let candidate = format!("{prefix}{base}.rs");
239 if ctx.file_exists(&candidate) {
240 return Some(candidate);
241 }
242 let mod_candidate = format!("{prefix}{base}/mod.rs");
243 if ctx.file_exists(&mod_candidate) {
244 return Some(mod_candidate);
245 }
246 }
247
248 let parts: Vec<&str> = base.rsplitn(2, '/').collect();
249 if parts.len() == 2 {
250 let parent = parts[1];
251 for prefix in &prefixes {
252 let candidate = format!("{prefix}{parent}.rs");
253 if ctx.file_exists(&candidate) {
254 return Some(candidate);
255 }
256 }
257 }
258
259 None
260}
261
262fn resolve_python(
267 imp: &ImportInfo,
268 file_path: &str,
269 ctx: &ResolverContext,
270) -> (Option<String>, bool) {
271 let source = &imp.source;
272
273 if source.starts_with('.') {
274 let dot_count = source.chars().take_while(|c| *c == '.').count();
275 let module_part = &source[dot_count..];
276
277 let mut dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
278 for _ in 1..dot_count {
279 dir = dir.parent().unwrap_or(Path::new(""));
280 }
281
282 let as_path = module_part.replace('.', "/");
283 let base = if as_path.is_empty() {
284 dir.to_string_lossy().to_string()
285 } else {
286 format!("{}/{as_path}", dir.display())
287 };
288
289 if let Some(found) = try_python_paths(&base, ctx) {
290 return (Some(found), false);
291 }
292 return (None, false);
293 }
294
295 let as_path = source.replace('.', "/");
296
297 if let Some(found) = try_python_paths(&as_path, ctx) {
298 return (Some(found), false);
299 }
300
301 let is_stdlib = is_python_stdlib(source);
302 (
303 None,
304 is_stdlib || !ctx.file_paths.iter().any(|f| f.contains(&as_path)),
305 )
306}
307
308fn try_python_paths(base: &str, ctx: &ResolverContext) -> Option<String> {
309 let py_file = format!("{base}.py");
310 if ctx.file_exists(&py_file) {
311 return Some(py_file);
312 }
313
314 let init_file = format!("{base}/__init__.py");
315 if ctx.file_exists(&init_file) {
316 return Some(init_file);
317 }
318
319 let prefixes = ["src/", "lib/"];
320 for prefix in &prefixes {
321 let candidate = format!("{prefix}{base}.py");
322 if ctx.file_exists(&candidate) {
323 return Some(candidate);
324 }
325 let init = format!("{prefix}{base}/__init__.py");
326 if ctx.file_exists(&init) {
327 return Some(init);
328 }
329 }
330
331 None
332}
333
334fn is_python_stdlib(module: &str) -> bool {
335 let first = module.split('.').next().unwrap_or(module);
336 matches!(
337 first,
338 "os" | "sys"
339 | "json"
340 | "re"
341 | "math"
342 | "datetime"
343 | "typing"
344 | "collections"
345 | "itertools"
346 | "functools"
347 | "pathlib"
348 | "io"
349 | "abc"
350 | "enum"
351 | "dataclasses"
352 | "logging"
353 | "unittest"
354 | "argparse"
355 | "subprocess"
356 | "threading"
357 | "multiprocessing"
358 | "socket"
359 | "http"
360 | "urllib"
361 | "hashlib"
362 | "hmac"
363 | "secrets"
364 | "time"
365 | "copy"
366 | "pprint"
367 | "textwrap"
368 | "shutil"
369 | "tempfile"
370 | "glob"
371 | "fnmatch"
372 | "contextlib"
373 | "inspect"
374 | "importlib"
375 | "pickle"
376 | "shelve"
377 | "csv"
378 | "configparser"
379 | "struct"
380 | "codecs"
381 | "string"
382 | "difflib"
383 | "ast"
384 | "dis"
385 | "traceback"
386 | "warnings"
387 | "concurrent"
388 | "asyncio"
389 | "signal"
390 | "select"
391 )
392}
393
394fn resolve_go(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
399 let source = &imp.source;
400
401 if let Some(ref go_mod) = ctx.go_module {
402 if source.starts_with(go_mod.as_str()) {
403 let relative = source.strip_prefix(go_mod.as_str()).unwrap_or(source);
404 let relative = relative.trim_start_matches('/');
405
406 if let Some(found) = try_go_package(relative, ctx) {
407 return (Some(found), false);
408 }
409 return (None, false);
410 }
411 }
412
413 if let Some(found) = try_go_package(source, ctx) {
414 return (Some(found), false);
415 }
416
417 (None, true)
418}
419
420fn try_go_package(pkg_path: &str, ctx: &ResolverContext) -> Option<String> {
421 for file in &ctx.file_paths {
422 if file.ends_with(".go") {
423 let dir = Path::new(file).parent()?.to_string_lossy();
424 if dir == pkg_path || dir.ends_with(pkg_path) {
425 return Some(dir.to_string());
426 }
427 }
428 }
429 None
430}
431
432fn resolve_java(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
437 let source = &imp.source;
438
439 if source.starts_with("java.") || source.starts_with("javax.") || source.starts_with("sun.") {
440 return (None, true);
441 }
442
443 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
444 if parts.len() < 2 {
445 return (None, true);
446 }
447
448 let class_name = parts[0];
449 let package_path = parts[1].replace('.', "/");
450 let file_path = format!("{package_path}/{class_name}.java");
451
452 let search_roots = ["", "src/main/java/", "src/", "app/src/main/java/"];
453 for root in &search_roots {
454 let candidate = format!("{root}{file_path}");
455 if ctx.file_exists(&candidate) {
456 return (Some(candidate), false);
457 }
458 }
459
460 (
461 None,
462 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
463 )
464}
465
466fn resolve_c_like(
471 imp: &ImportInfo,
472 file_path: &str,
473 ctx: &ResolverContext,
474) -> (Option<String>, bool) {
475 let source = imp.source.trim();
476 if source.is_empty() {
477 return (None, true);
478 }
479
480 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
481 let rel = rel.trim_start_matches("./").trim_start_matches('/');
482 let mut candidates: Vec<String> = vec![rel.to_string()];
483 for ext in [".h", ".hpp", ".c", ".cpp"] {
484 if !rel.ends_with(ext) {
485 candidates.push(format!("{rel}{ext}"));
486 }
487 }
488 for prefix in prefixes {
489 for c in candidates.iter() {
490 let p = format!("{prefix}{c}");
491 if ctx.file_exists(&p) {
492 return Some(p);
493 }
494 }
495 }
496 None
497 };
498
499 if source.starts_with('.') {
500 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
501 let dir_prefix = if dir.as_os_str().is_empty() {
502 "".to_string()
503 } else {
504 format!("{}/", dir.to_string_lossy())
505 };
506 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
507 return (Some(found), false);
508 }
509 return (None, false);
510 }
511
512 if ctx.file_exists(source) {
513 return (Some(source.to_string()), false);
514 }
515
516 if let Some(found) = try_prefixes(&["", "include/", "src/"], source) {
517 return (Some(found), false);
518 }
519
520 (None, true)
521}
522
523fn resolve_ruby(
528 imp: &ImportInfo,
529 file_path: &str,
530 ctx: &ResolverContext,
531) -> (Option<String>, bool) {
532 let source = imp.source.trim();
533 if source.is_empty() {
534 return (None, true);
535 }
536 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
537
538 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
539 let mut candidates: Vec<String> = vec![source_rel.to_string()];
540 if !source_rel.ends_with(".rb") {
541 candidates.push(format!("{source_rel}.rb"));
542 }
543 for prefix in prefixes {
544 for c in candidates.iter() {
545 let p = format!("{prefix}{c}");
546 if ctx.file_exists(&p) {
547 return Some(p);
548 }
549 }
550 }
551 None
552 };
553
554 if source.starts_with('.') || source_rel.contains('/') {
555 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
556 let dir_prefix = if dir.as_os_str().is_empty() {
557 "".to_string()
558 } else {
559 format!("{}/", dir.to_string_lossy())
560 };
561 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
562 return (Some(found), false);
563 }
564 if let Some(found) = try_prefixes(&["", "lib/", "src/"]) {
565 return (Some(found), false);
566 }
567 return (None, false);
568 }
569
570 (None, true)
571}
572
573fn resolve_php(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
578 let source = imp.source.trim();
579 if source.is_empty() {
580 return (None, true);
581 }
582 if source.starts_with("http://") || source.starts_with("https://") {
583 return (None, true);
584 }
585 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
586
587 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
588 let mut candidates: Vec<String> = vec![source_rel.to_string()];
589 if !source_rel.ends_with(".php") {
590 candidates.push(format!("{source_rel}.php"));
591 }
592 for prefix in prefixes {
593 for c in candidates.iter() {
594 let p = format!("{prefix}{c}");
595 if ctx.file_exists(&p) {
596 return Some(p);
597 }
598 }
599 }
600 None
601 };
602
603 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
604 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
605 let dir_prefix = if dir.as_os_str().is_empty() {
606 "".to_string()
607 } else {
608 format!("{}/", dir.to_string_lossy())
609 };
610 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
611 return (Some(found), false);
612 }
613 if let Some(found) = try_prefixes(&["", "src/", "lib/"]) {
614 return (Some(found), false);
615 }
616 return (None, false);
617 }
618
619 (None, true)
620}
621
622fn resolve_bash(
627 imp: &ImportInfo,
628 file_path: &str,
629 ctx: &ResolverContext,
630) -> (Option<String>, bool) {
631 let source = imp.source.trim();
632 if source.is_empty() {
633 return (None, true);
634 }
635 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
636
637 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
638 let mut candidates: Vec<String> = vec![source_rel.to_string()];
639 if !source_rel.ends_with(".sh") {
640 candidates.push(format!("{source_rel}.sh"));
641 }
642 for prefix in prefixes {
643 for c in candidates.iter() {
644 let p = format!("{prefix}{c}");
645 if ctx.file_exists(&p) {
646 return Some(p);
647 }
648 }
649 }
650 None
651 };
652
653 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
654 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
655 let dir_prefix = if dir.as_os_str().is_empty() {
656 "".to_string()
657 } else {
658 format!("{}/", dir.to_string_lossy())
659 };
660 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
661 return (Some(found), false);
662 }
663 if let Some(found) = try_prefixes(&["", "scripts/", "bin/"]) {
664 return (Some(found), false);
665 }
666 return (None, false);
667 }
668
669 (None, true)
670}
671
672fn resolve_dart(
677 imp: &ImportInfo,
678 file_path: &str,
679 ctx: &ResolverContext,
680) -> (Option<String>, bool) {
681 let source = imp.source.trim();
682 if source.is_empty() {
683 return (None, true);
684 }
685 if source.starts_with("dart:") {
686 return (None, true);
687 }
688
689 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
690 let rel = rel.trim_start_matches("./").trim_start_matches('/').trim();
691 let mut candidates: Vec<String> = vec![rel.to_string()];
692 if !rel.ends_with(".dart") {
693 candidates.push(format!("{rel}.dart"));
694 }
695 for prefix in prefixes {
696 for c in candidates.iter() {
697 let p = format!("{prefix}{c}");
698 if ctx.file_exists(&p) {
699 return Some(p);
700 }
701 }
702 }
703 None
704 };
705
706 if source.starts_with("package:") {
707 if let Some(pkg) = ctx.dart_package.as_deref() {
708 let prefix = format!("package:{pkg}/");
709 if let Some(rest) = source.strip_prefix(&prefix) {
710 if let Some(found) = try_prefixes(&["lib/", ""], rest) {
711 return (Some(found), false);
712 }
713 return (None, false);
714 }
715 }
716 return (None, true);
717 }
718
719 if source.starts_with('.') || source.starts_with('/') {
720 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
721 let dir_prefix = if dir.as_os_str().is_empty() {
722 "".to_string()
723 } else {
724 format!("{}/", dir.to_string_lossy())
725 };
726 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
727 return (Some(found), false);
728 }
729 if let Some(found) = try_prefixes(&["", "lib/"], source) {
730 return (Some(found), false);
731 }
732 return (None, false);
733 }
734
735 (None, true)
736}
737
738fn resolve_zig(imp: &ImportInfo, file_path: &str, ctx: &ResolverContext) -> (Option<String>, bool) {
743 let source = imp.source.trim();
744 if source.is_empty() {
745 return (None, true);
746 }
747 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
748 if source_rel == "std" {
749 return (None, true);
750 }
751
752 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
753 let mut candidates: Vec<String> = vec![source_rel.to_string()];
754 if !source_rel.ends_with(".zig") {
755 candidates.push(format!("{source_rel}.zig"));
756 }
757 for prefix in prefixes {
758 for c in candidates.iter() {
759 let p = format!("{prefix}{c}");
760 if ctx.file_exists(&p) {
761 return Some(p);
762 }
763 }
764 }
765 None
766 };
767
768 if source.starts_with('.') || source_rel.contains('/') || source_rel.ends_with(".zig") {
769 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
770 let dir_prefix = if dir.as_os_str().is_empty() {
771 "".to_string()
772 } else {
773 format!("{}/", dir.to_string_lossy())
774 };
775 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
776 return (Some(found), false);
777 }
778 if let Some(found) = try_prefixes(&["", "src/"]) {
779 return (Some(found), false);
780 }
781 return (None, false);
782 }
783
784 (None, true)
785}
786
787fn load_tsconfig_paths(root: &Path) -> HashMap<String, String> {
792 let mut paths = HashMap::new();
793
794 let candidates = ["tsconfig.json", "tsconfig.base.json", "jsconfig.json"];
795 for name in &candidates {
796 let tsconfig_path = root.join(name);
797 if let Ok(content) = std::fs::read_to_string(&tsconfig_path) {
798 if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
799 if let Some(compiler) = json.get("compilerOptions") {
800 let base_url = compiler
801 .get("baseUrl")
802 .and_then(|v| v.as_str())
803 .unwrap_or(".");
804
805 if let Some(path_map) = compiler.get("paths").and_then(|v| v.as_object()) {
806 for (pattern, targets) in path_map {
807 if let Some(first_target) = targets
808 .as_array()
809 .and_then(|a| a.first())
810 .and_then(|v| v.as_str())
811 {
812 let resolved = if base_url == "." {
813 first_target.to_string()
814 } else {
815 format!("{base_url}/{first_target}")
816 };
817 paths.insert(pattern.clone(), resolved);
818 }
819 }
820 }
821 }
822 }
823 break;
824 }
825 }
826
827 paths
828}
829
830fn load_go_module(root: &Path) -> Option<String> {
831 let go_mod = root.join("go.mod");
832 let content = std::fs::read_to_string(go_mod).ok()?;
833 for line in content.lines() {
834 let trimmed = line.trim();
835 if trimmed.starts_with("module ") {
836 return Some(trimmed.strip_prefix("module ")?.trim().to_string());
837 }
838 }
839 None
840}
841
842fn load_dart_package(root: &Path) -> Option<String> {
843 let pubspec = root.join("pubspec.yaml");
844 let content = std::fs::read_to_string(pubspec).ok()?;
845 for line in content.lines() {
846 let trimmed = line.trim();
847 if let Some(rest) = trimmed.strip_prefix("name:") {
848 let name = rest.trim();
849 if !name.is_empty() {
850 return Some(name.to_string());
851 }
852 }
853 }
854 None
855}
856
857fn normalize_path(path: &Path) -> String {
862 let mut parts: Vec<&str> = Vec::new();
863 for component in path.components() {
864 match component {
865 std::path::Component::ParentDir => {
866 parts.pop();
867 }
868 std::path::Component::CurDir => {}
869 std::path::Component::Normal(s) => {
870 parts.push(s.to_str().unwrap_or(""));
871 }
872 _ => {}
873 }
874 }
875 parts.join("/")
876}
877
878#[cfg(test)]
883mod tests {
884 use super::*;
885 use crate::core::deep_queries::{ImportInfo, ImportKind};
886
887 fn make_ctx(files: &[&str]) -> ResolverContext {
888 ResolverContext {
889 project_root: PathBuf::from("/project"),
890 file_paths: files.iter().map(|s| s.to_string()).collect(),
891 tsconfig_paths: HashMap::new(),
892 go_module: None,
893 dart_package: None,
894 file_set: files.iter().map(|s| s.to_string()).collect(),
895 }
896 }
897
898 fn make_import(source: &str) -> ImportInfo {
899 ImportInfo {
900 source: source.to_string(),
901 names: Vec::new(),
902 kind: ImportKind::Named,
903 line: 1,
904 is_type_only: false,
905 }
906 }
907
908 #[test]
911 fn ts_relative_import() {
912 let ctx = make_ctx(&["src/components/Button.tsx", "src/utils/helpers.ts"]);
913 let imp = make_import("./helpers");
914 let results = resolve_imports(&[imp], "src/utils/index.ts", "ts", &ctx);
915 assert_eq!(
916 results[0].resolved_path.as_deref(),
917 Some("src/utils/helpers.ts")
918 );
919 assert!(!results[0].is_external);
920 }
921
922 #[test]
923 fn ts_relative_parent() {
924 let ctx = make_ctx(&["src/utils.ts", "src/components/Button.tsx"]);
925 let imp = make_import("../utils");
926 let results = resolve_imports(&[imp], "src/components/Button.tsx", "ts", &ctx);
927 assert_eq!(results[0].resolved_path.as_deref(), Some("src/utils.ts"));
928 }
929
930 #[test]
931 fn ts_index_file() {
932 let ctx = make_ctx(&["src/components/index.ts", "src/app.ts"]);
933 let imp = make_import("./components");
934 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
935 assert_eq!(
936 results[0].resolved_path.as_deref(),
937 Some("src/components/index.ts")
938 );
939 }
940
941 #[test]
942 fn ts_external_package() {
943 let ctx = make_ctx(&["src/app.ts"]);
944 let imp = make_import("react");
945 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
946 assert!(results[0].is_external);
947 assert!(results[0].resolved_path.is_none());
948 }
949
950 #[test]
951 fn ts_tsconfig_paths() {
952 let mut ctx = make_ctx(&["src/lib/utils/format.ts"]);
953 ctx.tsconfig_paths
954 .insert("@utils/*".to_string(), "src/lib/utils/*".to_string());
955 let imp = make_import("@utils/format");
956 let results = resolve_imports(&[imp], "src/app.ts", "ts", &ctx);
957 assert_eq!(
958 results[0].resolved_path.as_deref(),
959 Some("src/lib/utils/format.ts")
960 );
961 assert!(!results[0].is_external);
962 }
963
964 #[test]
967 fn rust_crate_import() {
968 let ctx = make_ctx(&["src/core/session.rs", "src/main.rs"]);
969 let imp = make_import("crate::core::session");
970 let results = resolve_imports(&[imp], "src/server.rs", "rs", &ctx);
971 assert_eq!(
972 results[0].resolved_path.as_deref(),
973 Some("src/core/session.rs")
974 );
975 assert!(!results[0].is_external);
976 }
977
978 #[test]
979 fn rust_mod_rs() {
980 let ctx = make_ctx(&["src/core/mod.rs", "src/main.rs"]);
981 let imp = make_import("crate::core");
982 let results = resolve_imports(&[imp], "src/main.rs", "rs", &ctx);
983 assert_eq!(results[0].resolved_path.as_deref(), Some("src/core/mod.rs"));
984 }
985
986 #[test]
987 fn rust_external_crate() {
988 let ctx = make_ctx(&["src/main.rs"]);
989 let imp = make_import("anyhow::Result");
990 let results = resolve_imports(&[imp], "src/main.rs", "rs", &ctx);
991 assert!(results[0].is_external);
992 }
993
994 #[test]
995 fn rust_symbol_in_module() {
996 let ctx = make_ctx(&["src/core/session.rs"]);
997 let imp = make_import("crate::core::session::SessionState");
998 let results = resolve_imports(&[imp], "src/server.rs", "rs", &ctx);
999 assert_eq!(
1000 results[0].resolved_path.as_deref(),
1001 Some("src/core/session.rs")
1002 );
1003 }
1004
1005 #[test]
1008 fn python_absolute_import() {
1009 let ctx = make_ctx(&["models/user.py", "app.py"]);
1010 let imp = make_import("models.user");
1011 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1012 assert_eq!(results[0].resolved_path.as_deref(), Some("models/user.py"));
1013 }
1014
1015 #[test]
1016 fn python_package_init() {
1017 let ctx = make_ctx(&["utils/__init__.py", "app.py"]);
1018 let imp = make_import("utils");
1019 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1020 assert_eq!(
1021 results[0].resolved_path.as_deref(),
1022 Some("utils/__init__.py")
1023 );
1024 }
1025
1026 #[test]
1027 fn python_relative_import() {
1028 let ctx = make_ctx(&["pkg/utils.py", "pkg/main.py"]);
1029 let imp = make_import(".utils");
1030 let results = resolve_imports(&[imp], "pkg/main.py", "py", &ctx);
1031 assert_eq!(results[0].resolved_path.as_deref(), Some("pkg/utils.py"));
1032 }
1033
1034 #[test]
1035 fn python_stdlib() {
1036 let ctx = make_ctx(&["app.py"]);
1037 let imp = make_import("os");
1038 let results = resolve_imports(&[imp], "app.py", "py", &ctx);
1039 assert!(results[0].is_external);
1040 }
1041
1042 #[test]
1045 fn go_internal_package() {
1046 let mut ctx = make_ctx(&["cmd/server/main.go", "internal/auth/auth.go"]);
1047 ctx.go_module = Some("github.com/org/project".to_string());
1048 let imp = make_import("github.com/org/project/internal/auth");
1049 let results = resolve_imports(&[imp], "cmd/server/main.go", "go", &ctx);
1050 assert_eq!(results[0].resolved_path.as_deref(), Some("internal/auth"));
1051 assert!(!results[0].is_external);
1052 }
1053
1054 #[test]
1055 fn go_external_package() {
1056 let ctx = make_ctx(&["main.go"]);
1057 let imp = make_import("fmt");
1058 let results = resolve_imports(&[imp], "main.go", "go", &ctx);
1059 assert!(results[0].is_external);
1060 }
1061
1062 #[test]
1065 fn java_internal_class() {
1066 let ctx = make_ctx(&[
1067 "src/main/java/com/example/service/UserService.java",
1068 "src/main/java/com/example/model/User.java",
1069 ]);
1070 let imp = make_import("com.example.model.User");
1071 let results = resolve_imports(
1072 &[imp],
1073 "src/main/java/com/example/service/UserService.java",
1074 "java",
1075 &ctx,
1076 );
1077 assert_eq!(
1078 results[0].resolved_path.as_deref(),
1079 Some("src/main/java/com/example/model/User.java")
1080 );
1081 assert!(!results[0].is_external);
1082 }
1083
1084 #[test]
1085 fn java_stdlib() {
1086 let ctx = make_ctx(&["Main.java"]);
1087 let imp = make_import("java.util.List");
1088 let results = resolve_imports(&[imp], "Main.java", "java", &ctx);
1089 assert!(results[0].is_external);
1090 }
1091
1092 #[test]
1095 fn empty_imports() {
1096 let ctx = make_ctx(&["src/main.rs"]);
1097 let results = resolve_imports(&[], "src/main.rs", "rs", &ctx);
1098 assert!(results.is_empty());
1099 }
1100
1101 #[test]
1102 fn unsupported_language() {
1103 let ctx = make_ctx(&["main.rb"]);
1104 let imp = make_import("some_module");
1105 let results = resolve_imports(&[imp], "main.rb", "rb", &ctx);
1106 assert!(results[0].is_external);
1107 }
1108
1109 #[test]
1110 fn c_include_resolves_from_include_dir() {
1111 let ctx = make_ctx(&["include/foo/bar.h", "src/main.c"]);
1112 let imp = make_import("foo/bar.h");
1113 let results = resolve_imports(&[imp], "src/main.c", "c", &ctx);
1114 assert_eq!(
1115 results[0].resolved_path.as_deref(),
1116 Some("include/foo/bar.h")
1117 );
1118 assert!(!results[0].is_external);
1119 }
1120
1121 #[test]
1122 fn ruby_require_relative_resolves() {
1123 let ctx = make_ctx(&["lib/utils.rb", "app.rb"]);
1124 let imp = make_import("./lib/utils");
1125 let results = resolve_imports(&[imp], "app.rb", "rb", &ctx);
1126 assert_eq!(results[0].resolved_path.as_deref(), Some("lib/utils.rb"));
1127 assert!(!results[0].is_external);
1128 }
1129
1130 #[test]
1131 fn php_require_resolves() {
1132 let ctx = make_ctx(&["vendor/autoload.php", "index.php"]);
1133 let imp = make_import("./vendor/autoload.php");
1134 let results = resolve_imports(&[imp], "index.php", "php", &ctx);
1135 assert_eq!(
1136 results[0].resolved_path.as_deref(),
1137 Some("vendor/autoload.php")
1138 );
1139 assert!(!results[0].is_external);
1140 }
1141
1142 #[test]
1143 fn bash_source_resolves() {
1144 let ctx = make_ctx(&["scripts/env.sh", "main.sh"]);
1145 let imp = make_import("./scripts/env.sh");
1146 let results = resolve_imports(&[imp], "main.sh", "sh", &ctx);
1147 assert_eq!(results[0].resolved_path.as_deref(), Some("scripts/env.sh"));
1148 assert!(!results[0].is_external);
1149 }
1150
1151 #[test]
1152 fn dart_package_import_resolves_to_lib() {
1153 let mut ctx = make_ctx(&["lib/src/util.dart", "lib/app.dart"]);
1154 ctx.dart_package = Some("myapp".to_string());
1155 let imp = make_import("package:myapp/src/util.dart");
1156 let results = resolve_imports(&[imp], "lib/app.dart", "dart", &ctx);
1157 assert_eq!(
1158 results[0].resolved_path.as_deref(),
1159 Some("lib/src/util.dart")
1160 );
1161 assert!(!results[0].is_external);
1162 }
1163}