1use std::fmt;
4use std::sync::Arc;
5
6use camino::{Utf8Path, Utf8PathBuf};
7use ruff_db::files::{
8 File, FilePath, directory_listing, system_path_to_file, vendored_path_to_file,
9};
10use ruff_db::source::source_text;
11use ruff_db::system::{System, SystemPath, SystemPathBuf};
12use ruff_db::vendored::{VendoredPath, VendoredPathBuf};
13
14use crate::Db;
15use crate::module_name::ModuleName;
16use crate::resolve::{PyTyped, ResolverContext};
17use crate::typeshed::TypeshedVersionsQueryResult;
18
19#[derive(Debug, Clone, PartialEq, Eq, Hash)]
28pub(crate) struct ModulePath {
29 search_path: SearchPath,
30 relative_path: Utf8PathBuf,
31}
32
33impl ModulePath {
34 #[must_use]
35 fn is_standard_library(&self) -> bool {
36 matches!(
37 &*self.search_path.0,
38 SearchPathInner::StandardLibraryCustom(_) | SearchPathInner::StandardLibraryVendored(_)
39 )
40 }
41
42 #[must_use]
46 pub(crate) fn is_stub_file(&self) -> bool {
47 self.relative_path.extension() == Some("pyi")
48 }
49
50 #[must_use]
55 pub(crate) fn is_stub_package(&self) -> bool {
56 let Some(first) = self.relative_path.components().next() else {
57 return false;
58 };
59 first.as_str().ends_with("-stubs")
60 }
61
62 pub(crate) fn push(&mut self, component: &str) {
63 if let Some(component_extension) = camino::Utf8Path::new(component).extension() {
64 assert!(
65 self.relative_path.extension().is_none(),
66 "Cannot push part {component} to {self:?}, which already has an extension"
67 );
68 if self.is_standard_library() {
69 assert_eq!(
70 component_extension, "pyi",
71 "Extension must be `pyi`; got `{component_extension}`"
72 );
73 } else {
74 assert!(
75 matches!(component_extension, "pyi" | "py"),
76 "Extension must be `py` or `pyi`; got `{component_extension}`"
77 );
78 }
79 }
80 self.relative_path.push(component);
81 }
82
83 pub(crate) fn pop(&mut self) -> bool {
84 self.relative_path.pop()
85 }
86
87 pub(super) fn search_path(&self) -> &SearchPath {
88 &self.search_path
89 }
90
91 #[must_use]
92 pub(super) fn is_directory(&self, resolver: &ResolverContext) -> bool {
93 let ModulePath {
94 search_path,
95 relative_path,
96 } = self;
97 match &*search_path.0 {
98 SearchPathInner::Extra(search_path)
99 | SearchPathInner::FirstParty(search_path)
100 | SearchPathInner::SitePackages(search_path)
101 | SearchPathInner::Editable(search_path)
102 | SearchPathInner::StandardLibraryReal(search_path) => {
103 system_path_is_directory(resolver.db, &search_path.join(relative_path))
104 }
105 SearchPathInner::StandardLibraryCustom(stdlib_root) => {
106 match query_stdlib_version(relative_path, resolver) {
107 TypeshedVersionsQueryResult::DoesNotExist => false,
108 TypeshedVersionsQueryResult::Exists
109 | TypeshedVersionsQueryResult::MaybeExists => {
110 system_path_is_directory(resolver.db, &stdlib_root.join(relative_path))
111 }
112 }
113 }
114 SearchPathInner::StandardLibraryVendored(stdlib_root) => {
115 match query_stdlib_version(relative_path, resolver) {
116 TypeshedVersionsQueryResult::DoesNotExist => false,
117 TypeshedVersionsQueryResult::Exists
118 | TypeshedVersionsQueryResult::MaybeExists => resolver
119 .vendored()
120 .is_directory(stdlib_root.join(relative_path)),
121 }
122 }
123 }
124 }
125
126 #[must_use]
127 pub(super) fn is_regular_package(&self, resolver: &ResolverContext) -> bool {
128 let ModulePath {
129 search_path,
130 relative_path,
131 } = self;
132
133 match &*search_path.0 {
134 SearchPathInner::Extra(search_path)
135 | SearchPathInner::FirstParty(search_path)
136 | SearchPathInner::SitePackages(search_path)
137 | SearchPathInner::Editable(search_path) => {
138 let absolute_path = search_path.join(relative_path);
139
140 directory_contains_file(
141 resolver.db,
142 &absolute_path,
143 &["__init__.py", "__init__.pyi"],
144 )
145 }
146 SearchPathInner::StandardLibraryReal(search_path) => {
147 let absolute_path = search_path.join(relative_path);
148
149 directory_contains_file(resolver.db, &absolute_path, &["__init__.py"])
150 }
151 SearchPathInner::StandardLibraryCustom(search_path) => {
152 match query_stdlib_version(relative_path, resolver) {
153 TypeshedVersionsQueryResult::DoesNotExist => false,
154 TypeshedVersionsQueryResult::Exists
155 | TypeshedVersionsQueryResult::MaybeExists => directory_contains_file(
156 resolver.db,
157 &search_path.join(relative_path),
158 &["__init__.pyi"],
159 ),
160 }
161 }
162 SearchPathInner::StandardLibraryVendored(search_path) => {
163 match query_stdlib_version(relative_path, resolver) {
164 TypeshedVersionsQueryResult::DoesNotExist => false,
165 TypeshedVersionsQueryResult::Exists
166 | TypeshedVersionsQueryResult::MaybeExists => resolver
167 .vendored()
168 .exists(search_path.join(relative_path).join("__init__.pyi")),
169 }
170 }
171 }
172 }
173
174 pub(super) fn py_typed(&self, resolver: &ResolverContext) -> PyTyped {
176 let Some(py_typed_file) = self.to_system_path().and_then(|path| {
177 if !directory_contains_file(resolver.db, &path, &["py.typed"]) {
178 return None;
179 }
180 let py_typed_path = path.join("py.typed");
181 system_path_to_file(resolver.db, py_typed_path).ok()
182 }) else {
183 return PyTyped::Untyped;
184 };
185
186 let py_typed_contents = source_text(resolver.db, py_typed_file);
189 if py_typed_contents.read_error().is_some() {
192 return PyTyped::Untyped;
193 }
194
195 if py_typed_contents.to_ascii_lowercase().contains("partial") {
203 PyTyped::Partial
204 } else {
205 PyTyped::Full
206 }
207 }
208
209 pub(super) fn to_system_path(&self) -> Option<SystemPathBuf> {
210 let ModulePath {
211 search_path,
212 relative_path,
213 } = self;
214 match &*search_path.0 {
215 SearchPathInner::Extra(search_path)
216 | SearchPathInner::FirstParty(search_path)
217 | SearchPathInner::SitePackages(search_path)
218 | SearchPathInner::Editable(search_path) => Some(search_path.join(relative_path)),
219 SearchPathInner::StandardLibraryReal(stdlib_root)
220 | SearchPathInner::StandardLibraryCustom(stdlib_root) => {
221 Some(stdlib_root.join(relative_path))
222 }
223 SearchPathInner::StandardLibraryVendored(_) => None,
224 }
225 }
226
227 #[must_use]
228 pub(super) fn to_file(&self, resolver: &ResolverContext) -> Option<File> {
229 let db = resolver.db;
230 let ModulePath {
231 search_path,
232 relative_path,
233 } = self;
234 match &*search_path.0 {
235 SearchPathInner::Extra(search_path)
236 | SearchPathInner::FirstParty(search_path)
237 | SearchPathInner::SitePackages(search_path)
238 | SearchPathInner::Editable(search_path) => {
239 system_path_to_file_if_listed(db, &search_path.join(relative_path))
240 }
241 SearchPathInner::StandardLibraryReal(search_path) => {
242 system_path_to_file_if_listed(db, &search_path.join(relative_path))
243 }
244 SearchPathInner::StandardLibraryCustom(stdlib_root) => {
245 match query_stdlib_version(relative_path, resolver) {
246 TypeshedVersionsQueryResult::DoesNotExist => None,
247 TypeshedVersionsQueryResult::Exists
248 | TypeshedVersionsQueryResult::MaybeExists => {
249 system_path_to_file_if_listed(db, &stdlib_root.join(relative_path))
250 }
251 }
252 }
253 SearchPathInner::StandardLibraryVendored(stdlib_root) => {
254 match query_stdlib_version(relative_path, resolver) {
255 TypeshedVersionsQueryResult::DoesNotExist => None,
256 TypeshedVersionsQueryResult::Exists
257 | TypeshedVersionsQueryResult::MaybeExists => {
258 vendored_path_to_file(db, stdlib_root.join(relative_path)).ok()
259 }
260 }
261 }
262 }
263 }
264
265 #[must_use]
266 pub(crate) fn to_module_name(&self) -> Option<ModuleName> {
267 fn strip_stubs(component: &str) -> &str {
268 component.strip_suffix("-stubs").unwrap_or(component)
269 }
270
271 let ModulePath {
272 search_path: _,
273 relative_path,
274 } = self;
275 if self.is_standard_library() {
276 stdlib_path_to_module_name(relative_path)
277 } else {
278 let parent = relative_path.parent()?;
279 let name = relative_path.file_stem()?;
280 if parent.as_str().is_empty() {
281 return ModuleName::new(if relative_path.extension().is_some() {
286 name
287 } else {
288 strip_stubs(relative_path.as_str())
289 });
290 }
291
292 let parent_components = parent.components().enumerate().map(|(index, component)| {
293 let component = component.as_str();
294
295 if index == 0 {
300 strip_stubs(component)
301 } else {
302 component
303 }
304 });
305
306 let skip_final_part =
307 relative_path.ends_with("__init__.py") || relative_path.ends_with("__init__.pyi");
308 if skip_final_part {
309 ModuleName::from_components(parent_components)
310 } else {
311 ModuleName::from_components(parent_components.chain([name]))
312 }
313 }
314 }
315
316 #[must_use]
317 pub(crate) fn with_pyi_extension(&self) -> Self {
318 let ModulePath {
319 search_path,
320 relative_path,
321 } = self;
322 ModulePath {
323 search_path: search_path.clone(),
324 relative_path: relative_path.with_extension("pyi"),
325 }
326 }
327
328 #[must_use]
329 pub(crate) fn with_py_extension(&self) -> Option<Self> {
330 if self.is_standard_library() {
331 return None;
332 }
333 let ModulePath {
334 search_path,
335 relative_path,
336 } = self;
337 Some(ModulePath {
338 search_path: search_path.clone(),
339 relative_path: relative_path.with_extension("py"),
340 })
341 }
342
343 pub(crate) fn into_search_path(self) -> SearchPath {
344 self.search_path
345 }
346}
347
348impl PartialEq<SystemPathBuf> for ModulePath {
349 fn eq(&self, other: &SystemPathBuf) -> bool {
350 let ModulePath {
351 search_path,
352 relative_path,
353 } = self;
354 search_path
355 .as_system_path()
356 .and_then(|search_path| other.strip_prefix(search_path).ok())
357 .is_some_and(|other_relative_path| other_relative_path.as_utf8_path() == relative_path)
358 }
359}
360
361impl PartialEq<ModulePath> for SystemPathBuf {
362 fn eq(&self, other: &ModulePath) -> bool {
363 other.eq(self)
364 }
365}
366
367impl PartialEq<VendoredPathBuf> for ModulePath {
368 fn eq(&self, other: &VendoredPathBuf) -> bool {
369 let ModulePath {
370 search_path,
371 relative_path,
372 } = self;
373 search_path
374 .as_vendored_path()
375 .and_then(|search_path| other.strip_prefix(search_path).ok())
376 .is_some_and(|other_relative_path| other_relative_path.as_utf8_path() == relative_path)
377 }
378}
379
380impl PartialEq<ModulePath> for VendoredPathBuf {
381 fn eq(&self, other: &ModulePath) -> bool {
382 other.eq(self)
383 }
384}
385
386fn directory_contains_file(db: &dyn Db, directory: &SystemPath, names: &[&str]) -> bool {
387 let Ok(listing) = directory_listing(db, directory) else {
388 return false;
389 };
390
391 names
392 .iter()
393 .any(|name| listing.entry_is_file(db, directory, name))
394}
395
396fn system_path_to_file_if_listed(db: &dyn Db, path: &SystemPath) -> Option<File> {
397 let Some((parent, name)) = path.parent().zip(path.file_name()) else {
398 return system_path_to_file(db, path).ok();
399 };
400
401 let listing = directory_listing(db, parent).ok()?;
402 if listing.entry_is_file(db, parent, name) {
403 system_path_to_file(db, path).ok()
404 } else {
405 None
406 }
407}
408
409fn system_path_is_directory(db: &dyn Db, path: &SystemPath) -> bool {
410 let Some((parent, name)) = path.parent().zip(path.file_name()) else {
411 return db.system().is_directory(path);
412 };
413
414 directory_listing(db, parent).is_ok_and(|listing| listing.entry_is_directory(db, parent, name))
415}
416
417#[must_use]
418fn stdlib_path_to_module_name(relative_path: &Utf8Path) -> Option<ModuleName> {
419 let parent_components = relative_path
420 .parent()?
421 .components()
422 .map(|component| component.as_str());
423 let skip_final_part = relative_path.ends_with("__init__.pyi");
424 if skip_final_part {
425 ModuleName::from_components(parent_components)
426 } else {
427 ModuleName::from_components(parent_components.chain(relative_path.file_stem()))
428 }
429}
430
431#[must_use]
432fn query_stdlib_version(
433 relative_path: &Utf8Path,
434 context: &ResolverContext,
435) -> TypeshedVersionsQueryResult {
436 let Some(module_name) = stdlib_path_to_module_name(relative_path) else {
437 return TypeshedVersionsQueryResult::DoesNotExist;
438 };
439 context
440 .resolver_environment
441 .search_paths(context.db)
442 .typeshed_versions()
443 .query_module(
444 &module_name,
445 context.resolver_environment.python_version(context.db),
446 )
447}
448
449#[derive(Debug, thiserror::Error)]
450pub enum SearchPathError {
451 #[error("{0} does not point to a directory")]
453 NotADirectory(SystemPathBuf),
454
455 #[error("The directory at {0} has no `stdlib/` subdirectory")]
459 NoStdlibSubdirectory(SystemPathBuf),
460}
461
462type SearchPathResult<T> = Result<T, SearchPathError>;
463
464#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
465enum SearchPathInner {
466 Extra(SystemPathBuf),
467 FirstParty(SystemPathBuf),
468 StandardLibraryCustom(SystemPathBuf),
469 StandardLibraryVendored(VendoredPathBuf),
470 StandardLibraryReal(SystemPathBuf),
471 SitePackages(SystemPathBuf),
472 Editable(SystemPathBuf),
473}
474
475#[derive(Debug, Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
503pub struct SearchPath(Arc<SearchPathInner>);
504
505impl SearchPath {
506 fn directory_path(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<SystemPathBuf> {
507 if system.is_directory(&root) {
508 Ok(root)
509 } else {
510 Err(SearchPathError::NotADirectory(root))
511 }
512 }
513
514 pub(crate) fn extra(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
516 Ok(Self(Arc::new(SearchPathInner::Extra(
517 Self::directory_path(system, root)?,
518 ))))
519 }
520
521 pub(crate) fn first_party(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
523 Ok(Self(Arc::new(SearchPathInner::FirstParty(
524 Self::directory_path(system, root)?,
525 ))))
526 }
527
528 pub(crate) fn custom_stdlib(
530 system: &dyn System,
531 typeshed: &SystemPath,
532 ) -> SearchPathResult<Self> {
533 if !system.is_directory(typeshed) {
534 return Err(SearchPathError::NotADirectory(typeshed.to_path_buf()));
535 }
536
537 let stdlib =
538 Self::directory_path(system, typeshed.join("stdlib")).map_err(|err| match err {
539 SearchPathError::NotADirectory(_) => {
540 SearchPathError::NoStdlibSubdirectory(typeshed.to_path_buf())
541 }
542 SearchPathError::NoStdlibSubdirectory(_) => err,
543 })?;
544
545 Ok(Self(Arc::new(SearchPathInner::StandardLibraryCustom(
546 stdlib,
547 ))))
548 }
549
550 #[must_use]
552 pub(crate) fn vendored_stdlib() -> Self {
553 Self(Arc::new(SearchPathInner::StandardLibraryVendored(
554 VendoredPathBuf::from("stdlib"),
555 )))
556 }
557
558 pub(crate) fn real_stdlib(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
560 Ok(Self(Arc::new(SearchPathInner::StandardLibraryReal(
561 Self::directory_path(system, root)?,
562 ))))
563 }
564
565 pub(crate) fn site_packages(
573 system: &dyn System,
574 root: SystemPathBuf,
575 ) -> SearchPathResult<Self> {
576 Ok(Self(Arc::new(SearchPathInner::SitePackages(
577 Self::directory_path(system, root)?,
578 ))))
579 }
580
581 pub(crate) fn editable(system: &dyn System, root: SystemPathBuf) -> SearchPathResult<Self> {
583 Ok(Self(Arc::new(SearchPathInner::Editable(
584 Self::directory_path(system, root)?,
585 ))))
586 }
587
588 #[must_use]
589 pub(crate) fn to_module_path(&self) -> ModulePath {
590 ModulePath {
591 search_path: self.clone(),
592 relative_path: Utf8PathBuf::new(),
593 }
594 }
595
596 #[must_use]
598 pub fn is_standard_library(&self) -> bool {
599 matches!(
600 &*self.0,
601 SearchPathInner::StandardLibraryCustom(_)
602 | SearchPathInner::StandardLibraryVendored(_)
603 | SearchPathInner::StandardLibraryReal(_)
604 )
605 }
606
607 pub(crate) fn is_extra(&self) -> bool {
609 matches!(&*self.0, SearchPathInner::Extra(_))
610 }
611
612 pub fn is_first_party(&self) -> bool {
615 matches!(&*self.0, SearchPathInner::FirstParty(_))
616 }
617
618 pub fn is_site_packages(&self) -> bool {
620 matches!(&*self.0, SearchPathInner::SitePackages(_))
621 }
622
623 pub(crate) fn can_contain_third_party_code(&self) -> bool {
625 match &*self.0 {
626 SearchPathInner::SitePackages(_)
627 | SearchPathInner::Editable(_)
628 | SearchPathInner::Extra(_) => true,
629 SearchPathInner::FirstParty(_)
630 | SearchPathInner::StandardLibraryCustom(_)
631 | SearchPathInner::StandardLibraryVendored(_)
632 | SearchPathInner::StandardLibraryReal(_) => false,
633 }
634 }
635
636 fn is_valid_extension(&self, extension: &str) -> bool {
637 if self.is_standard_library() {
638 extension == "pyi"
639 } else {
640 matches!(extension, "pyi" | "py")
641 }
642 }
643
644 #[must_use]
645 pub(crate) fn relativize_system_path(&self, path: &SystemPath) -> Option<ModulePath> {
646 self.relativize_system_path_only(path)
647 .map(|relative_path| ModulePath {
648 search_path: self.clone(),
649 relative_path: relative_path.as_utf8_path().to_path_buf(),
650 })
651 }
652
653 #[must_use]
654 pub(crate) fn relativize_system_path_only<'a>(
655 &self,
656 path: &'a SystemPath,
657 ) -> Option<&'a SystemPath> {
658 if path
659 .extension()
660 .is_some_and(|extension| !self.is_valid_extension(extension))
661 {
662 return None;
663 }
664
665 match &*self.0 {
666 SearchPathInner::Extra(search_path)
667 | SearchPathInner::FirstParty(search_path)
668 | SearchPathInner::StandardLibraryCustom(search_path)
669 | SearchPathInner::StandardLibraryReal(search_path)
670 | SearchPathInner::SitePackages(search_path)
671 | SearchPathInner::Editable(search_path) => path.strip_prefix(search_path).ok(),
672 SearchPathInner::StandardLibraryVendored(_) => None,
673 }
674 }
675
676 #[must_use]
677 pub(crate) fn relativize_vendored_path(&self, path: &VendoredPath) -> Option<ModulePath> {
678 if path
679 .extension()
680 .is_some_and(|extension| !self.is_valid_extension(extension))
681 {
682 return None;
683 }
684
685 match &*self.0 {
686 SearchPathInner::Extra(_)
687 | SearchPathInner::FirstParty(_)
688 | SearchPathInner::StandardLibraryCustom(_)
689 | SearchPathInner::StandardLibraryReal(_)
690 | SearchPathInner::SitePackages(_)
691 | SearchPathInner::Editable(_) => None,
692 SearchPathInner::StandardLibraryVendored(search_path) => path
693 .strip_prefix(search_path)
694 .ok()
695 .map(|relative_path| ModulePath {
696 search_path: self.clone(),
697 relative_path: relative_path.as_utf8_path().to_path_buf(),
698 }),
699 }
700 }
701
702 #[must_use]
703 pub(super) fn as_path(&self) -> SystemOrVendoredPathRef<'_> {
704 match *self.0 {
705 SearchPathInner::Extra(ref path)
706 | SearchPathInner::FirstParty(ref path)
707 | SearchPathInner::StandardLibraryCustom(ref path)
708 | SearchPathInner::StandardLibraryReal(ref path)
709 | SearchPathInner::SitePackages(ref path)
710 | SearchPathInner::Editable(ref path) => SystemOrVendoredPathRef::System(path),
711 SearchPathInner::StandardLibraryVendored(ref path) => {
712 SystemOrVendoredPathRef::Vendored(path)
713 }
714 }
715 }
716
717 #[must_use]
718 pub(crate) fn as_system_path(&self) -> Option<&SystemPath> {
719 self.as_path().as_system_path()
720 }
721
722 #[must_use]
723 fn as_vendored_path(&self) -> Option<&VendoredPath> {
724 self.as_path().as_vendored_path()
725 }
726
727 #[cfg(test)]
731 #[must_use]
732 pub(crate) fn debug_kind(&self) -> &'static str {
733 match *self.0 {
734 SearchPathInner::Extra(_) => "extra",
735 SearchPathInner::FirstParty(_) => "first-party",
736 SearchPathInner::StandardLibraryCustom(_) => "std-custom",
737 SearchPathInner::StandardLibraryReal(_) => "std-real",
738 SearchPathInner::SitePackages(_) => "site-packages",
739 SearchPathInner::Editable(_) => "editable",
740 SearchPathInner::StandardLibraryVendored(_) => "std-vendored",
741 }
742 }
743
744 #[must_use]
747 pub fn describe_kind(&self) -> &'static str {
748 match *self.0 {
749 SearchPathInner::Extra(_) => {
750 "extra search path specified on the CLI or in your config file"
751 }
752 SearchPathInner::FirstParty(_) => "first-party code",
753 SearchPathInner::StandardLibraryCustom(_) => {
754 "custom stdlib stubs specified on the CLI or in your config file"
755 }
756 SearchPathInner::StandardLibraryReal(_) => "runtime stdlib source code",
757 SearchPathInner::SitePackages(_) => "site-packages",
758 SearchPathInner::Editable(_) => "editable install",
759 SearchPathInner::StandardLibraryVendored(_) => "stdlib typeshed stubs vendored by ty",
760 }
761 }
762}
763
764impl PartialEq<SystemPath> for SearchPath {
765 fn eq(&self, other: &SystemPath) -> bool {
766 self.as_system_path().is_some_and(|path| path == other)
767 }
768}
769
770impl PartialEq<SearchPath> for SystemPath {
771 fn eq(&self, other: &SearchPath) -> bool {
772 other.eq(self)
773 }
774}
775
776impl PartialEq<SystemPathBuf> for SearchPath {
777 fn eq(&self, other: &SystemPathBuf) -> bool {
778 self.eq(&**other)
779 }
780}
781
782impl PartialEq<SearchPath> for SystemPathBuf {
783 fn eq(&self, other: &SearchPath) -> bool {
784 other.eq(self)
785 }
786}
787
788impl PartialEq<VendoredPath> for SearchPath {
789 fn eq(&self, other: &VendoredPath) -> bool {
790 self.as_vendored_path().is_some_and(|path| path == other)
791 }
792}
793
794impl PartialEq<SearchPath> for VendoredPath {
795 fn eq(&self, other: &SearchPath) -> bool {
796 other.eq(self)
797 }
798}
799
800impl PartialEq<VendoredPathBuf> for SearchPath {
801 fn eq(&self, other: &VendoredPathBuf) -> bool {
802 self.eq(&**other)
803 }
804}
805
806impl PartialEq<SearchPath> for VendoredPathBuf {
807 fn eq(&self, other: &SearchPath) -> bool {
808 other.eq(self)
809 }
810}
811
812impl fmt::Display for SearchPath {
813 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
814 match &*self.0 {
815 SearchPathInner::Extra(system_path_buf)
816 | SearchPathInner::FirstParty(system_path_buf)
817 | SearchPathInner::SitePackages(system_path_buf)
818 | SearchPathInner::Editable(system_path_buf)
819 | SearchPathInner::StandardLibraryReal(system_path_buf)
820 | SearchPathInner::StandardLibraryCustom(system_path_buf) => system_path_buf.fmt(f),
821 SearchPathInner::StandardLibraryVendored(vendored_path_buf) => vendored_path_buf.fmt(f),
822 }
823 }
824}
825
826#[derive(Debug, Clone, Copy)]
827pub(super) enum SystemOrVendoredPathRef<'db> {
828 System(&'db SystemPath),
829 Vendored(&'db VendoredPath),
830}
831
832impl<'db> SystemOrVendoredPathRef<'db> {
833 pub(super) fn try_from_file(db: &'db dyn Db, file: File) -> Option<Self> {
834 match file.path(db) {
835 FilePath::System(system) => Some(Self::System(system)),
836 FilePath::Vendored(vendored) => Some(Self::Vendored(vendored)),
837 FilePath::SystemVirtual(_) => None,
838 }
839 }
840
841 pub(super) fn file_name(&self) -> Option<&str> {
842 match self {
843 Self::System(system) => system.file_name(),
844 Self::Vendored(vendored) => vendored.file_name(),
845 }
846 }
847
848 pub(super) fn extension(&self) -> Option<&str> {
849 match self {
850 Self::System(system) => system.extension(),
851 Self::Vendored(vendored) => vendored.extension(),
852 }
853 }
854
855 pub(super) fn parent<'a>(&'a self) -> Option<SystemOrVendoredPathRef<'a>>
856 where
857 'a: 'db,
858 {
859 match self {
860 Self::System(system) => system.parent().map(Self::System),
861 Self::Vendored(vendored) => vendored.parent().map(Self::Vendored),
862 }
863 }
864
865 fn as_system_path(&self) -> Option<&'db SystemPath> {
866 match self {
867 SystemOrVendoredPathRef::System(path) => Some(path),
868 SystemOrVendoredPathRef::Vendored(_) => None,
869 }
870 }
871
872 fn as_vendored_path(&self) -> Option<&'db VendoredPath> {
873 match self {
874 SystemOrVendoredPathRef::Vendored(path) => Some(path),
875 SystemOrVendoredPathRef::System(_) => None,
876 }
877 }
878}
879
880impl<'a> From<&'a SystemPath> for SystemOrVendoredPathRef<'a> {
881 fn from(path: &'a SystemPath) -> SystemOrVendoredPathRef<'a> {
882 SystemOrVendoredPathRef::System(path)
883 }
884}
885
886impl<'a> From<&'a VendoredPath> for SystemOrVendoredPathRef<'a> {
887 fn from(path: &'a VendoredPath) -> SystemOrVendoredPathRef<'a> {
888 SystemOrVendoredPathRef::Vendored(path)
889 }
890}
891
892impl std::fmt::Display for SystemOrVendoredPathRef<'_> {
893 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
894 match self {
895 SystemOrVendoredPathRef::System(system) => system.fmt(f),
896 SystemOrVendoredPathRef::Vendored(vendored) => vendored.fmt(f),
897 }
898 }
899}
900
901#[cfg(test)]
902mod tests {
903 use ruff_db::Db;
904 use ruff_python_ast::PythonVersion;
905
906 use crate::ResolverEnvironment;
907 use crate::db::tests::TestDb;
908 use crate::resolve::ModuleResolveMode;
909 use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
910
911 use super::*;
912
913 impl ModulePath {
914 #[must_use]
915 fn join(&self, component: &str) -> ModulePath {
916 let mut result = self.clone();
917 result.push(component);
918 result
919 }
920 }
921
922 impl SearchPath {
923 fn join(&self, component: &str) -> ModulePath {
924 self.to_module_path().join(component)
925 }
926 }
927
928 #[test]
929 fn with_extension_methods() {
930 let TestCase {
931 db, src, stdlib, ..
932 } = TestCaseBuilder::new()
933 .with_mocked_typeshed(MockedTypeshed::default())
934 .build();
935
936 assert_eq!(
937 SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap())
938 .unwrap()
939 .to_module_path()
940 .with_py_extension(),
941 None
942 );
943
944 assert_eq!(
945 &SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap())
946 .unwrap()
947 .join("foo")
948 .with_pyi_extension(),
949 &stdlib.join("foo.pyi")
950 );
951
952 assert_eq!(
953 &SearchPath::first_party(db.system(), src.clone())
954 .unwrap()
955 .join("foo/bar")
956 .with_py_extension()
957 .unwrap(),
958 &src.join("foo/bar.py")
959 );
960 }
961
962 #[test]
963 fn module_name_1_part() {
964 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
965 let src_search_path = SearchPath::first_party(db.system(), src).unwrap();
966 let foo_module_name = ModuleName::new_static("foo").unwrap();
967
968 assert_eq!(
969 src_search_path
970 .to_module_path()
971 .join("foo")
972 .to_module_name()
973 .as_ref(),
974 Some(&foo_module_name)
975 );
976
977 assert_eq!(
978 src_search_path.join("foo.pyi").to_module_name().as_ref(),
979 Some(&foo_module_name)
980 );
981
982 assert_eq!(
983 src_search_path
984 .join("foo/__init__.pyi")
985 .to_module_name()
986 .as_ref(),
987 Some(&foo_module_name)
988 );
989 }
990
991 #[test]
992 fn module_name_2_parts() {
993 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
994 let src_search_path = SearchPath::first_party(db.system(), src).unwrap();
995 let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
996
997 assert_eq!(
998 src_search_path.join("foo/bar").to_module_name().as_ref(),
999 Some(&foo_bar_module_name)
1000 );
1001
1002 assert_eq!(
1003 src_search_path
1004 .join("foo/bar.pyi")
1005 .to_module_name()
1006 .as_ref(),
1007 Some(&foo_bar_module_name)
1008 );
1009
1010 assert_eq!(
1011 src_search_path
1012 .join("foo/bar/__init__.pyi")
1013 .to_module_name()
1014 .as_ref(),
1015 Some(&foo_bar_module_name)
1016 );
1017 }
1018
1019 #[test]
1020 fn module_name_3_parts() {
1021 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1022 let src_search_path = SearchPath::first_party(db.system(), src).unwrap();
1023 let foo_bar_baz_module_name = ModuleName::new_static("foo.bar.baz").unwrap();
1024
1025 assert_eq!(
1026 src_search_path
1027 .join("foo/bar/baz")
1028 .to_module_name()
1029 .as_ref(),
1030 Some(&foo_bar_baz_module_name)
1031 );
1032
1033 assert_eq!(
1034 src_search_path
1035 .join("foo/bar/baz.pyi")
1036 .to_module_name()
1037 .as_ref(),
1038 Some(&foo_bar_baz_module_name)
1039 );
1040
1041 assert_eq!(
1042 src_search_path
1043 .join("foo/bar/baz/__init__.pyi")
1044 .to_module_name()
1045 .as_ref(),
1046 Some(&foo_bar_baz_module_name)
1047 );
1048 }
1049
1050 #[test]
1051 #[should_panic(expected = "Extension must be `pyi`; got `py`")]
1052 fn stdlib_path_invalid_join_py() {
1053 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
1054 .with_mocked_typeshed(MockedTypeshed::default())
1055 .build();
1056 SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap())
1057 .unwrap()
1058 .to_module_path()
1059 .push("bar.py");
1060 }
1061
1062 #[test]
1063 #[should_panic(expected = "Extension must be `pyi`; got `rs`")]
1064 fn stdlib_path_invalid_join_rs() {
1065 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
1066 .with_mocked_typeshed(MockedTypeshed::default())
1067 .build();
1068 SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap())
1069 .unwrap()
1070 .to_module_path()
1071 .push("bar.rs");
1072 }
1073
1074 #[test]
1075 #[should_panic(expected = "Extension must be `py` or `pyi`; got `rs`")]
1076 fn non_stdlib_path_invalid_join_rs() {
1077 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1078 SearchPath::first_party(db.system(), src)
1079 .unwrap()
1080 .to_module_path()
1081 .push("bar.rs");
1082 }
1083
1084 #[test]
1085 #[should_panic(expected = "already has an extension")]
1086 fn too_many_extensions() {
1087 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1088 SearchPath::first_party(db.system(), src)
1089 .unwrap()
1090 .join("foo.py")
1091 .push("bar.py");
1092 }
1093
1094 #[test]
1095 fn relativize_stdlib_path_errors() {
1096 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
1097 .with_mocked_typeshed(MockedTypeshed::default())
1098 .build();
1099
1100 let root = SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()).unwrap();
1101
1102 let bad_absolute_path = SystemPath::new("foo/stdlib/x.py");
1104 assert_eq!(root.relativize_system_path(bad_absolute_path), None);
1105 let second_bad_absolute_path = SystemPath::new("foo/stdlib/x.rs");
1106 assert_eq!(root.relativize_system_path(second_bad_absolute_path), None);
1107
1108 let third_bad_absolute_path = SystemPath::new("bar/stdlib/x.pyi");
1110 assert_eq!(root.relativize_system_path(third_bad_absolute_path), None);
1111 }
1112
1113 #[test]
1114 fn relativize_non_stdlib_path_errors() {
1115 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1116
1117 let root = SearchPath::extra(db.system(), src.clone()).unwrap();
1118 let bad_absolute_path = src.join("x.rs");
1120 assert_eq!(root.relativize_system_path(&bad_absolute_path), None);
1121 let second_bad_absolute_path = SystemPath::new("bar/src/x.pyi");
1123 assert_eq!(root.relativize_system_path(second_bad_absolute_path), None);
1124 }
1125
1126 #[test]
1127 fn relativize_path() {
1128 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1129 let src_search_path = SearchPath::first_party(db.system(), src.clone()).unwrap();
1130 let eggs_package = src.join("eggs/__init__.pyi");
1131 let module_path = src_search_path
1132 .relativize_system_path(&eggs_package)
1133 .unwrap();
1134 assert_eq!(
1135 &module_path.relative_path,
1136 Utf8Path::new("eggs/__init__.pyi")
1137 );
1138 }
1139
1140 fn typeshed_test_case(
1141 typeshed: MockedTypeshed,
1142 python_version: PythonVersion,
1143 ) -> (TestDb, SearchPath) {
1144 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
1145 .with_mocked_typeshed(typeshed)
1146 .with_python_version(python_version)
1147 .build();
1148 let stdlib = SearchPath::custom_stdlib(db.system(), stdlib.parent().unwrap()).unwrap();
1149 (db, stdlib)
1150 }
1151
1152 fn py38_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, SearchPath) {
1153 typeshed_test_case(typeshed, PythonVersion::PY38)
1154 }
1155
1156 fn py39_typeshed_test_case(typeshed: MockedTypeshed) -> (TestDb, SearchPath) {
1157 typeshed_test_case(typeshed, PythonVersion::PY39)
1158 }
1159
1160 #[test]
1161 fn mocked_typeshed_existing_regular_stdlib_pkg_py38() {
1162 const VERSIONS: &str = "\
1163 asyncio: 3.8-
1164 asyncio.tasks: 3.9-3.11
1165 ";
1166
1167 const TYPESHED: MockedTypeshed = MockedTypeshed {
1168 versions: VERSIONS,
1169 stdlib_files: &[("asyncio/__init__.pyi", ""), ("asyncio/tasks.pyi", "")],
1170 };
1171
1172 let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
1173 let resolver = ResolverContext::new(
1174 &db,
1175 ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()),
1176 ModuleResolveMode::Typing,
1177 );
1178
1179 let asyncio_regular_package = stdlib_path.join("asyncio");
1180 assert!(asyncio_regular_package.is_directory(&resolver));
1181 assert!(asyncio_regular_package.is_regular_package(&resolver));
1182 assert_eq!(asyncio_regular_package.to_file(&resolver), None);
1184 assert!(
1185 asyncio_regular_package
1186 .join("__init__.pyi")
1187 .to_file(&resolver)
1188 .is_some()
1189 );
1190
1191 let asyncio_tasks_module = stdlib_path.join("asyncio/tasks.pyi");
1194 assert_eq!(asyncio_tasks_module.to_file(&resolver), None);
1195 assert!(!asyncio_tasks_module.is_directory(&resolver));
1196 assert!(!asyncio_tasks_module.is_regular_package(&resolver));
1197 }
1198
1199 #[test]
1200 fn mocked_typeshed_existing_namespace_stdlib_pkg_py38() {
1201 const TYPESHED: MockedTypeshed = MockedTypeshed {
1202 versions: "xml: 3.8-3.8",
1203 stdlib_files: &[("xml/etree.pyi", "")],
1204 };
1205
1206 let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
1207 let resolver = ResolverContext::new(
1208 &db,
1209 ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()),
1210 ModuleResolveMode::Typing,
1211 );
1212
1213 let xml_namespace_package = stdlib_path.join("xml");
1214 assert!(xml_namespace_package.is_directory(&resolver));
1215 assert_eq!(xml_namespace_package.to_file(&resolver), None);
1217 assert!(!xml_namespace_package.is_regular_package(&resolver));
1218
1219 let xml_etree = stdlib_path.join("xml/etree.pyi");
1220 assert!(!xml_etree.is_directory(&resolver));
1221 assert!(xml_etree.to_file(&resolver).is_some());
1222 assert!(!xml_etree.is_regular_package(&resolver));
1223 }
1224
1225 #[test]
1226 fn mocked_typeshed_single_file_stdlib_module_py38() {
1227 const TYPESHED: MockedTypeshed = MockedTypeshed {
1228 versions: "functools: 3.8-",
1229 stdlib_files: &[("functools.pyi", "")],
1230 };
1231
1232 let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
1233 let resolver = ResolverContext::new(
1234 &db,
1235 ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()),
1236 ModuleResolveMode::Typing,
1237 );
1238
1239 let functools_module = stdlib_path.join("functools.pyi");
1240 assert!(functools_module.to_file(&resolver).is_some());
1241 assert!(!functools_module.is_directory(&resolver));
1242 assert!(!functools_module.is_regular_package(&resolver));
1243 }
1244
1245 #[test]
1246 fn mocked_typeshed_nonexistent_regular_stdlib_pkg_py38() {
1247 const TYPESHED: MockedTypeshed = MockedTypeshed {
1248 versions: "collections: 3.9-",
1249 stdlib_files: &[("collections/__init__.pyi", "")],
1250 };
1251
1252 let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
1253 let resolver = ResolverContext::new(
1254 &db,
1255 ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()),
1256 ModuleResolveMode::Typing,
1257 );
1258
1259 let collections_regular_package = stdlib_path.join("collections");
1260 assert_eq!(collections_regular_package.to_file(&resolver), None);
1261 assert!(!collections_regular_package.is_directory(&resolver));
1262 assert!(!collections_regular_package.is_regular_package(&resolver));
1263 }
1264
1265 #[test]
1266 fn mocked_typeshed_nonexistent_namespace_stdlib_pkg_py38() {
1267 const TYPESHED: MockedTypeshed = MockedTypeshed {
1268 versions: "importlib: 3.9-",
1269 stdlib_files: &[("importlib/abc.pyi", "")],
1270 };
1271
1272 let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
1273 let resolver = ResolverContext::new(
1274 &db,
1275 ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()),
1276 ModuleResolveMode::Typing,
1277 );
1278
1279 let importlib_namespace_package = stdlib_path.join("importlib");
1280 assert_eq!(importlib_namespace_package.to_file(&resolver), None);
1281 assert!(!importlib_namespace_package.is_directory(&resolver));
1282 assert!(!importlib_namespace_package.is_regular_package(&resolver));
1283
1284 let importlib_abc = stdlib_path.join("importlib/abc.pyi");
1285 assert_eq!(importlib_abc.to_file(&resolver), None);
1286 assert!(!importlib_abc.is_directory(&resolver));
1287 assert!(!importlib_abc.is_regular_package(&resolver));
1288 }
1289
1290 #[test]
1291 fn mocked_typeshed_nonexistent_single_file_module_py38() {
1292 const TYPESHED: MockedTypeshed = MockedTypeshed {
1293 versions: "foo: 2.6-",
1294 stdlib_files: &[("foo.pyi", "")],
1295 };
1296
1297 let (db, stdlib_path) = py38_typeshed_test_case(TYPESHED);
1298 let resolver = ResolverContext::new(
1299 &db,
1300 ResolverEnvironment::new(&db, PythonVersion::PY38, db.search_paths()),
1301 ModuleResolveMode::Typing,
1302 );
1303
1304 let non_existent = stdlib_path.join("doesnt_even_exist");
1305 assert_eq!(non_existent.to_file(&resolver), None);
1306 assert!(!non_existent.is_directory(&resolver));
1307 assert!(!non_existent.is_regular_package(&resolver));
1308 }
1309
1310 #[test]
1311 fn mocked_typeshed_existing_regular_stdlib_pkgs_py39() {
1312 const VERSIONS: &str = "\
1313 asyncio: 3.8-
1314 asyncio.tasks: 3.9-3.11
1315 collections: 3.9-
1316 ";
1317
1318 const STDLIB: &[FileSpec] = &[
1319 ("asyncio/__init__.pyi", ""),
1320 ("asyncio/tasks.pyi", ""),
1321 ("collections/__init__.pyi", ""),
1322 ];
1323
1324 const TYPESHED: MockedTypeshed = MockedTypeshed {
1325 versions: VERSIONS,
1326 stdlib_files: STDLIB,
1327 };
1328
1329 let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
1330 let resolver = ResolverContext::new(
1331 &db,
1332 ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()),
1333 ModuleResolveMode::Typing,
1334 );
1335
1336 let collections_regular_package = stdlib_path.join("collections");
1339 assert!(collections_regular_package.is_directory(&resolver));
1340 assert!(collections_regular_package.is_regular_package(&resolver));
1341 assert_eq!(collections_regular_package.to_file(&resolver), None);
1343 assert!(
1344 collections_regular_package
1345 .join("__init__.pyi")
1346 .to_file(&resolver)
1347 .is_some()
1348 );
1349
1350 let asyncio_tasks_module = stdlib_path.join("asyncio/tasks.pyi");
1352 assert!(asyncio_tasks_module.to_file(&resolver).is_some());
1353 assert!(!asyncio_tasks_module.is_directory(&resolver));
1354 assert!(!asyncio_tasks_module.is_regular_package(&resolver));
1355 }
1356
1357 #[test]
1358 fn mocked_typeshed_existing_namespace_stdlib_pkg_py39() {
1359 const TYPESHED: MockedTypeshed = MockedTypeshed {
1360 versions: "importlib: 3.9-",
1361 stdlib_files: &[("importlib/abc.pyi", "")],
1362 };
1363
1364 let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
1365 let resolver = ResolverContext::new(
1366 &db,
1367 ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()),
1368 ModuleResolveMode::Typing,
1369 );
1370
1371 let importlib_namespace_package = stdlib_path.join("importlib");
1373 assert!(importlib_namespace_package.is_directory(&resolver));
1374 assert!(!importlib_namespace_package.is_regular_package(&resolver));
1375 assert_eq!(importlib_namespace_package.to_file(&resolver), None);
1377
1378 let importlib_abc = importlib_namespace_package.join("abc.pyi");
1380 assert!(!importlib_abc.is_directory(&resolver));
1381 assert!(!importlib_abc.is_regular_package(&resolver));
1382 assert!(importlib_abc.to_file(&resolver).is_some());
1383 }
1384
1385 #[test]
1386 fn mocked_typeshed_nonexistent_namespace_stdlib_pkg_py39() {
1387 const TYPESHED: MockedTypeshed = MockedTypeshed {
1388 versions: "xml: 3.8-3.8",
1389 stdlib_files: &[("xml/etree.pyi", "")],
1390 };
1391
1392 let (db, stdlib_path) = py39_typeshed_test_case(TYPESHED);
1393 let resolver = ResolverContext::new(
1394 &db,
1395 ResolverEnvironment::new(&db, PythonVersion::PY39, db.search_paths()),
1396 ModuleResolveMode::Typing,
1397 );
1398
1399 let xml_namespace_package = stdlib_path.join("xml");
1401 assert_eq!(xml_namespace_package.to_file(&resolver), None);
1402 assert!(!xml_namespace_package.is_directory(&resolver));
1403 assert!(!xml_namespace_package.is_regular_package(&resolver));
1404
1405 let xml_etree = xml_namespace_package.join("etree.pyi");
1406 assert_eq!(xml_etree.to_file(&resolver), None);
1407 assert!(!xml_etree.is_directory(&resolver));
1408 assert!(!xml_etree.is_regular_package(&resolver));
1409 }
1410
1411 #[test]
1412 fn strip_not_top_level_stubs_suffix() {
1413 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1414 let sp = SearchPath::first_party(db.system(), src).unwrap();
1415 let mut mp = sp.to_module_path();
1416 mp.push("foo-stubs");
1417 mp.push("quux");
1418 assert_eq!(
1419 mp.to_module_name(),
1420 Some(ModuleName::new_static("foo.quux").unwrap())
1421 );
1422 }
1423
1424 #[test]
1431 fn strip_top_level_stubs_suffix() {
1432 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1433 let sp = SearchPath::first_party(db.system(), src).unwrap();
1434 let mut mp = sp.to_module_path();
1435 mp.push("foo-stubs");
1436 assert_eq!(
1437 mp.to_module_name(),
1438 Some(ModuleName::new_static("foo").unwrap())
1439 );
1440 }
1441
1442 #[test]
1446 fn no_strip_with_extension() {
1447 let TestCase { db, src, .. } = TestCaseBuilder::new().build();
1448 let sp = SearchPath::first_party(db.system(), src).unwrap();
1449 let mut mp = sp.to_module_path();
1450 mp.push("foo-stubs.pyi");
1451 assert_eq!(mp.to_module_name(), None);
1452 }
1453}