[ty] Make Module a Salsa ingredient

We want to write queries that depend on `Module` for caching. While it
seems it can be done without making `Module` an ingredient, it seems it
is best practice to do so.

[best practice to do so]: https://github.com/astral-sh/ruff/pull/19408#discussion_r2215867301
This commit is contained in:
Andrew Gallant 2025-07-18 14:15:42 -04:00 committed by Andrew Gallant
parent 905b9d7f51
commit 4573a0f6a0
24 changed files with 258 additions and 253 deletions

View file

@ -1,10 +1,11 @@
use std::fmt::Formatter;
use std::str::FromStr;
use std::sync::Arc;
use ruff_db::files::File;
use ruff_python_ast::name::Name;
use ruff_python_stdlib::identifiers::is_identifier;
use salsa::Database;
use salsa::plumbing::AsId;
use super::path::SearchPath;
use crate::Db;
@ -12,14 +13,19 @@ use crate::module_name::ModuleName;
use crate::module_resolver::path::SystemOrVendoredPathRef;
/// Representation of a Python module.
#[derive(Clone, PartialEq, Eq, Hash, get_size2::GetSize)]
pub struct Module {
inner: Arc<ModuleInner>,
#[derive(Clone, Copy, Eq, Hash, PartialEq, salsa::Supertype, salsa::Update)]
pub enum Module<'db> {
File(FileModule<'db>),
Namespace(NamespacePackage<'db>),
}
// The Salsa heap is tracked separately.
impl get_size2::GetSize for Module<'_> {}
#[salsa::tracked]
impl Module {
impl<'db> Module<'db> {
pub(crate) fn file_module(
db: &'db dyn Db,
name: ModuleName,
kind: ModuleKind,
search_path: SearchPath,
@ -27,67 +33,57 @@ impl Module {
) -> Self {
let known = KnownModule::try_from_search_path_and_name(&search_path, &name);
Self {
inner: Arc::new(ModuleInner::FileModule {
name,
kind,
search_path,
file,
known,
}),
}
Self::File(FileModule::new(db, name, kind, search_path, file, known))
}
pub(crate) fn namespace_package(name: ModuleName) -> Self {
Self {
inner: Arc::new(ModuleInner::NamespacePackage { name }),
}
pub(crate) fn namespace_package(db: &'db dyn Db, name: ModuleName) -> Self {
Self::Namespace(NamespacePackage::new(db, name))
}
/// The absolute name of the module (e.g. `foo.bar`)
pub fn name(&self) -> &ModuleName {
match &*self.inner {
ModuleInner::FileModule { name, .. } => name,
ModuleInner::NamespacePackage { name, .. } => name,
pub fn name(self, db: &'db dyn Database) -> &'db ModuleName {
match self {
Module::File(module) => module.name(db),
Module::Namespace(ref package) => package.name(db),
}
}
/// The file to the source code that defines this module
///
/// This is `None` for namespace packages.
pub fn file(&self) -> Option<File> {
match &*self.inner {
ModuleInner::FileModule { file, .. } => Some(*file),
ModuleInner::NamespacePackage { .. } => None,
pub fn file(self, db: &'db dyn Database) -> Option<File> {
match self {
Module::File(module) => Some(module.file(db)),
Module::Namespace(_) => None,
}
}
/// Is this a module that we special-case somehow? If so, which one?
pub fn known(&self) -> Option<KnownModule> {
match &*self.inner {
ModuleInner::FileModule { known, .. } => *known,
ModuleInner::NamespacePackage { .. } => None,
pub fn known(self, db: &'db dyn Database) -> Option<KnownModule> {
match self {
Module::File(module) => module.known(db),
Module::Namespace(_) => None,
}
}
/// Does this module represent the given known module?
pub fn is_known(&self, known_module: KnownModule) -> bool {
self.known() == Some(known_module)
pub fn is_known(self, db: &'db dyn Database, known_module: KnownModule) -> bool {
self.known(db) == Some(known_module)
}
/// The search path from which the module was resolved.
pub(crate) fn search_path(&self) -> Option<&SearchPath> {
match &*self.inner {
ModuleInner::FileModule { search_path, .. } => Some(search_path),
ModuleInner::NamespacePackage { .. } => None,
pub(crate) fn search_path(self, db: &'db dyn Database) -> Option<&'db SearchPath> {
match self {
Module::File(module) => Some(module.search_path(db)),
Module::Namespace(_) => None,
}
}
/// Determine whether this module is a single-file module or a package
pub fn kind(&self) -> ModuleKind {
match &*self.inner {
ModuleInner::FileModule { kind, .. } => *kind,
ModuleInner::NamespacePackage { .. } => ModuleKind::Package,
pub fn kind(self, db: &'db dyn Database) -> ModuleKind {
match self {
Module::File(module) => module.kind(db),
Module::Namespace(_) => ModuleKind::Package,
}
}
@ -98,16 +94,13 @@ impl Module {
///
/// The names returned correspond to the "base" name of the module.
/// That is, `{self.name}.{basename}` should give the full module name.
pub fn all_submodules<'db>(&self, db: &'db dyn Db) -> &'db [Name] {
self.clone()
.all_submodules_inner(db, ())
.as_deref()
.unwrap_or_default()
pub fn all_submodules(self, db: &'db dyn Db) -> &'db [Name] {
self.all_submodules_inner(db).as_deref().unwrap_or_default()
}
#[allow(clippy::ref_option, clippy::used_underscore_binding)]
#[allow(clippy::ref_option)]
#[salsa::tracked(returns(ref))]
fn all_submodules_inner(self, db: &dyn Db, _dummy: ()) -> Option<Vec<Name>> {
fn all_submodules_inner(self, db: &'db dyn Db) -> Option<Vec<Name>> {
fn is_submodule(
is_dir: bool,
is_file: bool,
@ -125,16 +118,14 @@ impl Module {
// to a single file; it can span multiple directories across multiple
// search paths. For now, we only compute submodules for traditional
// packages that exist in a single directory on a single search path.
let ModuleInner::FileModule {
kind: ModuleKind::Package,
file,
..
} = &*self.inner
else {
let Module::File(module) = self else {
return None;
};
if !matches!(module.kind(db), ModuleKind::Package) {
return None;
}
let path = SystemOrVendoredPathRef::try_from_file(db, *file)?;
let path = SystemOrVendoredPathRef::try_from_file(db, module.file(db))?;
debug_assert!(
matches!(path.file_name(), Some("__init__.py" | "__init__.pyi")),
"expected package file `{:?}` to be `__init__.py` or `__init__.pyi`",
@ -201,33 +192,41 @@ impl Module {
}
}
impl std::fmt::Debug for Module {
impl std::fmt::Debug for Module<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Module")
.field("name", &self.name())
.field("kind", &self.kind())
.field("file", &self.file())
.field("search_path", &self.search_path())
.field("known", &self.known())
.finish()
salsa::with_attached_database(|db| {
f.debug_struct("Module")
.field("name", &self.name(db))
.field("kind", &self.kind(db))
.field("file", &self.file(db))
.field("search_path", &self.search_path(db))
.field("known", &self.known(db))
.finish()
})
.unwrap_or_else(|| f.debug_tuple("Module").field(&self.as_id()).finish())
}
}
#[derive(PartialEq, Eq, Hash, get_size2::GetSize)]
enum ModuleInner {
/// A module that resolves to a file (`lib.py` or `package/__init__.py`)
FileModule {
name: ModuleName,
kind: ModuleKind,
search_path: SearchPath,
file: File,
known: Option<KnownModule>,
},
/// A module that resolves to a file (`lib.py` or `package/__init__.py`)
#[salsa::tracked(debug)]
pub struct FileModule<'db> {
#[returns(ref)]
name: ModuleName,
kind: ModuleKind,
#[returns(ref)]
search_path: SearchPath,
file: File,
known: Option<KnownModule>,
}
/// A namespace package. Namespace packages are special because
/// there are multiple possible paths and they have no corresponding
/// code file.
NamespacePackage { name: ModuleName },
/// A namespace package.
///
/// Namespace packages are special because there are
/// multiple possible paths and they have no corresponding code file.
#[salsa::tracked(debug)]
pub struct NamespacePackage<'db> {
#[returns(ref)]
name: ModuleName,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, get_size2::GetSize)]