1#[allow(clippy::wildcard_imports)]
10use super::*;
11
12pub fn resolve_ts(
17 imp: &ImportInfo,
18 file_path: &str,
19 ctx: &ResolverContext,
20) -> (Option<String>, bool) {
21 let source = &imp.source;
22
23 if source.starts_with('.') {
24 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
25 let resolved = dir.join(source);
26 let normalized = normalize_path(&resolved);
27
28 if let Some(found) = try_ts_with_js_remap(&normalized, ctx) {
29 return (Some(found), false);
30 }
31 return (None, false);
32 }
33
34 if let Some(mapped) = resolve_tsconfig_path(source, ctx) {
35 return (Some(mapped), false);
36 }
37
38 (None, true)
39}
40
41fn try_ts_with_js_remap(base: &str, ctx: &ResolverContext) -> Option<String> {
45 const JS_TO_TS: &[(&str, &[&str])] = &[
46 (".js", &[".ts", ".tsx"]),
47 (".jsx", &[".tsx", ".ts"]),
48 (".mjs", &[".mts"]),
49 (".cjs", &[".cts"]),
50 ];
51
52 for &(js_ext, ts_exts) in JS_TO_TS {
53 if let Some(stem) = base.strip_suffix(js_ext) {
54 for ts_ext in ts_exts {
55 let candidate = format!("{stem}{ts_ext}");
56 if ctx.file_exists(&candidate) {
57 return Some(candidate);
58 }
59 }
60 }
61 }
62
63 try_ts_extensions(base, ctx)
64}
65
66fn try_ts_extensions(base: &str, ctx: &ResolverContext) -> Option<String> {
67 let extensions = [".ts", ".tsx", ".js", ".jsx", ".d.ts"];
68
69 if ctx.file_exists(base) {
70 return Some(base.to_string());
71 }
72
73 for ext in &extensions {
74 let with_ext = format!("{base}{ext}");
75 if ctx.file_exists(&with_ext) {
76 return Some(with_ext);
77 }
78 }
79
80 let index_extensions = ["index.ts", "index.tsx", "index.js", "index.jsx"];
81 for idx in &index_extensions {
82 let index_path = format!("{base}/{idx}");
83 if ctx.file_exists(&index_path) {
84 return Some(index_path);
85 }
86 }
87
88 None
89}
90
91fn resolve_tsconfig_path(source: &str, ctx: &ResolverContext) -> Option<String> {
92 for (pattern, target) in &ctx.tsconfig_paths {
93 let prefix = pattern.trim_end_matches('*');
94 if let Some(remainder) = source.strip_prefix(prefix) {
95 let target_base = target.trim_end_matches('*');
96 let candidate = format!("{target_base}{remainder}");
97 if let Some(found) = try_ts_with_js_remap(&candidate, ctx) {
98 return Some(found);
99 }
100 }
101 }
102 None
103}
104
105pub fn resolve_rust(
110 imp: &ImportInfo,
111 file_path: &str,
112 ctx: &ResolverContext,
113) -> (Option<String>, bool) {
114 let source = &imp.source;
115
116 if source.starts_with("crate::")
117 || source.starts_with("super::")
118 || source.starts_with("self::")
119 {
120 let cleaned = source.replace("crate::", "").replace("self::", "");
121
122 let resolved = if source.starts_with("super::") {
123 let dir = Path::new(file_path).parent().and_then(|p| p.parent());
124 match dir {
125 Some(d) => {
126 let rest = source.trim_start_matches("super::");
127 d.join(rest.replace("::", "/"))
128 .to_string_lossy()
129 .to_string()
130 }
131 None => cleaned.replace("::", "/"),
132 }
133 } else {
134 cleaned.replace("::", "/")
135 };
136
137 if let Some(found) = try_rust_paths(&resolved, ctx) {
138 return (Some(found), false);
139 }
140 return (None, false);
141 }
142
143 let parts: Vec<&str> = source.split("::").collect();
144 if parts.is_empty() {
145 return (None, true);
146 }
147
148 let is_external = !source.starts_with("crate")
149 && !source.starts_with("super")
150 && !source.starts_with("self")
151 && !ctx.file_paths.iter().any(|f| {
152 let stem = Path::new(f)
153 .file_stem()
154 .and_then(|s| s.to_str())
155 .unwrap_or("");
156 stem == parts[0] || f.contains(&format!("/{}/", parts[0]))
157 });
158
159 if is_external {
160 return (None, true);
161 }
162
163 let as_path = source.replace("::", "/");
164 if let Some(found) = try_rust_paths(&as_path, ctx) {
165 return (Some(found), false);
166 }
167
168 (None, is_external)
169}
170
171fn try_rust_paths(base: &str, ctx: &ResolverContext) -> Option<String> {
172 let prefixes = ["", "src/", "rust/src/"];
173 for prefix in &prefixes {
174 let candidate = format!("{prefix}{base}.rs");
175 if ctx.file_exists(&candidate) {
176 return Some(candidate);
177 }
178 let mod_candidate = format!("{prefix}{base}/mod.rs");
179 if ctx.file_exists(&mod_candidate) {
180 return Some(mod_candidate);
181 }
182 }
183
184 let parts: Vec<&str> = base.rsplitn(2, '/').collect();
185 if parts.len() == 2 {
186 let parent = parts[1];
187 for prefix in &prefixes {
188 let candidate = format!("{prefix}{parent}.rs");
189 if ctx.file_exists(&candidate) {
190 return Some(candidate);
191 }
192 }
193 }
194
195 None
196}
197
198pub fn resolve_python(
203 imp: &ImportInfo,
204 file_path: &str,
205 ctx: &ResolverContext,
206) -> (Option<String>, bool) {
207 let source = &imp.source;
208
209 if source.starts_with('.') {
210 let dot_count = source.chars().take_while(|c| *c == '.').count();
211 let module_part = &source[dot_count..];
212
213 let mut dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
214 for _ in 1..dot_count {
215 dir = dir.parent().unwrap_or(Path::new(""));
216 }
217
218 let as_path = module_part.replace('.', "/");
219 let base = if as_path.is_empty() {
220 dir.to_string_lossy().to_string()
221 } else {
222 format!("{}/{as_path}", dir.display())
223 };
224
225 if let Some(found) = try_python_paths(&base, ctx) {
226 return (Some(found), false);
227 }
228 return (None, false);
229 }
230
231 let as_path = source.replace('.', "/");
232
233 if let Some(found) = try_python_paths(&as_path, ctx) {
234 return (Some(found), false);
235 }
236
237 let is_stdlib = is_python_stdlib(source);
238 (
239 None,
240 is_stdlib || !ctx.file_paths.iter().any(|f| f.contains(&as_path)),
241 )
242}
243
244fn try_python_paths(base: &str, ctx: &ResolverContext) -> Option<String> {
245 let py_file = format!("{base}.py");
246 if ctx.file_exists(&py_file) {
247 return Some(py_file);
248 }
249
250 let init_file = format!("{base}/__init__.py");
251 if ctx.file_exists(&init_file) {
252 return Some(init_file);
253 }
254
255 let prefixes = ["src/", "lib/"];
256 for prefix in &prefixes {
257 let candidate = format!("{prefix}{base}.py");
258 if ctx.file_exists(&candidate) {
259 return Some(candidate);
260 }
261 let init = format!("{prefix}{base}/__init__.py");
262 if ctx.file_exists(&init) {
263 return Some(init);
264 }
265 }
266
267 None
268}
269
270fn is_python_stdlib(module: &str) -> bool {
271 let first = module.split('.').next().unwrap_or(module);
272 matches!(
273 first,
274 "os" | "sys"
275 | "json"
276 | "re"
277 | "math"
278 | "datetime"
279 | "typing"
280 | "collections"
281 | "itertools"
282 | "functools"
283 | "pathlib"
284 | "io"
285 | "abc"
286 | "enum"
287 | "dataclasses"
288 | "logging"
289 | "unittest"
290 | "argparse"
291 | "subprocess"
292 | "threading"
293 | "multiprocessing"
294 | "socket"
295 | "http"
296 | "urllib"
297 | "hashlib"
298 | "hmac"
299 | "secrets"
300 | "time"
301 | "copy"
302 | "pprint"
303 | "textwrap"
304 | "shutil"
305 | "tempfile"
306 | "glob"
307 | "fnmatch"
308 | "contextlib"
309 | "inspect"
310 | "importlib"
311 | "pickle"
312 | "shelve"
313 | "csv"
314 | "configparser"
315 | "struct"
316 | "codecs"
317 | "string"
318 | "difflib"
319 | "ast"
320 | "dis"
321 | "traceback"
322 | "warnings"
323 | "concurrent"
324 | "asyncio"
325 | "signal"
326 | "select"
327 )
328}
329
330pub fn resolve_go(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
335 let source = &imp.source;
336
337 if let Some(ref go_mod) = ctx.go_module
338 && source.starts_with(go_mod.as_str())
339 {
340 let relative = source.strip_prefix(go_mod.as_str()).unwrap_or(source);
341 let relative = relative.trim_start_matches('/');
342
343 if let Some(found) = try_go_package(relative, ctx) {
344 return (Some(found), false);
345 }
346 return (None, false);
347 }
348
349 if let Some(found) = try_go_package(source, ctx) {
350 return (Some(found), false);
351 }
352
353 (None, true)
354}
355
356fn try_go_package(pkg_path: &str, ctx: &ResolverContext) -> Option<String> {
357 for file in &ctx.file_paths {
358 let p = Path::new(file.as_str());
359 if p.extension().and_then(|e| e.to_str()) != Some("go") {
360 continue;
361 }
362 if file.ends_with("_test.go") {
363 continue;
364 }
365 let dir = p.parent()?.to_string_lossy();
366 if dir == pkg_path || dir.ends_with(pkg_path) {
367 return Some(file.clone());
368 }
369 }
370 None
371}
372
373pub fn resolve_java(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
378 let source = &imp.source;
379
380 if source.starts_with("java.") || source.starts_with("javax.") || source.starts_with("sun.") {
381 return (None, true);
382 }
383
384 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
385 if parts.len() < 2 {
386 return (None, true);
387 }
388
389 let class_name = parts[0];
390 let package_path = parts[1].replace('.', "/");
391 let file_path = format!("{package_path}/{class_name}.java");
392
393 let search_roots = ["", "src/main/java/", "src/", "app/src/main/java/"];
394 for root in &search_roots {
395 let candidate = format!("{root}{file_path}");
396 if ctx.file_exists(&candidate) {
397 return (Some(candidate), false);
398 }
399 }
400
401 (
402 None,
403 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
404 )
405}
406
407pub fn resolve_kotlin(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
412 let source = &imp.source;
413
414 if source.starts_with("java.")
415 || source.starts_with("javax.")
416 || source.starts_with("kotlin.")
417 || source.starts_with("kotlinx.")
418 || source.starts_with("android.")
419 || source.starts_with("androidx.")
420 || source.starts_with("org.junit.")
421 || source.starts_with("org.jetbrains.")
422 {
423 return (None, true);
424 }
425
426 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
427 if parts.len() < 2 {
428 return (None, true);
429 }
430
431 let class_name = parts[0];
432 let package_path = parts[1].replace('.', "/");
433
434 let search_roots = [
435 "",
436 "src/main/kotlin/",
437 "src/main/java/",
438 "src/",
439 "app/src/main/kotlin/",
440 "app/src/main/java/",
441 "src/commonMain/kotlin/",
442 ];
443
444 for root in &search_roots {
445 for ext in &["kt", "kts", "java"] {
446 let candidate = format!("{root}{package_path}/{class_name}.{ext}");
447 if ctx.file_exists(&candidate) {
448 return (Some(candidate), false);
449 }
450 }
451 }
452
453 (
454 None,
455 !ctx.file_paths.iter().any(|f| f.contains(&package_path)),
456 )
457}
458
459pub fn resolve_c_like(
464 imp: &ImportInfo,
465 file_path: &str,
466 ctx: &ResolverContext,
467) -> (Option<String>, bool) {
468 let source = imp.source.trim();
469 if source.is_empty() {
470 return (None, true);
471 }
472
473 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
474 let rel = rel.trim_start_matches("./").trim_start_matches('/');
475 let mut candidates: Vec<String> = vec![rel.to_string()];
476 for ext in [".h", ".hpp", ".c", ".cpp"] {
477 if !rel.ends_with(ext) {
478 candidates.push(format!("{rel}{ext}"));
479 }
480 }
481 for prefix in prefixes {
482 for c in &candidates {
483 let p = format!("{prefix}{c}");
484 if ctx.file_exists(&p) {
485 return Some(p);
486 }
487 }
488 }
489 None
490 };
491
492 if source.starts_with('.') {
493 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
494 let dir_prefix = if dir.as_os_str().is_empty() {
495 String::new()
496 } else {
497 format!("{}/", dir.to_string_lossy())
498 };
499 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
500 return (Some(found), false);
501 }
502 return (None, false);
503 }
504
505 if ctx.file_exists(source) {
506 return (Some(source.to_string()), false);
507 }
508
509 if let Some(found) = try_prefixes(&["", "include/", "src/"], source) {
510 return (Some(found), false);
511 }
512
513 (None, true)
514}
515
516pub fn resolve_ruby(
521 imp: &ImportInfo,
522 file_path: &str,
523 ctx: &ResolverContext,
524) -> (Option<String>, bool) {
525 let source = imp.source.trim();
526 if source.is_empty() {
527 return (None, true);
528 }
529 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
530
531 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
532 let mut candidates: Vec<String> = vec![source_rel.to_string()];
533 if !Path::new(source_rel)
534 .extension()
535 .is_some_and(|e| e.eq_ignore_ascii_case("rb"))
536 {
537 candidates.push(format!("{source_rel}.rb"));
538 }
539 for prefix in prefixes {
540 for c in &candidates {
541 let p = format!("{prefix}{c}");
542 if ctx.file_exists(&p) {
543 return Some(p);
544 }
545 }
546 }
547 None
548 };
549
550 if source.starts_with('.') || source_rel.contains('/') {
551 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
552 let dir_prefix = if dir.as_os_str().is_empty() {
553 String::new()
554 } else {
555 format!("{}/", dir.to_string_lossy())
556 };
557 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
558 return (Some(found), false);
559 }
560 if let Some(found) = try_prefixes(&["", "lib/", "src/"]) {
561 return (Some(found), false);
562 }
563 return (None, false);
564 }
565
566 (None, true)
567}
568
569pub fn resolve_php(
574 imp: &ImportInfo,
575 file_path: &str,
576 ctx: &ResolverContext,
577) -> (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 !Path::new(source_rel)
590 .extension()
591 .is_some_and(|e| e.eq_ignore_ascii_case("php"))
592 {
593 candidates.push(format!("{source_rel}.php"));
594 }
595 for prefix in prefixes {
596 for c in &candidates {
597 let p = format!("{prefix}{c}");
598 if ctx.file_exists(&p) {
599 return Some(p);
600 }
601 }
602 }
603 None
604 };
605
606 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
607 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
608 let dir_prefix = if dir.as_os_str().is_empty() {
609 String::new()
610 } else {
611 format!("{}/", dir.to_string_lossy())
612 };
613 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
614 return (Some(found), false);
615 }
616 if let Some(found) = try_prefixes(&["", "src/", "lib/"]) {
617 return (Some(found), false);
618 }
619 return (None, false);
620 }
621
622 (None, true)
623}
624
625pub fn resolve_bash(
630 imp: &ImportInfo,
631 file_path: &str,
632 ctx: &ResolverContext,
633) -> (Option<String>, bool) {
634 let source = imp.source.trim();
635 if source.is_empty() {
636 return (None, true);
637 }
638 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
639
640 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
641 let mut candidates: Vec<String> = vec![source_rel.to_string()];
642 if !Path::new(source_rel)
643 .extension()
644 .is_some_and(|e| e.eq_ignore_ascii_case("sh"))
645 {
646 candidates.push(format!("{source_rel}.sh"));
647 }
648 for prefix in prefixes {
649 for c in &candidates {
650 let p = format!("{prefix}{c}");
651 if ctx.file_exists(&p) {
652 return Some(p);
653 }
654 }
655 }
656 None
657 };
658
659 if source.starts_with('.') || source.starts_with('/') || source_rel.contains('/') {
660 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
661 let dir_prefix = if dir.as_os_str().is_empty() {
662 String::new()
663 } else {
664 format!("{}/", dir.to_string_lossy())
665 };
666 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
667 return (Some(found), false);
668 }
669 if let Some(found) = try_prefixes(&["", "scripts/", "bin/"]) {
670 return (Some(found), false);
671 }
672 return (None, false);
673 }
674
675 (None, true)
676}
677
678pub fn resolve_dart(
683 imp: &ImportInfo,
684 file_path: &str,
685 ctx: &ResolverContext,
686) -> (Option<String>, bool) {
687 let source = imp.source.trim();
688 if source.is_empty() {
689 return (None, true);
690 }
691 if source.starts_with("dart:") {
692 return (None, true);
693 }
694
695 let try_prefixes = |prefixes: &[&str], rel: &str| -> Option<String> {
696 let rel = rel.trim_start_matches("./").trim_start_matches('/').trim();
697 let mut candidates: Vec<String> = vec![rel.to_string()];
698 if !Path::new(rel)
699 .extension()
700 .is_some_and(|e| e.eq_ignore_ascii_case("dart"))
701 {
702 candidates.push(format!("{rel}.dart"));
703 }
704 for prefix in prefixes {
705 for c in &candidates {
706 let p = format!("{prefix}{c}");
707 if ctx.file_exists(&p) {
708 return Some(p);
709 }
710 }
711 }
712 None
713 };
714
715 if source.starts_with("package:") {
716 if let Some(pkg) = ctx.dart_package.as_deref() {
717 let prefix = format!("package:{pkg}/");
718 if let Some(rest) = source.strip_prefix(&prefix) {
719 if let Some(found) = try_prefixes(&["lib/", ""], rest) {
720 return (Some(found), false);
721 }
722 return (None, false);
723 }
724 }
725 return (None, true);
726 }
727
728 if source.starts_with('.') || source.starts_with('/') {
729 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
730 let dir_prefix = if dir.as_os_str().is_empty() {
731 String::new()
732 } else {
733 format!("{}/", dir.to_string_lossy())
734 };
735 if let Some(found) = try_prefixes(&[dir_prefix.as_str()], source) {
736 return (Some(found), false);
737 }
738 if let Some(found) = try_prefixes(&["", "lib/"], source) {
739 return (Some(found), false);
740 }
741 return (None, false);
742 }
743
744 (None, true)
745}
746
747pub fn resolve_gd(
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
764 let try_paths = |rel: &str| -> Option<String> {
768 let rel = rel.trim();
769 if ctx.file_exists(rel) {
770 return Some(rel.to_string());
771 }
772 if Path::new(rel).extension().is_none() {
773 for ext in ["gd", "tscn", "tres"] {
774 let candidate = format!("{rel}.{ext}");
775 if ctx.file_exists(&candidate) {
776 return Some(candidate);
777 }
778 }
779 }
780 None
781 };
782
783 if let Some(rest) = source.strip_prefix("res://") {
784 let rel = rest.trim_start_matches('/');
785 if let Some(found) = try_paths(rel) {
786 return (Some(found), false);
787 }
788 if !rel.is_empty() && Path::new(rel).extension().is_some() {
793 return (Some(rel.to_string()), false);
794 }
795 return (None, false);
796 }
797
798 if source.starts_with("user://") {
800 return (None, true);
801 }
802
803 if source.starts_with('.') || source.contains('/') {
804 let stripped = source.trim_start_matches("./");
805 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
806 let joined = if dir.as_os_str().is_empty() {
807 stripped.to_string()
808 } else {
809 format!("{}/{stripped}", dir.to_string_lossy())
810 };
811 if let Some(found) = try_paths(&joined) {
812 return (Some(found), false);
813 }
814 return (try_paths(stripped), false);
815 }
816
817 (None, true)
818}
819
820pub fn resolve_lua(
830 imp: &ImportInfo,
831 file_path: &str,
832 ctx: &ResolverContext,
833) -> (Option<String>, bool) {
834 let source = imp.source.trim();
835 if source.is_empty() {
836 return (None, true);
837 }
838
839 let is_pathy = source.contains('/') || source.starts_with('.');
842 let rel = if is_pathy {
843 source
844 .trim_start_matches("./")
845 .trim_start_matches('/')
846 .to_string()
847 } else {
848 source.replace('.', "/")
849 };
850 if rel.is_empty() {
851 return (None, true);
852 }
853
854 let try_paths = |base: &str| -> Option<String> {
855 let base = base.trim_start_matches('/');
856 if ctx.file_exists(base) {
857 return Some(base.to_string());
858 }
859 for ext in ["lua", "luau"] {
860 let file = format!("{base}.{ext}");
861 if ctx.file_exists(&file) {
862 return Some(file);
863 }
864 let init = format!("{base}/init.{ext}");
865 if ctx.file_exists(&init) {
866 return Some(init);
867 }
868 }
869 None
870 };
871
872 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
873 let relative_to_importer = || -> Option<String> {
874 if dir.as_os_str().is_empty() {
875 return None;
876 }
877 try_paths(&format!("{}/{rel}", dir.to_string_lossy()))
878 };
879 let from_roots = || -> Option<String> {
881 try_paths(&rel).or_else(|| {
882 ["src/", "lua/", "lib/"]
883 .iter()
884 .find_map(|prefix| try_paths(&format!("{prefix}{rel}")))
885 })
886 };
887
888 let found = if is_pathy {
891 relative_to_importer().or_else(from_roots)
892 } else {
893 from_roots().or_else(relative_to_importer)
894 };
895
896 match found {
897 Some(path) => (Some(path), false),
898 None => (None, true),
899 }
900}
901
902pub fn resolve_zig(
907 imp: &ImportInfo,
908 file_path: &str,
909 ctx: &ResolverContext,
910) -> (Option<String>, bool) {
911 let source = imp.source.trim();
912 if source.is_empty() {
913 return (None, true);
914 }
915 let source_rel = source.trim_start_matches("./").trim_start_matches('/');
916 if source_rel == "std" {
917 return (None, true);
918 }
919
920 let try_prefixes = |prefixes: &[&str]| -> Option<String> {
921 let mut candidates: Vec<String> = vec![source_rel.to_string()];
922 if !Path::new(source_rel)
923 .extension()
924 .is_some_and(|e| e.eq_ignore_ascii_case("zig"))
925 {
926 candidates.push(format!("{source_rel}.zig"));
927 }
928 for prefix in prefixes {
929 for c in &candidates {
930 let p = format!("{prefix}{c}");
931 if ctx.file_exists(&p) {
932 return Some(p);
933 }
934 }
935 }
936 None
937 };
938
939 if source.starts_with('.')
940 || source_rel.contains('/')
941 || Path::new(source_rel)
942 .extension()
943 .is_some_and(|e| e.eq_ignore_ascii_case("zig"))
944 {
945 let dir = Path::new(file_path).parent().unwrap_or(Path::new(""));
946 let dir_prefix = if dir.as_os_str().is_empty() {
947 String::new()
948 } else {
949 format!("{}/", dir.to_string_lossy())
950 };
951 if let Some(found) = try_prefixes(&[dir_prefix.as_str()]) {
952 return (Some(found), false);
953 }
954 if let Some(found) = try_prefixes(&["", "src/"]) {
955 return (Some(found), false);
956 }
957 return (None, false);
958 }
959
960 (None, true)
961}
962
963pub fn resolve_csharp(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
968 let ns = imp.source.trim();
969 if ns.is_empty() {
970 return (None, true);
971 }
972
973 if is_csharp_external_namespace(ns) {
977 return (None, true);
978 }
979
980 let segs: Vec<&str> = ns.split('.').filter(|s| !s.is_empty()).collect();
981 if segs.is_empty() {
982 return (None, true);
983 }
984
985 if let Some(file) = probe_csharp_namespace(&segs, ctx) {
989 return (Some(file), false);
990 }
991
992 if segs.len() >= 2
995 && let Some(file) = probe_csharp_namespace(&segs[..segs.len() - 1], ctx)
996 {
997 return (Some(file), false);
998 }
999
1000 (None, true)
1003}
1004
1005fn probe_csharp_namespace(segs: &[&str], ctx: &ResolverContext) -> Option<String> {
1010 (0..segs.len())
1011 .map(|start| segs[start..].join("/"))
1012 .find_map(|key| ctx.csharp_namespace_file(&key).map(str::to_string))
1013}
1014
1015fn is_csharp_external_namespace(ns: &str) -> bool {
1018 const EXTERNAL_ROOTS: &[&str] = &[
1019 "System",
1020 "Microsoft",
1021 "Windows",
1022 "Mono",
1023 "Newtonsoft",
1024 "NUnit",
1025 "Xunit",
1026 "Moq",
1027 "Serilog",
1028 "AutoMapper",
1029 "MediatR",
1030 "FluentValidation",
1031 "Polly",
1032 "Castle",
1033 "Dapper",
1034 "RestSharp",
1035 "Google",
1036 "Amazon",
1037 "Azure",
1038 "Grpc",
1039 "Nito",
1040 "StackExchange",
1041 ];
1042 let root = ns.split('.').next().unwrap_or(ns);
1043 EXTERNAL_ROOTS.contains(&root)
1044}
1045
1046pub fn resolve_swift(
1051 imp: &ImportInfo,
1052 file_path: &str,
1053 ctx: &ResolverContext,
1054) -> (Option<String>, bool) {
1055 let source = &imp.source;
1056
1057 if matches!(
1058 source.as_str(),
1059 "Foundation" | "UIKit" | "SwiftUI" | "Combine" | "CoreData" | "Darwin"
1060 ) {
1061 return (None, true);
1062 }
1063
1064 let dir = Path::new(file_path)
1065 .parent()
1066 .map(|p| p.to_string_lossy().to_string())
1067 .unwrap_or_default();
1068
1069 let candidate = if dir.is_empty() {
1070 format!("{source}.swift")
1071 } else {
1072 format!("{dir}/{source}.swift")
1073 };
1074 if ctx.file_exists(&candidate) {
1075 return (Some(candidate), false);
1076 }
1077 (None, false)
1078}
1079
1080pub fn resolve_scala(imp: &ImportInfo, ctx: &ResolverContext) -> (Option<String>, bool) {
1085 let source = &imp.source;
1086
1087 if source.starts_with("scala.") || source.starts_with("java.") {
1088 return (None, true);
1089 }
1090
1091 let parts: Vec<&str> = source.rsplitn(2, '.').collect();
1092 if parts.len() < 2 {
1093 return (None, true);
1094 }
1095
1096 let name = parts[0];
1097 let package_path = parts[1].replace('.', "/");
1098
1099 for ext in ["scala", "sc"] {
1100 let candidate = format!("{package_path}/{name}.{ext}");
1101 if ctx.file_exists(&candidate) {
1102 return (Some(candidate), false);
1103 }
1104 }
1105 (None, false)
1106}
1107
1108pub fn resolve_elixir(
1113 imp: &ImportInfo,
1114 file_path: &str,
1115 ctx: &ResolverContext,
1116) -> (Option<String>, bool) {
1117 let source = &imp.source;
1118
1119 if source.starts_with("Kernel") || source.starts_with("Enum") || source.starts_with("IO") {
1120 return (None, true);
1121 }
1122
1123 let snake =
1124 source
1125 .replace('.', "/")
1126 .chars()
1127 .enumerate()
1128 .fold(String::new(), |mut acc, (i, c)| {
1129 if c.is_uppercase() && i > 0 && !acc.ends_with('/') {
1130 acc.push('_');
1131 }
1132 acc.push(c.to_ascii_lowercase());
1133 acc
1134 });
1135
1136 let dir = Path::new(file_path)
1137 .parent()
1138 .map(|p| p.to_string_lossy().to_string())
1139 .unwrap_or_default();
1140
1141 for ext in ["ex", "exs"] {
1142 let candidate = format!("lib/{snake}.{ext}");
1143 if ctx.file_exists(&candidate) {
1144 return (Some(candidate), false);
1145 }
1146 if !dir.is_empty() {
1147 let local = format!("{dir}/{snake}.{ext}");
1148 if ctx.file_exists(&local) {
1149 return (Some(local), false);
1150 }
1151 }
1152 }
1153 (None, false)
1154}