1use std::borrow::Cow;
2use std::collections::btree_map::{BTreeMap, Entry};
3
4use ruff_db::files::directory_listing;
5
6use crate::ResolverEnvironment;
7use crate::db::Db;
8use crate::module::{Module, ModuleKind};
9use crate::module_name::ModuleName;
10use crate::path::{ModulePath, SearchPath, SystemOrVendoredPathRef};
11use crate::resolve::{ModuleResolveMode, ResolverContext, resolve_file_module, search_paths};
12
13pub fn all_modules<'db>(
15 db: &'db dyn Db,
16 resolver_environment: ResolverEnvironment<'db>,
17) -> Vec<Module<'db>> {
18 let mut modules = list_modules(db, resolver_environment).to_vec();
19 let mut stack = modules.clone();
20 while let Some(module) = stack.pop() {
21 for &submodule in module.all_submodules(db) {
22 modules.push(submodule);
23 stack.push(submodule);
24 }
25 }
26 modules.sort_by_key(|module| module.name(db));
27 modules
28}
29
30pub fn list_modules<'db>(
32 db: &'db dyn Db,
33 resolver_environment: ResolverEnvironment<'db>,
34) -> &'db [Module<'db>] {
35 list_modules_impl(db, resolver_environment)
36}
37
38#[salsa::tracked(returns(deref))]
39fn list_modules_impl<'db>(
40 db: &'db dyn Db,
41 resolver_environment: ResolverEnvironment<'db>,
42) -> Box<[Module<'db>]> {
43 let mut modules: BTreeMap<&ModuleName, ListedModule<'_>> = BTreeMap::new();
44 for search_path in search_paths(db, resolver_environment, ModuleResolveMode::Typing) {
45 for &new in list_modules_in(
46 db,
47 SearchPathIngredient::new(db, resolver_environment, search_path.clone()),
48 ) {
49 match modules.entry(new.module(db).name(db)) {
50 Entry::Vacant(entry) => {
51 entry.insert(new);
52 }
53 Entry::Occupied(mut entry) => {
54 let existing = entry.get();
65 let existing_is_namespace = existing.module(db).search_path(db).is_none();
66 let new_is_non_namespace = new.module(db).search_path(db).is_some();
67 if (existing_is_namespace && new_is_non_namespace)
68 || (!existing.is_stub_package(db) && new.is_stub_package(db))
69 {
70 entry.insert(new);
71 }
72 }
73 }
74 }
75 }
76 modules
77 .into_values()
78 .map(|listed| listed.module(db))
79 .collect()
80}
81
82#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
83struct SearchPathIngredient<'db> {
84 #[returns(copy)]
85 resolver_environment: ResolverEnvironment<'db>,
86 #[returns(ref)]
87 path: SearchPath,
88}
89
90#[salsa::tracked(returns(deref))]
92fn list_modules_in<'db>(
93 db: &'db dyn Db,
94 search_path: SearchPathIngredient<'db>,
95) -> Vec<ListedModule<'db>> {
96 let path = search_path.path(db);
97 tracing::debug!("Listing modules in search path '{}'", path);
98 let mut lister = Lister::new(db, search_path.resolver_environment(db), path);
99 match path.as_path() {
100 SystemOrVendoredPathRef::System(system_search_path) => {
101 let Ok(listing) = directory_listing(db, system_search_path) else {
102 return vec![];
103 };
104 for (name, file_type) in listing.iter() {
105 let path = system_search_path.join(name);
106 lister.add_path(&path.as_path().into(), file_type.into());
107 }
108 }
109 SystemOrVendoredPathRef::Vendored(vendored_search_path) => {
110 for entry in db.vendored().read_directory(vendored_search_path) {
111 lister.add_path(&entry.path().into(), entry.file_type().into());
112 }
113 }
114 }
115 lister.into_modules()
116}
117
118#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
120struct ListedModule<'db> {
121 #[returns(copy)]
122 module: Module<'db>,
123 #[returns(copy)]
124 is_stub_package: bool,
125}
126
127impl get_size2::GetSize for ListedModule<'_> {}
128
129struct Lister<'db> {
137 db: &'db dyn Db,
138 search_path: &'db SearchPath,
139 resolver_environment: ResolverEnvironment<'db>,
140 modules: BTreeMap<&'db ModuleName, ListedModule<'db>>,
141}
142
143impl<'db> Lister<'db> {
144 fn new(
147 db: &'db dyn Db,
148 resolver_environment: ResolverEnvironment<'db>,
149 search_path: &'db SearchPath,
150 ) -> Lister<'db> {
151 Lister {
152 db,
153 search_path,
154 resolver_environment,
155 modules: BTreeMap::new(),
156 }
157 }
158
159 fn into_modules(self) -> Vec<ListedModule<'db>> {
161 self.modules.into_values().collect()
162 }
163
164 fn add_path(&mut self, path: &SystemOrVendoredPathRef<'_>, file_type: FileType) {
175 let mut has_py_extension = false;
176 if let Some(ext) = path.extension() {
179 has_py_extension = is_python_extension(ext);
180 if !has_py_extension {
181 return;
182 }
183 }
184
185 let Some(name) = path.file_name() else { return };
186 let mut module_path = self.search_path.to_module_path();
187 module_path.push(name);
188 let Some(module_name) = module_path.to_module_name() else {
189 return;
190 };
191
192 if !self.search_path.is_standard_library() && self.is_non_shadowable(&module_name) {
195 return;
196 }
197
198 if file_type.is_possibly_directory() {
199 if module_path.is_regular_package(&self.context()) {
200 module_path.push("__init__");
201 if let Some(file) = resolve_file_module(&module_path, &self.context()) {
202 self.add_module(
203 &module_path,
204 Module::file_module(
205 self.db,
206 file,
207 self.resolver_environment,
208 Cow::Owned(module_name),
209 ModuleKind::Package,
210 self.search_path.clone(),
211 ),
212 );
213 return;
214 }
215 module_path.pop();
216 }
217
218 let is_dir =
246 file_type.is_definitely_directory() || module_path.is_directory(&self.context());
247 if is_dir {
248 if !self.search_path.is_standard_library() {
249 self.add_module(
250 &module_path,
251 Module::namespace_package(
252 self.db,
253 self.resolver_environment,
254 Cow::Owned(module_name),
255 ),
256 );
257 }
258 return;
259 }
260 }
263
264 if !has_py_extension {
268 return;
269 }
270 if module_path.is_stub_package() {
273 return;
274 }
275
276 let Some(file) = module_path.to_file(&self.context()) else {
277 return;
278 };
279 self.add_module(
280 &module_path,
281 Module::file_module(
282 self.db,
283 file,
284 self.resolver_environment,
285 Cow::Owned(module_name),
286 ModuleKind::Module,
287 self.search_path.clone(),
288 ),
289 );
290 }
291
292 fn add_module(&mut self, path: &ModulePath, module: Module<'db>) {
298 let listed = ListedModule::new(self.db, module, path.is_stub_package());
299 let mut entry = match self.modules.entry(module.name(self.db)) {
300 Entry::Vacant(entry) => {
301 entry.insert(listed);
302 return;
303 }
304 Entry::Occupied(entry) => entry,
305 };
306
307 let existing = entry.get().module(self.db);
308 match (existing.search_path(self.db), module.search_path(self.db)) {
309 (None, Some(_)) => {
314 entry.insert(listed);
315 }
316 (Some(_), Some(_)) => {
317 if existing.kind(self.db) == ModuleKind::Module
331 && let module_kind = module.kind(self.db)
332 && (module_kind == ModuleKind::Package
333 || module_kind == ModuleKind::Module && path.is_stub_file())
334 {
335 entry.insert(listed);
336 return;
337 }
338 if path.is_stub_package() {
341 entry.insert(listed);
342 }
343 }
344 _ => {}
345 }
346 }
347
348 fn is_non_shadowable(&self, name: &ModuleName) -> bool {
350 ModuleResolveMode::Typing.is_non_shadowable(
351 self.resolver_environment.python_version(self.db).minor,
352 name.as_str(),
353 )
354 }
355
356 fn context(&self) -> ResolverContext<'db> {
358 ResolverContext {
359 db: self.db,
360 resolver_environment: self.resolver_environment,
361 mode: ModuleResolveMode::Typing,
364 }
365 }
366}
367
368#[derive(Clone, Copy, Debug)]
370enum FileType {
371 File,
372 Directory,
373 Symlink,
374}
375
376impl FileType {
377 fn is_possibly_directory(self) -> bool {
378 matches!(self, FileType::Directory | FileType::Symlink)
379 }
380
381 fn is_definitely_directory(self) -> bool {
382 matches!(self, FileType::Directory)
383 }
384}
385
386impl From<ruff_db::vendored::FileType> for FileType {
387 fn from(ft: ruff_db::vendored::FileType) -> FileType {
388 match ft {
389 ruff_db::vendored::FileType::File => FileType::File,
390 ruff_db::vendored::FileType::Directory => FileType::Directory,
391 }
392 }
393}
394
395impl From<ruff_db::system::FileType> for FileType {
396 fn from(ft: ruff_db::system::FileType) -> FileType {
397 match ft {
398 ruff_db::system::FileType::File => FileType::File,
399 ruff_db::system::FileType::Directory => FileType::Directory,
400 ruff_db::system::FileType::Symlink => FileType::Symlink,
401 }
402 }
403}
404
405fn is_python_extension(ext: &str) -> bool {
408 matches!(ext, "py" | "pyi")
409}
410
411#[cfg(test)]
412mod tests {
413 #![expect(
414 clippy::disallowed_methods,
415 reason = "These are tests, so it's fine to do I/O by-passing System."
416 )]
417
418 use camino::{Utf8Component, Utf8Path};
419 use ruff_db::Db as _;
420 use ruff_db::files::{File, FilePath, FileRootKind};
421 use ruff_db::system::{DbWithTestSystem, DbWithWritableSystem, SystemPath, SystemPathBuf};
422 use ruff_db::testing::{
423 assert_function_query_was_not_run, assert_function_query_was_not_run_by_name,
424 };
425 use ruff_python_ast::PythonVersion;
426 use salsa::plumbing::AsId as _;
427
428 use crate::db::{Db, tests::TestDb};
429 use crate::module::Module;
430 use crate::resolve::{
431 ModuleResolveMode, ModuleResolveModeIngredient, dynamic_resolution_paths,
432 };
433 use crate::settings::SearchPathSettings;
434 use crate::strategy::FallibleStrategy;
435 use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
436
437 fn list_modules(db: &TestDb) -> &[Module<'_>] {
438 super::list_modules(db, db.resolver_environment())
439 }
440
441 struct ModuleDebugSnapshot<'db> {
442 db: &'db dyn Db,
443 module: Module<'db>,
444 }
445
446 impl std::fmt::Debug for ModuleDebugSnapshot<'_> {
447 fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
448 match self.module {
449 Module::Namespace(pkg) => {
450 write!(f, "Module::Namespace({name:?})", name = pkg.name(self.db))
451 }
452 Module::File(module) => {
453 let path_components = match module.file(self.db).path(self.db) {
456 FilePath::System(path) => path.components(),
457 FilePath::Vendored(path) => path.components(),
458 FilePath::SystemVirtual(path) => Utf8Path::new(path.as_str()).components(),
459 };
460 let nice_path = path_components
461 .map(|component| {
466 if let Utf8Component::RootDir = component {
467 Utf8Component::Normal("")
468 } else {
469 component
470 }
471 })
472 .map(|component| component.as_str())
473 .collect::<Vec<&str>>()
474 .join("/");
475 write!(
476 f,
477 "Module::File({name:?}, {search_path:?}, {path:?}, {kind:?}, {known:?})",
478 name = module.name(self.db).as_str(),
479 search_path = module.search_path(self.db).debug_kind(),
480 path = nice_path,
481 kind = module.kind(self.db),
482 known = module.known(self.db),
483 )
484 }
485 }
486 }
487 }
488
489 fn sorted_list(db: &TestDb) -> Vec<Module<'_>> {
490 let mut modules = list_modules(db).to_vec();
491 modules.sort_by(|m1, m2| m1.name(db).cmp(m2.name(db)));
492 modules
493 }
494
495 fn list_snapshot(db: &TestDb) -> Vec<ModuleDebugSnapshot<'_>> {
496 list_snapshot_filter(db, |_| true)
497 }
498
499 fn list_snapshot_filter<'db>(
500 db: &'db TestDb,
501 predicate: impl Fn(&Module<'db>) -> bool,
502 ) -> Vec<ModuleDebugSnapshot<'db>> {
503 sorted_list(db)
504 .into_iter()
505 .filter(predicate)
506 .map(|module| ModuleDebugSnapshot { db, module })
507 .collect()
508 }
509
510 #[test]
511 fn first_party_module() {
512 let TestCase { db, .. } = TestCaseBuilder::new()
513 .with_src_files(&[("foo.py", "")])
514 .build();
515
516 insta::assert_debug_snapshot!(
517 list_snapshot(&db),
518 @r#"
519 [
520 Module::File("foo", "first-party", "/src/foo.py", Module, None),
521 ]
522 "#,
523 );
524 }
525
526 #[test]
527 fn stubs_over_module_source() {
528 let TestCase { db, .. } = TestCaseBuilder::new()
529 .with_src_files(&[("foo.py", ""), ("foo.pyi", "")])
530 .build();
531
532 insta::assert_debug_snapshot!(
533 list_snapshot(&db),
534 @r#"
535 [
536 Module::File("foo", "first-party", "/src/foo.pyi", Module, None),
537 ]
538 "#,
539 );
540 }
541
542 #[test]
543 fn stubs_over_package_source() {
544 let TestCase { db, .. } = TestCaseBuilder::new()
545 .with_src_files(&[("foo/__init__.py", ""), ("foo.pyi", "")])
546 .build();
547
548 insta::assert_debug_snapshot!(
557 list_snapshot(&db),
558 @r#"
559 [
560 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
561 ]
562 "#,
563 );
564 }
565
566 #[test]
576 fn package_over_module1() {
577 let TestCase { db, .. } = TestCaseBuilder::new()
578 .with_src_files(&[("foo.py", ""), ("foo/__init__.py", "")])
579 .build();
580
581 insta::assert_debug_snapshot!(
582 list_snapshot(&db),
583 @r#"
584 [
585 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
586 ]
587 "#,
588 );
589 }
590
591 #[test]
596 fn package_over_module2() {
597 let TestCase { db, .. } = TestCaseBuilder::new()
598 .with_src_files(&[("foo/__init__.py", ""), ("foo.py", "")])
599 .build();
600
601 insta::assert_debug_snapshot!(
602 list_snapshot(&db),
603 @r#"
604 [
605 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
606 ]
607 "#,
608 );
609 }
610
611 #[test]
612 fn builtins_vendored() {
613 let TestCase { db, .. } = TestCaseBuilder::new()
614 .with_vendored_typeshed()
615 .with_src_files(&[("builtins.py", "FOOOO = 42")])
616 .build();
617
618 insta::assert_debug_snapshot!(
619 list_snapshot_filter(&db, |m| m.name(&db).as_str() == "builtins"),
620 @r#"
621 [
622 Module::File("builtins", "std-vendored", "stdlib/builtins.pyi", Module, Some(Builtins)),
623 ]
624 "#,
625 );
626 }
627
628 #[test]
629 fn ty_extensions_vendored() {
630 let TestCase { db, .. } = TestCaseBuilder::new().with_vendored_typeshed().build();
631
632 insta::assert_debug_snapshot!(
633 list_snapshot_filter(&db, |module| module.name(&db).as_str() == "ty_extensions"),
634 @r#"
635 [
636 Module::File("ty_extensions", "std-vendored", "stdlib/ty_extensions/__init__.pyi", Package, Some(TyExtensions)),
637 ]
638 "#,
639 );
640 }
641
642 #[test]
643 fn builtins_custom() {
644 const TYPESHED: MockedTypeshed = MockedTypeshed {
645 stdlib_files: &[("builtins.pyi", "def min(a, b): ...")],
646 versions: "builtins: 3.8-",
647 };
648
649 const SRC: &[FileSpec] = &[("builtins.py", "FOOOO = 42")];
650
651 let TestCase { db, .. } = TestCaseBuilder::new()
652 .with_src_files(SRC)
653 .with_mocked_typeshed(TYPESHED)
654 .with_python_version(PythonVersion::PY38)
655 .build();
656
657 insta::assert_debug_snapshot!(
658 list_snapshot(&db),
659 @r#"
660 [
661 Module::File("builtins", "std-custom", "/typeshed/stdlib/builtins.pyi", Module, Some(Builtins)),
662 ]
663 "#,
664 );
665 }
666
667 #[test]
668 fn stdlib() {
669 const TYPESHED: MockedTypeshed = MockedTypeshed {
670 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
671 versions: "functools: 3.8-",
672 };
673
674 let TestCase { db, .. } = TestCaseBuilder::new()
675 .with_mocked_typeshed(TYPESHED)
676 .with_python_version(PythonVersion::PY38)
677 .build();
678
679 insta::assert_debug_snapshot!(
680 list_snapshot(&db),
681 @r#"
682 [
683 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
684 ]
685 "#,
686 );
687 }
688
689 #[test]
690 fn stdlib_resolution_respects_versions_file_py38_existing_modules() {
691 const VERSIONS: &str = "\
692 asyncio: 3.8- # 'Regular' package on py38+
693 asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
694 functools: 3.8- # Top-level single-file module
695 random: 3.8- # 'Regular' file module on py38+
696 xml: 3.8-3.8 # Namespace package on py38 only
697 ";
698
699 const STDLIB: &[FileSpec] = &[
700 ("asyncio/__init__.pyi", ""),
701 ("asyncio/tasks.pyi", ""),
702 ("functools.pyi", ""),
703 ("random.pyi", ""),
704 ("xml/etree.pyi", ""),
705 ];
706
707 const TYPESHED: MockedTypeshed = MockedTypeshed {
708 stdlib_files: STDLIB,
709 versions: VERSIONS,
710 };
711
712 let TestCase { db, .. } = TestCaseBuilder::new()
713 .with_mocked_typeshed(TYPESHED)
714 .with_python_version(PythonVersion::PY38)
715 .build();
716
717 insta::assert_debug_snapshot!(
722 list_snapshot(&db),
723 @r#"
724 [
725 Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
726 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
727 Module::File("random", "std-custom", "/typeshed/stdlib/random.pyi", Module, None),
728 ]
729 "#,
730 );
731 }
732
733 #[test]
734 fn stdlib_resolution_respects_versions_file_py38_nonexisting_modules() {
735 const VERSIONS: &str = "\
736 asyncio: 3.8- # 'Regular' package on py38+
737 asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
738 collections: 3.9- # 'Regular' package on py39+
739 importlib: 3.9- # Namespace package on py39+
740 random: 3.9- # 'Regular' file module on py39+
741 xml: 3.8-3.8 # Namespace package on 3.8 only
742 foo: 3.9-
743 ";
744
745 const STDLIB: &[FileSpec] = &[
746 ("collections/__init__.pyi", ""),
747 ("asyncio/__init__.pyi", ""),
748 ("asyncio/tasks.pyi", ""),
749 ("importlib/abc.pyi", ""),
750 ("random.pyi", ""),
751 ("xml/etree.pyi", ""),
752 ];
753
754 const TYPESHED: MockedTypeshed = MockedTypeshed {
755 stdlib_files: STDLIB,
756 versions: VERSIONS,
757 };
758
759 let TestCase { db, .. } = TestCaseBuilder::new()
760 .with_mocked_typeshed(TYPESHED)
761 .with_python_version(PythonVersion::PY38)
762 .build();
763
764 insta::assert_debug_snapshot!(
772 list_snapshot(&db),
773 @r#"
774 [
775 Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
776 ]
777 "#,
778 );
779 }
780
781 #[test]
782 fn stdlib_resolution_respects_versions_file_py39_existing_modules() {
783 const VERSIONS: &str = "\
784 asyncio: 3.8- # 'Regular' package on py38+
785 asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
786 collections: 3.9- # 'Regular' package on py39+
787 functools: 3.8- # Top-level single-file module
788 importlib: 3.9- # Namespace package on py39+
789 ";
790
791 const STDLIB: &[FileSpec] = &[
792 ("asyncio/__init__.pyi", ""),
793 ("asyncio/tasks.pyi", ""),
794 ("collections/__init__.pyi", ""),
795 ("functools.pyi", ""),
796 ("importlib/abc.pyi", ""),
797 ];
798
799 const TYPESHED: MockedTypeshed = MockedTypeshed {
800 stdlib_files: STDLIB,
801 versions: VERSIONS,
802 };
803
804 let TestCase { db, .. } = TestCaseBuilder::new()
805 .with_mocked_typeshed(TYPESHED)
806 .with_python_version(PythonVersion::PY39)
807 .build();
808
809 insta::assert_debug_snapshot!(
814 list_snapshot(&db),
815 @r#"
816 [
817 Module::File("asyncio", "std-custom", "/typeshed/stdlib/asyncio/__init__.pyi", Package, None),
818 Module::File("collections", "std-custom", "/typeshed/stdlib/collections/__init__.pyi", Package, Some(Collections)),
819 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
820 ]
821 "#,
822 );
823 }
824
825 #[test]
826 fn stdlib_resolution_respects_versions_file_py39_nonexisting_modules() {
827 const VERSIONS: &str = "\
828 importlib: 3.9- # 'Regular' package on py39+
829 xml: 3.8-3.8 # 'Regular' package on 3.8 only
830 ";
831
832 const STDLIB: &[FileSpec] = &[
838 ("importlib/__init__.pyi", ""),
839 ("importlib/abc.pyi", ""),
840 ("xml/__init__.pyi", ""),
841 ("xml/etree.pyi", ""),
842 ];
843
844 const TYPESHED: MockedTypeshed = MockedTypeshed {
845 stdlib_files: STDLIB,
846 versions: VERSIONS,
847 };
848
849 let TestCase { db, .. } = TestCaseBuilder::new()
850 .with_mocked_typeshed(TYPESHED)
851 .with_python_version(PythonVersion::PY39)
852 .build();
853
854 insta::assert_debug_snapshot!(
855 list_snapshot(&db),
856 @r#"
857 [
858 Module::File("importlib", "std-custom", "/typeshed/stdlib/importlib/__init__.pyi", Package, Some(ImportLib)),
859 ]
860 "#,
861 );
862 }
863
864 #[test]
865 fn first_party_precedence_over_stdlib() {
866 const SRC: &[FileSpec] = &[("functools.py", "def update_wrapper(): ...")];
867
868 const TYPESHED: MockedTypeshed = MockedTypeshed {
869 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
870 versions: "functools: 3.8-",
871 };
872
873 let TestCase { db, .. } = TestCaseBuilder::new()
874 .with_src_files(SRC)
875 .with_mocked_typeshed(TYPESHED)
876 .with_python_version(PythonVersion::PY38)
877 .build();
878
879 insta::assert_debug_snapshot!(
880 list_snapshot(&db),
881 @r#"
882 [
883 Module::File("functools", "first-party", "/src/functools.py", Module, None),
884 ]
885 "#,
886 );
887 }
888
889 #[test]
890 fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
891 let TestCase { db, .. } = TestCaseBuilder::new().with_vendored_typeshed().build();
892
893 insta::assert_debug_snapshot!(
894 list_snapshot_filter(&db, |m| m.name(&db).as_str().contains("pydoc_data")),
895 @r#"
896 [
897 Module::File("pydoc_data", "std-vendored", "stdlib/pydoc_data/__init__.pyi", Package, None),
898 ]
899 "#,
900 );
901 }
902
903 #[test]
904 fn resolve_package() {
905 let TestCase { db, .. } = TestCaseBuilder::new()
906 .with_src_files(&[("foo/__init__.py", "print('Hello, world!'")])
907 .build();
908
909 insta::assert_debug_snapshot!(
910 list_snapshot(&db),
911 @r#"
912 [
913 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
914 ]
915 "#,
916 );
917 }
918
919 #[test]
920 fn package_priority_over_module() {
921 const SRC: &[FileSpec] = &[
922 ("foo/__init__.py", "print('Hello, world!')"),
923 ("foo.py", "print('Hello, world!')"),
924 ];
925
926 let TestCase { db, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
927
928 insta::assert_debug_snapshot!(
929 list_snapshot(&db),
930 @r#"
931 [
932 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
933 ]
934 "#,
935 );
936 }
937
938 #[test]
939 fn typing_stub_over_module() {
940 const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];
941
942 let TestCase { db, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
943
944 insta::assert_debug_snapshot!(
945 list_snapshot(&db),
946 @r#"
947 [
948 Module::File("foo", "first-party", "/src/foo.pyi", Module, None),
949 ]
950 "#,
951 );
952 }
953
954 #[test]
955 fn sub_packages() {
956 const SRC: &[FileSpec] = &[
957 ("foo/__init__.py", ""),
958 ("foo/bar/__init__.py", ""),
959 ("foo/bar/baz.py", "print('Hello, world!)'"),
960 ];
961
962 let TestCase { db, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
963
964 insta::assert_debug_snapshot!(
965 list_snapshot(&db),
966 @r#"
967 [
968 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
969 ]
970 "#,
971 );
972 }
973
974 #[test]
975 fn module_search_path_priority() {
976 let TestCase { db, .. } = TestCaseBuilder::new()
977 .with_src_files(&[("foo.py", "")])
978 .with_site_packages_files(&[("foo.py", "")])
979 .build();
980
981 insta::assert_debug_snapshot!(
982 list_snapshot(&db),
983 @r#"
984 [
985 Module::File("foo", "first-party", "/src/foo.py", Module, None),
986 ]
987 "#,
988 );
989 }
990
991 #[test]
992 #[cfg(target_family = "unix")]
993 fn symlink() -> anyhow::Result<()> {
994 use anyhow::Context;
995
996 let mut db = TestDb::new().with_python_version(PythonVersion::PY38);
997
998 let temp_dir = tempfile::TempDir::with_prefix("PREFIX-SENTINEL")?;
999 let root = temp_dir
1000 .path()
1001 .canonicalize()
1002 .context("Failed to canonicalize temp dir")?;
1003 let root = SystemPath::from_std_path(&root).unwrap();
1004 db.use_system(ruff_db::system::OsSystem::new(root));
1005
1006 let src = root.join("src");
1007 let site_packages = root.join("site-packages");
1008 let custom_typeshed = root.join("typeshed");
1009
1010 let foo = src.join("foo.py");
1011 let bar = src.join("bar.py");
1012
1013 std::fs::create_dir_all(src.as_std_path())?;
1014 std::fs::create_dir_all(site_packages.as_std_path())?;
1015 std::fs::create_dir_all(custom_typeshed.join("stdlib").as_std_path())?;
1016 std::fs::File::create(custom_typeshed.join("stdlib/VERSIONS").as_std_path())?;
1017
1018 std::fs::write(foo.as_std_path(), "")?;
1019 std::os::unix::fs::symlink(foo.as_std_path(), bar.as_std_path())?;
1020
1021 let settings = SearchPathSettings {
1022 src_roots: vec![src.clone()],
1023 custom_typeshed: Some(custom_typeshed),
1024 site_packages_paths: vec![site_packages],
1025 ..SearchPathSettings::empty()
1026 };
1027
1028 db.set_search_paths(
1029 settings
1030 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
1031 .expect("Valid search path settings"),
1032 );
1033
1034 db.files().try_add_root(&db, &src, FileRootKind::Project);
1035
1036 insta::with_settings!({
1041 filters => [(r#""\S*PREFIX-SENTINEL.*?/"#, r#""/"#)],
1044 }, {
1045 insta::assert_debug_snapshot!(
1046 list_snapshot(&db),
1047 @r#"
1048 [
1049 Module::File("bar", "first-party", "/src/bar.py", Module, None),
1050 Module::File("foo", "first-party", "/src/foo.py", Module, None),
1051 ]
1052 "#,
1053 );
1054 });
1055
1056 Ok(())
1057 }
1058
1059 #[test]
1065 fn adding_file_on_which_module_resolution_depends_invalidates_previously_failing_query_that_now_succeeds()
1066 -> anyhow::Result<()> {
1067 let TestCase { mut db, src, .. } = TestCaseBuilder::new().build();
1068 let foo_path = src.join("foo.py");
1069
1070 insta::assert_debug_snapshot!(
1071 list_snapshot(&db),
1072 @"[]",
1073 );
1074
1075 db.write_file(&foo_path, "x = 1")?;
1077
1078 insta::assert_debug_snapshot!(
1079 list_snapshot(&db),
1080 @r#"
1081 [
1082 Module::File("foo", "first-party", "/src/foo.py", Module, None),
1083 ]
1084 "#,
1085 );
1086
1087 Ok(())
1088 }
1089
1090 #[test]
1091 fn deeply_nested_file_does_not_invalidate_top_level_listing() -> anyhow::Result<()> {
1092 let TestCase { mut db, src, .. } = TestCaseBuilder::new()
1093 .with_src_files(&[("package/__init__.py", ""), ("package/sub/__init__.py", "")])
1094 .build();
1095
1096 list_modules(&db);
1097 db.clear_salsa_events();
1098
1099 db.write_file(src.join("package/sub/nested.py"), "")?;
1100 list_modules(&db);
1101
1102 let events = db.take_salsa_events();
1103 assert_function_query_was_not_run_by_name(&db, "list_modules_in", None, &events);
1104
1105 Ok(())
1106 }
1107
1108 #[test]
1109 fn sibling_file_does_not_invalidate_package_submodules() -> anyhow::Result<()> {
1110 let TestCase { mut db, src, .. } = TestCaseBuilder::new()
1111 .with_src_files(&[("package/__init__.py", "")])
1112 .build();
1113
1114 let package_id = {
1115 let package = list_modules(&db)
1116 .iter()
1117 .find(|module| module.name(&db).as_str() == "package")
1118 .copied()
1119 .expect("package to exist");
1120 package.all_submodules(&db);
1121 package.as_id()
1122 };
1123 db.clear_salsa_events();
1124
1125 db.write_file(src.join("sibling.py"), "")?;
1126 let package = list_modules(&db)
1127 .iter()
1128 .find(|module| module.name(&db).as_str() == "package")
1129 .copied()
1130 .expect("package to exist");
1131 package.all_submodules(&db);
1132
1133 let events = db.take_salsa_events();
1134 assert_function_query_was_not_run_by_name(
1135 &db,
1136 "all_submodule_names_for_package",
1137 Some(package_id),
1138 &events,
1139 );
1140
1141 Ok(())
1142 }
1143
1144 #[test]
1145 fn removing_file_on_which_module_resolution_depends_invalidates_previously_successful_query_that_now_fails()
1146 -> anyhow::Result<()> {
1147 const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/__init__.py", "x = 2")];
1148
1149 let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
1150 let foo_path = src.join("foo/__init__.py");
1151
1152 insta::assert_debug_snapshot!(
1153 list_snapshot(&db),
1154 @r#"
1155 [
1156 Module::File("foo", "first-party", "/src/foo/__init__.py", Package, None),
1157 ]
1158 "#,
1159 );
1160
1161 db.memory_file_system().remove_file(&foo_path)?;
1164 db.memory_file_system()
1165 .remove_directory(foo_path.parent().unwrap())?;
1166 File::sync_path(&mut db, &foo_path);
1171 File::sync_path(&mut db, foo_path.parent().unwrap());
1172
1173 insta::assert_debug_snapshot!(
1174 list_snapshot(&db),
1175 @r#"
1176 [
1177 Module::File("foo", "first-party", "/src/foo.py", Module, None),
1178 ]
1179 "#,
1180 );
1181
1182 Ok(())
1183 }
1184
1185 #[test]
1190 fn adding_file_to_search_path_with_lower_priority_does_not_change_results() {
1191 const TYPESHED: MockedTypeshed = MockedTypeshed {
1192 versions: "functools: 3.8-",
1193 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
1194 };
1195
1196 let TestCase {
1197 mut db,
1198 site_packages,
1199 ..
1200 } = TestCaseBuilder::new()
1201 .with_mocked_typeshed(TYPESHED)
1202 .with_python_version(PythonVersion::PY38)
1203 .build();
1204
1205 insta::assert_debug_snapshot!(
1206 list_snapshot(&db),
1207 @r#"
1208 [
1209 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
1210 ]
1211 "#,
1212 );
1213
1214 db.clear_salsa_events();
1217 let site_packages_functools_path = site_packages.join("functools.py");
1218 db.write_file(&site_packages_functools_path, "f: int")
1219 .unwrap();
1220
1221 insta::assert_debug_snapshot!(
1222 list_snapshot(&db),
1223 @r#"
1224 [
1225 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
1226 ]
1227 "#,
1228 );
1229 }
1230
1231 #[test]
1232 fn adding_file_to_search_path_with_higher_priority_invalidates_the_query() {
1233 const TYPESHED: MockedTypeshed = MockedTypeshed {
1234 versions: "functools: 3.8-",
1235 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
1236 };
1237
1238 let TestCase { mut db, src, .. } = TestCaseBuilder::new()
1239 .with_mocked_typeshed(TYPESHED)
1240 .with_python_version(PythonVersion::PY38)
1241 .build();
1242
1243 insta::assert_debug_snapshot!(
1244 list_snapshot(&db),
1245 @r#"
1246 [
1247 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
1248 ]
1249 "#,
1250 );
1251
1252 let src_functools_path = src.join("functools.py");
1256 db.write_file(&src_functools_path, "FOO: int").unwrap();
1257
1258 insta::assert_debug_snapshot!(
1259 list_snapshot(&db),
1260 @r#"
1261 [
1262 Module::File("functools", "first-party", "/src/functools.py", Module, None),
1263 ]
1264 "#,
1265 );
1266 }
1267
1268 #[test]
1269 fn deleting_file_from_higher_priority_search_path_invalidates_the_query() {
1270 const SRC: &[FileSpec] = &[("functools.py", "FOO: int")];
1271
1272 const TYPESHED: MockedTypeshed = MockedTypeshed {
1273 versions: "functools: 3.8-",
1274 stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
1275 };
1276
1277 let TestCase { mut db, src, .. } = TestCaseBuilder::new()
1278 .with_src_files(SRC)
1279 .with_mocked_typeshed(TYPESHED)
1280 .with_python_version(PythonVersion::PY38)
1281 .build();
1282 let src_functools_path = src.join("functools.py");
1283
1284 insta::assert_debug_snapshot!(
1285 list_snapshot(&db),
1286 @r#"
1287 [
1288 Module::File("functools", "first-party", "/src/functools.py", Module, None),
1289 ]
1290 "#,
1291 );
1292
1293 db.memory_file_system()
1296 .remove_file(&src_functools_path)
1297 .unwrap();
1298 File::sync_path(&mut db, &src_functools_path);
1304
1305 insta::assert_debug_snapshot!(
1306 list_snapshot(&db),
1307 @r#"
1308 [
1309 Module::File("functools", "std-custom", "/typeshed/stdlib/functools.pyi", Module, Some(Functools)),
1310 ]
1311 "#,
1312 );
1313 }
1314
1315 #[test]
1316 fn editable_install_absolute_path() {
1317 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")];
1318 let x_directory = [("/x/src/foo/__init__.py", ""), ("/x/src/foo/bar.py", "")];
1319
1320 let TestCase { mut db, .. } = TestCaseBuilder::new()
1321 .with_site_packages_files(SITE_PACKAGES)
1322 .with_library_root("/x")
1323 .build();
1324
1325 db.write_files(x_directory).unwrap();
1326
1327 insta::assert_debug_snapshot!(
1328 list_snapshot(&db),
1329 @r#"
1330 [
1331 Module::File("foo", "editable", "/x/src/foo/__init__.py", Package, None),
1332 ]
1333 "#,
1334 );
1335 }
1336
1337 #[test]
1338 fn editable_install_pth_file_with_whitespace() {
1339 const SITE_PACKAGES: &[FileSpec] = &[
1340 ("_foo.pth", " /x/src"),
1341 ("_bar.pth", "/y/src "),
1342 ];
1343 let external_files = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")];
1344
1345 let TestCase { mut db, .. } = TestCaseBuilder::new()
1346 .with_site_packages_files(SITE_PACKAGES)
1347 .with_library_root("/y/src")
1348 .build();
1349
1350 db.write_files(external_files).unwrap();
1351
1352 insta::assert_debug_snapshot!(
1356 list_snapshot(&db),
1357 @r#"
1358 [
1359 Module::File("bar", "editable", "/y/src/bar.py", Module, None),
1360 ]
1361 "#,
1362 );
1363 }
1364
1365 #[test]
1366 fn editable_install_relative_path() {
1367 const SITE_PACKAGES: &[FileSpec] = &[
1368 ("_foo.pth", "../../x/../x/y/src"),
1369 ("../x/y/src/foo.pyi", ""),
1370 ];
1371
1372 let TestCase { db, .. } = TestCaseBuilder::new()
1373 .with_site_packages_files(SITE_PACKAGES)
1374 .with_library_root("/x")
1375 .build();
1376
1377 insta::assert_debug_snapshot!(
1378 list_snapshot(&db),
1379 @r#"
1380 [
1381 Module::File("foo", "editable", "/x/y/src/foo.pyi", Module, None),
1382 ]
1383 "#,
1384 );
1385 }
1386
1387 #[test]
1388 fn editable_install_multiple_pth_files_with_multiple_paths() {
1389 const COMPLEX_PTH_FILE: &str = "\
1390/
1391
1392# a comment
1393/baz
1394
1395import not_an_editable_install; do_something_else_crazy_dynamic()
1396
1397# another comment
1398spam
1399
1400not_a_directory
1401";
1402
1403 const SITE_PACKAGES: &[FileSpec] = &[
1404 ("_foo.pth", "../../x/../x/y/src"),
1405 ("_lots_of_others.pth", COMPLEX_PTH_FILE),
1406 ("../x/y/src/foo.pyi", ""),
1407 ("spam/spam.py", ""),
1408 ];
1409
1410 let root_files = [("/a.py", ""), ("/baz/b.py", "")];
1411
1412 let TestCase { mut db, .. } = TestCaseBuilder::new()
1413 .with_site_packages_files(SITE_PACKAGES)
1414 .with_library_root("/x/y/src")
1415 .with_library_root("/")
1416 .with_library_root("/baz")
1417 .build();
1418
1419 db.write_files(root_files).unwrap();
1420
1421 insta::assert_debug_snapshot!(
1427 list_snapshot(&db),
1428 @r#"
1429 [
1430 Module::File("a", "editable", "/a.py", Module, None),
1431 Module::File("b", "editable", "/baz/b.py", Module, None),
1432 Module::Namespace(ModuleName("baz")),
1433 Module::File("foo", "editable", "/x/y/src/foo.pyi", Module, None),
1434 Module::File("spam", "editable", "/site-packages/spam/spam.py", Module, None),
1435 Module::Namespace(ModuleName("src")),
1436 Module::Namespace(ModuleName("typeshed")),
1437 Module::Namespace(ModuleName("x")),
1438 ]
1439 "#,
1440 );
1441 }
1442
1443 #[test]
1444 fn module_resolution_paths_cached_between_different_module_resolutions() {
1445 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src"), ("_bar.pth", "/y/src")];
1446 let external_directories = [("/x/src/foo.py", ""), ("/y/src/bar.py", "")];
1447
1448 let TestCase { mut db, .. } = TestCaseBuilder::new()
1449 .with_site_packages_files(SITE_PACKAGES)
1450 .with_library_root("/x")
1451 .with_library_root("/y")
1452 .build();
1453
1454 db.write_files(external_directories).unwrap();
1455
1456 insta::assert_debug_snapshot!(
1457 list_snapshot(&db),
1458 @r#"
1459 [
1460 Module::File("bar", "editable", "/y/src/bar.py", Module, None),
1461 Module::File("foo", "editable", "/x/src/foo.py", Module, None),
1462 ]
1463 "#,
1464 );
1465
1466 db.clear_salsa_events();
1467
1468 insta::assert_debug_snapshot!(
1469 list_snapshot(&db),
1470 @r#"
1471 [
1472 Module::File("bar", "editable", "/y/src/bar.py", Module, None),
1473 Module::File("foo", "editable", "/x/src/foo.py", Module, None),
1474 ]
1475 "#,
1476 );
1477
1478 let events = db.take_salsa_events();
1479 assert_function_query_was_not_run(
1480 &db,
1481 dynamic_resolution_paths,
1482 ModuleResolveModeIngredient::new(
1483 &db,
1484 db.resolver_environment(),
1485 ModuleResolveMode::Typing,
1486 ),
1487 &events,
1488 );
1489 }
1490
1491 #[test]
1492 fn deleting_pth_file_on_which_module_resolution_depends_invalidates_cache() {
1493 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")];
1494 let x_directory = [("/x/src/foo.py", "")];
1495
1496 let TestCase {
1497 mut db,
1498 site_packages,
1499 ..
1500 } = TestCaseBuilder::new()
1501 .with_site_packages_files(SITE_PACKAGES)
1502 .with_library_root("/x")
1503 .build();
1504
1505 db.write_files(x_directory).unwrap();
1506
1507 insta::assert_debug_snapshot!(
1508 list_snapshot(&db),
1509 @r#"
1510 [
1511 Module::File("foo", "editable", "/x/src/foo.py", Module, None),
1512 ]
1513 "#,
1514 );
1515
1516 db.memory_file_system()
1517 .remove_file(site_packages.join("_foo.pth"))
1518 .unwrap();
1519 File::sync_path(&mut db, &site_packages.join("_foo.pth"));
1524
1525 insta::assert_debug_snapshot!(
1526 list_snapshot(&db),
1527 @"[]",
1528 );
1529 }
1530
1531 #[test]
1532 fn deleting_editable_install_on_which_module_resolution_depends_invalidates_cache() {
1533 const SITE_PACKAGES: &[FileSpec] = &[("_foo.pth", "/x/src")];
1534 let x_directory = [("/x/src/foo.py", "")];
1535
1536 let TestCase { mut db, .. } = TestCaseBuilder::new()
1537 .with_site_packages_files(SITE_PACKAGES)
1538 .with_library_root("/x")
1539 .build();
1540 let src_path = SystemPathBuf::from("/x/src");
1541
1542 db.write_files(x_directory).unwrap();
1543
1544 insta::assert_debug_snapshot!(
1545 list_snapshot(&db),
1546 @r#"
1547 [
1548 Module::File("foo", "editable", "/x/src/foo.py", Module, None),
1549 ]
1550 "#,
1551 );
1552
1553 db.memory_file_system()
1554 .remove_file(src_path.join("foo.py"))
1555 .unwrap();
1556 db.memory_file_system().remove_directory(&src_path).unwrap();
1557 File::sync_path(&mut db, &src_path.join("foo.py"));
1562 File::sync_path(&mut db, &src_path);
1563
1564 insta::assert_debug_snapshot!(
1565 list_snapshot(&db),
1566 @"[]",
1567 );
1568 }
1569
1570 #[test]
1571 fn editable_installs_into_first_party_search_path() {
1572 let mut db = TestDb::new();
1573
1574 let src = SystemPath::new("/src");
1575 let venv_site_packages = SystemPathBuf::from("/venv-site-packages");
1576 let site_packages_pth = venv_site_packages.join("foo.pth");
1577 let editable_install_location = src.join("x/y/a.py");
1578
1579 db.write_files([
1580 (&site_packages_pth, "/src/x/y/"),
1581 (&editable_install_location, ""),
1582 ])
1583 .unwrap();
1584
1585 db.files()
1586 .try_add_root(&db, SystemPath::new("/src"), FileRootKind::Project);
1587
1588 let settings = SearchPathSettings {
1589 site_packages_paths: vec![venv_site_packages],
1590 ..SearchPathSettings::new(vec![src.to_path_buf()])
1591 };
1592
1593 db.set_search_paths(
1594 settings
1595 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
1596 .expect("Valid search path settings"),
1597 );
1598
1599 insta::assert_debug_snapshot!(
1600 list_snapshot_filter(&db, |m| m.name(&db).as_str() == "a"),
1601 @r#"
1602 [
1603 Module::File("a", "editable", "/src/x/y/a.py", Module, None),
1604 ]
1605 "#,
1606 );
1607
1608 let editable_root = db
1609 .files()
1610 .root(&db, &editable_install_location)
1611 .expect("file root for editable install");
1612
1613 assert_eq!(editable_root.path(&db), src);
1614 }
1615
1616 #[test]
1617 fn multiple_site_packages_with_editables() {
1618 let mut db = TestDb::new();
1619
1620 let venv_site_packages = SystemPathBuf::from("/venv-site-packages");
1621 let site_packages_pth = venv_site_packages.join("foo.pth");
1622 let system_site_packages = SystemPathBuf::from("/system-site-packages");
1623 let editable_install_location = SystemPathBuf::from("/x/y/a.py");
1624 let system_site_packages_location = system_site_packages.join("a.py");
1625
1626 db.memory_file_system()
1627 .create_directory_all("/src")
1628 .unwrap();
1629 db.write_files([
1630 (&site_packages_pth, "/x/y"),
1631 (&editable_install_location, ""),
1632 (&system_site_packages_location, ""),
1633 ])
1634 .unwrap();
1635
1636 db.files()
1637 .try_add_root(&db, SystemPath::new("/src"), FileRootKind::Project);
1638
1639 let settings = SearchPathSettings {
1640 site_packages_paths: vec![venv_site_packages, system_site_packages],
1641 ..SearchPathSettings::new(vec![SystemPathBuf::from("/src")])
1642 };
1643
1644 db.set_search_paths(
1645 settings
1646 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
1647 .expect("Valid search path settings"),
1648 );
1649
1650 insta::assert_debug_snapshot!(
1654 list_snapshot_filter(&db, |m| m.name(&db).as_str() == "a"),
1655 @r#"
1656 [
1657 Module::File("a", "editable", "/x/y/a.py", Module, None),
1658 ]
1659 "#,
1660 );
1661
1662 db.memory_file_system()
1663 .remove_file(&site_packages_pth)
1664 .unwrap();
1665 File::sync_path(&mut db, &site_packages_pth);
1670
1671 insta::assert_debug_snapshot!(
1676 list_snapshot_filter(&db, |m| m.name(&db).as_str() == "a"),
1677 @r#"
1678 [
1679 Module::File("a", "site-packages", "/system-site-packages/a.py", Module, None),
1680 ]
1681 "#,
1682 );
1683 }
1684
1685 #[test]
1686 #[cfg(unix)]
1687 fn case_sensitive_resolution_with_symlinked_directory() -> anyhow::Result<()> {
1688 use anyhow::Context as _;
1689
1690 let temp_dir = tempfile::TempDir::with_prefix("PREFIX-SENTINEL")?;
1691 let root = SystemPathBuf::from_path_buf(
1692 temp_dir
1693 .path()
1694 .canonicalize()
1695 .context("Failed to canonicalized path")?,
1696 )
1697 .expect("UTF8 path for temp dir");
1698
1699 let mut db = TestDb::new();
1700
1701 let src = root.join("src");
1702 let a_package_target = root.join("a-package");
1703 let a_src = src.join("a");
1704
1705 db.use_system(ruff_db::system::OsSystem::new(&root));
1706
1707 db.write_file(
1708 a_package_target.join("__init__.py"),
1709 "class Foo: x: int = 4",
1710 )
1711 .context("Failed to write `a-package/__init__.py`")?;
1712
1713 db.write_file(src.join("main.py"), "print('Hy')")
1714 .context("Failed to write `main.py`")?;
1715
1716 std::os::unix::fs::symlink(a_package_target.as_std_path(), a_src.as_std_path())
1720 .context("Failed to symlink `src/a` to `a-package`")?;
1721
1722 db.files().try_add_root(&db, &root, FileRootKind::Project);
1723
1724 let settings = SearchPathSettings::new(vec![src]);
1725 let search_paths = settings
1726 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
1727 .expect("valid search path settings");
1728 db.set_search_paths(search_paths);
1729
1730 insta::with_settings!({
1731 filters => [(r#""\S*PREFIX-SENTINEL.*?/"#, r#""/"#)],
1734 }, {
1735 insta::assert_debug_snapshot!(
1736 list_snapshot_filter(&db, |m| matches!(m.name(&db).as_str(), "A" | "a")),
1737 @r#"
1738 [
1739 Module::File("a", "first-party", "/src/a/__init__.py", Package, None),
1740 ]
1741 "#,
1742 );
1743 });
1744
1745 Ok(())
1746 }
1747
1748 #[test]
1749 fn file_to_module_where_one_search_path_is_subdirectory_of_other() {
1750 let project_directory = SystemPathBuf::from("/project");
1751 let site_packages = project_directory.join(".venv/lib/python3.13/site-packages");
1752 let installed_foo_module = site_packages.join("foo/__init__.py");
1753
1754 let mut db = TestDb::new();
1755 db.write_file(&installed_foo_module, "").unwrap();
1756
1757 db.files()
1758 .try_add_root(&db, &project_directory, FileRootKind::Project);
1759
1760 let settings = SearchPathSettings {
1761 site_packages_paths: vec![site_packages],
1762 ..SearchPathSettings::new(vec![project_directory])
1763 };
1764 db.set_search_paths(
1765 settings
1766 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
1767 .unwrap(),
1768 );
1769
1770 insta::assert_debug_snapshot!(
1771 list_snapshot_filter(&db, |m| m.name(&db).as_str() == "foo"),
1772 @r#"
1773 [
1774 Module::File("foo", "site-packages", "/project/.venv/lib/python3.13/site-packages/foo/__init__.py", Package, None),
1775 ]
1776 "#,
1777 );
1778 }
1779
1780 #[test]
1781 fn namespace_package() {
1782 let TestCase { db, .. } = TestCaseBuilder::new()
1783 .with_src_files(&[("foo/bar.py", "")])
1784 .build();
1785
1786 insta::assert_debug_snapshot!(
1787 list_snapshot(&db),
1788 @r#"
1789 [
1790 Module::Namespace(ModuleName("foo")),
1791 ]
1792 "#,
1793 );
1794 }
1795
1796 #[test]
1800 fn namespace_package_precedence() {
1801 let TestCase { db, .. } = TestCaseBuilder::new()
1802 .with_src_files(&[("foo/bar.py", "")])
1803 .with_site_packages_files(&[("foo.py", "")])
1804 .build();
1805
1806 insta::assert_debug_snapshot!(
1807 list_snapshot(&db),
1808 @r#"
1809 [
1810 Module::File("foo", "site-packages", "/site-packages/foo.py", Module, None),
1811 ]
1812 "#,
1813 );
1814
1815 let TestCase { db, .. } = TestCaseBuilder::new()
1816 .with_src_files(&[("foo.py", "")])
1817 .with_site_packages_files(&[("foo/bar.py", "")])
1818 .build();
1819
1820 insta::assert_debug_snapshot!(
1821 list_snapshot(&db),
1822 @r#"
1823 [
1824 Module::File("foo", "first-party", "/src/foo.py", Module, None),
1825 ]
1826 "#,
1827 );
1828 }
1829
1830 #[test]
1831 fn stub_package() {
1832 let TestCase { db, .. } = TestCaseBuilder::new()
1833 .with_src_files(&[("foo-stubs/__init__.pyi", "")])
1834 .build();
1835
1836 insta::assert_debug_snapshot!(
1837 list_snapshot(&db),
1838 @r#"
1839 [
1840 Module::File("foo", "first-party", "/src/foo-stubs/__init__.pyi", Package, None),
1841 ]
1842 "#,
1843 );
1844 }
1845
1846 #[test]
1847 fn stub_file_module_not_allowed() {
1848 let TestCase { db, .. } = TestCaseBuilder::new()
1849 .with_src_files(&[("foo-stubs.pyi", "")])
1850 .build();
1851
1852 insta::assert_debug_snapshot!(
1853 list_snapshot(&db),
1854 @"[]",
1855 );
1856 }
1857
1858 #[test]
1859 fn stub_package_precedence() {
1860 let TestCase { db, .. } = TestCaseBuilder::new()
1861 .with_src_files(&[("foo/__init__.py", ""), ("foo-stubs/__init__.pyi", "")])
1862 .build();
1863
1864 insta::assert_debug_snapshot!(
1865 list_snapshot(&db),
1866 @r#"
1867 [
1868 Module::File("foo", "first-party", "/src/foo-stubs/__init__.pyi", Package, None),
1869 ]
1870 "#,
1871 );
1872 }
1873
1874 #[test]
1875 fn stub_package_not_allowed_in_typeshed() {
1876 const TYPESHED: MockedTypeshed = MockedTypeshed {
1877 versions: "foo: 3.8-",
1878 stdlib_files: &[("foo-stubs/__init__.pyi", "")],
1879 };
1880
1881 let TestCase { db, .. } = TestCaseBuilder::new()
1882 .with_mocked_typeshed(TYPESHED)
1883 .with_python_version(PythonVersion::PY38)
1884 .build();
1885
1886 insta::assert_debug_snapshot!(
1887 list_snapshot(&db),
1888 @"[]",
1889 );
1890 }
1891
1892 #[test]
1902 fn root_directory_for_search_path_is_okay() {
1903 let project_directory = SystemPathBuf::from("/project");
1904 let installed_foo_module = project_directory.join("foo/__init__.py");
1905
1906 let mut db = TestDb::new();
1907 db.write_file(&installed_foo_module, "").unwrap();
1908
1909 db.files()
1910 .try_add_root(&db, SystemPath::new("/"), FileRootKind::Project);
1911
1912 let settings = SearchPathSettings::new(vec![project_directory]);
1913 let search_paths = settings
1914 .to_search_paths(db.system(), db.vendored(), &FallibleStrategy)
1915 .expect("Valid search path settings");
1916 db.set_search_paths(search_paths);
1917
1918 insta::assert_debug_snapshot!(
1919 list_snapshot_filter(&db, |m| m.name(&db).as_str() == "foo"),
1920 @r#"
1921 [
1922 Module::File("foo", "first-party", "/project/foo/__init__.py", Package, None),
1923 ]
1924 "#,
1925 );
1926 }
1927}