1use std::borrow::Cow;
2use std::fmt::Formatter;
3use std::str::FromStr;
4
5use ruff_db::files::{File, directory_listing, system_path_to_file, vendored_path_to_file};
6use ruff_db::system::SystemPath;
7use ruff_db::vendored::VendoredPath;
8use ruff_python_ast::PythonVersion;
9use salsa::Database;
10use salsa::plumbing::AsId;
11
12use crate::module_name::ModuleName;
13use crate::path::{SearchPath, SystemOrVendoredPathRef};
14use crate::{Db, ResolverEnvironment};
15
16#[derive(Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::SalsaValue)]
18pub enum Module<'db> {
19 File(FileModule<'db>),
20 Namespace(NamespacePackage<'db>),
21}
22
23impl get_size2::GetSize for Module<'_> {}
25
26#[salsa::tracked]
27impl<'db> Module<'db> {
28 pub(crate) fn file_module(
29 db: &'db dyn Db,
30 file: File,
31 resolver_environment: ResolverEnvironment<'db>,
32 name: Cow<'_, ModuleName>,
33 kind: ModuleKind,
34 search_path: SearchPath,
35 ) -> Self {
36 let known = KnownModule::try_from_search_path_and_name(&search_path, &name);
37
38 Self::File(FileModule::new(
39 db,
40 name,
41 kind,
42 search_path,
43 file,
44 resolver_environment,
45 known,
46 ))
47 }
48
49 pub(crate) fn namespace_package(
50 db: &'db dyn Db,
51 resolver_environment: ResolverEnvironment<'db>,
52 name: Cow<'_, ModuleName>,
53 ) -> Self {
54 Self::Namespace(NamespacePackage::new(db, resolver_environment, name))
55 }
56
57 pub fn resolver_environment(self, db: &'db dyn Database) -> ResolverEnvironment<'db> {
59 match self {
60 Module::File(module) => module.resolver_environment(db),
61 Module::Namespace(module) => module.resolver_environment(db),
62 }
63 }
64
65 pub fn name(self, db: &'db dyn Database) -> &'db ModuleName {
67 match self {
68 Module::File(module) => module.name(db),
69 Module::Namespace(ref package) => package.name(db),
70 }
71 }
72
73 pub fn file(self, db: &'db dyn Database) -> Option<File> {
77 match self {
78 Module::File(module) => Some(module.file(db)),
79 Module::Namespace(_) => None,
80 }
81 }
82
83 pub fn python_version(self, db: &'db dyn Database) -> PythonVersion {
85 self.resolver_environment(db).python_version(db)
86 }
87
88 pub fn known(self, db: &'db dyn Database) -> Option<KnownModule> {
90 match self {
91 Module::File(module) => module.known(db),
92 Module::Namespace(_) => None,
93 }
94 }
95
96 pub fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
98 self.known(db) == Some(known_module)
99 }
100
101 pub fn search_path(self, db: &'db dyn Database) -> Option<&'db SearchPath> {
106 match self {
107 Module::File(module) => Some(module.search_path(db)),
108 Module::Namespace(_) => None,
109 }
110 }
111
112 pub fn is_type_check_only(self, db: &'db dyn Database) -> bool {
117 self.search_path(db)
118 .is_some_and(SearchPath::is_standard_library)
119 && matches!(
120 self.name(db).first_component(),
121 "_typeshed" | "typing_extensions" | "ty_extensions"
122 )
123 }
124
125 pub fn kind(self, db: &'db dyn Database) -> ModuleKind {
127 match self {
128 Module::File(module) => module.kind(db),
129 Module::Namespace(_) => ModuleKind::Package,
130 }
131 }
132
133 pub fn all_submodules(self, db: &'db dyn Db) -> &'db [Module<'db>] {
141 all_submodule_names_for_package(db, self).unwrap_or_default()
142 }
143}
144
145impl std::fmt::Debug for Module<'_> {
146 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
147 salsa::with_attached_database(|db| {
148 f.debug_struct("Module")
149 .field("name", &self.name(db))
150 .field("kind", &self.kind(db))
151 .field("file", &self.file(db))
152 .field("search_path", &self.search_path(db))
153 .field("known", &self.known(db))
154 .finish()
155 })
156 .unwrap_or_else(|| f.debug_tuple("Module").field(&self.as_id()).finish())
157 }
158}
159
160#[salsa::tracked(returns(as_deref), heap_size=ruff_memory_usage::heap_size)]
161fn all_submodule_names_for_package<'db>(
162 db: &'db dyn Db,
163 module: Module<'db>,
164) -> Option<Box<[Module<'db>]>> {
165 fn is_submodule(
166 is_dir: bool,
167 is_file: bool,
168 basename: Option<&str>,
169 extension: Option<&str>,
170 ) -> bool {
171 is_dir
172 || (is_file
173 && matches!(extension, Some("py" | "pyi"))
174 && !matches!(basename, Some("__init__.py" | "__init__.pyi")))
175 }
176
177 fn find_package_init_system(db: &dyn Db, dir: &SystemPath) -> Option<File> {
178 let listing = directory_listing(db, dir).ok()?;
179 if listing.entry_is_file(db, dir, "__init__.pyi") {
180 system_path_to_file(db, dir.join("__init__.pyi")).ok()
181 } else if listing.entry_is_file(db, dir, "__init__.py") {
182 system_path_to_file(db, dir.join("__init__.py")).ok()
183 } else {
184 None
185 }
186 }
187
188 fn find_package_init_vendored(db: &dyn Db, dir: &VendoredPath) -> Option<File> {
189 vendored_path_to_file(db, dir.join("__init__.pyi"))
190 .or_else(|_| vendored_path_to_file(db, dir.join("__init__.py")))
191 .ok()
192 }
193
194 let Module::File(module) = module else {
200 return None;
201 };
202 if !matches!(module.kind(db), ModuleKind::Package) {
203 return None;
204 }
205
206 let path = SystemOrVendoredPathRef::try_from_file(db, module.file(db))?;
207 debug_assert!(
208 matches!(path.file_name(), Some("__init__.py" | "__init__.pyi")),
209 "expected package file `{:?}` to be `__init__.py` or `__init__.pyi`",
210 path.file_name(),
211 );
212
213 let resolver_environment = module.resolver_environment(db);
214 Some(match path.parent()? {
215 SystemOrVendoredPathRef::System(parent_directory) => {
216 directory_listing(db, parent_directory)
217 .inspect_err(|error| {
218 tracing::debug!(
219 "Failed to read {parent_directory:?} when looking for \
220 its possible submodules: {error}"
221 );
222 })
223 .ok()?
224 .iter()
225 .filter(|(name, ty)| {
226 let path = SystemPath::new(name);
227 is_submodule(
228 ty.is_directory(),
229 ty.is_file(),
230 path.file_name(),
231 path.extension(),
232 )
233 })
234 .filter_map(|(entry_name, file_type)| {
235 let relative = SystemPath::new(entry_name);
236 let stem = relative.file_stem()?;
237 let path = parent_directory.join(relative);
238 let mut name = module.name(db).clone();
239 name.extend(&ModuleName::new(stem)?);
240
241 let (kind, file) = if file_type.is_directory() {
242 (ModuleKind::Package, find_package_init_system(db, &path)?)
243 } else {
244 let file = system_path_to_file(db, &path).ok()?;
245 (ModuleKind::Module, file)
246 };
247 Some(Module::file_module(
248 db,
249 file,
250 resolver_environment,
251 Cow::Owned(name),
252 kind,
253 module.search_path(db).clone(),
254 ))
255 })
256 .collect()
257 }
258 SystemOrVendoredPathRef::Vendored(parent_directory) => db
259 .vendored()
260 .read_directory(parent_directory)
261 .filter(|entry| {
262 let ty = entry.file_type();
263 let path = entry.path();
264 is_submodule(
265 ty.is_directory(),
266 ty.is_file(),
267 path.file_name(),
268 path.extension(),
269 )
270 })
271 .filter_map(|entry| {
272 let stem = entry.path().file_stem()?;
273 let mut name = module.name(db).clone();
274 name.extend(&ModuleName::new(stem)?);
275
276 let (kind, file) = if entry.file_type().is_directory() {
277 (
278 ModuleKind::Package,
279 find_package_init_vendored(db, entry.path())?,
280 )
281 } else {
282 let file = vendored_path_to_file(db, entry.path()).ok()?;
283 (ModuleKind::Module, file)
284 };
285 Some(Module::file_module(
286 db,
287 file,
288 resolver_environment,
289 Cow::Owned(name),
290 kind,
291 module.search_path(db).clone(),
292 ))
293 })
294 .collect(),
295 })
296}
297
298#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
300pub struct FileModule<'db> {
301 #[returns(ref)]
302 pub(super) name: ModuleName,
303 #[returns(copy)]
304 pub(super) kind: ModuleKind,
305 #[returns(ref)]
306 pub(super) search_path: SearchPath,
307 #[returns(copy)]
308 pub(super) file: File,
309 #[returns(copy)]
310 pub(super) resolver_environment: ResolverEnvironment<'db>,
311 #[returns(copy)]
312 pub(super) known: Option<KnownModule>,
313}
314
315#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
320pub struct NamespacePackage<'db> {
321 #[returns(copy)]
322 pub(super) resolver_environment: ResolverEnvironment<'db>,
323 #[returns(ref)]
324 pub(super) name: ModuleName,
325}
326
327#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]
328pub enum ModuleKind {
329 Module,
331
332 Package,
334}
335
336impl ModuleKind {
337 pub const fn is_package(self) -> bool {
338 matches!(self, ModuleKind::Package)
339 }
340 pub const fn is_module(self) -> bool {
341 matches!(self, ModuleKind::Module)
342 }
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, strum_macros::EnumString, get_size2::GetSize)]
347#[cfg_attr(test, derive(strum_macros::EnumIter))]
348#[strum(serialize_all = "snake_case")]
349pub enum KnownModule {
350 Builtins,
351 Enum,
352 Types,
353 #[strum(serialize = "_typeshed")]
354 Typeshed,
355 TypingExtensions,
356 Typing,
357 Sys,
358 Os,
359 Tempfile,
360 Pathlib,
361 Datetime,
362 Decimal,
363 Ipaddress,
364 Re,
365 Abc,
366 Dataclasses,
367 Functools,
368 Collections,
369 #[strum(serialize = "collections.abc")]
370 CollectionsAbc,
371 #[strum(serialize = "_collections_abc")]
372 CollectionsAbcInternal,
373 Inspect,
374 #[strum(serialize = "string.templatelib")]
375 Templatelib,
376 #[strum(serialize = "_typeshed._type_checker_internals")]
377 TypeCheckerInternals,
378 TyExtensions,
379 #[strum(serialize = "ty_extensions._internal")]
380 TyExtensionsInternal,
381 #[strum(serialize = "ty_extensions.pydantic")]
382 TyExtensionsPydantic,
383 #[strum(serialize = "importlib")]
384 ImportLib,
385 #[strum(serialize = "unittest.mock")]
386 UnittestMock,
387 Uuid,
388 Warnings,
389 Numbers,
390 #[strum(serialize = "struct", serialize = "_struct")]
391 Struct,
392 #[strum(serialize = "pydantic.config")]
394 PydanticConfig,
395 #[strum(serialize = "pydantic.fields")]
396 PydanticFields,
397 #[strum(serialize = "pydantic.functional_validators")]
398 PydanticFunctionalValidators,
399 #[strum(serialize = "pydantic.main")]
400 PydanticMain,
401 #[strum(serialize = "pydantic.root_model")]
402 PydanticRootModel,
403 #[strum(serialize = "pydantic_settings.main")]
404 PydanticSettingsMain,
405 #[strum(serialize = "pydantic.types")]
406 PydanticTypes,
407 Pytest,
408 #[strum(serialize = "_pytest.fixtures")]
409 PytestFixtures,
410 #[strum(serialize = "_pytest.mark.structures")]
411 PytestMarkStructures,
412}
413
414impl KnownModule {
415 pub const fn as_str(self) -> &'static str {
416 match self {
417 Self::Builtins => "builtins",
418 Self::Enum => "enum",
419 Self::Types => "types",
420 Self::Typing => "typing",
421 Self::Typeshed => "_typeshed",
422 Self::TypingExtensions => "typing_extensions",
423 Self::Sys => "sys",
424 Self::Os => "os",
425 Self::Tempfile => "tempfile",
426 Self::Pathlib => "pathlib",
427 Self::Datetime => "datetime",
428 Self::Decimal => "decimal",
429 Self::Ipaddress => "ipaddress",
430 Self::Re => "re",
431 Self::Abc => "abc",
432 Self::Dataclasses => "dataclasses",
433 Self::Functools => "functools",
434 Self::Collections => "collections",
435 Self::CollectionsAbc => "collections.abc",
436 Self::CollectionsAbcInternal => "_collections_abc",
437 Self::Inspect => "inspect",
438 Self::TypeCheckerInternals => "_typeshed._type_checker_internals",
439 Self::TyExtensions => "ty_extensions",
440 Self::TyExtensionsInternal => "ty_extensions._internal",
441 Self::TyExtensionsPydantic => "ty_extensions.pydantic",
442 Self::ImportLib => "importlib",
443 Self::Warnings => "warnings",
444 Self::UnittestMock => "unittest.mock",
445 Self::Uuid => "uuid",
446 Self::Templatelib => "string.templatelib",
447 Self::Numbers => "numbers",
448 Self::Struct => "struct",
449 Self::PydanticConfig => "pydantic.config",
450 Self::PydanticFields => "pydantic.fields",
451 Self::PydanticFunctionalValidators => "pydantic.functional_validators",
452 Self::PydanticMain => "pydantic.main",
453 Self::PydanticRootModel => "pydantic.root_model",
454 Self::PydanticSettingsMain => "pydantic_settings.main",
455 Self::PydanticTypes => "pydantic.types",
456 Self::Pytest => "pytest",
457 Self::PytestFixtures => "_pytest.fixtures",
458 Self::PytestMarkStructures => "_pytest.mark.structures",
459 }
460 }
461
462 pub fn name(self) -> ModuleName {
463 ModuleName::new_static(self.as_str())
464 .unwrap_or_else(|| panic!("{self} should be a valid module name!"))
465 }
466
467 fn try_from_search_path_and_name(search_path: &SearchPath, name: &ModuleName) -> Option<Self> {
468 let known_module = Self::from_str(name.as_str()).ok()?;
469
470 let is_expected_search_path = if known_module.is_third_party() {
471 search_path.can_contain_third_party_code()
472 } else {
473 search_path.is_standard_library()
474 };
475
476 is_expected_search_path.then_some(known_module)
477 }
478
479 pub const fn is_third_party(self) -> bool {
481 match self {
482 Self::PydanticConfig
483 | Self::PydanticFields
484 | Self::PydanticFunctionalValidators
485 | Self::PydanticMain
486 | Self::PydanticRootModel
487 | Self::PydanticSettingsMain
488 | Self::PydanticTypes
489 | Self::Pytest
490 | Self::PytestFixtures
491 | Self::PytestMarkStructures => true,
492 Self::Builtins
493 | Self::Enum
494 | Self::Types
495 | Self::Typeshed
496 | Self::TypingExtensions
497 | Self::Typing
498 | Self::Sys
499 | Self::Os
500 | Self::Tempfile
501 | Self::Pathlib
502 | Self::Datetime
503 | Self::Decimal
504 | Self::Ipaddress
505 | Self::Re
506 | Self::Abc
507 | Self::Dataclasses
508 | Self::Functools
509 | Self::Collections
510 | Self::CollectionsAbc
511 | Self::CollectionsAbcInternal
512 | Self::Inspect
513 | Self::Templatelib
514 | Self::TypeCheckerInternals
515 | Self::TyExtensions
516 | Self::TyExtensionsInternal
517 | Self::TyExtensionsPydantic
518 | Self::ImportLib
519 | Self::UnittestMock
520 | Self::Uuid
521 | Self::Warnings
522 | Self::Numbers
523 | Self::Struct => false,
524 }
525 }
526
527 pub const fn is_builtins(self) -> bool {
528 matches!(self, Self::Builtins)
529 }
530
531 pub const fn is_typing(self) -> bool {
532 matches!(self, Self::Typing)
533 }
534
535 pub const fn is_typing_extensions(self) -> bool {
536 matches!(self, Self::TypingExtensions)
537 }
538
539 pub const fn is_ty_extensions(self) -> bool {
540 matches!(self, Self::TyExtensions)
541 }
542
543 pub const fn is_ty_extensions_internal(self) -> bool {
544 matches!(self, Self::TyExtensionsInternal)
545 }
546
547 pub const fn is_inspect(self) -> bool {
548 matches!(self, Self::Inspect)
549 }
550
551 pub const fn is_importlib(self) -> bool {
552 matches!(self, Self::ImportLib)
553 }
554
555 pub const fn is_functools(self) -> bool {
556 matches!(self, Self::Functools)
557 }
558
559 pub const fn is_dataclasses(self) -> bool {
560 matches!(self, Self::Dataclasses)
561 }
562}
563
564impl std::fmt::Display for KnownModule {
565 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
566 f.write_str(self.as_str())
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use strum::IntoEnumIterator;
574
575 #[test]
576 fn known_module_roundtrip_from_str() {
577 let stdlib_search_path = SearchPath::vendored_stdlib();
578
579 for module in KnownModule::iter().filter(|module| !module.is_third_party()) {
580 let module_name = module.name();
581
582 assert_eq!(
583 KnownModule::try_from_search_path_and_name(&stdlib_search_path, &module_name),
584 Some(module),
585 "The strum `EnumString` implementation appears to be incorrect for `{module_name}`"
586 );
587 }
588 }
589}