1use std::borrow::Cow;
35use std::iter::FusedIterator;
36
37use rustc_hash::{FxBuildHasher, FxHashSet};
38
39use ruff_db::PythonFile;
40use ruff_db::files::{File, FilePath, FileRootKind, directory_listing, system_path_to_file};
41use ruff_db::source::source_text;
42use ruff_db::system::{System, SystemPath, SystemPathBuf};
43use ruff_db::vendored::VendoredFileSystem;
44use ruff_python_ast::{
45 self as ast, PySourceType,
46 visitor::{Visitor, walk_body},
47};
48
49use crate::db::Db;
50use crate::module::{Module, ModuleKind};
51use crate::module_name::{ImportingFile, ModuleName};
52use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
53use crate::strategy::MisconfigurationStrategy;
54use crate::typeshed::{TypeshedVersions, vendored_typeshed_versions};
55use crate::{ResolverEnvironment, ResolverFile, SearchPathSettings, SearchPathSettingsError};
56
57pub fn resolve_module<'db>(
59 db: &'db dyn Db,
60 importing_file: ImportingFile<'db>,
61 module_name: &ModuleName,
62) -> Option<Module<'db>> {
63 let resolver_environment = importing_file.resolver_environment(db);
64 let interned_name = ModuleNameIngredient::new(
65 db,
66 module_name,
67 ModuleResolveMode::Typing,
68 resolver_environment,
69 );
70
71 resolve_module_query(db, interned_name)
72 .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name))
73}
74
75pub fn resolve_module_for_import_from<'db>(
79 db: &'db dyn Db,
80 importing_file: ImportingFile<'db>,
81 import: &ast::StmtImportFrom,
82) -> Option<Module<'db>> {
83 let module_name = ModuleName::from_import_statement(db, importing_file, import).ok()?;
84 resolve_module(db, importing_file, &module_name)
85}
86
87pub fn resolve_module_confident<'db>(
92 db: &'db dyn Db,
93 resolver_environment: ResolverEnvironment<'db>,
94 module_name: &ModuleName,
95) -> Option<Module<'db>> {
96 let interned_name = ModuleNameIngredient::new(
97 db,
98 module_name,
99 ModuleResolveMode::Typing,
100 resolver_environment,
101 );
102
103 resolve_module_query(db, interned_name)
104}
105
106pub fn resolve_real_module<'db>(
108 db: &'db dyn Db,
109 importing_file: ImportingFile<'db>,
110 module_name: &ModuleName,
111) -> Option<Module<'db>> {
112 let resolver_environment = importing_file.resolver_environment(db);
113 let interned_name = ModuleNameIngredient::new(
114 db,
115 module_name,
116 ModuleResolveMode::Runtime,
117 resolver_environment,
118 );
119
120 resolve_module_query(db, interned_name)
121 .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name))
122}
123
124pub fn resolve_real_module_confident<'db>(
129 db: &'db dyn Db,
130 resolver_environment: ResolverEnvironment<'db>,
131 module_name: &ModuleName,
132) -> Option<Module<'db>> {
133 let interned_name = ModuleNameIngredient::new(
134 db,
135 module_name,
136 ModuleResolveMode::Runtime,
137 resolver_environment,
138 );
139
140 resolve_module_query(db, interned_name)
141}
142
143pub fn resolve_real_shadowable_module<'db>(
155 db: &'db dyn Db,
156 importing_file: ImportingFile<'db>,
157 module_name: &ModuleName,
158) -> Option<Module<'db>> {
159 let resolver_environment = importing_file.resolver_environment(db);
160 let interned_name = ModuleNameIngredient::new(
161 db,
162 module_name,
163 ModuleResolveMode::RuntimeSomeShadowingAllowed,
164 resolver_environment,
165 );
166
167 resolve_module_query(db, interned_name)
168 .or_else(|| desperately_resolve_module(db, importing_file.file(db), interned_name))
169}
170
171#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)]
173pub enum ModuleResolveMode {
174 Typing,
179
180 Runtime,
186
187 RuntimeSomeShadowingAllowed,
194}
195
196#[salsa::interned(heap_size=ruff_memory_usage::heap_size)]
197#[derive(Debug)]
198pub(crate) struct ModuleResolveModeIngredient<'db> {
199 #[returns(copy)]
200 resolver_environment: ResolverEnvironment<'db>,
201 #[returns(copy)]
202 mode: ModuleResolveMode,
203}
204
205impl ModuleResolveMode {
206 fn is_typing(self) -> bool {
207 matches!(self, Self::Typing)
208 }
209
210 pub(super) fn is_non_shadowable(self, minor_version: u8, module_name: &str) -> bool {
224 if ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name) {
226 return true;
227 }
228 if module_name == "types" {
230 return true;
231 }
232
233 match self {
236 ModuleResolveMode::Typing | ModuleResolveMode::Runtime => {
237 module_name == "typing_extensions"
238 }
239 ModuleResolveMode::RuntimeSomeShadowingAllowed => false,
240 }
241 }
242}
243
244#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)]
249fn resolve_module_query<'db>(
250 db: &'db dyn Db,
251 module_name: ModuleNameIngredient<'db>,
252) -> Option<Module<'db>> {
253 let name = module_name.name(db);
254 let mode = module_name.mode(db);
255 let resolver_environment = module_name.resolver_environment(db);
256 let _span = tracing::trace_span!("resolve_module", %name).entered();
257
258 let Some(resolved) = resolve_name(db, resolver_environment, name, mode) else {
259 tracing::debug!("Module `{name}` not found in search paths");
260 return None;
261 };
262
263 resolved
264 .into_iter()
265 .next()
266 .map(|candidate| candidate.into_module(db, resolver_environment, name))
267}
268
269#[salsa::tracked(returns(copy))]
282fn desperately_resolve_module<'db>(
283 db: &'db dyn Db,
284 importing_file: File,
285 module_name: ModuleNameIngredient<'db>,
286) -> Option<Module<'db>> {
287 let name = module_name.name(db);
288 let mode = module_name.mode(db);
289 let resolver_environment = module_name.resolver_environment(db);
290 let _span = tracing::trace_span!("desperately_resolve_module", %name).entered();
291
292 let Some(resolved) =
293 desperately_resolve_name(db, importing_file, resolver_environment, name, mode)
294 else {
295 let mode = match mode {
296 ModuleResolveMode::Typing => "typing mode",
297 ModuleResolveMode::Runtime => "runtime mode",
298 ModuleResolveMode::RuntimeSomeShadowingAllowed => {
299 "runtime mode with some shadowing allowed"
300 }
301 };
302 tracing::debug!("Module `{name}` not found while looking in parent dirs ({mode})");
303 return None;
304 };
305
306 resolved
307 .into_iter()
308 .next()
309 .map(|candidate| candidate.into_module(db, resolver_environment, name))
310}
311
312#[allow(unused)]
316pub(crate) fn path_to_module<'db>(
317 db: &'db dyn Db,
318 resolver_environment: ResolverEnvironment<'db>,
319 path: &FilePath,
320) -> Option<Module<'db>> {
321 let file = path.to_file(db)?;
330 file_to_module(db, ResolverFile::new(db, file, resolver_environment))
331}
332
333#[salsa::tracked(returns(copy), heap_size=ruff_memory_usage::heap_size)]
342pub fn file_to_module<'db>(
343 db: &'db dyn Db,
344 resolver_file: ResolverFile<'db>,
345) -> Option<Module<'db>> {
346 let resolver_environment = resolver_file.environment(db);
347 let file = resolver_file.file(db);
348 let _span = tracing::trace_span!("file_to_module", ?file).entered();
349
350 let path = SystemOrVendoredPathRef::try_from_file(db, file)?;
351
352 file_to_module_impl(
353 db,
354 resolver_file,
355 path,
356 search_paths(db, resolver_environment, ModuleResolveMode::Typing),
357 )
358 .or_else(|| {
359 file_to_module_impl(
360 db,
361 resolver_file,
362 path,
363 relative_desperate_search_paths(db, resolver_file).iter(),
364 )
365 })
366}
367
368fn file_to_module_impl<'db, 'a>(
369 db: &'db dyn Db,
370 resolver_file: ResolverFile<'db>,
371 path: SystemOrVendoredPathRef<'a>,
372 mut search_paths: impl Iterator<Item = &'a SearchPath>,
373) -> Option<Module<'db>> {
374 let module_name = search_paths.find_map(|candidate: &SearchPath| {
375 let relative_path = match path {
376 SystemOrVendoredPathRef::System(path) => candidate.relativize_system_path(path),
377 SystemOrVendoredPathRef::Vendored(path) => candidate.relativize_vendored_path(path),
378 }?;
379 relative_path.to_module_name()
380 })?;
381
382 let module = resolve_module(db, ImportingFile::ResolverFile(resolver_file), &module_name)?;
387 let module_file = module.file(db)?;
388
389 let file: File = resolver_file.file(db);
390 let file_path = file.path(db);
391 if file_path == module_file.path(db) {
392 return Some(module);
393 } else if file.source_type(db) == PySourceType::Python
394 && module_file.source_type(db) == PySourceType::Stub
395 {
396 let module =
400 resolve_real_module(db, ImportingFile::ResolverFile(resolver_file), &module_name)?;
401 let module_file = module.file(db)?;
402 if file_path == module_file.path(db) {
403 return Some(module);
404 }
405 }
406 None
414}
415
416pub fn search_paths<'db>(
417 db: &'db dyn Db,
418 resolver_environment: ResolverEnvironment<'db>,
419 resolve_mode: ModuleResolveMode,
420) -> SearchPathIterator<'db> {
421 let search_paths = resolver_environment.search_paths(db);
422
423 SearchPathIterator {
424 db,
425 static_paths: search_paths.static_paths.iter(),
426 stdlib_path: search_paths.stdlib(resolve_mode),
427 dynamic_paths: None,
428 mode: ModuleResolveModeIngredient::new(db, resolver_environment, resolve_mode),
429 }
430}
431
432#[derive(Debug, Clone, Copy, Default)]
433struct StubPackagePaths<'a> {
434 before_stdlib: &'a [SearchPath],
435 after_stdlib: &'a [SearchPath],
436}
437
438impl StubPackagePaths<'_> {
439 fn is_empty(self) -> bool {
440 self.before_stdlib.is_empty() && self.after_stdlib.is_empty()
441 }
442}
443
444#[derive(Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
445struct StubPackageIndex {
446 paths: Box<[SearchPath]>,
447 stdlib_offset: usize,
448}
449
450impl StubPackageIndex {
451 fn from_search_paths<'a>(
454 db: &dyn Db,
455 search_paths: impl Iterator<Item = &'a SearchPath>,
456 ) -> Self {
457 let mut paths = Vec::new();
458 let mut stdlib_offset = None;
459
460 for search_path in search_paths {
461 if search_path.is_standard_library() {
462 stdlib_offset = Some(paths.len());
463 } else if search_path_may_contain_stub_package(db, search_path) {
464 paths.push(search_path.clone());
465 }
466 }
467
468 let stdlib_offset = stdlib_offset.unwrap_or(paths.len());
469 Self {
470 paths: paths.into_boxed_slice(),
471 stdlib_offset,
472 }
473 }
474
475 fn all(&self) -> StubPackagePaths<'_> {
477 StubPackagePaths {
478 before_stdlib: self.before_stdlib(),
479 after_stdlib: self.after_stdlib(),
480 }
481 }
482
483 fn split_overlay(&self) -> (StubPackagePaths<'_>, StubPackagePaths<'_>) {
488 let before_stdlib = self.before_stdlib();
489 let (extra, remaining) =
490 before_stdlib.split_at(before_stdlib.partition_point(SearchPath::is_extra));
491
492 (
493 StubPackagePaths {
494 before_stdlib: extra,
495 after_stdlib: &[],
496 },
497 StubPackagePaths {
498 before_stdlib: remaining,
499 after_stdlib: self.after_stdlib(),
500 },
501 )
502 }
503
504 fn before_stdlib(&self) -> &[SearchPath] {
506 &self.paths[..self.stdlib_offset]
507 }
508
509 fn after_stdlib(&self) -> &[SearchPath] {
511 &self.paths[self.stdlib_offset..]
512 }
513}
514
515#[salsa::tracked(returns(ref), heap_size=ruff_memory_usage::heap_size)]
518fn stub_package_index(
519 db: &dyn Db,
520 resolver_environment: ResolverEnvironment<'_>,
521) -> StubPackageIndex {
522 StubPackageIndex::from_search_paths(
523 db,
524 search_paths(db, resolver_environment, ModuleResolveMode::Typing),
525 )
526}
527
528fn search_path_may_contain_stub_package(db: &dyn Db, search_path: &SearchPath) -> bool {
529 let Some(path) = search_path.as_system_path() else {
530 return false;
531 };
532
533 directory_listing(db, path)
534 .is_ok_and(|listing| listing.iter().any(|(name, _)| name.ends_with("-stubs")))
535}
536
537#[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)]
547fn absolute_desperate_search_paths(
548 db: &dyn Db,
549 importing_file: ResolverFile<'_>,
550) -> Option<Box<[SearchPath]>> {
551 let resolver_environment = importing_file.environment(db);
552 let importing_file = importing_file.file(db);
553 let system = db.system();
554 let importing_path = importing_file.path(db).as_system_path()?;
555
556 let (base_path, rel_path) = search_paths(db, resolver_environment, ModuleResolveMode::Typing)
558 .find_map(|search_path| {
559 if !search_path.is_first_party() {
560 return None;
561 }
562 Some((
563 search_path.as_system_path()?,
564 search_path.relativize_system_path_only(importing_path)?,
565 ))
566 })?;
567
568 let mut search_paths = Vec::new();
570 for rel_dir in rel_path.ancestors() {
571 let candidate_path = base_path.join(rel_dir);
572 let Ok(listing) = directory_listing(db, &candidate_path) else {
573 continue;
574 };
575 let isnt_regular_package = !listing.entry_is_file(db, &candidate_path, "__init__.py")
580 && !listing.entry_is_file(db, &candidate_path, "__init__.pyi");
581 if isnt_regular_package
586 || listing.entry_is_file(db, &candidate_path, "pyproject.toml")
587 || listing.entry_is_file(db, &candidate_path, "ty.toml")
588 {
589 let search_path = SearchPath::first_party(system, candidate_path).ok()?;
590 search_paths.push(search_path);
591 }
592 }
593
594 if search_paths.is_empty() {
595 None
596 } else {
597 Some(search_paths.into_boxed_slice())
598 }
599}
600
601#[salsa::tracked(returns(clone), heap_size=ruff_memory_usage::heap_size)]
616fn relative_desperate_search_paths(
617 db: &dyn Db,
618 importing_file: ResolverFile<'_>,
619) -> Option<SearchPath> {
620 let resolver_environment = importing_file.environment(db);
621 let importing_file = importing_file.file(db);
622 let system = db.system();
623 let importing_path = importing_file.path(db).as_system_path()?;
624
625 let (base_path, rel_path) = search_paths(db, resolver_environment, ModuleResolveMode::Typing)
627 .find_map(|search_path| {
628 if !search_path.is_first_party() {
629 return None;
630 }
631 Some((
632 search_path.as_system_path()?,
633 search_path.relativize_system_path_only(importing_path)?,
634 ))
635 })?;
636
637 for rel_dir in rel_path.ancestors() {
639 let candidate_path = base_path.join(rel_dir);
640 let Ok(listing) = directory_listing(db, &candidate_path) else {
641 continue;
642 };
643 if listing.entry_is_file(db, &candidate_path, "pyproject.toml")
645 || listing.entry_is_file(db, &candidate_path, "ty.toml")
646 {
647 let search_path = SearchPath::first_party(system, candidate_path).ok()?;
648 return Some(search_path);
649 }
650 }
651
652 None
653}
654#[derive(Clone, Debug, PartialEq, Eq, Hash, get_size2::GetSize)]
655pub struct SearchPaths {
656 static_paths: Vec<SearchPath>,
660
661 stdlib_path: Option<SearchPath>,
665
666 real_stdlib_path: Option<SearchPath>,
669
670 site_packages: Vec<SearchPath>,
677
678 typeshed_versions: TypeshedVersions,
679}
680
681impl SearchPaths {
682 pub(crate) fn from_settings<Strategy: MisconfigurationStrategy>(
689 settings: &SearchPathSettings,
690 system: &dyn System,
691 vendored: &VendoredFileSystem,
692 strategy: &Strategy,
693 ) -> Result<Self, Strategy::Error<SearchPathSettingsError>> {
694 fn canonicalize(path: &SystemPath, system: &dyn System) -> SystemPathBuf {
695 system
696 .canonicalize_path(path)
697 .unwrap_or_else(|_| path.to_path_buf())
698 }
699
700 let SearchPathSettings {
701 extra_paths,
702 src_roots,
703 custom_typeshed: typeshed,
704 site_packages_paths,
705 real_stdlib_path,
706 } = settings;
707
708 let mut static_paths = vec![];
709
710 for path in extra_paths {
711 let path = canonicalize(path, system);
712 tracing::debug!("Adding extra search-path `{path}`");
713
714 let path = strategy.fallback_opt(
715 SearchPath::extra(system, path).map_err(SearchPathSettingsError::from),
716 |err| {
717 tracing::debug!("Skipping invalid extra search-path: {err}");
718 },
719 )?;
720 static_paths.extend(path);
721 }
722
723 for src_root in src_roots {
724 tracing::debug!("Adding first-party search path `{src_root}`");
725 let path = strategy.fallback_opt(
726 SearchPath::first_party(system, src_root.to_path_buf())
727 .map_err(SearchPathSettingsError::from),
728 |err| {
729 tracing::debug!("Skipping invalid first-party search-path: {err}");
730 },
731 )?;
732 static_paths.extend(path);
733 }
734
735 let (typeshed_versions, stdlib_path) = if let Some(typeshed) = typeshed {
736 let typeshed = canonicalize(typeshed, system);
737 tracing::debug!("Adding custom-stdlib search path `{typeshed}`");
738
739 let versions_path = typeshed.join("stdlib/VERSIONS");
740
741 let results = system
742 .read_to_string(&versions_path)
743 .map_err(|error| SearchPathSettingsError::FailedToReadVersionsFile {
744 path: versions_path,
745 error,
746 })
747 .and_then(|versions_content| Ok(versions_content.parse()?))
748 .and_then(|parsed| Ok((parsed, SearchPath::custom_stdlib(system, &typeshed)?)));
749
750 strategy.fallback(results, |err| {
751 tracing::debug!("Skipping custom-stdlib search-path: {err}");
752 (
753 vendored_typeshed_versions(vendored),
754 SearchPath::vendored_stdlib(),
755 )
756 })?
757 } else {
758 tracing::debug!("Using vendored stdlib");
759 (
760 vendored_typeshed_versions(vendored),
761 SearchPath::vendored_stdlib(),
762 )
763 };
764
765 let real_stdlib_path = if let Some(path) = real_stdlib_path {
766 strategy.fallback_opt(
767 SearchPath::real_stdlib(system, path.clone())
768 .map_err(SearchPathSettingsError::from),
769 |err| {
770 tracing::debug!("Skipping invalid real-stdlib search-path: {err}");
771 },
772 )?
773 } else {
774 None
775 };
776
777 let mut site_packages: Vec<_> = Vec::with_capacity(site_packages_paths.len());
778
779 for path in site_packages_paths {
780 tracing::debug!("Adding site-packages search path `{path}`");
781 let path = strategy.fallback_opt(
782 SearchPath::site_packages(system, path.clone())
783 .map_err(SearchPathSettingsError::from),
784 |err| {
785 tracing::debug!("Skipping invalid site-packages search-path: {err}");
786 },
787 )?;
788 site_packages.extend(path);
789 }
790
791 let mut seen_paths = FxHashSet::with_capacity_and_hasher(static_paths.len(), FxBuildHasher);
806
807 static_paths.retain(|path| {
808 if let Some(path) = path.as_system_path() {
809 seen_paths.insert(path.to_path_buf())
810 } else {
811 true
812 }
813 });
814
815 let stdlib_path_is_shadowed = stdlib_path
822 .as_system_path()
823 .is_some_and(|path| seen_paths.contains(path));
824 let real_stdlib_path_is_shadowed = real_stdlib_path
825 .as_ref()
826 .and_then(SearchPath::as_system_path)
827 .is_some_and(|path| seen_paths.contains(path));
828
829 let stdlib_path = if stdlib_path_is_shadowed {
830 None
831 } else {
832 Some(stdlib_path)
833 };
834 let real_stdlib_path = if real_stdlib_path_is_shadowed {
835 None
836 } else {
837 real_stdlib_path
838 };
839
840 Ok(SearchPaths {
841 static_paths,
842 stdlib_path,
843 real_stdlib_path,
844 site_packages,
845 typeshed_versions,
846 })
847 }
848
849 pub fn empty(vendored: &VendoredFileSystem) -> Self {
853 Self {
854 static_paths: vec![],
855 stdlib_path: Some(SearchPath::vendored_stdlib()),
856 real_stdlib_path: None,
857 site_packages: vec![],
858 typeshed_versions: vendored_typeshed_versions(vendored),
859 }
860 }
861
862 pub fn try_register_static_roots(&self, db: &dyn Db) {
864 let files = db.files();
865 for path in self
866 .static_paths
867 .iter()
868 .chain(self.site_packages.iter())
869 .chain(&self.stdlib_path)
870 {
871 if let Some(system_path) = path.as_system_path() {
872 if !path.is_first_party() || files.root(db, system_path).is_none() {
875 files.try_add_root(db, system_path, FileRootKind::SearchPath);
876 }
877 }
878 }
879 }
880
881 fn stdlib(&self, mode: ModuleResolveMode) -> Option<&SearchPath> {
882 match mode {
883 ModuleResolveMode::Typing => self.stdlib_path.as_ref(),
884 ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => {
885 self.real_stdlib_path.as_ref()
886 }
887 }
888 }
889
890 pub fn custom_stdlib(&self) -> Option<&SystemPath> {
891 self.stdlib_path
892 .as_ref()
893 .and_then(SearchPath::as_system_path)
894 }
895
896 pub fn typeshed_versions(&self) -> &TypeshedVersions {
897 &self.typeshed_versions
898 }
899}
900
901#[salsa::tracked(returns(deref), heap_size=ruff_memory_usage::heap_size)]
910pub(crate) fn dynamic_resolution_paths<'db>(
911 db: &'db dyn Db,
912 mode: ModuleResolveModeIngredient<'db>,
913) -> Vec<SearchPath> {
914 tracing::debug!("Resolving dynamic module resolution paths");
915
916 let SearchPaths {
917 static_paths,
918 stdlib_path,
919 site_packages,
920 typeshed_versions: _,
921 real_stdlib_path,
922 } = mode.resolver_environment(db).search_paths(db);
923
924 let mut dynamic_paths = Vec::new();
925
926 if site_packages.is_empty() {
927 return dynamic_paths;
928 }
929
930 let mut existing_paths: FxHashSet<_> = static_paths
931 .iter()
932 .filter_map(|path| path.as_system_path())
933 .map(Cow::Borrowed)
934 .collect();
935
936 let stdlib = match mode.mode(db) {
938 ModuleResolveMode::Typing => stdlib_path,
939 ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => {
940 real_stdlib_path
941 }
942 };
943 if let Some(path) = stdlib.as_ref().and_then(SearchPath::as_system_path) {
944 existing_paths.insert(Cow::Borrowed(path));
945 }
946
947 let files = db.files();
948 let system = db.system();
949
950 for site_packages_search_path in site_packages {
951 let site_packages_dir = site_packages_search_path
952 .as_system_path()
953 .expect("Expected site package path to be a system path");
954
955 if !existing_paths.insert(Cow::Borrowed(site_packages_dir)) {
956 continue;
957 }
958
959 dynamic_paths.push(site_packages_search_path.clone());
960
961 let listing = match directory_listing(db, site_packages_dir) {
968 Ok(listing) => listing,
969 Err(error) => {
970 tracing::warn!(
971 "Failed to search for editable installation in {site_packages_dir}: {error}"
972 );
973 continue;
974 }
975 };
976
977 let pth_files = listing.iter().filter(|(name, file_type)| {
981 !file_type.is_directory() && SystemPath::new(name).extension() == Some("pth")
982 });
983
984 for (name, _) in pth_files {
985 let path = site_packages_dir.join(name);
986 let Ok(file) = system_path_to_file(db, &path).inspect_err(|error| {
988 tracing::warn!("Failed to open .pth file `{path}`: {error}");
989 }) else {
990 continue;
991 };
992 let contents = source_text(db, file);
993 if let Some(error) = contents.read_error() {
994 tracing::warn!("Failed to read .pth file `{path}`: {error}");
995 continue;
996 }
997
998 let installations = contents.lines().filter_map(|line| {
999 let line = line.trim_end();
1000 if line.is_empty()
1001 || line.starts_with('#')
1002 || line.starts_with("import ")
1003 || line.starts_with("import\t")
1004 {
1005 return None;
1006 }
1007
1008 Some(SystemPath::absolute(line, site_packages_dir))
1009 });
1010
1011 for installation in installations {
1012 let installation = system
1013 .canonicalize_path(&installation)
1014 .unwrap_or(installation);
1015
1016 if existing_paths.insert(Cow::Owned(installation.clone())) {
1017 match SearchPath::editable(system, installation.clone()) {
1018 Ok(search_path) => {
1019 tracing::debug!(
1020 "Adding editable installation to module resolution path {path}",
1021 path = installation
1022 );
1023
1024 if let Some(dynamic_path) = search_path.as_system_path() {
1031 if files.root(db, dynamic_path).is_none() {
1032 files.try_add_root(db, dynamic_path, FileRootKind::SearchPath);
1033 }
1034 }
1035
1036 dynamic_paths.push(search_path);
1037 }
1038
1039 Err(error) => {
1040 tracing::debug!("Skipping editable installation: {error}");
1041 }
1042 }
1043 }
1044 }
1045 }
1046 }
1047
1048 dynamic_paths
1049}
1050
1051pub struct SearchPathIterator<'db> {
1059 db: &'db dyn Db,
1060 static_paths: std::slice::Iter<'db, SearchPath>,
1061 stdlib_path: Option<&'db SearchPath>,
1062 dynamic_paths: Option<std::slice::Iter<'db, SearchPath>>,
1063 mode: ModuleResolveModeIngredient<'db>,
1064}
1065
1066impl<'db> Iterator for SearchPathIterator<'db> {
1067 type Item = &'db SearchPath;
1068
1069 fn next(&mut self) -> Option<Self::Item> {
1070 let SearchPathIterator {
1071 db,
1072 static_paths,
1073 stdlib_path,
1074 mode,
1075 dynamic_paths,
1076 } = self;
1077
1078 static_paths
1079 .next()
1080 .or_else(|| stdlib_path.take())
1081 .or_else(|| {
1082 dynamic_paths
1083 .get_or_insert_with(|| dynamic_resolution_paths(*db, *mode).iter())
1084 .next()
1085 })
1086 }
1087}
1088
1089impl FusedIterator for SearchPathIterator<'_> {}
1090
1091#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
1096struct ModuleNameIngredient<'db> {
1097 #[returns(ref)]
1098 pub(super) name: ModuleName,
1099 #[returns(copy)]
1100 pub(super) mode: ModuleResolveMode,
1101 #[returns(copy)]
1102 pub(super) resolver_environment: ResolverEnvironment<'db>,
1103}
1104
1105fn resolve_name<'db>(
1108 db: &'db dyn Db,
1109 resolver_environment: ResolverEnvironment<'db>,
1110 name: &ModuleName,
1111 mode: ModuleResolveMode,
1112) -> Option<ResolvedNames> {
1113 let resolver = NameResolver::new(db, resolver_environment, name, mode);
1114
1115 match mode {
1116 ModuleResolveMode::Typing => {
1117 resolver.resolve_typing(stub_package_index(db, resolver_environment))
1118 }
1119 ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => {
1120 resolver.resolve_runtime(search_paths(db, resolver_environment, mode))
1121 }
1122 }
1123}
1124
1125fn desperately_resolve_name<'db>(
1130 db: &'db dyn Db,
1131 importing_file: File,
1132 resolver_environment: ResolverEnvironment<'db>,
1133 name: &ModuleName,
1134 mode: ModuleResolveMode,
1135) -> Option<ResolvedNames> {
1136 let importing_file = ResolverFile::new(db, importing_file, resolver_environment);
1137 let search_paths = absolute_desperate_search_paths(db, importing_file).unwrap_or_default();
1138 let resolver = NameResolver::new(db, resolver_environment, name, mode);
1139
1140 match mode {
1141 ModuleResolveMode::Typing => resolver.resolve_desperate_typing(search_paths),
1142 ModuleResolveMode::Runtime | ModuleResolveMode::RuntimeSomeShadowingAllowed => {
1143 resolver.resolve_runtime(search_paths.iter())
1144 }
1145 }
1146}
1147
1148#[derive(Debug, Clone, Copy)]
1149enum ResolvedModule {
1150 NamespacePackage,
1151 LegacyNamespacePackage(File),
1152 RegularPackage(File),
1153 Module(File),
1154}
1155
1156#[derive(Debug, Copy, Clone, PartialEq, Eq)]
1157enum ComponentFileFilter {
1158 ByMode,
1160
1161 StubOnly,
1163}
1164
1165#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1171enum CandidatePrecedence {
1172 StubPackage,
1177
1178 SearchPathOrder,
1183}
1184
1185#[derive(Debug, Clone)]
1186struct ModuleResolutionCandidate {
1187 path: ModulePath,
1188 module: ResolvedModule,
1189 py_typed: PyTyped,
1190 precedence: CandidatePrecedence,
1191}
1192
1193impl ModuleResolutionCandidate {
1194 fn root(search_path: &SearchPath) -> Self {
1195 Self::with_precedence(search_path, CandidatePrecedence::SearchPathOrder)
1196 }
1197
1198 fn stub(search_path: &SearchPath) -> Self {
1199 Self::with_precedence(search_path, CandidatePrecedence::StubPackage)
1200 }
1201
1202 fn with_precedence(search_path: &SearchPath, precedence: CandidatePrecedence) -> Self {
1203 Self {
1204 path: search_path.to_module_path(),
1205 module: ResolvedModule::NamespacePackage,
1206 py_typed: PyTyped::Untyped,
1207 precedence,
1208 }
1209 }
1210
1211 fn is_any_namespace_package(&self) -> bool {
1213 match self.module {
1214 ResolvedModule::NamespacePackage => true,
1215 ResolvedModule::LegacyNamespacePackage(_) => true,
1216 ResolvedModule::RegularPackage(_) => false,
1217 ResolvedModule::Module(_) => false,
1218 }
1219 }
1220
1221 fn into_module<'db>(
1223 self,
1224 db: &'db dyn Db,
1225 resolver_environment: ResolverEnvironment<'db>,
1226 name: &ModuleName,
1227 ) -> Module<'db> {
1228 match self.module {
1229 ResolvedModule::NamespacePackage => {
1230 tracing::trace!("Resolve namespace package `{name}`");
1231 Module::namespace_package(db, resolver_environment, Cow::Borrowed(name))
1232 }
1233 ResolvedModule::LegacyNamespacePackage(file) => {
1234 tracing::trace!(
1237 "Resolved legacy namespace package `{name}` to `{path}`",
1238 path = file.path(db)
1239 );
1240 Module::file_module(
1241 db,
1242 file,
1243 resolver_environment,
1244 Cow::Borrowed(name),
1245 ModuleKind::Package,
1246 self.path.into_search_path(),
1247 )
1248 }
1249 ResolvedModule::RegularPackage(file) => {
1250 tracing::trace!(
1251 "Resolved package `{name}` to `{path}`",
1252 path = file.path(db)
1253 );
1254 Module::file_module(
1255 db,
1256 file,
1257 resolver_environment,
1258 Cow::Borrowed(name),
1259 ModuleKind::Package,
1260 self.path.into_search_path(),
1261 )
1262 }
1263 ResolvedModule::Module(file) => {
1264 tracing::trace!("Resolved module `{name}` to `{path}`", path = file.path(db));
1265 Module::file_module(
1266 db,
1267 file,
1268 resolver_environment,
1269 Cow::Borrowed(name),
1270 ModuleKind::Module,
1271 self.path.into_search_path(),
1272 )
1273 }
1274 }
1275 }
1276
1277 fn missing_submodule_is_terminal(&self) -> bool {
1278 if matches!(self.py_typed, PyTyped::Partial) {
1279 return false;
1280 }
1281
1282 matches!(
1287 self.module,
1288 ResolvedModule::RegularPackage(_) | ResolvedModule::Module(_)
1289 )
1290 }
1291
1292 fn to_str<'a>(&self, db: &'a dyn Db) -> Cow<'a, str> {
1293 match self.module {
1294 ResolvedModule::NamespacePackage => {
1295 Cow::Owned(self.path.to_system_path().unwrap_or_default().to_string())
1296 }
1297 ResolvedModule::LegacyNamespacePackage(file) => Cow::Borrowed(file.path(db).as_str()),
1298 ResolvedModule::RegularPackage(file) => Cow::Borrowed(file.path(db).as_str()),
1299 ResolvedModule::Module(file) => Cow::Borrowed(file.path(db).as_str()),
1300 }
1301 }
1302}
1303
1304struct NameResolver<'db, 'name> {
1305 context: ResolverContext<'db>,
1306 name: &'name ModuleName,
1307 is_non_shadowable: bool,
1308}
1309
1310impl<'db, 'name> NameResolver<'db, 'name> {
1311 fn new(
1312 db: &'db dyn Db,
1313 resolver_environment: ResolverEnvironment<'db>,
1314 name: &'name ModuleName,
1315 mode: ModuleResolveMode,
1316 ) -> Self {
1317 let python_version = resolver_environment.python_version(db);
1318 Self {
1319 context: ResolverContext::new(db, resolver_environment, mode),
1320 name,
1321 is_non_shadowable: mode.is_non_shadowable(python_version.minor, name.as_str()),
1322 }
1323 }
1324
1325 fn resolve_typing(&self, stub_packages: &StubPackageIndex) -> Option<ResolvedNames> {
1331 if self.name.components().nth(1).is_none() {
1332 let candidates = self.discover_roots(
1333 search_paths(
1334 self.context.db,
1335 self.context.resolver_environment,
1336 ModuleResolveMode::Typing,
1337 ),
1338 stub_packages.all(),
1339 );
1340 return self.resolve_remaining(candidates, ComponentFileFilter::ByMode);
1341 }
1342
1343 let (overlay_stub_packages, remaining_stub_packages) = stub_packages.split_overlay();
1347 let mut candidates = self.discover_roots(
1348 search_paths(
1349 self.context.db,
1350 self.context.resolver_environment,
1351 ModuleResolveMode::Typing,
1352 )
1353 .take_while(|search_path| search_path.is_extra()),
1354 overlay_stub_packages,
1355 );
1356 if let Some(resolved) =
1357 self.resolve_remaining(candidates.clone(), ComponentFileFilter::StubOnly)
1358 {
1359 return Some(resolved);
1360 }
1361
1362 let remaining_candidates = self.discover_roots(
1363 search_paths(
1364 self.context.db,
1365 self.context.resolver_environment,
1366 ModuleResolveMode::Typing,
1367 )
1368 .skip_while(|search_path| search_path.is_extra()),
1369 remaining_stub_packages,
1370 );
1371 candidates.extend(remaining_candidates);
1372
1373 self.resolve_remaining(candidates, ComponentFileFilter::ByMode)
1374 }
1375
1376 fn resolve_desperate_typing(&self, search_paths: &[SearchPath]) -> Option<ResolvedNames> {
1382 let stub_packages =
1383 StubPackageIndex::from_search_paths(self.context.db, search_paths.iter());
1384 let candidates = self.discover_roots(search_paths.iter(), stub_packages.all());
1385 self.resolve_remaining(candidates, ComponentFileFilter::ByMode)
1386 }
1387
1388 fn resolve_runtime<'a>(
1393 &self,
1394 search_paths: impl Iterator<Item = &'a SearchPath>,
1395 ) -> Option<ResolvedNames> {
1396 let candidates = self.discover_roots(search_paths, StubPackagePaths::default());
1397 self.resolve_remaining(candidates, ComponentFileFilter::ByMode)
1398 }
1399
1400 fn discover_roots<'a>(
1401 &self,
1402 search_paths: impl Iterator<Item = &'a SearchPath>,
1403 stub_paths: StubPackagePaths<'_>,
1404 ) -> ResolvedNames {
1405 let root_component = self.name.first_component();
1406 let mut cur_candidates = Vec::new();
1407 let stub_name = (!stub_paths.is_empty() && !self.is_non_shadowable)
1408 .then(|| format!("{root_component}-stubs"));
1409 let mut pending_stub_paths = Vec::new();
1410
1411 if let Some(stub_name) = &stub_name {
1412 cur_candidates.extend(stub_paths.before_stdlib.iter().filter_map(|search_path| {
1413 resolve_stub_package_in_search_path(&self.context, search_path, stub_name)
1414 }));
1415 pending_stub_paths.extend(stub_paths.after_stdlib.iter().filter(|search_path| {
1417 candidate_may_exist(
1418 &self.context,
1419 &ModuleResolutionCandidate::stub(search_path),
1420 stub_name,
1421 )
1422 }));
1423 }
1424
1425 for search_path in search_paths {
1426 if self.is_non_shadowable && !search_path.is_standard_library() {
1432 continue;
1433 }
1434
1435 let is_stdlib = search_path.is_standard_library();
1436 let can_stop = is_stdlib || pending_stub_paths.is_empty();
1439 let mut candidate = ModuleResolutionCandidate::root(search_path);
1440 let resolved = resolve_component(
1441 &self.context,
1442 &mut candidate,
1443 root_component,
1444 ComponentFileFilter::ByMode,
1445 )
1446 .is_ok();
1447 let terminal = candidate.missing_submodule_is_terminal();
1448 if resolved {
1449 cur_candidates.push(candidate);
1450 }
1451 if terminal && can_stop {
1454 break;
1455 }
1456
1457 if is_stdlib && let Some(stub_name) = &stub_name {
1460 cur_candidates.extend(pending_stub_paths.drain(..).filter_map(|search_path| {
1461 resolve_stub_package_in_search_path(&self.context, search_path, stub_name)
1462 }));
1463 }
1464 }
1465
1466 cur_candidates
1467 }
1468
1469 fn resolve_remaining(
1470 &self,
1471 mut cur_candidates: ResolvedNames,
1472 final_filter: ComponentFileFilter,
1473 ) -> Option<ResolvedNames> {
1474 if cur_candidates.is_empty() {
1475 return None;
1476 }
1477
1478 let mut components = self.name.components().skip(1).peekable();
1479
1480 loop {
1481 let has_remaining_components = components.peek().is_some();
1485 cur_candidates =
1486 normalize_candidates(self.context.db, cur_candidates, has_remaining_components);
1487
1488 let Some(component) = components.next() else {
1489 return Some(cur_candidates);
1490 };
1491 let file_filter = if components.peek().is_some() {
1492 ComponentFileFilter::ByMode
1493 } else {
1494 final_filter
1495 };
1496
1497 let mut remaining_are_shadowed = false;
1498 cur_candidates.retain_mut(|candidate| {
1499 if remaining_are_shadowed {
1500 return false;
1501 }
1502
1503 let resolved =
1504 resolve_component(&self.context, candidate, component, file_filter).is_ok();
1505
1506 remaining_are_shadowed = candidate.missing_submodule_is_terminal();
1509
1510 resolved
1511 });
1512
1513 if cur_candidates.is_empty() {
1514 return None;
1515 }
1516 }
1517 }
1518}
1519
1520fn resolve_stub_package_in_search_path(
1521 context: &ResolverContext,
1522 search_path: &SearchPath,
1523 stub_name: &str,
1524) -> Option<ModuleResolutionCandidate> {
1525 let mut candidate = ModuleResolutionCandidate::stub(search_path);
1526 resolve_component(
1527 context,
1528 &mut candidate,
1529 stub_name,
1530 ComponentFileFilter::ByMode,
1531 )
1532 .ok()?;
1533
1534 if matches!(candidate.module, ResolvedModule::Module(_)) {
1536 tracing::debug!(
1537 "Search path `{search_path}` contains a module named `{stub_name}` but a standalone \
1538 module isn't a valid stub."
1539 );
1540 None
1541 } else {
1542 Some(candidate)
1543 }
1544}
1545
1546fn normalize_candidates(
1547 db: &dyn Db,
1548 mut candidates: ResolvedNames,
1549 has_remaining_components: bool,
1550) -> ResolvedNames {
1551 let best_concrete_precedence = candidates
1552 .iter()
1553 .filter(|candidate| !candidate.is_any_namespace_package())
1554 .map(|candidate| candidate.precedence)
1555 .min();
1556
1557 candidates.sort_by_key(|candidate| candidate.precedence);
1558
1559 candidates.retain(|candidate| {
1565 if !candidate.is_any_namespace_package() {
1566 return true;
1567 }
1568
1569 let preserved_for_descendants = best_concrete_precedence.is_none_or(|precedence| {
1572 has_remaining_components
1573 && candidate.py_typed == PyTyped::Partial
1574 && candidate.precedence < precedence
1575 });
1576
1577 if preserved_for_descendants {
1578 return true;
1579 }
1580
1581 tracing::trace!(
1586 "Discarding namespace package `{}` because a non-namespace entry of the same name \
1587 was found",
1588 candidate.to_str(db),
1589 );
1590 false
1591 });
1592
1593 candidates
1594}
1595
1596fn resolve_component(
1598 context: &ResolverContext,
1599 candidate: &mut ModuleResolutionCandidate,
1600 module_name: &str,
1601 file_filter: ComponentFileFilter,
1602) -> Result<(), ()> {
1603 if matches!(candidate.module, ResolvedModule::Module(_)) {
1604 tracing::trace!(
1605 "Non-package module {} cannot have a child",
1606 candidate.to_str(context.db)
1607 );
1608 return Err(());
1609 }
1610
1611 if !candidate_may_exist(context, candidate, module_name) {
1612 return Err(());
1613 }
1614
1615 let package_path = &mut candidate.path;
1616 package_path.push(module_name);
1617
1618 package_path.push("__init__");
1620 if let Some(init) = resolve_file_module_with_filter(package_path, context, file_filter) {
1621 package_path.pop();
1623 candidate.py_typed = package_path
1624 .py_typed(context)
1625 .inherit_parent(candidate.py_typed);
1626 if is_legacy_namespace_package(package_path, context, init) {
1627 candidate.module = ResolvedModule::LegacyNamespacePackage(init);
1628 } else {
1629 candidate.module = ResolvedModule::RegularPackage(init);
1630 }
1631 return Ok(());
1632 }
1633
1634 package_path.pop();
1636
1637 if let Some(file_module) = resolve_file_module_with_filter(package_path, context, file_filter) {
1638 candidate.module = ResolvedModule::Module(file_module);
1639 return Ok(());
1640 }
1641
1642 if file_filter != ComponentFileFilter::StubOnly
1661 && !package_path.search_path().is_standard_library()
1662 && package_path.is_directory(context)
1663 {
1664 candidate.py_typed = package_path
1665 .py_typed(context)
1666 .inherit_parent(candidate.py_typed);
1667 candidate.module = ResolvedModule::NamespacePackage;
1668 return Ok(());
1669 }
1670
1671 Err(())
1672}
1673
1674fn candidate_may_exist(
1677 context: &ResolverContext,
1678 candidate: &ModuleResolutionCandidate,
1679 module_name: &str,
1680) -> bool {
1681 let Some(parent) = candidate.path.to_system_path() else {
1682 return true;
1683 };
1684
1685 let Ok(listing) = directory_listing(context.db, &parent) else {
1686 return false;
1687 };
1688
1689 listing.contains_name_with_prefix(module_name)
1692}
1693
1694type ResolvedNames = Vec<ModuleResolutionCandidate>;
1695
1696pub(super) fn resolve_file_module(
1701 module: &ModulePath,
1702 resolver_state: &ResolverContext,
1703) -> Option<File> {
1704 resolve_file_module_with_filter(module, resolver_state, ComponentFileFilter::ByMode)
1705}
1706
1707fn resolve_file_module_with_filter(
1708 module: &ModulePath,
1709 resolver_state: &ResolverContext,
1710 filter: ComponentFileFilter,
1711) -> Option<File> {
1712 let stub_file = if resolver_state.mode.is_typing() {
1713 module.with_pyi_extension().to_file(resolver_state)
1714 } else {
1715 None
1716 };
1717 if filter == ComponentFileFilter::StubOnly {
1718 return stub_file;
1719 }
1720
1721 stub_file.or_else(|| {
1722 module
1723 .with_py_extension()
1724 .and_then(|path| path.to_file(resolver_state))
1725 })
1726}
1727
1728fn is_legacy_namespace_package(
1756 package_path: &ModulePath,
1757 context: &ResolverContext,
1758 init: File,
1759) -> bool {
1760 if package_path.search_path().is_standard_library() {
1762 return false;
1763 }
1764
1765 let parsed = ruff_db::parsed::parsed_module(
1773 context.db,
1774 PythonFile::new(
1775 context.db,
1776 init,
1777 context.resolver_environment.python_version(context.db),
1778 ),
1779 );
1780 let mut visitor = LegacyNamespacePackageVisitor::default();
1781 visitor.visit_body(parsed.load(context.db).suite());
1782
1783 visitor.is_legacy_namespace_package
1784}
1785
1786#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1788pub(crate) enum PyTyped {
1789 Untyped,
1791 Partial,
1793 Full,
1795}
1796
1797impl PyTyped {
1798 fn inherit_parent(self, parent: Self) -> Self {
1808 if self == Self::Untyped { parent } else { self }
1809 }
1810}
1811
1812pub(super) struct ResolverContext<'db> {
1813 pub(super) db: &'db dyn Db,
1814 pub(super) resolver_environment: ResolverEnvironment<'db>,
1815 pub(super) mode: ModuleResolveMode,
1816}
1817
1818impl<'db> ResolverContext<'db> {
1819 pub(super) fn new(
1820 db: &'db dyn Db,
1821 resolver_environment: ResolverEnvironment<'db>,
1822 mode: ModuleResolveMode,
1823 ) -> Self {
1824 Self {
1825 db,
1826 resolver_environment,
1827 mode,
1828 }
1829 }
1830
1831 pub(super) fn vendored(&self) -> &VendoredFileSystem {
1832 self.db.vendored()
1833 }
1834}
1835
1836#[derive(Default)]
1849struct LegacyNamespacePackageVisitor {
1850 is_legacy_namespace_package: bool,
1851 in_body: bool,
1852}
1853
1854impl Visitor<'_> for LegacyNamespacePackageVisitor {
1855 fn visit_body(&mut self, body: &[ruff_python_ast::Stmt]) {
1856 if self.is_legacy_namespace_package {
1857 return;
1858 }
1859
1860 if self.in_body {
1862 return;
1863 }
1864
1865 self.in_body = true;
1866
1867 walk_body(self, body);
1868 }
1869
1870 fn visit_stmt(&mut self, stmt: &ast::Stmt) {
1871 if self.is_legacy_namespace_package {
1872 return;
1873 }
1874
1875 match stmt {
1876 ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
1879 self.check_pkgutil_extend_path(targets, value);
1880 }
1881 ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
1883 self.check_pkg_resources_declare_namespace(value);
1884 }
1885 _ => {}
1886 }
1887 }
1888}
1889
1890impl LegacyNamespacePackageVisitor {
1891 fn check_pkgutil_extend_path(&mut self, targets: &[ast::Expr], value: &ast::Expr) {
1894 let [ast::Expr::Name(maybe_path)] = targets else {
1895 return;
1896 };
1897
1898 if &*maybe_path.id != "__path__" {
1899 return;
1900 }
1901
1902 let ast::Expr::Call(ast::ExprCall {
1903 func: extend_func,
1904 arguments: extend_arguments,
1905 ..
1906 }) = value
1907 else {
1908 return;
1909 };
1910
1911 let ast::Expr::Attribute(ast::ExprAttribute {
1912 value: maybe_pkg_util,
1913 attr: maybe_extend_path,
1914 ..
1915 }) = &**extend_func
1916 else {
1917 return;
1918 };
1919
1920 match &**maybe_pkg_util {
1922 ast::Expr::Call(ruff_python_ast::ExprCall {
1924 func: maybe_import,
1925 arguments: import_arguments,
1926 ..
1927 }) => {
1928 let ast::Expr::Name(maybe_import) = &**maybe_import else {
1929 return;
1930 };
1931
1932 if maybe_import.id() != "__import__" {
1933 return;
1934 }
1935
1936 let Some(ast::Expr::StringLiteral(name)) =
1937 import_arguments.find_argument_value("name", 0)
1938 else {
1939 return;
1940 };
1941
1942 if name.value.to_str() != "pkgutil" {
1943 return;
1944 }
1945 }
1946 ast::Expr::Name(name) => {
1948 if name.id() != "pkgutil" {
1949 return;
1950 }
1951 }
1952 _ => {
1953 return;
1954 }
1955 }
1956
1957 if maybe_extend_path != "extend_path" {
1959 return;
1960 }
1961
1962 let Some(ast::Expr::Name(path)) = extend_arguments.find_argument_value("path", 0) else {
1963 return;
1964 };
1965 let Some(ast::Expr::Name(name)) = extend_arguments.find_argument_value("name", 1) else {
1966 return;
1967 };
1968
1969 self.is_legacy_namespace_package = path.id() == "__path__" && name.id() == "__name__";
1970 }
1971
1972 fn check_pkg_resources_declare_namespace(&mut self, value: &ast::Expr) {
1974 let ast::Expr::Call(ast::ExprCall {
1975 func,
1976 arguments: declare_arguments,
1977 ..
1978 }) = value
1979 else {
1980 return;
1981 };
1982
1983 let ast::Expr::Attribute(ast::ExprAttribute {
1984 value: maybe_pkg_resources,
1985 attr: maybe_declare_namespace,
1986 ..
1987 }) = &**func
1988 else {
1989 return;
1990 };
1991
1992 if maybe_declare_namespace != "declare_namespace" {
1993 return;
1994 }
1995
1996 let ast::Expr::Call(ast::ExprCall {
1998 func: maybe_import,
1999 arguments: import_arguments,
2000 ..
2001 }) = &**maybe_pkg_resources
2002 else {
2003 return;
2004 };
2005
2006 let ast::Expr::Name(maybe_import) = &**maybe_import else {
2007 return;
2008 };
2009
2010 if maybe_import.id() != "__import__" {
2011 return;
2012 }
2013
2014 let Some(ast::Expr::StringLiteral(name)) = import_arguments.find_argument_value("name", 0)
2015 else {
2016 return;
2017 };
2018
2019 if name.value.to_str() != "pkg_resources" {
2020 return;
2021 }
2022
2023 let Some(ast::Expr::Name(name_arg)) = declare_arguments.find_argument_value("name", 0)
2025 else {
2026 return;
2027 };
2028
2029 self.is_legacy_namespace_package = name_arg.id() == "__name__";
2030 }
2031}
2032
2033#[cfg(test)]
2034mod tests {
2035 #![expect(
2036 clippy::disallowed_methods,
2037 reason = "These are tests, so it's fine to do I/O by-passing System."
2038 )]
2039 use ruff_db::Db;
2040 use ruff_db::files::{File, FilePath, system_path_to_file};
2041 use ruff_db::system::{DbWithTestSystem as _, DbWithWritableSystem as _};
2042 use ruff_db::testing::assert_function_query_was_not_run;
2043 use ruff_python_ast::PythonVersion;
2044
2045 use crate::db::tests::TestDb;
2046 use crate::module::ModuleKind;
2047 use crate::module_name::ModuleName;
2048 use crate::strategy::FallibleStrategy;
2049 use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
2050
2051 use super::*;
2052
2053 fn resolve_module_confident<'db>(
2054 db: &'db TestDb,
2055 module_name: &ModuleName,
2056 ) -> Option<Module<'db>> {
2057 super::resolve_module_confident(db, db.resolver_environment(), module_name)
2058 }
2059
2060 fn resolve_real_module_confident<'db>(
2061 db: &'db TestDb,
2062 module_name: &ModuleName,
2063 ) -> Option<Module<'db>> {
2064 super::resolve_real_module_confident(db, db.resolver_environment(), module_name)
2065 }
2066
2067 fn path_to_module<'db>(db: &'db TestDb, path: &FilePath) -> Option<Module<'db>> {
2068 super::path_to_module(db, db.resolver_environment(), path)
2069 }
2070
2071 #[test]
2072 fn first_party_module() {
2073 let TestCase { db, src, .. } = TestCaseBuilder::new()
2074 .with_src_files(&[("foo.py", "print('Hello, world!')")])
2075 .build();
2076
2077 let foo_module_name = ModuleName::new_static("foo").unwrap();
2078 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
2079
2080 assert_eq!(
2081 Some(&foo_module),
2082 resolve_module_confident(&db, &foo_module_name).as_ref()
2083 );
2084
2085 assert_eq!("foo", foo_module.name(&db));
2086 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2087 assert_eq!(ModuleKind::Module, foo_module.kind(&db));
2088
2089 let expected_foo_path = src.join("foo.py");
2090 assert_eq!(&expected_foo_path, foo_module.file(&db).unwrap().path(&db));
2091 assert_eq!(
2092 Some(foo_module),
2093 path_to_module(&db, &FilePath::from(expected_foo_path))
2094 );
2095 }
2096
2097 #[test]
2098 fn site_packages_stub_overrides_first_party_package_when_stdlib_is_missing() {
2099 let TestCase {
2100 db, site_packages, ..
2101 } = TestCaseBuilder::new()
2102 .with_src_files(&[("foo/__init__.py", "")])
2103 .with_site_packages_files(&[("foo-stubs/__init__.pyi", "")])
2104 .build();
2105
2106 let foo = resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2107 assert_eq!(
2108 foo.file(&db).unwrap().path(&db),
2109 &site_packages.join("foo-stubs/__init__.pyi")
2110 );
2111 }
2112
2113 #[test]
2114 fn first_party_stub_package_precedes_stdlib() {
2115 const TYPESHED: MockedTypeshed = MockedTypeshed {
2116 stdlib_files: &[("foo.pyi", "")],
2117 versions: "foo: 3.8-",
2118 };
2119
2120 let TestCase { db, src, .. } = TestCaseBuilder::new()
2121 .with_mocked_typeshed(TYPESHED)
2122 .with_src_files(&[("foo-stubs/__init__.pyi", "")])
2123 .build();
2124
2125 let foo = resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2126 assert_eq!(
2127 foo.file(&db).unwrap().path(&db),
2128 &src.join("foo-stubs/__init__.pyi")
2129 );
2130 }
2131
2132 #[test]
2133 fn desperate_resolution_finds_stub_package() {
2134 let TestCase { db, src, .. } = TestCaseBuilder::new()
2135 .with_src_files(&[
2136 ("nested/main.py", ""),
2137 ("nested/foo/__init__.py", ""),
2138 ("nested/foo-stubs/__init__.pyi", ""),
2139 ])
2140 .build();
2141 let importing_file = system_path_to_file(&db, src.join("nested/main.py")).unwrap();
2142
2143 let foo = resolve_module(
2144 &db,
2145 ImportingFile::File(importing_file, db.resolver_environment()),
2146 &ModuleName::new_static("foo").unwrap(),
2147 )
2148 .unwrap();
2149 assert_eq!(
2150 foo.file(&db).unwrap().path(&db),
2151 &src.join("nested/foo-stubs/__init__.pyi")
2152 );
2153 }
2154
2155 #[test]
2156 fn missing_modules_do_not_create_file_inputs() {
2157 let TestCase { db, src, .. } = TestCaseBuilder::new()
2158 .with_src_files(&[("other.py", ""), ("package/__init__.py", "")])
2159 .build();
2160
2161 for name in ["missing", "package.missing"] {
2162 assert!(
2163 resolve_module_confident(&db, &ModuleName::new_static(name).unwrap()).is_none()
2164 );
2165 }
2166
2167 for relative_path in [
2168 "missing-stubs/__init__.pyi",
2169 "missing-stubs/__init__.py",
2170 "missing/__init__.pyi",
2171 "missing/__init__.py",
2172 "missing.pyi",
2173 "missing.py",
2174 "package/missing/__init__.pyi",
2175 "package/missing/__init__.py",
2176 "package/missing.pyi",
2177 "package/missing.py",
2178 ] {
2179 assert_eq!(
2180 db.files().try_system(&db, &src.join(relative_path)),
2181 None,
2182 "unexpected point probe for {relative_path}"
2183 );
2184 }
2185 }
2186
2187 #[test]
2188 fn stdlib_precedes_stub_package_in_site_packages() {
2189 const TYPESHED: MockedTypeshed = MockedTypeshed {
2190 stdlib_files: &[("foo.pyi", "")],
2191 versions: "foo: 3.8-",
2192 };
2193
2194 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2195 .with_mocked_typeshed(TYPESHED)
2196 .with_site_packages_files(&[("foo-stubs/__init__.pyi", "")])
2197 .build();
2198
2199 let foo = resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2200 assert_eq!(foo.file(&db).unwrap().path(&db), &stdlib.join("foo.pyi"));
2201 }
2202
2203 #[test]
2204 fn stubs_over_module_source() {
2205 let TestCase { db, src, .. } = TestCaseBuilder::new()
2206 .with_src_files(&[("foo.py", ""), ("foo.pyi", "")])
2207 .build();
2208
2209 let foo_module_name = ModuleName::new_static("foo").unwrap();
2210 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
2211
2212 assert_eq!(
2213 Some(&foo_module),
2214 resolve_module_confident(&db, &foo_module_name).as_ref()
2215 );
2216
2217 assert_eq!("foo", foo_module.name(&db));
2218 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2219 assert_eq!(ModuleKind::Module, foo_module.kind(&db));
2220
2221 let expected_foo_path = src.join("foo.pyi");
2222 assert_eq!(&expected_foo_path, foo_module.file(&db).unwrap().path(&db));
2223 assert_eq!(
2224 Some(foo_module),
2225 path_to_module(&db, &FilePath::from(expected_foo_path))
2226 );
2227 }
2228
2229 #[test]
2236 fn stubs_over_package_source() {
2237 let TestCase { db, src, .. } = TestCaseBuilder::new()
2238 .with_src_files(&[("foo/__init__.py", ""), ("foo.pyi", "")])
2239 .build();
2240
2241 let foo_module_name = ModuleName::new_static("foo").unwrap();
2242 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
2243
2244 assert_eq!(
2245 Some(&foo_module),
2246 resolve_module_confident(&db, &foo_module_name).as_ref()
2247 );
2248
2249 assert_eq!("foo", foo_module.name(&db));
2250 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2251 assert_eq!(ModuleKind::Package, foo_module.kind(&db));
2252
2253 let expected_foo_path = src.join("foo/__init__.py");
2254 assert_eq!(&expected_foo_path, foo_module.file(&db).unwrap().path(&db));
2255 assert_eq!(
2256 Some(foo_module),
2257 path_to_module(&db, &FilePath::from(expected_foo_path))
2258 );
2259 }
2260
2261 #[test]
2262 fn builtins_vendored() {
2263 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2264 .with_vendored_typeshed()
2265 .with_src_files(&[("builtins.py", "FOOOO = 42")])
2266 .build();
2267
2268 let builtins_module_name = ModuleName::new_static("builtins").unwrap();
2269 let builtins =
2270 resolve_module_confident(&db, &builtins_module_name).expect("builtins to resolve");
2271
2272 assert_eq!(
2273 builtins.file(&db).unwrap().path(&db),
2274 &stdlib.join("builtins.pyi")
2275 );
2276 }
2277
2278 #[test]
2279 fn builtins_custom() {
2280 const TYPESHED: MockedTypeshed = MockedTypeshed {
2281 stdlib_files: &[("builtins.pyi", "def min(a, b): ...")],
2282 versions: "builtins: 3.8-",
2283 };
2284
2285 const SRC: &[FileSpec] = &[("builtins.py", "FOOOO = 42")];
2286
2287 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2288 .with_src_files(SRC)
2289 .with_mocked_typeshed(TYPESHED)
2290 .with_python_version(PythonVersion::PY38)
2291 .build();
2292
2293 let builtins_module_name = ModuleName::new_static("builtins").unwrap();
2294 let builtins =
2295 resolve_module_confident(&db, &builtins_module_name).expect("builtins to resolve");
2296
2297 assert_eq!(
2298 builtins.file(&db).unwrap().path(&db),
2299 &stdlib.join("builtins.pyi")
2300 );
2301 }
2302
2303 #[test]
2304 fn stdlib() {
2305 const TYPESHED: MockedTypeshed = MockedTypeshed {
2306 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
2307 versions: "functools: 3.8-",
2308 };
2309
2310 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2311 .with_mocked_typeshed(TYPESHED)
2312 .with_python_version(PythonVersion::PY38)
2313 .build();
2314
2315 let functools_module_name = ModuleName::new_static("functools").unwrap();
2316 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
2317
2318 assert_eq!(
2319 Some(&functools_module),
2320 resolve_module_confident(&db, &functools_module_name).as_ref()
2321 );
2322
2323 assert_eq!(&stdlib, functools_module.search_path(&db).unwrap());
2324 assert_eq!(ModuleKind::Module, functools_module.kind(&db));
2325
2326 let expected_functools_path = stdlib.join("functools.pyi");
2327 assert_eq!(
2328 &expected_functools_path,
2329 functools_module.file(&db).unwrap().path(&db)
2330 );
2331
2332 assert_eq!(
2333 Some(functools_module),
2334 path_to_module(&db, &FilePath::from(expected_functools_path))
2335 );
2336 }
2337
2338 fn create_module_names(raw_names: &[&str]) -> Vec<ModuleName> {
2339 raw_names
2340 .iter()
2341 .map(|raw| ModuleName::new(raw).unwrap())
2342 .collect()
2343 }
2344
2345 #[test]
2346 fn resolve_module_uses_resolver_environment_python_version() {
2347 const TYPESHED: MockedTypeshed = MockedTypeshed {
2348 stdlib_files: &[("_sha256.pyi", ""), ("py312_only.pyi", "")],
2349 versions: "_sha256: 3.11-\npy312_only: 3.12-",
2350 };
2351
2352 let TestCase {
2353 db, src, stdlib, ..
2354 } = TestCaseBuilder::new()
2355 .with_src_files(&[
2356 ("main.py", ""),
2357 ("_sha256.py", ""),
2358 ("namespace/module.py", ""),
2359 ])
2360 .with_mocked_typeshed(TYPESHED)
2361 .with_python_version(PythonVersion::PY311)
2362 .build();
2363 let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap();
2364 let py311 = ResolverEnvironment::new(&db, PythonVersion::PY311, db.search_paths());
2365 let py312 = ResolverEnvironment::new(&db, PythonVersion::PY312, db.search_paths());
2366 let sha256 = ModuleName::new_static("_sha256").unwrap();
2367 let py311_module =
2368 resolve_module(&db, ImportingFile::File(importing_file, py311), &sha256).unwrap();
2369 let py312_module =
2370 resolve_module(&db, ImportingFile::File(importing_file, py312), &sha256).unwrap();
2371 assert_eq!(
2372 py311_module.file(&db).unwrap().path(&db),
2373 &stdlib.join("_sha256.pyi")
2374 );
2375 assert_eq!(
2376 py312_module.file(&db).unwrap().path(&db),
2377 &src.join("_sha256.py")
2378 );
2379 assert_eq!(py311_module.python_version(&db), PythonVersion::PY311);
2380 assert_eq!(py312_module.python_version(&db), PythonVersion::PY312);
2381
2382 let namespace = ModuleName::new_static("namespace").unwrap();
2383 let py311_namespace =
2384 resolve_module(&db, ImportingFile::File(importing_file, py311), &namespace).unwrap();
2385 let py312_namespace =
2386 resolve_module(&db, ImportingFile::File(importing_file, py312), &namespace).unwrap();
2387 assert!(matches!(py311_namespace, Module::Namespace(_)));
2388 assert!(matches!(py312_namespace, Module::Namespace(_)));
2389 assert_eq!(py311_namespace.python_version(&db), PythonVersion::PY311);
2390 assert_eq!(py312_namespace.python_version(&db), PythonVersion::PY312);
2391 assert_ne!(py311_namespace, py312_namespace);
2392
2393 let py312_only = ModuleName::new_static("py312_only").unwrap();
2394 assert!(
2395 resolve_module(&db, ImportingFile::File(importing_file, py311), &py312_only).is_none()
2396 );
2397 assert_eq!(
2398 resolve_module(&db, ImportingFile::File(importing_file, py312), &py312_only)
2399 .and_then(|module| module.file(&db))
2400 .unwrap()
2401 .path(&db),
2402 &stdlib.join("py312_only.pyi")
2403 );
2404 }
2405
2406 #[test]
2407 fn resolve_module_uses_resolver_environment_search_paths() {
2408 let TestCase { mut db, src, .. } = TestCaseBuilder::new()
2409 .with_src_files(&[("main.py", ""), ("shared.py", "from_src = True")])
2410 .with_vendored_typeshed()
2411 .build();
2412 db.write_file("/alternate/shared.py", "from_alternate = True")
2413 .unwrap();
2414
2415 let alternate_paths = SearchPathSettings {
2416 src_roots: vec![SystemPathBuf::from("/alternate")],
2417 ..SearchPathSettings::empty()
2418 }
2419 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
2420 .unwrap();
2421 alternate_paths.try_register_static_roots(&db);
2422
2423 let primary = db.resolver_environment();
2424 let alternate = ResolverEnvironment::new(&db, PythonVersion::default(), &alternate_paths);
2425 let importing_file = system_path_to_file(&db, src.join("main.py")).unwrap();
2426 let name = ModuleName::new_static("shared").unwrap();
2427
2428 let primary_module =
2429 resolve_module(&db, ImportingFile::File(importing_file, primary), &name).unwrap();
2430 let alternate_module =
2431 resolve_module(&db, ImportingFile::File(importing_file, alternate), &name).unwrap();
2432
2433 assert_eq!(
2434 primary_module.file(&db).unwrap().path(&db),
2435 &src.join("shared.py")
2436 );
2437 assert_eq!(
2438 alternate_module.file(&db).unwrap().path(&db),
2439 &SystemPathBuf::from("/alternate/shared.py")
2440 );
2441 assert_ne!(primary_module, alternate_module);
2442 }
2443
2444 #[test]
2445 fn stdlib_resolution_respects_versions_file_py38_existing_modules() {
2446 const VERSIONS: &str = "\
2447 asyncio: 3.8- # 'Regular' package on py38+
2448 asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
2449 functools: 3.8- # Top-level single-file module
2450 ";
2451
2452 const STDLIB: &[FileSpec] = &[
2453 ("asyncio/__init__.pyi", ""),
2454 ("asyncio/tasks.pyi", ""),
2455 ("functools.pyi", ""),
2456 ];
2457
2458 const TYPESHED: MockedTypeshed = MockedTypeshed {
2459 stdlib_files: STDLIB,
2460 versions: VERSIONS,
2461 };
2462
2463 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2464 .with_mocked_typeshed(TYPESHED)
2465 .with_python_version(PythonVersion::PY38)
2466 .build();
2467
2468 let existing_modules = create_module_names(&["asyncio", "functools"]);
2469 for module_name in existing_modules {
2470 let resolved_module =
2471 resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
2472 panic!("Expected module {module_name} to exist in the mock stdlib")
2473 });
2474 let search_path = resolved_module.search_path(&db).unwrap();
2475 assert_eq!(
2476 &stdlib, search_path,
2477 "Search path for {module_name} was unexpectedly {search_path:?}"
2478 );
2479 assert!(
2480 search_path.is_standard_library(),
2481 "Expected a stdlib search path, but got {search_path:?}"
2482 );
2483 }
2484 }
2485
2486 #[test]
2487 fn stdlib_resolution_respects_versions_file_py38_nonexisting_modules() {
2488 const VERSIONS: &str = "\
2489 asyncio: 3.8- # 'Regular' package on py38+
2490 asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
2491 collections: 3.9- # 'Regular' package on py39+
2492 ";
2493
2494 const STDLIB: &[FileSpec] = &[
2495 ("collections/__init__.pyi", ""),
2496 ("asyncio/__init__.pyi", ""),
2497 ("asyncio/tasks.pyi", ""),
2498 ];
2499
2500 const TYPESHED: MockedTypeshed = MockedTypeshed {
2501 stdlib_files: STDLIB,
2502 versions: VERSIONS,
2503 };
2504
2505 let TestCase { db, .. } = TestCaseBuilder::new()
2506 .with_mocked_typeshed(TYPESHED)
2507 .with_python_version(PythonVersion::PY38)
2508 .build();
2509
2510 let nonexisting_modules = create_module_names(&["collections", "asyncio.tasks"]);
2511
2512 for module_name in nonexisting_modules {
2513 assert!(
2514 resolve_module_confident(&db, &module_name).is_none(),
2515 "Unexpectedly resolved a module for {module_name}"
2516 );
2517 }
2518 }
2519
2520 #[test]
2521 fn stdlib_resolution_respects_versions_file_py39_existing_modules() {
2522 const VERSIONS: &str = "\
2523 asyncio: 3.8- # 'Regular' package on py38+
2524 asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
2525 collections: 3.9- # 'Regular' package on py39+
2526 functools: 3.8- # Top-level single-file module
2527 ";
2528
2529 const STDLIB: &[FileSpec] = &[
2530 ("asyncio/__init__.pyi", ""),
2531 ("asyncio/tasks.pyi", ""),
2532 ("collections/__init__.pyi", ""),
2533 ("functools.pyi", ""),
2534 ];
2535
2536 const TYPESHED: MockedTypeshed = MockedTypeshed {
2537 stdlib_files: STDLIB,
2538 versions: VERSIONS,
2539 };
2540
2541 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2542 .with_mocked_typeshed(TYPESHED)
2543 .with_python_version(PythonVersion::PY39)
2544 .build();
2545
2546 let existing_modules =
2547 create_module_names(&["asyncio", "functools", "collections", "asyncio.tasks"]);
2548
2549 for module_name in existing_modules {
2550 let resolved_module =
2551 resolve_module_confident(&db, &module_name).unwrap_or_else(|| {
2552 panic!("Expected module {module_name} to exist in the mock stdlib")
2553 });
2554 let search_path = resolved_module.search_path(&db).unwrap();
2555 assert_eq!(
2556 &stdlib, search_path,
2557 "Search path for {module_name} was unexpectedly {search_path:?}"
2558 );
2559 assert!(
2560 search_path.is_standard_library(),
2561 "Expected a stdlib search path, but got {search_path:?}"
2562 );
2563 }
2564 }
2565 #[test]
2566 fn stdlib_resolution_respects_versions_file_py39_nonexisting_modules() {
2567 const VERSIONS: &str = "\
2568 importlib: 3.9- # Namespace package on py39+
2569 xml: 3.8-3.8 # Namespace package on 3.8 only
2570 ";
2571
2572 const STDLIB: &[FileSpec] = &[("importlib/abc.pyi", ""), ("xml/etree.pyi", "")];
2573
2574 const TYPESHED: MockedTypeshed = MockedTypeshed {
2575 stdlib_files: STDLIB,
2576 versions: VERSIONS,
2577 };
2578
2579 let TestCase { db, .. } = TestCaseBuilder::new()
2580 .with_mocked_typeshed(TYPESHED)
2581 .with_python_version(PythonVersion::PY39)
2582 .build();
2583
2584 let nonexisting_modules = create_module_names(&["importlib", "xml", "xml.etree"]);
2585 for module_name in nonexisting_modules {
2586 assert!(
2587 resolve_module_confident(&db, &module_name).is_none(),
2588 "Unexpectedly resolved a module for {module_name}"
2589 );
2590 }
2591 }
2592
2593 #[test]
2594 fn first_party_precedence_over_stdlib() {
2595 const SRC: &[FileSpec] = &[("functools.py", "def update_wrapper(): ...")];
2596
2597 const TYPESHED: MockedTypeshed = MockedTypeshed {
2598 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
2599 versions: "functools: 3.8-",
2600 };
2601
2602 let TestCase { db, src, .. } = TestCaseBuilder::new()
2603 .with_src_files(SRC)
2604 .with_mocked_typeshed(TYPESHED)
2605 .with_python_version(PythonVersion::PY38)
2606 .build();
2607
2608 let functools_module_name = ModuleName::new_static("functools").unwrap();
2609 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
2610
2611 assert_eq!(
2612 Some(&functools_module),
2613 resolve_module_confident(&db, &functools_module_name).as_ref()
2614 );
2615 assert_eq!(&src, functools_module.search_path(&db).unwrap());
2616 assert_eq!(ModuleKind::Module, functools_module.kind(&db));
2617 assert_eq!(
2618 &src.join("functools.py"),
2619 functools_module.file(&db).unwrap().path(&db)
2620 );
2621
2622 assert_eq!(
2623 Some(functools_module),
2624 path_to_module(&db, &FilePath::from(src.join("functools.py")))
2625 );
2626 }
2627
2628 #[test]
2629 fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
2630 let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
2631 .with_vendored_typeshed()
2632 .with_python_version(PythonVersion::default())
2633 .build();
2634
2635 let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
2636 let pydoc_data_topics = resolve_module_confident(&db, &pydoc_data_topics_name).unwrap();
2637
2638 assert_eq!("pydoc_data.topics", pydoc_data_topics.name(&db));
2639 assert_eq!(pydoc_data_topics.search_path(&db).unwrap(), &stdlib);
2640 assert_eq!(
2641 pydoc_data_topics.file(&db).unwrap().path(&db),
2642 &stdlib.join("pydoc_data/topics.pyi")
2643 );
2644 }
2645
2646 #[test]
2647 fn resolve_package() {
2648 let TestCase { src, db, .. } = TestCaseBuilder::new()
2649 .with_src_files(&[("foo/__init__.py", "print('Hello, world!'")])
2650 .build();
2651
2652 let foo_path = src.join("foo/__init__.py");
2653 let foo_module =
2654 resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2655
2656 assert_eq!("foo", foo_module.name(&db));
2657 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2658 assert_eq!(&foo_path, foo_module.file(&db).unwrap().path(&db));
2659
2660 assert_eq!(
2661 Some(&foo_module),
2662 path_to_module(&db, &FilePath::from(foo_path)).as_ref()
2663 );
2664
2665 assert_eq!(None, path_to_module(&db, &FilePath::from(src.join("foo"))));
2667 }
2668
2669 #[test]
2670 fn package_priority_over_module() {
2671 const SRC: &[FileSpec] = &[
2672 ("foo/__init__.py", "print('Hello, world!')"),
2673 ("foo.py", "print('Hello, world!')"),
2674 ];
2675
2676 let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
2677
2678 let foo_module =
2679 resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2680 let foo_init_path = src.join("foo/__init__.py");
2681
2682 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2683 assert_eq!(&foo_init_path, foo_module.file(&db).unwrap().path(&db));
2684 assert_eq!(ModuleKind::Package, foo_module.kind(&db));
2685
2686 assert_eq!(
2687 Some(foo_module),
2688 path_to_module(&db, &FilePath::from(foo_init_path))
2689 );
2690 assert_eq!(
2691 None,
2692 path_to_module(&db, &FilePath::from(src.join("foo.py")))
2693 );
2694 }
2695
2696 #[test]
2697 fn typing_stub_over_module() {
2698 const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];
2699
2700 let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
2701
2702 let foo = resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2703 let foo_real =
2704 resolve_real_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2705 let foo_stub = src.join("foo.pyi");
2706
2707 assert_eq!(&src, foo.search_path(&db).unwrap());
2708 assert_eq!(&foo_stub, foo.file(&db).unwrap().path(&db));
2709
2710 assert_eq!(Some(foo), path_to_module(&db, &FilePath::from(foo_stub)));
2711 assert_eq!(
2712 Some(foo_real),
2713 path_to_module(&db, &FilePath::from(src.join("foo.py")))
2714 );
2715 assert_ne!(foo_real, foo);
2716 }
2717
2718 #[test]
2719 fn sub_packages() {
2720 const SRC: &[FileSpec] = &[
2721 ("foo/__init__.py", ""),
2722 ("foo/bar/__init__.py", ""),
2723 ("foo/bar/baz.py", "print('Hello, world!)'"),
2724 ];
2725
2726 let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
2727
2728 let baz_module =
2729 resolve_module_confident(&db, &ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
2730 let baz_path = src.join("foo/bar/baz.py");
2731
2732 assert_eq!(&src, baz_module.search_path(&db).unwrap());
2733 assert_eq!(&baz_path, baz_module.file(&db).unwrap().path(&db));
2734
2735 assert_eq!(
2736 Some(baz_module),
2737 path_to_module(&db, &FilePath::from(baz_path))
2738 );
2739 }
2740
2741 #[test]
2742 fn module_search_path_priority() {
2743 let TestCase {
2744 db,
2745 src,
2746 site_packages,
2747 ..
2748 } = TestCaseBuilder::new()
2749 .with_src_files(&[("foo.py", "")])
2750 .with_site_packages_files(&[("foo.py", "")])
2751 .build();
2752
2753 let foo_module =
2754 resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2755 let foo_src_path = src.join("foo.py");
2756
2757 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2758 assert_eq!(&foo_src_path, foo_module.file(&db).unwrap().path(&db));
2759 assert_eq!(
2760 Some(foo_module),
2761 path_to_module(&db, &FilePath::from(foo_src_path))
2762 );
2763
2764 assert_eq!(
2765 None,
2766 path_to_module(&db, &FilePath::from(site_packages.join("foo.py")))
2767 );
2768 }
2769
2770 #[test]
2771 #[cfg(target_family = "unix")]
2772 fn symlink() -> anyhow::Result<()> {
2773 use anyhow::Context;
2774 use ruff_db::system::{OsSystem, SystemPath};
2775
2776 use crate::db::tests::TestDb;
2777
2778 let mut db = TestDb::new().with_python_version(PythonVersion::PY38);
2779
2780 let temp_dir = tempfile::tempdir()?;
2781 let root = temp_dir
2782 .path()
2783 .canonicalize()
2784 .context("Failed to canonicalize temp dir")?;
2785 let root = SystemPath::from_std_path(&root).unwrap();
2786 db.use_system(OsSystem::new(root));
2787
2788 let src = root.join("src");
2789 let site_packages = root.join("site-packages");
2790 let custom_typeshed = root.join("typeshed");
2791
2792 let foo = src.join("foo.py");
2793 let bar = src.join("bar.py");
2794
2795 std::fs::create_dir_all(src.as_std_path())?;
2796 std::fs::create_dir_all(site_packages.as_std_path())?;
2797 std::fs::create_dir_all(custom_typeshed.join("stdlib").as_std_path())?;
2798 std::fs::File::create(custom_typeshed.join("stdlib/VERSIONS").as_std_path())?;
2799
2800 std::fs::write(foo.as_std_path(), "")?;
2801 std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?;
2802
2803 db.set_search_paths(
2804 SearchPathSettings {
2805 src_roots: vec![src.clone()],
2806 custom_typeshed: Some(custom_typeshed),
2807 site_packages_paths: vec![site_packages],
2808 ..SearchPathSettings::empty()
2809 }
2810 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
2811 .expect("Valid search path settings"),
2812 );
2813
2814 let foo_module =
2815 resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).unwrap();
2816 let bar_module =
2817 resolve_module_confident(&db, &ModuleName::new_static("bar").unwrap()).unwrap();
2818
2819 assert_ne!(foo_module, bar_module);
2820
2821 assert_eq!(&src, foo_module.search_path(&db).unwrap());
2822 assert_eq!(&foo, foo_module.file(&db).unwrap().path(&db));
2823
2824 assert_eq!(&src, bar_module.search_path(&db).unwrap());
2827 assert_eq!(&bar, bar_module.file(&db).unwrap().path(&db));
2828 assert_eq!(&foo, foo_module.file(&db).unwrap().path(&db));
2829
2830 assert_ne!(&foo_module, &bar_module);
2831
2832 assert_eq!(Some(foo_module), path_to_module(&db, &FilePath::from(foo)));
2833 assert_eq!(Some(bar_module), path_to_module(&db, &FilePath::from(bar)));
2834
2835 Ok(())
2836 }
2837
2838 #[test]
2839 fn deleting_file_from_different_directory_doesnt_change_module_resolution() {
2840 let TestCase { mut db, src, .. } = TestCaseBuilder::new()
2841 .with_src_files(&[("foo.py", "x = 1"), ("other/bar.py", "x = 2")])
2842 .with_python_version(PythonVersion::PY38)
2843 .build();
2844
2845 let foo_module_name = ModuleName::new_static("foo").unwrap();
2846 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
2847 let foo_pieces = (
2848 foo_module.name(&db).clone(),
2849 foo_module.file(&db),
2850 foo_module.known(&db),
2851 foo_module.search_path(&db).cloned(),
2852 foo_module.kind(&db),
2853 );
2854
2855 let bar_path = src.join("other/bar.py");
2856 let bar = system_path_to_file(&db, &bar_path).expect("bar.py to exist");
2857
2858 db.clear_salsa_events();
2859
2860 db.memory_file_system().remove_file(&bar_path).unwrap();
2862 bar.sync(&mut db);
2863
2864 let foo_module2 = resolve_module_confident(&db, &foo_module_name);
2868 let foo_pieces2 = foo_module2.map(|foo_module2| {
2869 (
2870 foo_module2.name(&db).clone(),
2871 foo_module2.file(&db),
2872 foo_module2.known(&db),
2873 foo_module2.search_path(&db).cloned(),
2874 foo_module2.kind(&db),
2875 )
2876 });
2877
2878 assert!(
2879 !db.take_salsa_events()
2880 .iter()
2881 .any(|event| { matches!(event.kind, salsa::EventKind::WillExecute { .. }) })
2882 );
2883
2884 assert_eq!(Some(foo_pieces), foo_pieces2);
2885 }
2886
2887 #[test]
2888 fn adding_file_on_which_module_resolution_depends_invalidates_previously_failing_query_that_now_succeeds()
2889 -> anyhow::Result<()> {
2890 let TestCase { mut db, src, .. } = TestCaseBuilder::new().build();
2891 let foo_path = src.join("foo.py");
2892
2893 let foo_module_name = ModuleName::new_static("foo").unwrap();
2894 assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
2895
2896 db.write_file(&foo_path, "x = 1")?;
2898
2899 let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist");
2900
2901 let foo_module =
2902 resolve_module_confident(&db, &foo_module_name).expect("Foo module to resolve");
2903 assert_eq!(foo_file, foo_module.file(&db).unwrap());
2904
2905 Ok(())
2906 }
2907
2908 #[test]
2909 fn removing_file_on_which_module_resolution_depends_invalidates_previously_successful_query_that_now_fails()
2910 -> anyhow::Result<()> {
2911 const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/__init__.py", "x = 2")];
2912
2913 let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
2914
2915 let foo_module_name = ModuleName::new_static("foo").unwrap();
2916 let foo_module =
2917 resolve_module_confident(&db, &foo_module_name).expect("foo module to exist");
2918 let foo_init_path = src.join("foo/__init__.py");
2919
2920 assert_eq!(&foo_init_path, foo_module.file(&db).unwrap().path(&db));
2921
2922 db.memory_file_system().remove_file(&foo_init_path)?;
2924 db.memory_file_system()
2925 .remove_directory(foo_init_path.parent().unwrap())?;
2926 File::sync_path(&mut db, &foo_init_path);
2927 File::sync_path(&mut db, foo_init_path.parent().unwrap());
2928
2929 let foo_module =
2930 resolve_module_confident(&db, &foo_module_name).expect("Foo module to resolve");
2931 assert_eq!(&src.join("foo.py"), foo_module.file(&db).unwrap().path(&db));
2932
2933 Ok(())
2934 }
2935
2936 #[test]
2937 fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() {
2938 const TYPESHED: MockedTypeshed = MockedTypeshed {
2939 versions: "functools: 3.8-",
2940 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
2941 };
2942
2943 let TestCase {
2944 mut db,
2945 stdlib,
2946 site_packages,
2947 ..
2948 } = TestCaseBuilder::new()
2949 .with_mocked_typeshed(TYPESHED)
2950 .with_python_version(PythonVersion::PY38)
2951 .build();
2952
2953 let functools_module_name = ModuleName::new_static("functools").unwrap();
2954 let stdlib_functools_path = stdlib.join("functools.pyi");
2955
2956 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
2957 assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
2958 assert_eq!(
2959 Ok(functools_module.file(&db).unwrap()),
2960 system_path_to_file(&db, &stdlib_functools_path)
2961 );
2962
2963 db.clear_salsa_events();
2966 let site_packages_functools_path = site_packages.join("functools.py");
2967 db.write_file(&site_packages_functools_path, "f: int")
2968 .unwrap();
2969 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
2970 let functools_file = functools_module.file(&db).unwrap();
2971 let functools_search_path = functools_module.search_path(&db).unwrap().clone();
2972 let events = db.take_salsa_events();
2973 assert_function_query_was_not_run(
2974 &db,
2975 resolve_module_query,
2976 ModuleNameIngredient::new(
2977 &db,
2978 functools_module_name,
2979 ModuleResolveMode::Typing,
2980 db.resolver_environment(),
2981 ),
2982 &events,
2983 );
2984 assert_eq!(&functools_search_path, &stdlib);
2985 assert_eq!(
2986 Ok(functools_file),
2987 system_path_to_file(&db, &stdlib_functools_path)
2988 );
2989 }
2990
2991 #[test]
2992 fn adding_file_to_search_path_with_higher_priority_invalidates_the_query() {
2993 const TYPESHED: MockedTypeshed = MockedTypeshed {
2994 versions: "functools: 3.8-",
2995 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
2996 };
2997
2998 let TestCase {
2999 mut db,
3000 stdlib,
3001 src,
3002 ..
3003 } = TestCaseBuilder::new()
3004 .with_mocked_typeshed(TYPESHED)
3005 .with_python_version(PythonVersion::PY38)
3006 .build();
3007
3008 let functools_module_name = ModuleName::new_static("functools").unwrap();
3009 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
3010 assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
3011 assert_eq!(
3012 Ok(functools_module.file(&db).unwrap()),
3013 system_path_to_file(&db, stdlib.join("functools.pyi"))
3014 );
3015
3016 let src_functools_path = src.join("functools.py");
3019 db.write_file(&src_functools_path, "FOO: int").unwrap();
3020 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
3021 assert_eq!(functools_module.search_path(&db).unwrap(), &src);
3022 assert_eq!(
3023 Ok(functools_module.file(&db).unwrap()),
3024 system_path_to_file(&db, &src_functools_path)
3025 );
3026 }
3027
3028 #[test]
3029 fn deleting_file_from_higher_priority_search_path_invalidates_the_query() {
3030 const SRC: &[FileSpec] = &[("functools.py", "FOO: int")];
3031
3032 const TYPESHED: MockedTypeshed = MockedTypeshed {
3033 versions: "functools: 3.8-",
3034 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
3035 };
3036
3037 let TestCase {
3038 mut db,
3039 stdlib,
3040 src,
3041 ..
3042 } = TestCaseBuilder::new()
3043 .with_src_files(SRC)
3044 .with_mocked_typeshed(TYPESHED)
3045 .with_python_version(PythonVersion::PY38)
3046 .build();
3047
3048 let functools_module_name = ModuleName::new_static("functools").unwrap();
3049 let src_functools_path = src.join("functools.py");
3050
3051 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
3052 assert_eq!(functools_module.search_path(&db).unwrap(), &src);
3053 assert_eq!(
3054 Ok(functools_module.file(&db).unwrap()),
3055 system_path_to_file(&db, &src_functools_path)
3056 );
3057
3058 db.memory_file_system()
3061 .remove_file(&src_functools_path)
3062 .unwrap();
3063 File::sync_path(&mut db, &src_functools_path);
3064 let functools_module = resolve_module_confident(&db, &functools_module_name).unwrap();
3065 assert_eq!(functools_module.search_path(&db).unwrap(), &stdlib);
3066 assert_eq!(
3067 Ok(functools_module.file(&db).unwrap()),
3068 system_path_to_file(&db, stdlib.join("functools.pyi"))
3069 );
3070 }
3071
3072 #[test]
3073 fn editable_install_absolute_path() {
3074 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")];
3075 let x_directory = [("/x/src/foo/__init__.py", ""), ("/x/src/foo/bar.py", "")];
3076
3077 let TestCase { mut db, .. } = TestCaseBuilder::new()
3078 .with_site_packages_files(SITE_PACKAGES)
3079 .build();
3080
3081 db.write_files(x_directory).unwrap();
3082
3083 let foo_module_name = ModuleName::new_static("foo").unwrap();
3084 let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
3085
3086 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
3087 let foo_bar_module = resolve_module_confident(&db, &foo_bar_module_name).unwrap();
3088
3089 assert_eq!(
3090 foo_module.file(&db).unwrap().path(&db),
3091 &FilePath::system("/x/src/foo/__init__.py")
3092 );
3093 assert_eq!(
3094 foo_bar_module.file(&db).unwrap().path(&db),
3095 &FilePath::system("/x/src/foo/bar.py")
3096 );
3097 }
3098
3099 #[test]
3100 fn editable_install_pth_file_with_whitespace() {
3101 const SITE_PACKAGES: &[FileSpec] = &[
3102 ("_foo.pth", " /x/src"),
3103 ("_bar.pth", "/y/src "),
3104 ];
3105 let external_files = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")];
3106
3107 let TestCase { mut db, .. } = TestCaseBuilder::new()
3108 .with_site_packages_files(SITE_PACKAGES)
3109 .build();
3110
3111 db.write_files(external_files).unwrap();
3112
3113 let foo_module_name = ModuleName::new_static("foo").unwrap();
3115 assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
3116
3117 let bar_module_name = ModuleName::new_static("bar").unwrap();
3119 let bar_module = resolve_module_confident(&db, &bar_module_name).unwrap();
3120 assert_eq!(
3121 bar_module.file(&db).unwrap().path(&db),
3122 &FilePath::system("/y/src/bar.py")
3123 );
3124 }
3125
3126 #[test]
3127 fn editable_install_relative_path() {
3128 const SITE_PACKAGES: &[FileSpec] = &[
3129 ("_foo.pth", "../../x/../x/y/src"),
3130 ("../x/y/src/foo.pyi", ""),
3131 ];
3132
3133 let TestCase { db, .. } = TestCaseBuilder::new()
3134 .with_site_packages_files(SITE_PACKAGES)
3135 .build();
3136
3137 let foo_module_name = ModuleName::new_static("foo").unwrap();
3138 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
3139
3140 assert_eq!(
3141 foo_module.file(&db).unwrap().path(&db),
3142 &FilePath::system("/x/y/src/foo.pyi")
3143 );
3144 }
3145
3146 #[test]
3147 fn editable_install_multiple_pth_files_with_multiple_paths() {
3148 const COMPLEX_PTH_FILE: &str = "\
3149/
3150
3151# a comment
3152/baz
3153
3154import not_an_editable_install; do_something_else_crazy_dynamic()
3155
3156# another comment
3157spam
3158
3159not_a_directory
3160";
3161
3162 const SITE_PACKAGES: &[FileSpec] = &[
3163 ("_foo.pth", "../../x/../x/y/src"),
3164 ("_lots_of_others.pth", COMPLEX_PTH_FILE),
3165 ("../x/y/src/foo.pyi", ""),
3166 ("spam/spam.py", ""),
3167 ];
3168
3169 let root_files = [("/a.py", ""), ("/baz/b.py", "")];
3170
3171 let TestCase {
3172 mut db,
3173 site_packages,
3174 ..
3175 } = TestCaseBuilder::new()
3176 .with_site_packages_files(SITE_PACKAGES)
3177 .build();
3178
3179 db.write_files(root_files).unwrap();
3180
3181 let foo_module_name = ModuleName::new_static("foo").unwrap();
3182 let a_module_name = ModuleName::new_static("a").unwrap();
3183 let b_module_name = ModuleName::new_static("b").unwrap();
3184 let spam_module_name = ModuleName::new_static("spam").unwrap();
3185
3186 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
3187 let a_module = resolve_module_confident(&db, &a_module_name).unwrap();
3188 let b_module = resolve_module_confident(&db, &b_module_name).unwrap();
3189 let spam_module = resolve_module_confident(&db, &spam_module_name).unwrap();
3190
3191 assert_eq!(
3192 foo_module.file(&db).unwrap().path(&db),
3193 &FilePath::system("/x/y/src/foo.pyi")
3194 );
3195 assert_eq!(
3196 a_module.file(&db).unwrap().path(&db),
3197 &FilePath::system("/a.py")
3198 );
3199 assert_eq!(
3200 b_module.file(&db).unwrap().path(&db),
3201 &FilePath::system("/baz/b.py")
3202 );
3203 assert_eq!(
3204 spam_module.file(&db).unwrap().path(&db),
3205 &FilePath::from(site_packages.join("spam/spam.py"))
3206 );
3207 }
3208
3209 #[test]
3210 fn module_resolution_paths_cached_between_different_module_resolutions() {
3211 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src"), ("_bar.pth", "/y/src")];
3212 let external_directories = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")];
3213
3214 let TestCase { mut db, .. } = TestCaseBuilder::new()
3215 .with_site_packages_files(SITE_PACKAGES)
3216 .build();
3217
3218 db.write_files(external_directories).unwrap();
3219
3220 let foo_module_name = ModuleName::new_static("foo").unwrap();
3221 let bar_module_name = ModuleName::new_static("bar").unwrap();
3222
3223 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
3224 assert_eq!(
3225 foo_module.file(&db).unwrap().path(&db),
3226 &FilePath::system("/x/src/foo.py")
3227 );
3228
3229 db.clear_salsa_events();
3230 let bar_module = resolve_module_confident(&db, &bar_module_name).unwrap();
3231 assert_eq!(
3232 bar_module.file(&db).unwrap().path(&db),
3233 &FilePath::system("/y/src/bar.py")
3234 );
3235 let events = db.take_salsa_events();
3236 assert_function_query_was_not_run(
3237 &db,
3238 dynamic_resolution_paths,
3239 ModuleResolveModeIngredient::new(
3240 &db,
3241 db.resolver_environment(),
3242 ModuleResolveMode::Typing,
3243 ),
3244 &events,
3245 );
3246 }
3247
3248 #[test]
3249 fn nested_site_packages_change_does_not_invalidate_dynamic_resolution_paths() {
3250 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src"), ("package/__init__.py", "")];
3251
3252 let TestCase {
3253 mut db,
3254 site_packages,
3255 ..
3256 } = TestCaseBuilder::new()
3257 .with_site_packages_files(SITE_PACKAGES)
3258 .build();
3259
3260 dynamic_resolution_paths(
3261 &db,
3262 ModuleResolveModeIngredient::new(
3263 &db,
3264 db.resolver_environment(),
3265 ModuleResolveMode::Typing,
3266 ),
3267 );
3268 db.clear_salsa_events();
3269
3270 db.write_file(site_packages.join("package/nested.py"), "")
3271 .unwrap();
3272 dynamic_resolution_paths(
3273 &db,
3274 ModuleResolveModeIngredient::new(
3275 &db,
3276 db.resolver_environment(),
3277 ModuleResolveMode::Typing,
3278 ),
3279 );
3280
3281 let events = db.take_salsa_events();
3282 assert_function_query_was_not_run(
3283 &db,
3284 dynamic_resolution_paths,
3285 ModuleResolveModeIngredient::new(
3286 &db,
3287 db.resolver_environment(),
3288 ModuleResolveMode::Typing,
3289 ),
3290 &events,
3291 );
3292 }
3293
3294 #[test]
3295 fn modifying_pth_file_invalidates_dynamic_resolution_paths() {
3296 const SITE_PACKAGES: &[FileSpec] = &[("_editable.pth", "/x/src")];
3297
3298 let TestCase {
3299 mut db,
3300 site_packages,
3301 ..
3302 } = TestCaseBuilder::new()
3303 .with_site_packages_files(SITE_PACKAGES)
3304 .build();
3305 db.write_files([("/x/src/foo.py", ""), ("/y/src/bar.py", "")])
3306 .unwrap();
3307
3308 assert!(resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).is_some());
3309
3310 let pth_path = site_packages.join("_editable.pth");
3311 db.memory_file_system()
3312 .write_file(&pth_path, "/y/src")
3313 .unwrap();
3314 File::sync_path_only(&mut db, &pth_path);
3315
3316 assert!(resolve_module_confident(&db, &ModuleName::new_static("foo").unwrap()).is_none());
3317 assert!(resolve_module_confident(&db, &ModuleName::new_static("bar").unwrap()).is_some());
3318 }
3319
3320 #[test]
3321 fn deleting_pth_file_on_which_module_resolution_depends_invalidates_cache() {
3322 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")];
3323 let x_directory = [("/x/src/foo.py", "")];
3324
3325 let TestCase {
3326 mut db,
3327 site_packages,
3328 ..
3329 } = TestCaseBuilder::new()
3330 .with_site_packages_files(SITE_PACKAGES)
3331 .build();
3332
3333 db.write_files(x_directory).unwrap();
3334
3335 let foo_module_name = ModuleName::new_static("foo").unwrap();
3336 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
3337 assert_eq!(
3338 foo_module.file(&db).unwrap().path(&db),
3339 &FilePath::system("/x/src/foo.py")
3340 );
3341
3342 db.memory_file_system()
3343 .remove_file(site_packages.join("_foo.pth"))
3344 .unwrap();
3345
3346 File::sync_path(&mut db, &site_packages.join("_foo.pth"));
3347
3348 assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
3349 }
3350
3351 #[test]
3352 fn deleting_editable_install_on_which_module_resolution_depends_invalidates_cache() {
3353 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")];
3354 let x_directory = [("/x/src/foo.py", "")];
3355
3356 let TestCase { mut db, .. } = TestCaseBuilder::new()
3357 .with_site_packages_files(SITE_PACKAGES)
3358 .build();
3359
3360 db.write_files(x_directory).unwrap();
3361
3362 let foo_module_name = ModuleName::new_static("foo").unwrap();
3363 let foo_module = resolve_module_confident(&db, &foo_module_name).unwrap();
3364 let src_path = SystemPathBuf::from("/x/src");
3365 assert_eq!(
3366 foo_module.file(&db).unwrap().path(&db),
3367 &FilePath::from(src_path.join("foo.py"))
3368 );
3369
3370 db.memory_file_system()
3371 .remove_file(src_path.join("foo.py"))
3372 .unwrap();
3373 db.memory_file_system().remove_directory(&src_path).unwrap();
3374 File::sync_path(&mut db, &src_path.join("foo.py"));
3375 File::sync_path(&mut db, &src_path);
3376 assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
3377 }
3378
3379 #[test]
3380 fn no_duplicate_search_paths_added() {
3381 let TestCase { db, .. } = TestCaseBuilder::new()
3382 .with_src_files(&[("foo.py", "")])
3383 .with_site_packages_files(&[("_foo.pth", "/src")])
3384 .build();
3385
3386 let search_paths: Vec<&SearchPath> =
3387 search_paths(&db, db.resolver_environment(), ModuleResolveMode::Typing).collect();
3388
3389 assert!(search_paths.contains(
3390 &&SearchPath::first_party(db.system(), SystemPathBuf::from("/src")).unwrap()
3391 ));
3392 assert!(
3393 !search_paths.contains(
3394 &&SearchPath::editable(db.system(), SystemPathBuf::from("/src")).unwrap()
3395 )
3396 );
3397 }
3398
3399 #[test]
3400 fn multiple_site_packages_with_editables() {
3401 let mut db = TestDb::new();
3402
3403 let venv_site_packages = SystemPathBuf::from("/venv-site-packages");
3404 let site_packages_pth = venv_site_packages.join("foo.pth");
3405 let system_site_packages = SystemPathBuf::from("/system-site-packages");
3406 let editable_install_location = SystemPathBuf::from("/x/y/a.py");
3407 let system_site_packages_location = system_site_packages.join("a.py");
3408
3409 db.memory_file_system()
3410 .create_directory_all("/src")
3411 .unwrap();
3412 db.write_files([
3413 (&site_packages_pth, "/x/y"),
3414 (&editable_install_location, ""),
3415 (&system_site_packages_location, ""),
3416 ])
3417 .unwrap();
3418
3419 db.set_search_paths(
3420 SearchPathSettings {
3421 site_packages_paths: vec![venv_site_packages, system_site_packages],
3422 ..SearchPathSettings::new(vec![SystemPathBuf::from("/src")])
3423 }
3424 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
3425 .expect("Valid search path settings"),
3426 );
3427
3428 let a_module_name = ModuleName::new_static("a").unwrap();
3431 let a_module = resolve_module_confident(&db, &a_module_name).unwrap();
3432 assert_eq!(
3433 a_module.file(&db).unwrap().path(&db),
3434 &editable_install_location
3435 );
3436
3437 db.memory_file_system()
3438 .remove_file(&site_packages_pth)
3439 .unwrap();
3440 File::sync_path(&mut db, &site_packages_pth);
3441
3442 let a_module = resolve_module_confident(&db, &a_module_name).unwrap();
3446 assert_eq!(
3447 a_module.file(&db).unwrap().path(&db),
3448 &system_site_packages_location
3449 );
3450 }
3451
3452 #[test]
3453 #[cfg(unix)]
3454 fn case_sensitive_resolution_with_symlinked_directory() -> anyhow::Result<()> {
3455 use anyhow::Context;
3456 use ruff_db::system::OsSystem;
3457
3458 let temp_dir = tempfile::TempDir::new()?;
3459 let root = SystemPathBuf::from_path_buf(
3460 temp_dir
3461 .path()
3462 .canonicalize()
3463 .context("Failed to canonicalized path")?,
3464 )
3465 .expect("UTF8 path for temp dir");
3466
3467 let mut db = TestDb::new();
3468
3469 let src = root.join("src");
3470 let a_package_target = root.join("a-package");
3471 let a_src = src.join("a");
3472
3473 db.use_system(OsSystem::new(&root));
3474
3475 db.write_file(
3476 a_package_target.join("__init__.py"),
3477 "class Foo: x: int = 4",
3478 )
3479 .context("Failed to write `a-package/__init__.py`")?;
3480
3481 db.write_file(src.join("main.py"), "print('Hy')")
3482 .context("Failed to write `main.py`")?;
3483
3484 std::os::unix::fs::symlink(a_package_target.as_std_path(), a_src.as_std_path())
3486 .context("Failed to symlink `src/a` to `a-package`")?;
3487
3488 db.set_search_paths(
3489 SearchPathSettings::new(vec![src])
3490 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
3491 .expect("Valid search path settings"),
3492 );
3493
3494 let a_module_name = ModuleName::new_static("A").unwrap();
3496 assert_eq!(resolve_module_confident(&db, &a_module_name), None);
3497
3498 let a_module_name = ModuleName::new_static("a").unwrap();
3501 let a_module = resolve_module_confident(&db, &a_module_name).expect("a.py to resolve");
3502 assert!(
3503 a_module
3504 .file(&db)
3505 .unwrap()
3506 .path(&db)
3507 .as_str()
3508 .ends_with("src/a/__init__.py"),
3509 );
3510
3511 Ok(())
3512 }
3513
3514 #[test]
3515 fn file_to_module_where_one_search_path_is_subdirectory_of_other() {
3516 let project_directory = SystemPathBuf::from("/project");
3517 let site_packages = project_directory.join(".venv/lib/python3.13/site-packages");
3518 let installed_foo_module = site_packages.join("foo/__init__.py");
3519
3520 let mut db = TestDb::new();
3521 db.write_file(&installed_foo_module, "").unwrap();
3522
3523 let search_paths = SearchPathSettings {
3524 src_roots: vec![project_directory],
3525 site_packages_paths: vec![site_packages.clone()],
3526 ..SearchPathSettings::empty()
3527 }
3528 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
3529 .expect("Valid search path settings");
3530 db.set_search_paths(search_paths);
3531
3532 let foo_module_file = File::new(&db, FilePath::from(installed_foo_module));
3533 let module = file_to_module(
3534 &db,
3535 ResolverFile::new(&db, foo_module_file, db.resolver_environment()),
3536 )
3537 .unwrap();
3538 assert_eq!(module.search_path(&db).unwrap(), &site_packages);
3539 }
3540}