1use std::{
10 cmp::Ordering,
11 fs, io,
12 path::{Path, PathBuf},
13};
14
15use compact_str::CompactString;
16
17use super::{
18 FailedKind, ResolutionCompleteness, ResolutionOutcome, Resolve, Resolved, UnresolvedReason,
19};
20use crate::imports::{ImportKind, RawImport};
21
22const COMPLETENESS: ResolutionCompleteness = ResolutionCompleteness::Partial;
23
24#[derive(Debug, Clone)]
26pub struct RustResolveOptions {
27 pub crate_roots: Vec<CompactString>,
29}
30
31pub fn rust_resolver(options: RustResolveOptions) -> Box<dyn Resolve> {
33 Box::new(RustResolver { options })
34}
35
36struct RustResolver {
37 options: RustResolveOptions,
38}
39
40impl Resolve for RustResolver {
41 fn baseline_completeness(&self) -> ResolutionCompleteness {
42 COMPLETENESS
43 }
44
45 fn resolve(&self, from_file: &str, import: &RawImport) -> ResolutionOutcome {
46 let from_path = Path::new(from_file);
47 debug_assert!(
48 from_path.is_absolute(),
49 "from_file must be absolute: {from_file}"
50 );
51 if !from_path.is_absolute() {
52 return unresolved(
53 failed(FailedKind::InvalidSpecifier, "from_file must be absolute"),
54 Vec::new(),
55 );
56 }
57 let Some(_) = from_path.parent() else {
58 return unresolved(
59 failed(
60 FailedKind::InvalidSpecifier,
61 "from_file must have a parent directory",
62 ),
63 Vec::new(),
64 );
65 };
66
67 match import.kind {
68 ImportKind::RustMod => self.resolve_mod(from_path, import.specifier.as_str()),
69 ImportKind::RustUse => self.resolve_use(from_path, import.specifier.as_str()),
70 _ => unresolved(UnresolvedReason::Unsupported, Vec::new()),
71 }
72 }
73
74 fn clear_cache(&self) {}
75}
76
77impl RustResolver {
78 fn resolve_use(&self, from_path: &Path, specifier: &str) -> ResolutionOutcome {
79 let segments: Vec<&str> = specifier.split("::").collect();
80 let Some(first) = segments.first().copied() else {
81 return unresolved(UnresolvedReason::NotFound, Vec::new());
82 };
83
84 match first {
85 "crate" => self.resolve_from_crate_root(from_path, &segments[1..]),
86 "self" => {
87 let Some(base) = children_directory(from_path, self.is_crate_root_file(from_path))
88 else {
89 return invalid_from_file();
90 };
91 resolve_segments(base, &segments[1..], Some(from_path.to_path_buf()))
92 }
93 "super" => self.resolve_super(from_path, &segments),
94 external => self.resolve_bare(from_path, external, &segments[1..]),
95 }
96 }
97
98 fn resolve_mod(&self, from_file: &Path, specifier: &str) -> ResolutionOutcome {
99 let Some(base) = children_directory(from_file, self.is_crate_root_file(from_file)) else {
100 return invalid_from_file();
101 };
102 let mut dependencies = Vec::new();
103 match probe_module(&base, specifier, &mut dependencies, false) {
104 Ok(Some(found)) => resolved_path(found, dependencies),
105 Ok(None) => unresolved(UnresolvedReason::NotFound, dependencies),
106 Err(error) => io_failure(error, dependencies),
107 }
108 }
109
110 fn resolve_from_crate_root(&self, from_file: &Path, segments: &[&str]) -> ResolutionOutcome {
111 let Some(root) = self.select_crate_root(from_file) else {
112 return unresolved(UnresolvedReason::NotFound, Vec::new());
113 };
114 let Some(root_dir) = root.parent() else {
115 return invalid_from_file();
116 };
117 resolve_segments(root_dir.to_path_buf(), segments, Some(root))
118 }
119
120 fn resolve_bare(&self, from_file: &Path, first: &str, remaining: &[&str]) -> ResolutionOutcome {
121 let Some(base) = children_directory(from_file, self.is_crate_root_file(from_file)) else {
122 return invalid_from_file();
123 };
124 let mut dependencies = Vec::new();
125 match probe_module(&base, first, &mut dependencies, true) {
126 Ok(Some(found)) => resolve_segments_with_dependencies(
127 base.join(first),
128 remaining,
129 Some(found),
130 dependencies,
131 ),
132 Ok(None) => ResolutionOutcome {
133 resolved: Resolved::External(first.into()),
134 dependencies,
135 notes: Vec::new(),
136 completeness: COMPLETENESS,
137 },
138 Err(error) => io_failure(error, dependencies),
139 }
140 }
141
142 fn resolve_super(&self, from_file: &Path, segments: &[&str]) -> ResolutionOutcome {
143 if self.is_crate_root_file(from_file) {
144 return unresolved(UnresolvedReason::NotFound, Vec::new());
145 }
146
147 let crate_root = self.select_crate_root(from_file);
148 let root_directory = crate_root
149 .as_deref()
150 .and_then(Path::parent)
151 .map(Path::to_path_buf);
152 let Some(mut logical_directory) = children_directory(from_file, false) else {
153 return invalid_from_file();
154 };
155 let super_count = segments
156 .iter()
157 .take_while(|segment| **segment == "super")
158 .count();
159 let mut dependencies = Vec::new();
160 let mut seed = None;
161
162 for _ in 0..super_count {
163 if root_directory
164 .as_deref()
165 .is_some_and(|root| logical_directory == root)
166 || is_standard_source_root(&logical_directory)
167 {
168 return unresolved(UnresolvedReason::NotFound, dependencies);
169 }
170 let Some(ancestor_directory) = logical_directory.parent().map(Path::to_path_buf) else {
171 return unresolved(UnresolvedReason::NotFound, dependencies);
172 };
173 if root_directory
174 .as_deref()
175 .is_some_and(|root| !ancestor_directory.starts_with(root))
176 {
177 return unresolved(UnresolvedReason::NotFound, dependencies);
178 }
179
180 seed = match ancestor_module_file(
181 &ancestor_directory,
182 crate_root.as_deref(),
183 &mut dependencies,
184 ) {
185 Ok(seed) => seed,
186 Err(error) => {
187 return io_failure(error, dependencies);
188 }
189 };
190 logical_directory = ancestor_directory;
191 }
192
193 resolve_segments_with_dependencies(
194 logical_directory,
195 &segments[super_count..],
196 seed,
197 dependencies,
198 )
199 }
200
201 fn select_crate_root(&self, from_file: &Path) -> Option<PathBuf> {
202 if is_implicit_crate_root(from_file) {
203 return Some(from_file.to_path_buf());
204 }
205 let from_dir = from_file.parent()?;
206 let mut roots: Vec<PathBuf> = self
207 .options
208 .crate_roots
209 .iter()
210 .map(|root| PathBuf::from(root.as_str()))
211 .filter(|root| {
212 root.parent()
213 .is_some_and(|root_dir| from_dir.starts_with(root_dir))
214 })
215 .collect();
216 roots.sort_unstable_by(|left, right| compare_crate_roots(left, right));
217 roots.dedup();
218 roots.into_iter().next()
219 }
220
221 fn is_crate_root_file(&self, path: &Path) -> bool {
222 is_implicit_crate_root(path)
223 || matches!(
224 path.file_name().and_then(|name| name.to_str()),
225 Some("lib.rs" | "main.rs")
226 )
227 || self
228 .options
229 .crate_roots
230 .iter()
231 .any(|root| Path::new(root.as_str()) == path)
232 }
233}
234
235fn resolve_segments(base: PathBuf, segments: &[&str], seed: Option<PathBuf>) -> ResolutionOutcome {
236 resolve_segments_with_dependencies(base, segments, seed, Vec::new())
237}
238
239fn resolve_segments_with_dependencies(
240 base: PathBuf,
241 segments: &[&str],
242 seed: Option<PathBuf>,
243 mut dependencies: Vec<CompactString>,
244) -> ResolutionOutcome {
245 match walk_segments(base, segments, seed, &mut dependencies) {
246 Ok(Some(found)) => resolved_path(found, dependencies),
247 Ok(None) => unresolved(UnresolvedReason::NotFound, dependencies),
248 Err(error) => io_failure(error, dependencies),
249 }
250}
251
252fn walk_segments(
253 mut directory: PathBuf,
254 segments: &[&str],
255 seed: Option<PathBuf>,
256 dependencies: &mut Vec<CompactString>,
257) -> Result<Option<PathBuf>, CandidateIoError> {
258 let mut last_found = seed;
259 for segment in segments {
260 let file_candidate = directory.join(format!("{segment}.rs"));
261 dependencies.push(compact_path(&file_candidate));
262 if candidate_is_file(&file_candidate)? {
263 last_found = Some(file_candidate);
264 directory.push(segment);
265 continue;
266 }
267
268 let module_candidate = directory.join(segment).join("mod.rs");
269 dependencies.push(compact_path(&module_candidate));
270 if candidate_is_file(&module_candidate)? {
271 last_found = Some(module_candidate);
272 directory.push(segment);
273 continue;
274 }
275 break;
276 }
277 Ok(last_found)
278}
279
280fn probe_module(
281 directory: &Path,
282 segment: &str,
283 dependencies: &mut Vec<CompactString>,
284 track_both_candidates: bool,
285) -> Result<Option<PathBuf>, CandidateIoError> {
286 let file_candidate = directory.join(format!("{segment}.rs"));
287 let module_candidate = directory.join(segment).join("mod.rs");
288 dependencies.push(compact_path(&file_candidate));
289 if track_both_candidates {
290 dependencies.push(compact_path(&module_candidate));
291 }
292 if candidate_is_file(&file_candidate)? {
293 return Ok(Some(file_candidate));
294 }
295 if !track_both_candidates {
296 dependencies.push(compact_path(&module_candidate));
297 }
298 candidate_is_file(&module_candidate).map(|found| found.then_some(module_candidate))
299}
300
301fn ancestor_module_file(
302 directory: &Path,
303 crate_root: Option<&Path>,
304 dependencies: &mut Vec<CompactString>,
305) -> Result<Option<PathBuf>, CandidateIoError> {
306 if let Some(root) = crate_root
307 && root.parent() == Some(directory)
308 {
309 return Ok(Some(root.to_path_buf()));
310 }
311
312 if is_standard_source_root(directory) {
313 for name in ["lib.rs", "main.rs"] {
314 let candidate = directory.join(name);
315 dependencies.push(compact_path(&candidate));
316 if candidate_is_file(&candidate)? {
317 return Ok(Some(candidate));
318 }
319 }
320 return Ok(None);
321 }
322
323 let file_candidate = directory.with_extension("rs");
324 dependencies.push(compact_path(&file_candidate));
325 if candidate_is_file(&file_candidate)? {
326 return Ok(Some(file_candidate));
327 }
328 let module_candidate = directory.join("mod.rs");
329 dependencies.push(compact_path(&module_candidate));
330 candidate_is_file(&module_candidate).map(|found| found.then_some(module_candidate))
331}
332
333fn children_directory(from_file: &Path, crate_root: bool) -> Option<PathBuf> {
334 let parent = from_file.parent()?;
335 if crate_root
336 || matches!(
337 from_file.file_name().and_then(|name| name.to_str()),
338 Some("mod.rs" | "lib.rs" | "main.rs")
339 )
340 {
341 Some(parent.to_path_buf())
342 } else {
343 Some(parent.join(from_file.file_stem()?))
344 }
345}
346
347fn is_implicit_crate_root(path: &Path) -> bool {
348 if path.extension().and_then(|extension| extension.to_str()) != Some("rs") {
349 return false;
350 }
351 let Some(parent) = path.parent() else {
352 return false;
353 };
354 match parent.file_name().and_then(|name| name.to_str()) {
355 Some("examples" | "tests") => true,
356 Some("bin") => parent
357 .parent()
358 .and_then(Path::file_name)
359 .is_some_and(|name| name == "src"),
360 _ => false,
361 }
362}
363
364fn is_standard_source_root(path: &Path) -> bool {
365 path.file_name().is_some_and(|name| name == "src")
366}
367
368fn compare_crate_roots(left: &Path, right: &Path) -> Ordering {
369 let left_dir = left.parent().expect("filtered crate root has a parent");
370 let right_dir = right.parent().expect("filtered crate root has a parent");
371 right_dir
372 .components()
373 .count()
374 .cmp(&left_dir.components().count())
375 .then_with(|| crate_root_priority(left).cmp(&crate_root_priority(right)))
376 .then_with(|| left.cmp(right))
377}
378
379fn crate_root_priority(path: &Path) -> u8 {
380 match path.file_name().and_then(|name| name.to_str()) {
381 Some("lib.rs") => 0,
382 Some("main.rs") => 1,
383 _ => 2,
384 }
385}
386
387fn is_file(path: &Path) -> io::Result<bool> {
388 match fs::metadata(path) {
389 Ok(metadata) => Ok(metadata.is_file()),
390 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
391 Err(error) => Err(error),
392 }
393}
394
395fn candidate_is_file(path: &Path) -> Result<bool, CandidateIoError> {
396 is_file(path).map_err(|source| CandidateIoError {
397 path: path.to_path_buf(),
398 source,
399 })
400}
401
402fn resolved_path(path: PathBuf, dependencies: Vec<CompactString>) -> ResolutionOutcome {
403 ResolutionOutcome {
404 resolved: Resolved::Path(compact_path(&path)),
405 dependencies,
406 notes: Vec::new(),
407 completeness: COMPLETENESS,
408 }
409}
410
411fn unresolved(reason: UnresolvedReason, dependencies: Vec<CompactString>) -> ResolutionOutcome {
412 ResolutionOutcome {
413 resolved: Resolved::Unresolved(reason),
414 dependencies,
415 notes: Vec::new(),
416 completeness: COMPLETENESS,
417 }
418}
419
420fn invalid_from_file() -> ResolutionOutcome {
421 unresolved(
422 failed(
423 FailedKind::InvalidSpecifier,
424 "from_file must have a file name and parent directory",
425 ),
426 Vec::new(),
427 )
428}
429
430fn io_failure(error: CandidateIoError, dependencies: Vec<CompactString>) -> ResolutionOutcome {
431 unresolved(
432 failed(
433 FailedKind::Io,
434 format!(
435 "could not inspect {}: {}",
436 error.path.display(),
437 error.source
438 ),
439 ),
440 dependencies,
441 )
442}
443
444fn failed(kind: FailedKind, detail: impl Into<CompactString>) -> UnresolvedReason {
445 UnresolvedReason::Failed {
446 kind,
447 detail: detail.into(),
448 }
449}
450
451fn compact_path(path: &Path) -> CompactString {
452 CompactString::from(path.to_string_lossy().as_ref())
453}
454
455struct CandidateIoError {
456 path: PathBuf,
457 source: io::Error,
458}