Skip to main content

ty_module_resolver/
resolve.rs

1/*!
2This module principally provides several routines for resolving a particular module
3name to a `Module`:
4
5* [`file_to_module`][]: resolves the module `.<self>` (often as the first step in resolving `.`)
6* [`resolve_module`][]: resolves an absolute module name
7
8You may notice that we actually provide `resolve_(real)_(shadowable)_module_(confident)`.
9You almost certainly just want [`resolve_module`][]. The other variations represent
10restrictions to answer specific kinds of questions, usually to empower IDE features.
11
12* The `real` variation disallows all stub files, including the vendored typeshed.
13  This enables the goto-definition ("real") vs goto-declaration ("stub or real") distinction.
14
15* The `confident` variation disallows "desperate resolution", which is a fallback
16  mode where we start trying to use ancestor directories of the importing file
17  as search-paths, but only if we failed to resolve it with the normal search-paths.
18  This is mostly just a convenience for cases where we don't want to try to define
19  the importing file (resolving a `KnownModule` and tests).
20
21* The `shadowable` variation disables some guards that prevents third-party code
22  from shadowing any vendored non-stdlib `KnownModule`. In particular `typing_extensions`,
23  which we vendor and heavily assume the contents of (and so don't ever want to shadow).
24  This enables checking if the user *actually* has `typing_extensions` installed,
25  in which case it's ok to suggest it in features like auto-imports.
26
27There is some awkwardness to the structure of the code to specifically enable caching
28of queries, as module resolution happens a lot and involves a lot of disk access.
29
30For implementors, see `import-resolution-diagram.svg` for a flow diagram that
31specifies ty's implementation of Python's import resolution algorithm.
32*/
33
34use 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
57/// Resolves a module name to a module.
58pub 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
75/// Resolves the module referenced by a `from` import statement.
76///
77/// Returns `None` if the statement does not name a valid module or the module cannot be resolved.
78pub 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
87/// Resolves a module name to a module, without desperate resolution available.
88///
89/// This is appropriate for resolving a `KnownModule`, or cases where for whatever reason
90/// we don't have a well-defined importing file.
91pub 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
106/// Resolves a module name to a module (stubs not allowed).
107pub 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
124/// Resolves a module name to a module, without desperate resolution available (stubs not allowed).
125///
126/// This is appropriate for resolving a `KnownModule`, or cases where for whatever reason
127/// we don't have a well-defined importing file.
128pub 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
143/// Resolves a module name to a module (stubs not allowed, some shadowing is
144/// allowed).
145///
146/// In particular, this allows `typing_extensions` to be shadowed by a
147/// non-standard library module. This is useful in the context of the LSP
148/// where we don't want to pretend as if these modules are always available at
149/// runtime.
150///
151/// This should generally only be used within the context of the LSP. Using it
152/// within ty proper risks being unable to resolve builtin modules since they
153/// are involved in an import cycle with `builtins`.
154pub 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/// Selects typing or runtime module-resolution semantics.
172#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, get_size2::GetSize)]
173pub enum ModuleResolveMode {
174    /// Resolve modules for type checking, preferring stubs over runtime implementations.
175    ///
176    /// This is the "normal" mode almost everything uses, as type checkers are in fact supposed
177    /// to *prefer* stubs over the actual implementations.
178    Typing,
179
180    /// Resolve modules to their runtime implementations without considering stubs.
181    ///
182    /// This is the "goto definition" mode, where we need to ignore the typing spec and find actual
183    /// implementations. When querying searchpaths this also notably replaces typeshed with
184    /// the "real" stdlib.
185    Runtime,
186
187    /// Like [`ModuleResolveMode::Runtime`], but permits some modules to be shadowed.
188    ///
189    /// In particular, this allows `typing_extensions` to be shadowed by a
190    /// non-standard library module. This is useful in the context of the LSP
191    /// where we don't want to pretend as if these modules are always available
192    /// at runtime.
193    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    /// Returns `true` if the module name refers to a standard library module
211    /// which can't be shadowed by a first-party module.
212    ///
213    /// This includes "builtin" modules, which can never be shadowed at runtime
214    /// either. Additionally, certain other modules that are involved in an
215    /// import cycle with `builtins` (`types`, `typing_extensions`, etc.) are
216    /// also considered non-shadowable, unless the module resolution mode
217    /// specifically opts into allowing some of them to be shadowed. This
218    /// latter set of modules cannot be allowed to be shadowed by first-party
219    /// or "extra-path" modules in ty proper, or we risk panics in unexpected
220    /// places due to being unable to resolve builtin symbols. This is similar
221    /// behaviour to other type checkers such as mypy:
222    /// <https://github.com/python/mypy/blob/3807423e9d98e678bf16b13ec8b4f909fe181908/mypy/build.py#L104-L117>
223    pub(super) fn is_non_shadowable(self, minor_version: u8, module_name: &str) -> bool {
224        // Builtin modules are never shadowable, no matter what.
225        if ruff_python_stdlib::sys::is_builtin_module(minor_version, module_name) {
226            return true;
227        }
228        // Similarly for `types`, which is always available at runtime.
229        if module_name == "types" {
230            return true;
231        }
232
233        // Otherwise, some modules should only be conditionally allowed
234        // to be shadowed, depending on the module resolution mode.
235        match self {
236            ModuleResolveMode::Typing | ModuleResolveMode::Runtime => {
237                module_name == "typing_extensions"
238            }
239            ModuleResolveMode::RuntimeSomeShadowingAllowed => false,
240        }
241    }
242}
243
244/// Salsa query that resolves an interned [`ModuleNameIngredient`] to a module.
245///
246/// This query should not be called directly. Instead, use [`resolve_module`]. It only exists
247/// because Salsa requires the module name to be an ingredient.
248#[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/// Like `resolve_module_query` but for cases where it failed to resolve the module
270/// and we are now Getting Desperate and willing to try the ancestor directories of
271/// the `importing_file` as potential temporary search paths that are private
272/// to this import.
273///
274/// The reason this is split out is because in 99.9% of cases `resolve_module_query`
275/// will find the right answer (or no valid answer exists), and we want it to be
276/// aggressively cached. Including the `importing_file` as part of that query would
277/// trash the caching of import resolution between files.
278///
279/// Cache desperate resolution because repeated unresolved imports in a project can otherwise
280/// re-walk the same importing-file-relative search paths many times.
281#[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/// Resolves the module for the given path.
313///
314/// Returns `None` if the path is not a module locatable via any of the known search paths.
315#[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    // It's not entirely clear on first sight why this method calls `file_to_module` instead of
322    // it being the other way round, considering that the first thing that `file_to_module` does
323    // is to retrieve the file's path.
324    //
325    // The reason is that `file_to_module` is a tracked Salsa query and salsa queries require that
326    // all arguments are Salsa ingredients (something stored in Salsa). `Path`s aren't salsa ingredients but
327    // `VfsFile` is. So what we do here is to retrieve the `path`'s `VfsFile` so that we can make
328    // use of Salsa's caching and invalidation.
329    let file = path.to_file(db)?;
330    file_to_module(db, ResolverFile::new(db, file, resolver_environment))
331}
332
333/// Resolves the module for the file with the given id.
334///
335/// Returns `None` if the file is not a module locatable via any of the known search paths.
336///
337/// This function can be understood as essentially resolving `import .<self>` in the file itself,
338/// and indeed, one of its primary jobs is resolving `.<self>` to derive the module name of `.`.
339/// This intuition is particularly useful for understanding why it's correct that we pass
340/// the file itself as `importing_file` to various subroutines.
341#[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    // Resolve the module name to see if Python would resolve the name to the same path.
383    // If it doesn't, then that means that multiple modules have the same name in different
384    // root paths, but that the module corresponding to `path` is in a lower priority search path,
385    // in which case we ignore it.
386    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        // If a .py and .pyi are both defined, the .pyi will be the one returned by `resolve_module().file`,
397        // which would make us erroneously believe the `.py` is *not* also this module (breaking things
398        // like relative imports). So here we try `resolve_real_module().file` to cover both cases.
399        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    // This path is for a module with the same name but with a different precedence. For example:
407    // ```
408    // src/foo.py
409    // src/foo/__init__.py
410    // ```
411    // The module name of `src/foo.py` is `foo`, but the module loaded by Python is `src/foo/__init__.py`.
412    // That means we need to ignore `src/foo.py` even though it resolves to the same module name.
413    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    /// Indexes search paths that may contain a stub package, preserving their position relative to
452    /// the standard library.
453    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    /// Returns all indexed paths in normal typing-resolution order.
476    fn all(&self) -> StubPackagePaths<'_> {
477        StubPackagePaths {
478            before_stdlib: self.before_stdlib(),
479            after_stdlib: self.after_stdlib(),
480        }
481    }
482
483    /// Splits the indexed paths between the stub-overlay pass and its normal fallback.
484    ///
485    /// The overlay contains only extra paths, which all precede stdlib. The fallback retains the
486    /// remaining paths' positions relative to stdlib.
487    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    /// Returns indexed paths that precede stdlib in normal typing resolution.
505    fn before_stdlib(&self) -> &[SearchPath] {
506        &self.paths[..self.stdlib_offset]
507    }
508
509    /// Returns indexed paths that follow stdlib in normal typing resolution.
510    fn after_stdlib(&self) -> &[SearchPath] {
511        &self.paths[self.stdlib_offset..]
512    }
513}
514
515/// Returns an index of search paths that may contain a top-level stub package, preserving their
516/// resolution order relative to stdlib.
517#[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/// Get the search-paths for desperate resolution of absolute imports in this file.
538///
539/// Currently this is "all ancestor directories that don't contain an `__init__.py(i)`"
540/// (from closest-to-importing-file to farthest).
541///
542/// (For paranoia purposes, all relative desperate search-paths are also absolute
543/// valid desperate search-paths, but don't worry about that.)
544///
545/// We exclude `__init__.py(i)` dirs to avoid truncating packages.
546#[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    // Only allow this if the importing_file is under the first-party search path
557    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    // Only allow searching up to the first-party path's root
569    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        // Any dir that isn't a proper package is plausibly some test/script dir that could be
576        // added as a search-path at runtime. Notably this reflects pytest's default mode where
577        // it adds every dir with a .py to the search-paths (making all test files root modules),
578        // unless they see an `__init__.py`, in which case they assume you don't want that.
579        let isnt_regular_package = !listing.entry_is_file(db, &candidate_path, "__init__.py")
580            && !listing.entry_is_file(db, &candidate_path, "__init__.pyi");
581        // Any dir with a pyproject.toml or ty.toml is a valid relative desperate search-path and
582        // we want all of those to also be valid absolute desperate search-paths. It doesn't
583        // make any sense for a folder to have `pyproject.toml` and `__init__.py` but let's
584        // not let something cursed and spooky happen, ok? d
585        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/// Get the search-paths for desperate resolution of relative imports in this file.
602///
603/// Currently this is "the closest ancestor dir that contains a pyproject.toml (or ty.toml)",
604/// which is a completely arbitrary decision. However it's fairly important that relative
605/// desperate search-paths pick a single "best" answer because every one is *valid* but one
606/// that's too long or too short may cause problems.
607///
608/// For now this works well in common cases where we have some larger workspace that contains
609/// one or more python projects in sub-directories, and those python projects assume that
610/// absolute imports resolve relative to the pyproject.toml they live under.
611///
612/// Being so strict minimizes concerns about this going off a lot and doing random
613/// chaotic things. In particular, all files under a given pyproject.toml will currently
614/// agree on this being their desperate search-path, which is really nice.
615#[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    // Only allow this if the importing_file is under the first-party search path
626    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    // Only allow searching up to the first-party path's root
638    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        // Any dir with a pyproject.toml or ty.toml might be a project root
644        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    /// Search paths that have been statically determined purely from reading
657    /// ty's configuration settings. These shouldn't ever change unless the
658    /// config settings themselves change.
659    static_paths: Vec<SearchPath>,
660
661    /// Path to typeshed, which should come immediately after static paths.
662    ///
663    /// This can currently only be None if the `SystemPath` this points to is already in `static_paths`.
664    stdlib_path: Option<SearchPath>,
665
666    /// Path to the real stdlib, this replaces typeshed (`stdlib_path`) for goto-definition searches
667    /// ([`ModuleResolveMode::Runtime`]).
668    real_stdlib_path: Option<SearchPath>,
669
670    /// site-packages paths are not included in the above fields:
671    /// if there are multiple site-packages paths, editable installations can appear
672    /// *between* the site-packages paths on `sys.path` at runtime.
673    /// That means we can't know where a second or third `site-packages` path should sit
674    /// in terms of module-resolution priority until we've discovered the editable installs
675    /// for the first `site-packages` path
676    site_packages: Vec<SearchPath>,
677
678    typeshed_versions: TypeshedVersions,
679}
680
681impl SearchPaths {
682    /// Validate and normalize the raw settings given by the user
683    /// into settings we can use for module resolution
684    ///
685    /// This method also implements the typing spec's [module resolution order].
686    ///
687    /// [module resolution order]: https://typing.python.org/en/latest/spec/distributing.html#import-resolution-ordering
688    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        // TODO vendor typeshed's third-party stubs as well as the stdlib and
792        // fallback to them as a final step?
793        //
794        // See: <https://github.com/astral-sh/ruff/pull/19620#discussion_r2240609135>
795
796        // Filter out module resolution paths that point to the same directory
797        // on disk (the same invariant maintained by [`sys.path` at runtime]).
798        // (Paths may, however, *overlap* -- e.g. you could have both `src/`
799        // and `src/foo` as module resolution paths simultaneously.)
800        //
801        // This code doesn't use an `IndexSet` because the key is the system
802        // path and not the search root.
803        //
804        // [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
805        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        // Users probably shouldn't do this but... if they've shadowed their stdlib we should deduplicate it away.
816        // This notably will mess up anything that checks if a search path "is the standard library" as we won't
817        // "remember" that fact for static paths.
818        //
819        // (We used to shove these into static_paths, so the above retain implicitly did this. I am opting to
820        // preserve this behaviour to avoid getting into the weeds of corner cases.)
821        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    /// Returns a new `SearchPaths` with no search paths configured.
850    ///
851    /// The vendored standard library remains available.
852    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    /// Registers file roots for all non-dynamically discovered search paths.
863    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                // Nested first-party paths reuse the project root. Other nested paths, such as
873                // site-packages inside `.venv`, need their own search-path root.
874                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/// Collect all dynamic search paths. For each `site-packages` path:
902/// - Collect that `site-packages` path
903/// - Collect any search paths listed in `.pth` files in that `site-packages` directory
904///   due to editable installations of third-party packages.
905///
906/// The editable-install search paths for the first `site-packages` directory
907/// should come between the two `site-packages` directories when it comes to
908/// module-resolution priority.
909#[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    // Use the `ModuleResolveMode` to determine which stdlib (if any) to mark as existing
937    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        // As well as modules installed directly into `site-packages`,
962        // the directory may also contain `.pth` files.
963        // Each `.pth` file in `site-packages` may contain one or more lines
964        // containing a (relative or absolute) path.
965        // Each of these paths may point to an editable install of a package,
966        // so should be considered an additional search path.
967        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        // The Python documentation specifies that `.pth` files in `site-packages`
978        // are processed in alphabetical order. `DirectoryListing` is already sorted.
979        // https://docs.python.org/3/library/site.html#module-site
980        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            // Track each `.pth` file independently so content changes invalidate this query.
987            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                            // Register a file root for editable installs that are outside any other root
1025                            // (Most importantly, don't register a root for editable installations from the project
1026                            // directory as that would change the durability of files within those folders).
1027                            // Not having an exact file root for editable installs just means that
1028                            // some queries (like `list_modules_in`) will run slightly more frequently
1029                            // than they would otherwise.
1030                            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
1051/// Iterate over the available module-resolution search paths,
1052/// following the invariants maintained by [`sys.path` at runtime]:
1053/// "No item is added to `sys.path` more than once."
1054/// Dynamic search paths (required for editable installs into `site-packages`)
1055/// are only calculated lazily.
1056///
1057/// [`sys.path` at runtime]: https://docs.python.org/3/library/site.html#module-site
1058pub 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/// A thin wrapper around a module name, resolution mode, and resolver environment to make them a Salsa
1092/// ingredient.
1093///
1094/// This is needed because Salsa requires that all query arguments are salsa ingredients.
1095#[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
1105/// Given a module name and a list of search paths in which to lookup modules,
1106/// attempt to resolve the module name
1107fn 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
1125/// Like `resolve_name` but for cases where it failed to resolve the module
1126/// and we are now Getting Desperate and willing to try the ancestor directories of
1127/// the `importing_file` as potential temporary search paths that are private
1128/// to this import.
1129fn 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    /// Prefer `.pyi` over `.py` in typing mode, or only accept `.py` in runtime mode.
1159    ByMode,
1160
1161    /// Only accept a `.pyi` file.
1162    StubOnly,
1163}
1164
1165/// Where a candidate sits in the typing specification's module-resolution order.
1166///
1167/// Variants are declared from highest to lowest precedence so that derived ordering can be used
1168/// when traversing candidates. This is a precedence tier rather than a total ordering: the stable
1169/// sorts used by the resolver preserve search-path order between candidates in the same tier.
1170#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
1171enum CandidatePrecedence {
1172    /// A PEP 561 stub-only package named `<package>-stubs`.
1173    ///
1174    /// Stub packages take precedence over candidates for `<package>` regardless of where those
1175    /// candidates appear in the search-path order.
1176    StubPackage,
1177
1178    /// A candidate whose precedence is determined by search-path order.
1179    ///
1180    /// This includes `.pyi` and `.py` packages and modules from extra paths, first-party code,
1181    /// editable installs, site-packages, and the standard library.
1182    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    // Is this some kind of namespace package?
1212    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    // This is the module we were actually interested in resolving, complete the resolution
1222    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                // legacy namespace packages behave like regular packages
1235                // when they're the target of the resolution
1236                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        // Regular packages and modules are both terminal. A `foo.py`
1283        // in a higher-priority search path is not shadowed by
1284        // `foo/__init__.py` in a lower-priority one. Note that both
1285        // shadow namespace packages.
1286        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    /// Resolves the name as seen by a type checker.
1326    ///
1327    /// This includes PEP 561 stub packages and user-provided stub overlays, with runtime source as
1328    /// a fallback when no stub provides the requested module. A stub overlay may use runtime
1329    /// packages as parents, but its final module must come from a stub file.
1330    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        // Only submodules need separate overlay resolution: their extra-path namespace parent can
1344        // be shadowed before the resolver reaches the requested stub. Reuse those roots for the
1345        // normal fallback so that each extra path is probed only once.
1346        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    /// Resolves the name for type checking against desperate ancestor search paths.
1377    ///
1378    /// These paths can contain PEP 561 stub packages, but never user-provided extra paths, so this
1379    /// indexes them for stub packages without performing a separate stub-overlay pass. Runtime
1380    /// resolution instead ignores stub packages and `.pyi` files entirely.
1381    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    /// Resolves the name to the implementation that is available at runtime.
1389    ///
1390    /// The runtime resolver ignores stub packages and `.pyi` files. Its search paths also use the
1391    /// real standard library instead of typeshed.
1392    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            // Defer file probes after stdlib until we know that stdlib does not win.
1416            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            // When a builtin module is imported, standard module resolution is bypassed:
1427            // the module name always resolves to the stdlib module,
1428            // even if there's a module of the same name in the first-party root
1429            // (which would normally result in the stdlib module being overridden).
1430            // TODO: offer a diagnostic if there is a first-party module of the same name
1431            if self.is_non_shadowable && !search_path.is_standard_library() {
1432                continue;
1433            }
1434
1435            let is_stdlib = search_path.is_standard_library();
1436            // A terminal candidate can stop the search unless a matching post-stdlib stub package
1437            // could still override it. A terminal stdlib candidate always stops the search.
1438            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            // A terminal candidate shadows all later search paths. Earlier candidates remain in
1452            // play because they already shadow this candidate.
1453            if terminal && can_stop {
1454                break;
1455            }
1456
1457            // Reaching this point for stdlib means that it did not provide a terminal candidate.
1458            // The deferred post-stdlib stub packages are therefore eligible, so resolve them now.
1459            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            // Keep a partial stub package's namespace while resolving the next part of the module
1482            // name. Once the complete name is resolved, a concrete package or module shadows that
1483            // namespace.
1484            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                // A terminal candidate shadows every lower-priority candidate, even if resolving
1507                // this component fails. Higher-priority candidates remain in play.
1508                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    // `mypackage-stubs.py(i)` is not a valid result.
1535    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    // Note that we intentionally do *not* filter out ordinary search-path candidates when a stub
1560    // package is found. Even when a non-namespace, non-partial stub package exists, we keep the
1561    // other candidates as fallbacks because sub-packages within the stubs may override py.typed to
1562    // partial. The stub-package candidate is ordered first so it takes priority. Other candidates
1563    // are only used when the stub package fails to find a submodule in a partial sub-package.
1564    candidates.retain(|candidate| {
1565        if !candidate.is_any_namespace_package() {
1566            return true;
1567        }
1568
1569        // A higher-precedence partial namespace remains available while resolving its descendants.
1570        // At the final component, a concrete package or module shadows it.
1571        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        // TODO: It might be useful to warn when a concrete package or module shadows a legacy
1582        // namespace package. If we only find legacy and non-legacy namespace packages, this logic
1583        // retains both.
1584
1585        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
1596/// Resolves one component relative to the candidate's current package.
1597fn 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    // Check for a regular package first (highest priority)
1619    package_path.push("__init__");
1620    if let Some(init) = resolve_file_module_with_filter(package_path, context, file_filter) {
1621        // Remove the `__init__` component for any potential next step
1622        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    // Check for a file module next
1635    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    // Last resort, check if a folder with the given name exists. If so,
1643    // then this is a namespace package. We need to skip this check for
1644    // typeshed because the `resolve_file_module` can also return `None` if the
1645    // `__init__.py` exists but isn't available for the current Python version.
1646    // Let's assume that the `xml` module is only available on Python 3.11+ and
1647    // we're resolving for Python 3.10:
1648    //
1649    // * `resolve_file_module("xml/__init__.pyi")` returns `None` even though
1650    //   the file exists but the module isn't available for the current Python
1651    //   version.
1652    // * The check here would now return `true` because the `xml` directory
1653    //   exists, resulting in a false positive for a namespace package.
1654    //
1655    // Since typeshed doesn't use any namespace packages today (May 2025),
1656    // simply skip this check which also helps performance. If typeshed
1657    // ever uses namespace packages, ensure that this check also takes the
1658    // `VERSIONS` file into consideration.
1659    // A namespace package is not backed by a file, so it cannot satisfy a stub-only lookup.
1660    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
1674/// Uses the parent directory's entries to reject candidates that cannot exist without performing
1675/// individual file-system probes for every supported module layout.
1676fn 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    // Other suffixes are harmless false positives; the normal probes still determine whether the
1690    // module exists.
1691    listing.contains_name_with_prefix(module_name)
1692}
1693
1694type ResolvedNames = Vec<ModuleResolutionCandidate>;
1695
1696/// If `module` exists on disk with an extension permitted by the resolver's mode, return its
1697/// [`File`].
1698///
1699/// Typing resolution prefers `.pyi` over `.py`; runtime resolution only considers `.py`.
1700pub(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
1728/// Determines whether a package is a legacy namespace package.
1729///
1730/// Before PEP 420 introduced implicit namespace packages, the ecosystem developed
1731/// its own form of namespace packages. These legacy namespace packages continue to persist
1732/// in modern codebases because they work with ancient Pythons and if it ain't broke, don't fix it.
1733///
1734/// A legacy namespace package is distinguished by having an `__init__.py` that contains an
1735/// expression to the effect of:
1736///
1737/// ```python
1738/// __path__ = __import__("pkgutil").extend_path(__path__, __name__)
1739/// ```
1740///
1741/// The resulting package simultaneously has properties of both regular packages and namespace ones:
1742///
1743/// * Like regular packages, `__init__.py` is defined and can contain items other than submodules
1744/// * Like implicit namespace packages, multiple copies of the package may exist with different
1745///   submodules, and they will be merged into one namespace at runtime by the interpreter
1746///
1747/// Now, you may rightly wonder: "What if the `__init__.py` files have different contents?"
1748/// The apparent official answer is: "Don't do that!"
1749/// And the reality is: "Of course people do that!"
1750///
1751/// In practice we think it's fine to, just like with regular packages, use the first one
1752/// we find on the search paths. To the extent that the different copies "need" to have the same
1753/// contents, they all "need" to have the legacy namespace idiom (we do nothing to enforce that,
1754/// we will just get confused if you mess it up).
1755fn is_legacy_namespace_package(
1756    package_path: &ModulePath,
1757    context: &ResolverContext,
1758    init: File,
1759) -> bool {
1760    // Just an optimization, the stdlib and typeshed are never legacy namespace packages
1761    if package_path.search_path().is_standard_library() {
1762        return false;
1763    }
1764
1765    // This is all syntax-only analysis so it *could* be fooled but it's really unlikely.
1766    //
1767    // The benefit of being syntax-only is speed and avoiding circular dependencies
1768    // between module resolution and semantic analysis.
1769    //
1770    // The downside is if you write slightly different syntax we will fail to detect the idiom,
1771    // but hey, this is better than nothing!
1772    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/// Info about the `py.typed` file for this package
1787#[derive(Copy, Clone, Eq, PartialEq, Debug)]
1788pub(crate) enum PyTyped {
1789    /// No `py.typed` was found
1790    Untyped,
1791    /// A `py.typed` was found containing "partial"
1792    Partial,
1793    /// A `py.typed` was found (not partial)
1794    Full,
1795}
1796
1797impl PyTyped {
1798    /// Inherit py.typed info from the parent package
1799    ///
1800    /// > This marker applies recursively: if a top-level package includes it,
1801    /// > all its sub-packages MUST support type checking as well.
1802    ///
1803    /// This implementation implies that once a `py.typed` is specified
1804    /// all child packages inherit it, so they can never become Untyped.
1805    /// However they can override whether that's Full or Partial by
1806    /// redeclaring a `py.typed` file of their own.
1807    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/// Detects if a module contains a statement of the form:
1837/// ```python
1838/// __path__ = pkgutil.extend_path(__path__, __name__)
1839/// ```
1840/// or
1841/// ```python
1842/// __path__ = __import__("pkgutil").extend_path(__path__, __name__)
1843/// ```
1844/// or
1845/// ```python
1846/// __import__('pkg_resources').declare_namespace(__name__)
1847/// ```
1848#[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        // Don't traverse into nested bodies.
1861        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            // __path__ = pkgutil.extend_path(__path__, __name__)
1877            // __path__ = __import__("pkgutil").extend_path(__path__, __name__)
1878            ast::Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
1879                self.check_pkgutil_extend_path(targets, value);
1880            }
1881            // __import__('pkg_resources').declare_namespace(__name__)
1882            ast::Stmt::Expr(ast::StmtExpr { value, .. }) => {
1883                self.check_pkg_resources_declare_namespace(value);
1884            }
1885            _ => {}
1886        }
1887    }
1888}
1889
1890impl LegacyNamespacePackageVisitor {
1891    /// Check for `__path__ = pkgutil.extend_path(__path__, __name__)` or
1892    /// `__path__ = __import__("pkgutil").extend_path(__path__, __name__)`
1893    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 if the left side of the attribute access is either `__import__("pkgutil")` or `pkgutil`
1921        match &**maybe_pkg_util {
1922            // __import__("pkgutil").extend_path(__path__, __name__)
1923            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            // "pkgutil.extend_path(__path__, __name__)"
1947            ast::Expr::Name(name) => {
1948                if name.id() != "pkgutil" {
1949                    return;
1950                }
1951            }
1952            _ => {
1953                return;
1954            }
1955        }
1956
1957        // Test that this is an `extend_path(__path__, __name__)` call
1958        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    /// Check for `__import__('pkg_resources').declare_namespace(__name__)`
1973    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        // Match `__import__("pkg_resources")`
1997        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        // Check that the argument is `__name__`
2024        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    /// Tests precedence when there is a package and a sibling stub file.
2230    ///
2231    /// NOTE: I am unsure if this is correct. I wrote this test to match
2232    /// behavior while implementing "list modules." Notably, in this case, the
2233    /// regular source file gets priority. But in `stubs_over_module_source`
2234    /// above, the stub file gets priority.
2235    #[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        // Resolving by directory doesn't resolve to the init file.
2666        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        // `foo` and `bar` shouldn't resolve to the same file
2825
2826        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        // Delete `bar.py`
2861        db.memory_file_system().remove_file(&bar_path).unwrap();
2862        bar.sync(&mut db);
2863
2864        // Re-query the foo module. The foo module should still be cached
2865        // because `bar.py` isn't relevant for resolving `foo`.
2866
2867        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        // Now write the foo file
2897        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        // Delete `foo/__init__.py` and the `foo` folder. `foo` should now resolve to `foo.py`
2923        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        // Adding a file to site-packages does not invalidate the query,
2964        // since site-packages takes lower priority in the module resolution
2965        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        // Adding a first-party file invalidates the query,
3017        // since first-party files take higher priority in module resolution:
3018        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        // If we now delete the first-party file,
3059        // it should resolve to the stdlib:
3060        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        // Lines with leading whitespace in `.pth` files do not parse:
3114        let foo_module_name = ModuleName::new_static("foo").unwrap();
3115        assert_eq!(resolve_module_confident(&db, &foo_module_name), None);
3116
3117        // Lines with trailing whitespace in `.pth` files do:
3118        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        // The editable installs discovered from the `.pth` file in the first `site-packages` directory
3429        // take precedence over the second `site-packages` directory...
3430        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        // ...But now that the `.pth` file in the first `site-packages` directory has been deleted,
3443        // the editable install no longer exists, so the module now resolves to the file in the
3444        // second `site-packages` directory
3445        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        // The lexical directory listing must accept the symlink named `a` while rejecting `A`.
3485        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        // Now try to resolve the module `A` (note the capital `A` instead of `a`).
3495        let a_module_name = ModuleName::new_static("A").unwrap();
3496        assert_eq!(resolve_module_confident(&db, &a_module_name), None);
3497
3498        // Now lookup the same module using the lowercase `a` and it should
3499        // resolve to the file in the system site-packages
3500        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}