Skip to main content

ty_module_resolver/
module_name.rs

1use std::fmt;
2use std::num::NonZeroU32;
3use std::ops::Deref;
4
5use compact_str::{CompactString, ToCompactString};
6
7use ruff_db::files::File;
8use ruff_python_ast::{self as ast, PythonVersion};
9use ruff_python_stdlib::identifiers::is_identifier;
10
11use crate::db::Db;
12use crate::resolve::file_to_module;
13use crate::{ResolverEnvironment, ResolverFile};
14
15/// A module name, e.g. `foo.bar`.
16///
17/// Always normalized to the absolute form (never a relative module name, i.e., never `.foo`).
18#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, get_size2::GetSize)]
19pub struct ModuleName(compact_str::CompactString);
20
21impl ModuleName {
22    /// Creates a new module name for `name`. Returns `Some` if `name` is a valid, absolute
23    /// module name and `None` otherwise.
24    ///
25    /// The module name is invalid if:
26    ///
27    /// * The name is empty
28    /// * The name is relative
29    /// * The name ends with a `.`
30    /// * The name contains a sequence of multiple dots
31    /// * A component of a name (the part between two dots) isn't a valid python identifier.
32    #[inline]
33    #[must_use]
34    pub fn new(name: &str) -> Option<Self> {
35        Self::is_valid_name(name).then(|| Self(CompactString::from(name)))
36    }
37
38    /// Creates a new module name for `name` where `name` is a static string.
39    /// Returns `Some` if `name` is a valid, absolute module name and `None` otherwise.
40    ///
41    /// The module name is invalid if:
42    ///
43    /// * The name is empty
44    /// * The name is relative
45    /// * The name ends with a `.`
46    /// * The name contains a sequence of multiple dots
47    /// * A component of a name (the part between two dots) isn't a valid python identifier.
48    ///
49    /// ## Examples
50    ///
51    /// ```
52    /// use ty_module_resolver::ModuleName;
53    ///
54    /// assert_eq!(ModuleName::new_static("foo.bar").as_deref(), Some("foo.bar"));
55    /// assert_eq!(ModuleName::new_static(""), None);
56    /// assert_eq!(ModuleName::new_static("..foo"), None);
57    /// assert_eq!(ModuleName::new_static(".foo"), None);
58    /// assert_eq!(ModuleName::new_static("foo."), None);
59    /// assert_eq!(ModuleName::new_static("foo..bar"), None);
60    /// assert_eq!(ModuleName::new_static("2000"), None);
61    /// ```
62    #[inline]
63    #[must_use]
64    pub fn new_static(name: &'static str) -> Option<Self> {
65        Self::is_valid_name(name).then(|| Self(CompactString::const_new(name)))
66    }
67
68    #[must_use]
69    fn is_valid_name(name: &str) -> bool {
70        !name.is_empty() && name.split('.').all(is_identifier)
71    }
72
73    /// An iterator over the components of the module name:
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use ty_module_resolver::ModuleName;
79    ///
80    /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().components().collect::<Vec<_>>(), vec!["foo", "bar", "baz"]);
81    /// ```
82    #[must_use]
83    pub fn components(&self) -> impl DoubleEndedIterator<Item = &str> {
84        self.0.split('.')
85    }
86
87    /// Returns the first component in this module name.
88    ///
89    /// # Examples
90    ///
91    /// ```
92    /// use ty_module_resolver::ModuleName;
93    ///
94    /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().first_component(), "foo");
95    /// ```
96    #[must_use]
97    pub fn first_component(&self) -> &str {
98        // OK because `Self::is_valid_name` guarantees that there is at least
99        // one component in the module name.
100        self.components()
101            .next()
102            .expect("at least one module component")
103    }
104
105    /// Returns the last component in this module name.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use ty_module_resolver::ModuleName;
111    ///
112    /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().last_component(), "baz");
113    /// ```
114    #[must_use]
115    pub fn last_component(&self) -> &str {
116        // OK because `Self::is_valid_name` guarantees that there is at least
117        // one component in the module name.
118        self.components()
119            .next_back()
120            .expect("at least one module component")
121    }
122
123    /// The name of this module's immediate parent, if it has a parent.
124    ///
125    /// # Examples
126    ///
127    /// ```
128    /// use ty_module_resolver::ModuleName;
129    ///
130    /// assert_eq!(ModuleName::new_static("foo.bar").unwrap().parent(), Some(ModuleName::new_static("foo").unwrap()));
131    /// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().parent(), Some(ModuleName::new_static("foo.bar").unwrap()));
132    /// assert_eq!(ModuleName::new_static("root").unwrap().parent(), None);
133    /// ```
134    #[must_use]
135    pub fn parent(&self) -> Option<ModuleName> {
136        let (parent, _) = self.0.rsplit_once('.')?;
137        Some(Self(parent.to_compact_string()))
138    }
139
140    /// Returns `true` if the name starts with `other`.
141    ///
142    /// This is equivalent to checking if `self` is a sub-module of `other`.
143    ///
144    /// # Examples
145    ///
146    /// ```
147    /// use ty_module_resolver::ModuleName;
148    ///
149    /// assert!(ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap()));
150    ///
151    /// assert!(!ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("bar").unwrap()));
152    /// assert!(!ModuleName::new_static("foo_bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap()));
153    /// ```
154    #[must_use]
155    pub fn starts_with(&self, other: &ModuleName) -> bool {
156        let mut self_components = self.components();
157        let other_components = other.components();
158
159        for other_component in other_components {
160            if self_components.next() != Some(other_component) {
161                return false;
162            }
163        }
164
165        true
166    }
167
168    /// Given a parent module name of this module name, return the relative
169    /// portion of this module name.
170    ///
171    /// For example, a parent module name of `importlib` with this module name
172    /// as `importlib.resources`, this returns `resources`.
173    ///
174    /// If `parent` isn't a parent name of this module name, then this returns
175    /// `None`.
176    ///
177    /// # Examples
178    ///
179    /// This example shows some cases where `parent` is an actual parent of the
180    /// module name:
181    ///
182    /// ```
183    /// use ty_module_resolver::ModuleName;
184    ///
185    /// let this = ModuleName::new_static("importlib.resources").unwrap();
186    /// let parent = ModuleName::new_static("importlib").unwrap();
187    /// assert_eq!(this.relative_to(&parent), ModuleName::new_static("resources"));
188    ///
189    /// let this = ModuleName::new_static("foo.bar.baz.quux").unwrap();
190    /// let parent = ModuleName::new_static("foo.bar").unwrap();
191    /// assert_eq!(this.relative_to(&parent), ModuleName::new_static("baz.quux"));
192    /// ```
193    ///
194    /// This shows some cases where it isn't a parent:
195    ///
196    /// ```
197    /// use ty_module_resolver::ModuleName;
198    ///
199    /// let this = ModuleName::new_static("importliblib.resources").unwrap();
200    /// let parent = ModuleName::new_static("importlib").unwrap();
201    /// assert_eq!(this.relative_to(&parent), None);
202    ///
203    /// let this = ModuleName::new_static("foo.bar.baz.quux").unwrap();
204    /// let parent = ModuleName::new_static("foo.barbaz").unwrap();
205    /// assert_eq!(this.relative_to(&parent), None);
206    ///
207    /// let this = ModuleName::new_static("importlibbbbb.resources").unwrap();
208    /// let parent = ModuleName::new_static("importlib").unwrap();
209    /// assert_eq!(this.relative_to(&parent), None);
210    /// ```
211    #[must_use]
212    pub fn relative_to(&self, parent: &ModuleName) -> Option<ModuleName> {
213        let relative_name = self.0.strip_prefix(&*parent.0)?.strip_prefix('.')?;
214        // At this point, `relative_name` *has* to be a
215        // proper suffix of `self`. Otherwise, one of the two
216        // `strip_prefix` calls above would return `None`.
217        // (Notably, a valid `ModuleName` cannot end with a `.`.)
218        assert!(!relative_name.is_empty());
219        // This must also be true for this implementation to be
220        // correct. That is, the parent must be a prefix of this
221        // module name according to the rules of how module name
222        // components are split up. This could technically trip if
223        // the implementation of `starts_with` diverges from the
224        // implementation in this routine. But that seems unlikely.
225        debug_assert!(self.starts_with(parent));
226        Some(ModuleName(CompactString::from(relative_name)))
227    }
228
229    #[must_use]
230    #[inline]
231    pub fn as_str(&self) -> &str {
232        &self.0
233    }
234
235    /// Construct a [`ModuleName`] from a sequence of parts.
236    ///
237    /// # Examples
238    ///
239    /// ```
240    /// use ty_module_resolver::ModuleName;
241    ///
242    /// assert_eq!(&*ModuleName::from_components(["a"]).unwrap(), "a");
243    /// assert_eq!(&*ModuleName::from_components(["a", "b"]).unwrap(), "a.b");
244    /// assert_eq!(&*ModuleName::from_components(["a", "b", "c"]).unwrap(), "a.b.c");
245    ///
246    /// assert_eq!(ModuleName::from_components(["a-b"]), None);
247    /// assert_eq!(ModuleName::from_components(["a", "a-b"]), None);
248    /// assert_eq!(ModuleName::from_components(["a", "b", "a-b-c"]), None);
249    /// ```
250    #[must_use]
251    pub fn from_components<'a>(components: impl IntoIterator<Item = &'a str>) -> Option<Self> {
252        let mut components = components.into_iter();
253        let first_part = components.next()?;
254        if !is_identifier(first_part) {
255            return None;
256        }
257        let mut name = CompactString::from(first_part);
258        for part in components {
259            if !is_identifier(part) {
260                return None;
261            }
262            name.push('.');
263            name.push_str(part);
264        }
265        Some(Self(name))
266    }
267
268    /// Extend `self` with the components of `other`
269    ///
270    /// # Examples
271    ///
272    /// ```
273    /// use ty_module_resolver::ModuleName;
274    ///
275    /// let mut module_name = ModuleName::new_static("foo").unwrap();
276    /// module_name.extend(&ModuleName::new_static("bar").unwrap());
277    /// assert_eq!(&module_name, "foo.bar");
278    /// module_name.extend(&ModuleName::new_static("baz.eggs.ham").unwrap());
279    /// assert_eq!(&module_name, "foo.bar.baz.eggs.ham");
280    /// ```
281    pub fn extend(&mut self, other: &ModuleName) {
282        self.0.push('.');
283        self.0.push_str(other);
284    }
285
286    /// Returns an iterator of this module name and all of its parent modules.
287    ///
288    /// # Examples
289    ///
290    /// ```
291    /// use ty_module_resolver::ModuleName;
292    ///
293    /// assert_eq!(
294    ///     ModuleName::new_static("foo.bar.baz").unwrap().ancestors().collect::<Vec<_>>(),
295    ///     vec![
296    ///         ModuleName::new_static("foo.bar.baz").unwrap(),
297    ///         ModuleName::new_static("foo.bar").unwrap(),
298    ///         ModuleName::new_static("foo").unwrap(),
299    ///     ],
300    /// );
301    /// ```
302    pub fn ancestors(&self) -> impl Iterator<Item = Self> {
303        std::iter::successors(Some(self.clone()), Self::parent)
304    }
305
306    /// Extracts a module name from the AST of a `from <module> import ...`
307    /// statement.
308    ///
309    /// `importing_file` must be the file that contains the import statement.
310    ///
311    /// This handles relative import statements.
312    pub fn from_import_statement<'db>(
313        db: &'db dyn Db,
314        importing_file: ImportingFile<'db>,
315        node: &ast::StmtImportFrom,
316    ) -> Result<Self, ModuleNameResolutionError> {
317        let ast::StmtImportFrom {
318            module,
319            level,
320            names: _,
321            is_lazy: _,
322            range: _,
323            node_index: _,
324        } = node;
325        Self::from_identifier_parts(db, importing_file, module.as_deref(), *level)
326    }
327
328    /// Computes the absolute module name from the LHS components of `from LHS import RHS`
329    pub fn from_identifier_parts<'db>(
330        db: &'db dyn Db,
331        importing_file: ImportingFile<'db>,
332        module: Option<&str>,
333        level: u32,
334    ) -> Result<Self, ModuleNameResolutionError> {
335        if let Some(level) = NonZeroU32::new(level) {
336            relative_module_name(db, importing_file.resolver_file(db), module, level)
337        } else {
338            module
339                .and_then(Self::new)
340                .ok_or(ModuleNameResolutionError::InvalidSyntax)
341        }
342    }
343
344    /// Computes the absolute module name for the package this file belongs to.
345    ///
346    /// i.e. this resolves `.`
347    pub fn package_for_file<'db>(
348        db: &'db dyn Db,
349        importing_file: ImportingFile<'db>,
350    ) -> Result<Self, ModuleNameResolutionError> {
351        Self::from_identifier_parts(db, importing_file, None, 1)
352    }
353
354    /// Returns `true` if the module name given appears to be a test module.
355    ///
356    /// This routine is meant to codify a Python ecosystem convention. That is,
357    /// a module is considered a test module if any of the following are true:
358    ///
359    /// * Any non-root component is `test` or `tests`
360    ///   (e.g., `numpy.tests.test_core`).
361    /// * The final component is `conftest` (pytest configuration).
362    ///
363    /// Note that top-level "testing" modules like `pandas.testing` are
364    /// intentionally not filtered, as they provide utilities meant for external
365    /// use.
366    ///
367    /// # Usage
368    ///
369    /// Callers should be mindful when using this routine to filter items
370    /// presented to end users. For example, auto-import uses this to filter
371    /// completions offered, but only for completions outside of the end
372    /// user's first party code. That is, end users still expect to see
373    /// suggestions from their own test modules, but not for test modules in
374    /// their dependencies.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use ty_module_resolver::ModuleName;
380    ///
381    /// // Some positive examples.
382    /// let module_name = ModuleName::new_static("numpy.tests").unwrap();
383    /// assert!(module_name.is_test_module());
384    /// let module_name = ModuleName::new_static("requests.test").unwrap();
385    /// assert!(module_name.is_test_module());
386    /// let module_name = ModuleName::new_static("conftest").unwrap();
387    /// assert!(module_name.is_test_module());
388    /// let module_name = ModuleName::new_static("foo.bar.conftest").unwrap();
389    /// assert!(module_name.is_test_module());
390    ///
391    /// // Some negative examples.
392    /// let module_name = ModuleName::new_static("foo.testing").unwrap();
393    /// assert!(!module_name.is_test_module());
394    /// let module_name = ModuleName::new_static("tests").unwrap();
395    /// assert!(!module_name.is_test_module());
396    /// let module_name = ModuleName::new_static("test").unwrap();
397    /// assert!(!module_name.is_test_module());
398    /// let module_name = ModuleName::new_static("pytest").unwrap();
399    /// assert!(!module_name.is_test_module());
400    /// let module_name = ModuleName::new_static("unittest").unwrap();
401    /// assert!(!module_name.is_test_module());
402    /// ```
403    pub fn is_test_module(&self) -> bool {
404        if self.last_component() == "conftest" {
405            return true;
406        }
407        self.components()
408            .skip(1)
409            .any(|c| c == "test" || c == "tests")
410    }
411
412    /// Returns `true` if the module name is considered private.
413    ///
414    /// This routine is meant to codify a Python ecosystem convention. That is,
415    /// a module is considered private if itself or any of its parent modules
416    /// starts with a `_`.
417    ///
418    /// # Usage
419    ///
420    /// Callers should be mindful when using this routine to filter items
421    /// presented to end users. For example, auto-import uses this to filter
422    /// completions offered, but only for completions outside of the end user's
423    /// first party code. That is, end users still expect to see suggestions
424    /// from their private modules, but not for private modules in their
425    /// dependencies.
426    ///
427    /// # Examples
428    ///
429    /// ```
430    /// use ty_module_resolver::ModuleName;
431    ///
432    /// let module_name = ModuleName::new_static("_foo").unwrap();
433    /// assert!(module_name.is_private());
434    /// let module_name = ModuleName::new_static("foo._bar").unwrap();
435    /// assert!(module_name.is_private());
436    /// let module_name = ModuleName::new_static("foo._bar.quux").unwrap();
437    /// assert!(module_name.is_private());
438    /// ```
439    pub fn is_private(&self) -> bool {
440        self.components().any(|c| c.starts_with('_'))
441    }
442}
443
444impl Deref for ModuleName {
445    type Target = str;
446
447    #[inline]
448    fn deref(&self) -> &Self::Target {
449        self.as_str()
450    }
451}
452
453impl PartialEq<str> for ModuleName {
454    fn eq(&self, other: &str) -> bool {
455        self.as_str() == other
456    }
457}
458
459impl PartialEq<ModuleName> for str {
460    fn eq(&self, other: &ModuleName) -> bool {
461        self == other.as_str()
462    }
463}
464
465impl std::fmt::Display for ModuleName {
466    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
467        f.write_str(&self.0)
468    }
469}
470
471/// The file from which an import is resolved.
472///
473/// Most absolute imports only need the resolver environment. Creating a [`ResolverFile`] for each
474/// such import would unnecessarily intern the file and environment together, even though that
475/// combined identity is never used:
476///
477/// ```text
478/// resolve_module(ImportingFile::File(shared.py, environment), "dependency")
479///     -> resolve using environment; no ResolverFile needed
480/// ```
481///
482/// Relative imports, on the other hand, need the importing file's module identity and therefore
483/// require a [`ResolverFile`]:
484///
485/// ```text
486/// from .dependency import value
487///     -> importing_file.resolver_file(db)
488///     -> ResolverFile(shared.py, environment)
489/// ```
490///
491/// [`ImportingFile::File`] defers interning until such a code path actually calls
492/// [`ImportingFile::resolver_file`]. Callers that already have an interned resolver file can pass
493/// [`ImportingFile::ResolverFile`] to reuse it directly.
494#[derive(Clone, Copy)]
495pub enum ImportingFile<'db> {
496    /// An already-interned resolver key that can be reused without materialization.
497    ResolverFile(ResolverFile<'db>),
498    /// An importing file and resolver environment whose combined key is materialized lazily.
499    File(File, ResolverEnvironment<'db>),
500}
501
502impl<'db> ImportingFile<'db> {
503    pub fn file(self, db: &dyn Db) -> File {
504        match self {
505            Self::ResolverFile(file) => file.file(db),
506            Self::File(file, _) => file,
507        }
508    }
509
510    pub fn resolver_environment(self, db: &'db dyn Db) -> ResolverEnvironment<'db> {
511        match self {
512            Self::ResolverFile(file) => file.environment(db),
513            Self::File(_, resolver_environment) => resolver_environment,
514        }
515    }
516
517    pub fn python_version(self, db: &'db dyn Db) -> PythonVersion {
518        self.resolver_environment(db).python_version(db)
519    }
520
521    /// Returns the existing resolver key or materializes one when required.
522    pub fn resolver_file(self, db: &'db dyn Db) -> ResolverFile<'db> {
523        match self {
524            Self::ResolverFile(file) => file,
525            Self::File(file, resolver_environment) => {
526                ResolverFile::new(db, file, resolver_environment)
527            }
528        }
529    }
530}
531
532/// Given a `from .foo import bar` relative import, resolve the relative module
533/// we're importing `bar` from into an absolute [`ModuleName`]
534/// using the name of the module we're currently analyzing.
535///
536/// - `level` is the number of dots at the beginning of the relative module name:
537///   - `from .foo.bar import baz` => `level == 1`
538///   - `from ...foo.bar import baz` => `level == 3`
539/// - `tail` is the relative module name stripped of all leading dots:
540///   - `from .foo import bar` => `tail == "foo"`
541///   - `from ..foo.bar import baz` => `tail == "foo.bar"`
542fn relative_module_name<'db>(
543    db: &'db dyn Db,
544    importing_file: ResolverFile<'db>,
545    tail: Option<&str>,
546    level: NonZeroU32,
547) -> Result<ModuleName, ModuleNameResolutionError> {
548    let module = file_to_module(db, importing_file)
549        .ok_or(ModuleNameResolutionError::UnknownCurrentModule)?;
550    let mut level = level.get();
551
552    if module.kind(db).is_package() {
553        level = level.saturating_sub(1);
554    }
555
556    let mut module_name = module
557        .name(db)
558        .ancestors()
559        .nth(level as usize)
560        .ok_or(ModuleNameResolutionError::TooManyDots)?;
561
562    if let Some(tail) = tail {
563        let tail = ModuleName::new(tail).ok_or(ModuleNameResolutionError::InvalidSyntax)?;
564        module_name.extend(&tail);
565    }
566
567    Ok(module_name)
568}
569
570/// Various ways in which resolving a [`ModuleName`]
571/// from an [`ast::StmtImport`] or [`ast::StmtImportFrom`] node might fail
572#[derive(Debug, Copy, Clone, PartialEq, Eq)]
573pub enum ModuleNameResolutionError {
574    /// The import statement has invalid syntax
575    InvalidSyntax,
576
577    /// We couldn't resolve the file we're currently analyzing back to a module
578    /// (Only necessary for relative import statements)
579    UnknownCurrentModule,
580
581    /// The relative import statement seems to take us outside of the module search path
582    /// (e.g. our current module is `foo.bar`, and the relative import statement in `foo.bar`
583    /// is `from ....baz import spam`)
584    TooManyDots,
585}