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}