1use std::path::Path;
9
10use super::{FileIndex, LanguageResolver};
11use crate::analysis::parser::ImportStatement;
12use crate::analysis::walker::Language;
13
14pub struct RustResolver;
16
17impl LanguageResolver for RustResolver {
18 fn resolve(
19 &self,
20 import: &ImportStatement,
21 importing_file: &str,
22 file_index: &FileIndex,
23 ) -> Option<String> {
24 resolve_rust(&import.path, importing_file, file_index)
25 }
26
27 fn language(&self) -> Language {
28 Language::Rust
29 }
30
31 fn name(&self) -> &'static str {
32 "rust"
33 }
34}
35
36pub fn resolve_cross_crate(import_path: &str, file_index: &FileIndex) -> Option<String> {
44 let clean = import_path
45 .split(" as ")
46 .next()
47 .unwrap_or(import_path)
48 .trim()
49 .trim_end_matches("::*");
50 let first_seg = clean.split("::").next()?;
51 if first_seg.is_empty() {
52 return None;
53 }
54 let member_root = file_index.workspace_member_root(first_seg)?;
55 let lib = format!("{member_root}lib.rs");
56 if file_index.contains(&lib) {
57 return Some(lib);
58 }
59 let mod_rs = format!("{member_root}mod.rs");
60 if file_index.contains(&mod_rs) {
61 return Some(mod_rs);
62 }
63 None
64}
65
66fn resolve_rust(import_path: &str, importing_file: &str, file_index: &FileIndex) -> Option<String> {
68 let crate_root = file_index.crate_root_for(importing_file).unwrap_or("src/");
71
72 let clean = import_path
74 .split(" as ")
75 .next()
76 .unwrap_or(import_path)
77 .trim()
78 .trim_end_matches("::*");
79
80 let current_module = rust_module_segments(importing_file, crate_root)?;
81
82 let segments = if clean == "crate" {
85 return None;
87 } else if clean == "self" {
88 current_module.clone()
90 } else if clean == "super" {
91 if current_module.is_empty() {
93 return None;
94 }
95 current_module[..current_module.len() - 1].to_vec()
96 } else if let Some(path) = clean.strip_prefix("crate::") {
97 parse_rust_segments(path)
98 } else if let Some(path) = clean.strip_prefix("self::") {
99 current_module
100 .iter()
101 .cloned()
102 .chain(parse_rust_segments(path))
103 .collect()
104 } else if clean.starts_with("super::") {
105 let mut remaining = clean;
106 let mut up = 0usize;
107 while let Some(rest) = remaining.strip_prefix("super::") {
108 remaining = rest;
109 up += 1;
110 }
111 if up > current_module.len() {
112 return None;
113 }
114 current_module[..current_module.len() - up]
115 .iter()
116 .cloned()
117 .chain(parse_rust_segments(remaining))
118 .collect()
119 } else {
120 return None;
121 };
122
123 if segments.is_empty() {
124 return None;
125 }
126
127 let mut depth = segments.len();
133 while depth > 0 {
134 let fs_path = format!("{crate_root}{}", segments[..depth].join("/"));
135
136 let direct = format!("{fs_path}.rs");
138 if file_index.contains(&direct) {
139 return Some(direct);
140 }
141
142 let mod_rs = format!("{fs_path}/mod.rs");
144 if file_index.contains(&mod_rs) {
145 return Some(mod_rs);
146 }
147
148 depth -= 1;
149 }
150
151 None
152}
153
154fn parse_rust_segments(path: &str) -> Vec<String> {
155 path.split("::")
156 .map(str::trim)
157 .filter(|segment| !segment.is_empty() && *segment != "self")
158 .map(|segment| segment.to_string())
159 .collect()
160}
161
162fn rust_module_segments(importing_file: &str, crate_root: &str) -> Option<Vec<String>> {
163 let rel = importing_file.strip_prefix(crate_root)?;
164
165 if rel == "lib.rs" || rel == "main.rs" {
166 return Some(Vec::new());
167 }
168
169 if let Some(parent) = rel.strip_suffix("/mod.rs") {
170 return Some(
171 parent
172 .split('/')
173 .filter(|segment| !segment.is_empty())
174 .map(|segment| segment.to_string())
175 .collect(),
176 );
177 }
178
179 let path = Path::new(rel);
180 let stem = path.file_stem()?.to_str()?;
181 let mut segments: Vec<String> = path
182 .parent()
183 .into_iter()
184 .flat_map(|parent| parent.iter())
185 .filter_map(|segment| segment.to_str())
186 .filter(|segment| !segment.is_empty())
187 .map(|segment| segment.to_string())
188 .collect();
189 segments.push(stem.to_string());
190 Some(segments)
191}
192
193#[cfg(test)]
196mod tests {
197 use super::*;
198 use crate::analysis::parser::import::ImportKind;
199
200 fn idx(paths: &[&str]) -> FileIndex {
201 FileIndex::new(paths.iter().map(|s| s.to_string()))
202 }
203
204 fn import(path: &str) -> ImportStatement {
205 ImportStatement::new(path, ImportKind::Normal, 1)
206 }
207
208 #[test]
209 fn crate_import_resolves_to_file() {
210 let file_index = idx(&["src/lib.rs", "src/utils.rs"]);
211 let resolver = RustResolver;
212 let result = resolver.resolve(&import("crate::utils"), "src/lib.rs", &file_index);
213 assert_eq!(result, Some("src/utils.rs".into()));
214 }
215
216 #[test]
217 fn crate_import_resolves_to_mod_rs() {
218 let file_index = idx(&["src/lib.rs", "src/store/mod.rs"]);
219 let resolver = RustResolver;
220 let result = resolver.resolve(&import("crate::store"), "src/lib.rs", &file_index);
221 assert_eq!(result, Some("src/store/mod.rs".into()));
222 }
223
224 #[test]
225 fn self_import_resolves() {
226 let file_index = idx(&["src/store/mod.rs", "src/store/helpers.rs"]);
227 let resolver = RustResolver;
228 let result = resolver.resolve(&import("self::helpers"), "src/store/mod.rs", &file_index);
229 assert_eq!(result, Some("src/store/helpers.rs".into()));
230 }
231
232 #[test]
233 fn super_import_resolves() {
234 let file_index = idx(&["src/store/db.rs", "src/store/helpers.rs"]);
235 let resolver = RustResolver;
236 let result = resolver.resolve(&import("super::helpers"), "src/store/db.rs", &file_index);
237 assert_eq!(result, Some("src/store/helpers.rs".into()));
238 }
239
240 #[test]
241 fn nested_crate_import() {
242 let file_index = idx(&["src/main.rs", "src/store/db.rs"]);
243 let resolver = RustResolver;
244 let result = resolver.resolve(&import("crate::store::db"), "src/main.rs", &file_index);
245 assert_eq!(result, Some("src/store/db.rs".into()));
246 }
247
248 #[test]
249 fn unresolvable_returns_none() {
250 let file_index = idx(&["src/lib.rs"]);
251 let resolver = RustResolver;
252 let result = resolver.resolve(&import("crate::nonexistent"), "src/lib.rs", &file_index);
253 assert_eq!(result, None);
254 }
255
256 #[test]
257 fn wildcard_stripped_before_resolution() {
258 let file_index = idx(&["src/lib.rs", "src/prelude.rs"]);
259 let resolver = RustResolver;
260 let imp = ImportStatement::new("crate::prelude::*", ImportKind::Wildcard, 1);
261 let result = resolver.resolve(&imp, "src/lib.rs", &file_index);
262 assert_eq!(result, Some("src/prelude.rs".into()));
263 }
264
265 #[test]
266 fn alias_stripped_before_resolution() {
267 let file_index = idx(&["src/lib.rs", "src/utils.rs"]);
268 let resolver = RustResolver;
269 let imp = ImportStatement::new("crate::utils as u", ImportKind::Normal, 1);
270 let result = resolver.resolve(&imp, "src/lib.rs", &file_index);
271 assert_eq!(result, Some("src/utils.rs".into()));
272 }
273
274 #[test]
277 fn crate_import_with_trailing_symbol_resolves_to_file() {
278 let file_index = idx(&["src/lib.rs", "src/store/record.rs"]);
280 let result = resolve_rust(
281 "crate::store::record::FileRecord",
282 "src/lib.rs",
283 &file_index,
284 );
285 assert_eq!(result, Some("src/store/record.rs".into()));
286 }
287
288 #[test]
289 fn crate_import_with_trailing_symbol_resolves_to_mod_rs() {
290 let file_index = idx(&["src/lib.rs", "src/analysis/parser/mod.rs"]);
292 let result = resolve_rust(
293 "crate::analysis::parser::Language",
294 "src/lib.rs",
295 &file_index,
296 );
297 assert_eq!(result, Some("src/analysis/parser/mod.rs".into()));
298 }
299
300 #[test]
301 fn crate_import_deep_symbol_chain_strips_multiple() {
302 let file_index = idx(&["src/lib.rs", "src/error.rs"]);
304 let result = resolve_rust(
305 "crate::error::MatiError::NotFound",
306 "src/lib.rs",
307 &file_index,
308 );
309 assert_eq!(result, Some("src/error.rs".into()));
310 }
311
312 #[test]
313 fn brace_group_import_resolves_to_parent_module() {
314 let file_index = idx(&["src/lib.rs", "src/store/mod.rs"]);
317 let result = resolve_rust(
318 "crate::store::{FileRecord, GotchaRecord}",
319 "src/lib.rs",
320 &file_index,
321 );
322 assert_eq!(result, Some("src/store/mod.rs".into()));
323 }
324
325 #[test]
326 fn super_import_with_trailing_symbol() {
327 let file_index = idx(&["src/cli/review.rs", "src/cli/helpers.rs"]);
329 let result = resolve_rust(
330 "super::helpers::format_score",
331 "src/cli/review.rs",
332 &file_index,
333 );
334 assert_eq!(result, Some("src/cli/helpers.rs".into()));
335 }
336
337 #[test]
338 fn self_import_with_trailing_symbol() {
339 let file_index = idx(&["src/store/mod.rs", "src/store/types.rs"]);
341 let result = resolve_rust("self::types::MyType", "src/store/mod.rs", &file_index);
342 assert_eq!(result, Some("src/store/types.rs".into()));
343 }
344
345 #[test]
346 fn crate_direct_module_still_resolves() {
347 let file_index = idx(&["src/lib.rs", "src/util.rs"]);
349 let result = resolve_rust("crate::util", "src/lib.rs", &file_index);
350 assert_eq!(result, Some("src/util.rs".into()));
351 }
352
353 #[test]
354 fn crate_direct_module_prefers_file_over_mod_rs() {
355 let file_index = idx(&["src/lib.rs", "src/util.rs", "src/util/mod.rs"]);
357 let result = resolve_rust("crate::util", "src/lib.rs", &file_index);
358 assert_eq!(result, Some("src/util.rs".into()));
359 }
360
361 #[test]
362 fn crate_direct_module_falls_back_to_mod_rs() {
363 let file_index = idx(&["src/lib.rs", "src/util/mod.rs"]);
365 let result = resolve_rust("crate::util", "src/lib.rs", &file_index);
366 assert_eq!(result, Some("src/util/mod.rs".into()));
367 }
368
369 #[test]
370 fn nonexistent_path_returns_none() {
371 let file_index = idx(&["src/lib.rs"]);
372 let result = resolve_rust("crate::nonexistent::thing", "src/lib.rs", &file_index);
373 assert_eq!(result, None);
374 }
375
376 #[test]
377 fn crate_root_alone_returns_none() {
378 let file_index = idx(&["src/lib.rs", "src/mod.rs"]);
380 let result = resolve_rust("crate::", "src/lib.rs", &file_index);
381 assert_eq!(result, None);
382 }
383
384 #[test]
385 fn prefix_stripping_stops_before_crate_root() {
386 let file_index = idx(&["src/lib.rs", "src.rs"]);
388 let result = resolve_rust("crate::x::y::z", "src/lib.rs", &file_index);
389 assert_eq!(result, None);
390 }
391
392 #[test]
393 fn existing_exact_match_preferred_over_stripped() {
394 let file_index = idx(&["src/lib.rs", "src/store.rs", "src/store/record.rs"]);
397 let result = resolve_rust("crate::store::record", "src/lib.rs", &file_index);
398 assert_eq!(result, Some("src/store/record.rs".into()));
399 }
400
401 #[test]
404 fn super_wildcard_resolves_to_parent_module() {
405 let file_index = idx(&["src/cli/review.rs", "src/cli/mod.rs"]);
407 let imp = ImportStatement::new("super::*", ImportKind::Wildcard, 1);
408 let result = RustResolver.resolve(&imp, "src/cli/review.rs", &file_index);
409 assert_eq!(result, Some("src/cli/mod.rs".into()));
410 }
411
412 #[test]
413 fn self_wildcard_resolves_to_current_module() {
414 let file_index = idx(&["src/store/mod.rs", "src/store/db.rs"]);
416 let imp = ImportStatement::new("self::*", ImportKind::Wildcard, 1);
417 let result = RustResolver.resolve(&imp, "src/store/mod.rs", &file_index);
418 assert_eq!(result, Some("src/store/mod.rs".into()));
419 }
420
421 #[test]
422 fn crate_wildcard_returns_none() {
423 let file_index = idx(&["src/lib.rs", "src/main.rs"]);
425 let imp = ImportStatement::new("crate::*", ImportKind::Wildcard, 1);
426 let result = RustResolver.resolve(&imp, "src/lib.rs", &file_index);
427 assert_eq!(result, None);
428 }
429
430 fn idx_with_roots(paths: &[&str], roots: Vec<&str>) -> FileIndex {
433 let mut fi = FileIndex::new(paths.iter().map(|s| s.to_string()));
434 fi.set_crate_roots(roots.into_iter().map(|s| s.to_string()).collect());
435 fi
436 }
437
438 #[test]
439 fn workspace_member_resolves_within_own_crate() {
440 let file_index = idx_with_roots(
441 &[
442 "crates/foo/src/lib.rs",
443 "crates/foo/src/helper.rs",
444 "crates/bar/src/lib.rs",
445 ],
446 vec!["crates/foo/src/", "crates/bar/src/"],
447 );
448 let result = resolve_rust("crate::helper", "crates/foo/src/lib.rs", &file_index);
449 assert_eq!(result, Some("crates/foo/src/helper.rs".into()));
450 }
451
452 #[test]
453 fn workspace_member_does_not_cross_crate_boundaries() {
454 let file_index = idx_with_roots(
455 &[
456 "crates/foo/src/lib.rs",
457 "crates/bar/src/lib.rs",
458 "crates/bar/src/util.rs",
459 ],
460 vec!["crates/foo/src/", "crates/bar/src/"],
461 );
462 let result = resolve_rust("crate::util", "crates/foo/src/lib.rs", &file_index);
465 assert_eq!(result, None);
466 }
467
468 #[test]
469 fn single_crate_project_still_works_with_explicit_root() {
470 let file_index = idx_with_roots(&["src/lib.rs", "src/utils.rs"], vec!["src/"]);
471 let result = resolve_rust("crate::utils", "src/lib.rs", &file_index);
472 assert_eq!(result, Some("src/utils.rs".into()));
473 }
474
475 #[test]
476 fn workspace_super_import_resolves() {
477 let file_index = idx_with_roots(
478 &[
479 "crates/searcher/src/searcher/core.rs",
480 "crates/searcher/src/searcher/mod.rs",
481 ],
482 vec!["crates/searcher/src/"],
483 );
484 let result = resolve_rust(
485 "super::core",
486 "crates/searcher/src/searcher/mod.rs",
487 &file_index,
488 );
489 assert_eq!(result, None); }
496
497 #[test]
498 fn workspace_self_import_resolves() {
499 let file_index = idx_with_roots(
500 &[
501 "crates/searcher/src/searcher/mod.rs",
502 "crates/searcher/src/searcher/glue.rs",
503 ],
504 vec!["crates/searcher/src/"],
505 );
506 let result = resolve_rust(
507 "self::glue",
508 "crates/searcher/src/searcher/mod.rs",
509 &file_index,
510 );
511 assert_eq!(result, Some("crates/searcher/src/searcher/glue.rs".into()));
512 }
513
514 #[test]
515 fn workspace_nested_crate_import() {
516 let file_index = idx_with_roots(
517 &[
518 "crates/printer/src/lib.rs",
519 "crates/printer/src/hyperlink/mod.rs",
520 ],
521 vec!["crates/printer/src/"],
522 );
523 let result = resolve_rust("crate::hyperlink", "crates/printer/src/lib.rs", &file_index);
524 assert_eq!(result, Some("crates/printer/src/hyperlink/mod.rs".into()));
525 }
526
527 #[test]
528 fn fallback_to_src_when_no_crate_roots_set() {
529 let file_index =
531 FileIndex::new(["src/lib.rs", "src/store.rs"].iter().map(|s| s.to_string()));
532 let result = resolve_rust("crate::store", "src/lib.rs", &file_index);
533 assert_eq!(result, Some("src/store.rs".into()));
534 }
535
536 fn idx_with_members(paths: &[&str], members: &[(&str, &str)]) -> FileIndex {
539 let mut fi = FileIndex::new(paths.iter().map(|s| s.to_string()));
540 let map: std::collections::HashMap<String, String> = members
541 .iter()
542 .map(|(name, root)| (name.to_string(), root.to_string()))
543 .collect();
544 fi.set_workspace_members(map);
545 fi
546 }
547
548 #[test]
549 fn cross_crate_import_resolves_to_lib_rs() {
550 let fi = idx_with_members(
551 &["crates/regex/src/lib.rs", "crates/searcher/src/searcher.rs"],
552 &[("grep_regex", "crates/regex/src/")],
553 );
554 let result = resolve_cross_crate("grep_regex::RegexMatcher", &fi);
555 assert_eq!(result, Some("crates/regex/src/lib.rs".into()));
556 }
557
558 #[test]
559 fn cross_crate_import_with_deep_path() {
560 let fi = idx_with_members(
561 &["crates/regex/src/lib.rs"],
562 &[("grep_regex", "crates/regex/src/")],
563 );
564 let result = resolve_cross_crate("grep_regex::matcher::Foo::Bar", &fi);
565 assert_eq!(result, Some("crates/regex/src/lib.rs".into()));
566 }
567
568 #[test]
569 fn cross_crate_falls_back_to_mod_rs() {
570 let fi = idx_with_members(
571 &["crates/regex/src/mod.rs"],
572 &[("grep_regex", "crates/regex/src/")],
573 );
574 let result = resolve_cross_crate("grep_regex::Foo", &fi);
575 assert_eq!(result, Some("crates/regex/src/mod.rs".into()));
576 }
577
578 #[test]
579 fn cross_crate_unknown_member_returns_none() {
580 let fi = idx_with_members(
581 &["crates/regex/src/lib.rs"],
582 &[("grep_regex", "crates/regex/src/")],
583 );
584 let result = resolve_cross_crate("serde::Deserialize", &fi);
586 assert_eq!(result, None);
587 }
588
589 #[test]
590 fn kebab_case_crate_name_normalized_to_snake_case() {
591 let fi = idx_with_members(
594 &["crates/regex/src/lib.rs"],
595 &[("grep_regex", "crates/regex/src/")],
596 );
597 let result = resolve_cross_crate("grep_regex::Foo", &fi);
598 assert_eq!(result, Some("crates/regex/src/lib.rs".into()));
599 }
600
601 #[test]
602 fn intra_crate_resolution_still_works_after_cross_crate() {
603 let mut fi = idx_with_members(
605 &[
606 "crates/foo/src/lib.rs",
607 "crates/foo/src/helper.rs",
608 "crates/bar/src/lib.rs",
609 ],
610 &[("foo", "crates/foo/src/"), ("bar", "crates/bar/src/")],
611 );
612 fi.set_crate_roots(vec![
613 "crates/foo/src/".to_string(),
614 "crates/bar/src/".to_string(),
615 ]);
616 let result = resolve_rust("crate::helper", "crates/foo/src/lib.rs", &fi);
617 assert_eq!(result, Some("crates/foo/src/helper.rs".into()));
618 }
619}