Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Respect typeshed's VERSIONS file when resolving stdlib modules #12141

Merged
merged 62 commits into from
Jul 5, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
7380511
Everything finally compiles
AlexWaygood Jun 29, 2024
75a140a
Move `ModuleName` into a new file
AlexWaygood Jun 29, 2024
8cd568c
Specify that the `Db` must allow typeshed versions to be queried
AlexWaygood Jun 29, 2024
eb6a377
The supported Python version is set at the same time as the search paths
AlexWaygood Jun 29, 2024
87419ce
`TypeshedVersions` returns a three-variant enum
AlexWaygood Jul 1, 2024
5d30cf8
Silly implementation for the `todo!()`s
AlexWaygood Jul 1, 2024
5c1f4e8
Implementation that actually looks at the target version we set
AlexWaygood Jul 1, 2024
1a82d21
Address easy review comments
AlexWaygood Jul 1, 2024
d91626c
Get rid of a lot of boilerplate
AlexWaygood Jul 2, 2024
1dc038e
Cleanup
AlexWaygood Jul 2, 2024
9e1ae5e
Merge branch 'main' into custom-typeshed-versions
AlexWaygood Jul 2, 2024
583e503
Make some public methods private
AlexWaygood Jul 2, 2024
906d801
Inline some simple helper methods
AlexWaygood Jul 2, 2024
ed088e3
Use Salsa queries to load `TypeshedVersions`
AlexWaygood Jul 2, 2024
dbe8826
fix test
AlexWaygood Jul 2, 2024
0a0a334
Address easy review comments
AlexWaygood Jul 3, 2024
ad2e987
Cleanup VERSIONS-parsing and add a todo for better error handling
AlexWaygood Jul 3, 2024
78c56f7
Move more logic internal to `path.rs`
AlexWaygood Jul 3, 2024
6723c7f
Get rid of the inner structs
AlexWaygood Jul 3, 2024
9ba9ac1
Bye bye `DoubleEndedIterator`
AlexWaygood Jul 3, 2024
9c556ba
Add tests for some helpers
AlexWaygood Jul 3, 2024
9c672f8
Simplify `ModulePartIterator`
AlexWaygood Jul 3, 2024
ad3bd7a
more tests
AlexWaygood Jul 3, 2024
21e60fe
fix Windows?
AlexWaygood Jul 3, 2024
76f5b3f
fix Windows???
AlexWaygood Jul 3, 2024
116f8f9
Add tests for `relativize_path`
AlexWaygood Jul 4, 2024
5ecc0f3
Add tests for `is_directory()` and `is_regular_package()`
AlexWaygood Jul 4, 2024
dde1393
Merge branch 'main' into custom-typeshed-versions
AlexWaygood Jul 4, 2024
c541dd0
Delete cruft from older tests
AlexWaygood Jul 4, 2024
2f0d349
Add tests for the resolver as a whole with `VERSIONS`
AlexWaygood Jul 4, 2024
9bbe5a8
More unit tests in `path.rs`
AlexWaygood Jul 4, 2024
e505b68
Merge branch 'main' into custom-typeshed-versions
AlexWaygood Jul 4, 2024
36b9288
Reduce use of `is_none()` in tests
AlexWaygood Jul 5, 2024
2f0d562
Add more todos for `into_ordered_search_paths()`
AlexWaygood Jul 5, 2024
86ee754
Merge branch 'main' into custom-typeshed-versions
AlexWaygood Jul 5, 2024
ea58e3a
Typeshed versions are looked up from the cache once per module resolu…
AlexWaygood Jul 5, 2024
8dda2e3
Move test-only methods into `test` submodules
AlexWaygood Jul 5, 2024
7efd84c
Elide some lifetimes
AlexWaygood Jul 5, 2024
553ce67
Delete and streamline some trait implementations
AlexWaygood Jul 5, 2024
9fda47a
Remove unnecessary `*`s
AlexWaygood Jul 5, 2024
b819680
Make `ModuleResolutionPathBuf::push()` assert invariants on release b…
AlexWaygood Jul 5, 2024
7eebb75
Reduce nesting in `ModuleName::from_components()`
AlexWaygood Jul 5, 2024
470f0c6
Document `TypeshedVersionsQueryResult` variants
AlexWaygood Jul 5, 2024
d4c11ee
Docs for `query_module()`
AlexWaygood Jul 5, 2024
b0c49c2
Add a `create_resolver_test()` helper
AlexWaygood Jul 5, 2024
55538bb
Fix `Debug` implementations
AlexWaygood Jul 5, 2024
a65c4dc
Get rid of `stdlib_path_test_case()` helper
AlexWaygood Jul 5, 2024
884c57b
Share more code between some methods in `path.rs`
AlexWaygood Jul 5, 2024
13e741e
Split up some tests in `resolver.rs`
AlexWaygood Jul 5, 2024
70cd43b
Reduce diff in `resolver.rs` tests
AlexWaygood Jul 5, 2024
82a9974
Split up a big test in `versions.rs`
AlexWaygood Jul 5, 2024
5f5ad70
Simplify tests in `path.rs`
AlexWaygood Jul 5, 2024
640d2fa
Small cleanup in `path.rs`
AlexWaygood Jul 5, 2024
4c7a106
Getting the target version is no longer a separate Salsa query
AlexWaygood Jul 5, 2024
777ec7d
`db` is now always the first argument
AlexWaygood Jul 5, 2024
f8e900e
Get rid of unneeded new dependency
AlexWaygood Jul 5, 2024
8fc3a7d
So many lifetimes
AlexWaygood Jul 5, 2024
b848415
Rename some things
AlexWaygood Jul 5, 2024
daf64b4
Audit docs in `resolver.rs`
AlexWaygood Jul 5, 2024
47c3467
Make assertions in `push()` make a little more sense
AlexWaygood Jul 5, 2024
401779b
Encapsulate all state passed around in a single struct
AlexWaygood Jul 5, 2024
38128e0
Add a TODO for better distinctions between absolute and relative paths
AlexWaygood Jul 5, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/red_knot_module_resolver/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ruff_db = { workspace = true }
ruff_python_stdlib = { workspace = true }

compact_str = { workspace = true }
camino = { workspace = true }
rustc-hash = { workspace = true }
salsa = { workspace = true }
tracing = { workspace = true }
Expand Down
4 changes: 4 additions & 0 deletions crates/red_knot_module_resolver/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ use crate::resolver::{
internal::{ModuleNameIngredient, ModuleResolverSearchPaths},
resolve_module_query,
};
use crate::supported_py_version::TargetPyVersion;
use crate::typeshed::parse_typeshed_versions;

#[salsa::jar(db=Db)]
pub struct Jar(
ModuleNameIngredient<'_>,
ModuleResolverSearchPaths,
TargetPyVersion,
resolve_module_query,
file_to_module,
parse_typeshed_versions,
);

pub trait Db: salsa::DbWithJar<Jar> + ruff_db::Db + Upcast<dyn ruff_db::Db> {}
Expand Down
9 changes: 7 additions & 2 deletions crates/red_knot_module_resolver/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
mod db;
mod module;
mod module_name;
mod path;
mod resolver;
mod supported_py_version;
mod typeshed;

pub use db::{Db, Jar};
pub use module::{Module, ModuleKind, ModuleName};
pub use module::{Module, ModuleKind};
pub use module_name::ModuleName;
pub use resolver::{resolve_module, set_module_resolution_settings, ModuleResolutionSettings};
pub use typeshed::versions::TypeshedVersions;
pub use supported_py_version::SupportedPyVersion;
pub use typeshed::TypeshedVersions;
266 changes: 7 additions & 259 deletions crates/red_knot_module_resolver/src/module.rs
Original file line number Diff line number Diff line change
@@ -1,189 +1,12 @@
use compact_str::ToCompactString;
use std::fmt::Formatter;
use std::ops::Deref;
use std::sync::Arc;

use ruff_db::file_system::FileSystemPath;
use ruff_db::vfs::{VfsFile, VfsPath};
use ruff_python_stdlib::identifiers::is_identifier;
use ruff_db::vfs::VfsFile;

use crate::module_name::ModuleName;
use crate::path::{ModuleResolutionPath, ModuleResolutionPathRef};
use crate::Db;

/// A module name, e.g. `foo.bar`.
///
/// Always normalized to the absolute form (never a relative module name, i.e., never `.foo`).
#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub struct ModuleName(compact_str::CompactString);

impl ModuleName {
/// Creates a new module name for `name`. Returns `Some` if `name` is a valid, absolute
/// module name and `None` otherwise.
///
/// The module name is invalid if:
///
/// * The name is empty
/// * The name is relative
/// * The name ends with a `.`
/// * The name contains a sequence of multiple dots
/// * A component of a name (the part between two dots) isn't a valid python identifier.
#[inline]
pub fn new(name: &str) -> Option<Self> {
Self::is_valid_name(name).then(|| Self(compact_str::CompactString::from(name)))
}

/// Creates a new module name for `name` where `name` is a static string.
/// Returns `Some` if `name` is a valid, absolute module name and `None` otherwise.
///
/// The module name is invalid if:
///
/// * The name is empty
/// * The name is relative
/// * The name ends with a `.`
/// * The name contains a sequence of multiple dots
/// * A component of a name (the part between two dots) isn't a valid python identifier.
///
/// ## Examples
///
/// ```
/// use red_knot_module_resolver::ModuleName;
///
/// assert_eq!(ModuleName::new_static("foo.bar").as_deref(), Some("foo.bar"));
/// assert_eq!(ModuleName::new_static(""), None);
/// assert_eq!(ModuleName::new_static("..foo"), None);
/// assert_eq!(ModuleName::new_static(".foo"), None);
/// assert_eq!(ModuleName::new_static("foo."), None);
/// assert_eq!(ModuleName::new_static("foo..bar"), None);
/// assert_eq!(ModuleName::new_static("2000"), None);
/// ```
#[inline]
pub fn new_static(name: &'static str) -> Option<Self> {
// TODO(Micha): Use CompactString::const_new once we upgrade to 0.8 https://github.com/ParkMyCar/compact_str/pull/336
Self::is_valid_name(name).then(|| Self(compact_str::CompactString::from(name)))
}

fn is_valid_name(name: &str) -> bool {
if name.is_empty() {
return false;
}

name.split('.').all(is_identifier)
}

/// An iterator over the components of the module name:
///
/// # Examples
///
/// ```
/// use red_knot_module_resolver::ModuleName;
///
/// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().components().collect::<Vec<_>>(), vec!["foo", "bar", "baz"]);
/// ```
pub fn components(&self) -> impl DoubleEndedIterator<Item = &str> {
self.0.split('.')
}

/// The name of this module's immediate parent, if it has a parent.
///
/// # Examples
///
/// ```
/// use red_knot_module_resolver::ModuleName;
///
/// assert_eq!(ModuleName::new_static("foo.bar").unwrap().parent(), Some(ModuleName::new_static("foo").unwrap()));
/// assert_eq!(ModuleName::new_static("foo.bar.baz").unwrap().parent(), Some(ModuleName::new_static("foo.bar").unwrap()));
/// assert_eq!(ModuleName::new_static("root").unwrap().parent(), None);
/// ```
pub fn parent(&self) -> Option<ModuleName> {
let (parent, _) = self.0.rsplit_once('.')?;
Some(Self(parent.to_compact_string()))
}

/// Returns `true` if the name starts with `other`.
///
/// This is equivalent to checking if `self` is a sub-module of `other`.
///
/// # Examples
///
/// ```
/// use red_knot_module_resolver::ModuleName;
///
/// assert!(ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap()));
///
/// assert!(!ModuleName::new_static("foo.bar").unwrap().starts_with(&ModuleName::new_static("bar").unwrap()));
/// assert!(!ModuleName::new_static("foo_bar").unwrap().starts_with(&ModuleName::new_static("foo").unwrap()));
/// ```
pub fn starts_with(&self, other: &ModuleName) -> bool {
let mut self_components = self.components();
let other_components = other.components();

for other_component in other_components {
if self_components.next() != Some(other_component) {
return false;
}
}

true
}

#[inline]
pub fn as_str(&self) -> &str {
&self.0
}

pub(crate) fn from_relative_path(path: &FileSystemPath) -> Option<Self> {
let path = if path.ends_with("__init__.py") || path.ends_with("__init__.pyi") {
path.parent()?
} else {
path
};

let name = if let Some(parent) = path.parent() {
let mut name = compact_str::CompactString::with_capacity(path.as_str().len());

for component in parent.components() {
name.push_str(component.as_os_str().to_str()?);
name.push('.');
}

// SAFETY: Unwrap is safe here or `parent` would have returned `None`.
name.push_str(path.file_stem().unwrap());

name
} else {
path.file_stem()?.to_compact_string()
};

Some(Self(name))
}
}

impl Deref for ModuleName {
type Target = str;

#[inline]
fn deref(&self) -> &Self::Target {
self.as_str()
}
}

impl PartialEq<str> for ModuleName {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}

impl PartialEq<ModuleName> for str {
fn eq(&self, other: &ModuleName) -> bool {
self == other.as_str()
}
}

impl std::fmt::Display for ModuleName {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}

/// Representation of a Python module.
#[derive(Clone, PartialEq, Eq)]
pub struct Module {
Expand All @@ -194,7 +17,7 @@ impl Module {
pub(crate) fn new(
name: ModuleName,
kind: ModuleKind,
search_path: ModuleSearchPath,
search_path: Arc<ModuleResolutionPath>,
file: VfsFile,
) -> Self {
Self {
Expand All @@ -218,8 +41,8 @@ impl Module {
}

/// The search path from which the module was resolved.
pub fn search_path(&self) -> &ModuleSearchPath {
&self.inner.search_path
pub(crate) fn search_path(&self) -> ModuleResolutionPathRef {
ModuleResolutionPathRef::from(&*self.inner.search_path)
}

/// Determine whether this module is a single-file module or a package
Expand Down Expand Up @@ -254,7 +77,7 @@ impl salsa::DebugWithDb<dyn Db> for Module {
struct ModuleInner {
name: ModuleName,
kind: ModuleKind,
search_path: ModuleSearchPath,
search_path: Arc<ModuleResolutionPath>,
file: VfsFile,
}

Expand All @@ -266,78 +89,3 @@ pub enum ModuleKind {
/// A python package (`foo/__init__.py` or `foo/__init__.pyi`)
Package,
}

/// A search path in which to search modules.
/// Corresponds to a path in [`sys.path`](https://docs.python.org/3/library/sys_path_init.html) at runtime.
///
/// Cloning a search path is cheap because it's an `Arc`.
#[derive(Clone, PartialEq, Eq)]
pub struct ModuleSearchPath {
inner: Arc<ModuleSearchPathInner>,
}

impl ModuleSearchPath {
pub fn new<P>(path: P, kind: ModuleSearchPathKind) -> Self
where
P: Into<VfsPath>,
{
Self {
inner: Arc::new(ModuleSearchPathInner {
path: path.into(),
kind,
}),
}
}

/// Determine whether this is a first-party, third-party or standard-library search path
pub fn kind(&self) -> ModuleSearchPathKind {
self.inner.kind
}

/// Return the location of the search path on the file system
pub fn path(&self) -> &VfsPath {
&self.inner.path
}
}

impl std::fmt::Debug for ModuleSearchPath {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ModuleSearchPath")
.field("path", &self.inner.path)
.field("kind", &self.kind())
.finish()
}
}

#[derive(Eq, PartialEq)]
struct ModuleSearchPathInner {
path: VfsPath,
kind: ModuleSearchPathKind,
}

/// Enumeration of the different kinds of search paths type checkers are expected to support.
///
/// N.B. Although we don't implement `Ord` for this enum, they are ordered in terms of the
/// priority that we want to give these modules when resolving them.
/// This is roughly [the order given in the typing spec], but typeshed's stubs
/// for the standard library are moved higher up to match Python's semantics at runtime.
///
/// [the order given in the typing spec]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-resolution-ordering
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum ModuleSearchPathKind {
/// "Extra" paths provided by the user in a config file, env var or CLI flag.
/// E.g. mypy's `MYPYPATH` env var, or pyright's `stubPath` configuration setting
Extra,

/// Files in the project we're directly being invoked on
FirstParty,

/// The `stdlib` directory of typeshed (either vendored or custom)
StandardLibrary,

/// Stubs or runtime modules installed in site-packages
SitePackagesThirdParty,

/// Vendored third-party stubs from typeshed
VendoredThirdParty,
}
Loading
Loading