From ac04380f360b1fca84435fbbf3154f61e551a871 Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Tue, 9 Jul 2024 09:20:51 +0200 Subject: [PATCH] [red-knot] Rename `FileSystem` to `System` (#12214) --- Cargo.lock | 2 +- crates/red_knot/src/lib.rs | 20 +- crates/red_knot/src/lint.rs | 6 +- crates/red_knot/src/main.rs | 14 +- crates/red_knot/src/program/check.rs | 6 +- crates/red_knot/src/program/mod.rs | 43 +-- crates/red_knot/src/watch.rs | 8 +- crates/red_knot_module_resolver/Cargo.toml | 1 + crates/red_knot_module_resolver/src/db.rs | 110 +++---- crates/red_knot_module_resolver/src/lib.rs | 4 +- crates/red_knot_module_resolver/src/module.rs | 8 +- crates/red_knot_module_resolver/src/path.rs | 212 ++++++------- .../red_knot_module_resolver/src/resolver.rs | 128 ++++---- crates/red_knot_module_resolver/src/state.rs | 6 +- .../red_knot_module_resolver/src/typeshed.rs | 33 +- .../src/typeshed/versions.rs | 15 +- crates/red_knot_python_semantic/src/db.rs | 76 ++--- .../src/semantic_index.rs | 21 +- .../src/semantic_index/builder.rs | 6 +- .../src/semantic_index/definition.rs | 4 +- .../src/semantic_index/symbol.rs | 12 +- .../src/semantic_model.rs | 26 +- crates/red_knot_python_semantic/src/types.rs | 40 +-- .../src/types/infer.rs | 62 ++-- crates/ruff_benchmark/benches/red_knot.rs | 25 +- crates/ruff_db/Cargo.toml | 1 - crates/ruff_db/src/{vfs.rs => files.rs} | 259 +++++----------- crates/ruff_db/src/files/path.rs | 176 +++++++++++ crates/ruff_db/src/lib.rs | 77 +++-- crates/ruff_db/src/parsed.rs | 42 +-- crates/ruff_db/src/source.rs | 45 ++- crates/ruff_db/src/system.rs | 97 ++++++ .../memory.rs => system/memory_fs.rs} | 273 ++++++++-------- .../ruff_db/src/{file_system => system}/os.rs | 21 +- .../src/{file_system.rs => system/path.rs} | 291 +++++++----------- crates/ruff_db/src/system/test.rs | 167 ++++++++++ crates/ruff_db/src/vendored.rs | 219 ++++++++----- crates/ruff_db/src/vfs/path.rs | 161 ---------- 38 files changed, 1429 insertions(+), 1288 deletions(-) rename crates/ruff_db/src/{vfs.rs => files.rs} (50%) create mode 100644 crates/ruff_db/src/files/path.rs create mode 100644 crates/ruff_db/src/system.rs rename crates/ruff_db/src/{file_system/memory.rs => system/memory_fs.rs} (66%) rename crates/ruff_db/src/{file_system => system}/os.rs (70%) rename crates/ruff_db/src/{file_system.rs => system/path.rs} (51%) create mode 100644 crates/ruff_db/src/system/test.rs delete mode 100644 crates/ruff_db/src/vfs/path.rs diff --git a/Cargo.lock b/Cargo.lock index 38efac3b1b31e..7c4ce1cc2003f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1888,6 +1888,7 @@ dependencies = [ "camino", "compact_str", "insta", + "once_cell", "path-slash", "ruff_db", "ruff_python_stdlib", @@ -2094,7 +2095,6 @@ dependencies = [ "dashmap 6.0.1", "filetime", "insta", - "once_cell", "ruff_python_ast", "ruff_python_parser", "ruff_source_file", diff --git a/crates/red_knot/src/lib.rs b/crates/red_knot/src/lib.rs index 7d1629c24bab6..1f8948a001acc 100644 --- a/crates/red_knot/src/lib.rs +++ b/crates/red_knot/src/lib.rs @@ -1,7 +1,7 @@ use rustc_hash::FxHashSet; -use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf}; -use ruff_db::vfs::VfsFile; +use ruff_db::files::File; +use ruff_db::system::{SystemPath, SystemPathBuf}; use crate::db::Jar; @@ -12,41 +12,41 @@ pub mod watch; #[derive(Debug, Clone)] pub struct Workspace { - root: FileSystemPathBuf, + root: SystemPathBuf, /// The files that are open in the workspace. /// /// * Editor: The files that are actively being edited in the editor (the user has a tab open with the file). /// * CLI: The resolved files passed as arguments to the CLI. - open_files: FxHashSet, + open_files: FxHashSet, } impl Workspace { - pub fn new(root: FileSystemPathBuf) -> Self { + pub fn new(root: SystemPathBuf) -> Self { Self { root, open_files: FxHashSet::default(), } } - pub fn root(&self) -> &FileSystemPath { + pub fn root(&self) -> &SystemPath { self.root.as_path() } // TODO having the content in workspace feels wrong. - pub fn open_file(&mut self, file_id: VfsFile) { + pub fn open_file(&mut self, file_id: File) { self.open_files.insert(file_id); } - pub fn close_file(&mut self, file_id: VfsFile) { + pub fn close_file(&mut self, file_id: File) { self.open_files.remove(&file_id); } // TODO introduce an `OpenFile` type instead of using an anonymous tuple. - pub fn open_files(&self) -> impl Iterator + '_ { + pub fn open_files(&self) -> impl Iterator + '_ { self.open_files.iter().copied() } - pub fn is_file_open(&self, file_id: VfsFile) -> bool { + pub fn is_file_open(&self, file_id: File) -> bool { self.open_files.contains(&file_id) } } diff --git a/crates/red_knot/src/lint.rs b/crates/red_knot/src/lint.rs index edef30d563e27..7abe9b7b1bd53 100644 --- a/crates/red_knot/src/lint.rs +++ b/crates/red_knot/src/lint.rs @@ -7,9 +7,9 @@ use tracing::trace_span; use red_knot_module_resolver::ModuleName; use red_knot_python_semantic::types::Type; use red_knot_python_semantic::{HasTy, SemanticModel}; +use ruff_db::files::File; use ruff_db::parsed::{parsed_module, ParsedModule}; use ruff_db::source::{source_text, SourceText}; -use ruff_db::vfs::VfsFile; use ruff_python_ast as ast; use ruff_python_ast::visitor::{walk_stmt, Visitor}; @@ -22,7 +22,7 @@ use crate::db::Db; pub(crate) fn unwind_if_cancelled(db: &dyn Db) {} #[salsa::tracked(return_ref)] -pub(crate) fn lint_syntax(db: &dyn Db, file_id: VfsFile) -> Diagnostics { +pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Diagnostics { #[allow(clippy::print_stdout)] if std::env::var("RED_KNOT_SLOW_LINT").is_ok() { for i in 0..10 { @@ -74,7 +74,7 @@ fn lint_lines(source: &str, diagnostics: &mut Vec) { } #[salsa::tracked(return_ref)] -pub(crate) fn lint_semantic(db: &dyn Db, file_id: VfsFile) -> Diagnostics { +pub(crate) fn lint_semantic(db: &dyn Db, file_id: File) -> Diagnostics { let _span = trace_span!("lint_semantic", ?file_id).entered(); let source = source_text(db.upcast(), file_id); diff --git a/crates/red_knot/src/main.rs b/crates/red_knot/src/main.rs index 85d26458c3919..dcc7eafa0a946 100644 --- a/crates/red_knot/src/main.rs +++ b/crates/red_knot/src/main.rs @@ -15,8 +15,8 @@ use red_knot::Workspace; use red_knot_module_resolver::{ set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion, }; -use ruff_db::file_system::{FileSystem, FileSystemPath, OsFileSystem}; -use ruff_db::vfs::system_path_to_file; +use ruff_db::files::system_path_to_file; +use ruff_db::system::{OsSystem, System, SystemPath}; #[allow( clippy::print_stdout, @@ -35,15 +35,15 @@ pub fn main() -> anyhow::Result<()> { return Err(anyhow::anyhow!("Invalid arguments")); } - let fs = OsFileSystem; - let entry_point = FileSystemPath::new(&arguments[1]); + let system = OsSystem; + let entry_point = SystemPath::new(&arguments[1]); - if !fs.exists(entry_point) { + if !system.path_exists(entry_point) { eprintln!("The entry point does not exist."); return Err(anyhow::anyhow!("Invalid arguments")); } - if !fs.is_file(entry_point) { + if !system.is_file(entry_point) { eprintln!("The entry point is not a file."); return Err(anyhow::anyhow!("Invalid arguments")); } @@ -55,7 +55,7 @@ pub fn main() -> anyhow::Result<()> { let workspace_search_path = workspace.root().to_path_buf(); - let mut program = Program::new(workspace, fs); + let mut program = Program::new(workspace, system); set_module_resolution_settings( &mut program, diff --git a/crates/red_knot/src/program/check.rs b/crates/red_knot/src/program/check.rs index 8fe0d58f5fe4b..9793a4faf7730 100644 --- a/crates/red_knot/src/program/check.rs +++ b/crates/red_knot/src/program/check.rs @@ -1,4 +1,4 @@ -use ruff_db::vfs::VfsFile; +use ruff_db::files::File; use salsa::Cancelled; use crate::lint::{lint_semantic, lint_syntax, Diagnostics}; @@ -19,11 +19,11 @@ impl Program { } #[tracing::instrument(level = "debug", skip(self))] - pub fn check_file(&self, file: VfsFile) -> Result { + pub fn check_file(&self, file: File) -> Result { self.with_db(|db| db.check_file_impl(file)) } - fn check_file_impl(&self, file: VfsFile) -> Diagnostics { + fn check_file_impl(&self, file: File) -> Diagnostics { let mut diagnostics = Vec::new(); diagnostics.extend_from_slice(lint_syntax(self, file)); diagnostics.extend_from_slice(lint_semantic(self, file)); diff --git a/crates/red_knot/src/program/mod.rs b/crates/red_knot/src/program/mod.rs index 92ab5a5a42a31..4490f94cb8c2b 100644 --- a/crates/red_knot/src/program/mod.rs +++ b/crates/red_knot/src/program/mod.rs @@ -3,10 +3,11 @@ use std::sync::Arc; use salsa::{Cancelled, Database}; -use red_knot_module_resolver::{Db as ResolverDb, Jar as ResolverJar}; +use red_knot_module_resolver::{vendored_typeshed_stubs, Db as ResolverDb, Jar as ResolverJar}; use red_knot_python_semantic::{Db as SemanticDb, Jar as SemanticJar}; -use ruff_db::file_system::{FileSystem, FileSystemPathBuf}; -use ruff_db::vfs::{Vfs, VfsFile, VfsPath}; +use ruff_db::files::{File, FilePath, Files}; +use ruff_db::system::{System, SystemPathBuf}; +use ruff_db::vendored::VendoredFileSystem; use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast}; use crate::db::{Db, Jar}; @@ -17,20 +18,20 @@ mod check; #[salsa::db(SourceJar, ResolverJar, SemanticJar, Jar)] pub struct Program { storage: salsa::Storage, - vfs: Vfs, - fs: Arc, + files: Files, + system: Arc, workspace: Workspace, } impl Program { - pub fn new(workspace: Workspace, file_system: Fs) -> Self + pub fn new(workspace: Workspace, system: S) -> Self where - Fs: FileSystem + 'static + Send + Sync + RefUnwindSafe, + S: System + 'static + Send + Sync + RefUnwindSafe, { Self { storage: salsa::Storage::default(), - vfs: Vfs::default(), - fs: Arc::new(file_system), + files: Files::default(), + system: Arc::new(system), workspace, } } @@ -40,7 +41,7 @@ impl Program { I: IntoIterator, { for change in changes { - VfsFile::touch_path(self, &VfsPath::file_system(change.path)); + File::touch_path(self, &FilePath::system(change.path)); } } @@ -57,7 +58,7 @@ impl Program { where F: FnOnce(&Program) -> T + UnwindSafe, { - // TODO: Catch in `Caancelled::catch` + // TODO: Catch in `Cancelled::catch` // See https://salsa.zulipchat.com/#narrow/stream/145099-general/topic/How.20to.20use.20.60Cancelled.3A.3Acatch.60 Ok(f(self)) } @@ -86,12 +87,16 @@ impl ResolverDb for Program {} impl SemanticDb for Program {} impl SourceDb for Program { - fn file_system(&self) -> &dyn FileSystem { - &*self.fs + fn vendored(&self) -> &VendoredFileSystem { + vendored_typeshed_stubs() } - fn vfs(&self) -> &Vfs { - &self.vfs + fn system(&self) -> &dyn System { + &*self.system + } + + fn files(&self) -> &Files { + &self.files } } @@ -103,8 +108,8 @@ impl salsa::ParallelDatabase for Program { fn snapshot(&self) -> salsa::Snapshot { salsa::Snapshot::new(Self { storage: self.storage.snapshot(), - vfs: self.vfs.snapshot(), - fs: self.fs.clone(), + files: self.files.snapshot(), + system: self.system.clone(), workspace: self.workspace.clone(), }) } @@ -112,13 +117,13 @@ impl salsa::ParallelDatabase for Program { #[derive(Clone, Debug)] pub struct FileWatcherChange { - path: FileSystemPathBuf, + path: SystemPathBuf, #[allow(unused)] kind: FileChangeKind, } impl FileWatcherChange { - pub fn new(path: FileSystemPathBuf, kind: FileChangeKind) -> Self { + pub fn new(path: SystemPathBuf, kind: FileChangeKind) -> Self { Self { path, kind } } } diff --git a/crates/red_knot/src/watch.rs b/crates/red_knot/src/watch.rs index bfc32f7f7fa9d..79578cdce6e6e 100644 --- a/crates/red_knot/src/watch.rs +++ b/crates/red_knot/src/watch.rs @@ -1,10 +1,12 @@ use std::path::Path; -use crate::program::{FileChangeKind, FileWatcherChange}; use anyhow::Context; use notify::event::{CreateKind, RemoveKind}; use notify::{recommended_watcher, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use ruff_db::file_system::FileSystemPath; + +use ruff_db::system::SystemPath; + +use crate::program::{FileChangeKind, FileWatcherChange}; pub struct FileWatcher { watcher: RecommendedWatcher, @@ -50,7 +52,7 @@ impl FileWatcher { for path in event.paths { if path.is_file() { - if let Some(fs_path) = FileSystemPath::from_std_path(&path) { + if let Some(fs_path) = SystemPath::from_std_path(&path) { changes.push(FileWatcherChange::new( fs_path.to_path_buf(), change_kind, diff --git a/crates/red_knot_module_resolver/Cargo.toml b/crates/red_knot_module_resolver/Cargo.toml index 99e69f35cc27f..a6761665d6116 100644 --- a/crates/red_knot_module_resolver/Cargo.toml +++ b/crates/red_knot_module_resolver/Cargo.toml @@ -16,6 +16,7 @@ ruff_python_stdlib = { workspace = true } compact_str = { workspace = true } camino = { workspace = true } +once_cell = { workspace = true } rustc-hash = { workspace = true } salsa = { workspace = true } tracing = { workspace = true } diff --git a/crates/red_knot_module_resolver/src/db.rs b/crates/red_knot_module_resolver/src/db.rs index 3d64ee76f4cc0..11eae4cfd685c 100644 --- a/crates/red_knot_module_resolver/src/db.rs +++ b/crates/red_knot_module_resolver/src/db.rs @@ -24,58 +24,37 @@ pub(crate) mod tests { use salsa::DebugWithDb; - use ruff_db::file_system::{FileSystem, FileSystemPathBuf, MemoryFileSystem, OsFileSystem}; - use ruff_db::vfs::Vfs; + use ruff_db::files::Files; + use ruff_db::system::TestSystem; + use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; + use ruff_db::vendored::VendoredFileSystem; use crate::resolver::{set_module_resolution_settings, RawModuleResolutionSettings}; use crate::supported_py_version::TargetVersion; + use crate::vendored_typeshed_stubs; use super::*; #[salsa::db(Jar, ruff_db::Jar)] pub(crate) struct TestDb { storage: salsa::Storage, - file_system: TestFileSystem, + system: TestSystem, + vendored: VendoredFileSystem, + files: Files, events: sync::Arc>>, - vfs: Vfs, } impl TestDb { pub(crate) fn new() -> Self { Self { storage: salsa::Storage::default(), - file_system: TestFileSystem::Memory(MemoryFileSystem::default()), + system: TestSystem::default(), + vendored: vendored_typeshed_stubs().snapshot(), events: sync::Arc::default(), - vfs: Vfs::with_stubbed_vendored(), + files: Files::default(), } } - /// Returns the memory file system. - /// - /// ## Panics - /// If this test db isn't using a memory file system. - pub(crate) fn memory_file_system(&self) -> &MemoryFileSystem { - if let TestFileSystem::Memory(fs) = &self.file_system { - fs - } else { - panic!("The test db is not using a memory file system"); - } - } - - /// Uses the real file system instead of the memory file system. - /// - /// This useful for testing advanced file system features like permissions, symlinks, etc. - /// - /// Note that any files written to the memory file system won't be copied over. - pub(crate) fn with_os_file_system(&mut self) { - self.file_system = TestFileSystem::Os(OsFileSystem); - } - - #[allow(unused)] - pub(crate) fn vfs_mut(&mut self) -> &mut Vfs { - &mut self.vfs - } - /// Takes the salsa events. /// /// ## Panics @@ -103,17 +82,31 @@ pub(crate) mod tests { } impl ruff_db::Db for TestDb { - fn file_system(&self) -> &dyn ruff_db::file_system::FileSystem { - self.file_system.inner() + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored } - fn vfs(&self) -> &ruff_db::vfs::Vfs { - &self.vfs + fn system(&self) -> &dyn ruff_db::system::System { + &self.system + } + + fn files(&self) -> &Files { + &self.files } } impl Db for TestDb {} + impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } + } + impl salsa::Database for TestDb { fn salsa_event(&self, event: salsa::Event) { tracing::trace!("event: {:?}", event.debug(self)); @@ -126,40 +119,19 @@ pub(crate) mod tests { fn snapshot(&self) -> salsa::Snapshot { salsa::Snapshot::new(Self { storage: self.storage.snapshot(), - file_system: self.file_system.snapshot(), + system: self.system.snapshot(), + vendored: self.vendored.snapshot(), + files: self.files.snapshot(), events: self.events.clone(), - vfs: self.vfs.snapshot(), }) } } - enum TestFileSystem { - Memory(MemoryFileSystem), - #[allow(unused)] - Os(OsFileSystem), - } - - impl TestFileSystem { - fn inner(&self) -> &dyn FileSystem { - match self { - Self::Memory(inner) => inner, - Self::Os(inner) => inner, - } - } - - fn snapshot(&self) -> Self { - match self { - Self::Memory(inner) => Self::Memory(inner.snapshot()), - Self::Os(inner) => Self::Os(inner.snapshot()), - } - } - } - pub(crate) struct TestCaseBuilder { db: TestDb, - src: FileSystemPathBuf, - custom_typeshed: FileSystemPathBuf, - site_packages: FileSystemPathBuf, + src: SystemPathBuf, + custom_typeshed: SystemPathBuf, + site_packages: SystemPathBuf, target_version: Option, } @@ -200,9 +172,9 @@ pub(crate) mod tests { pub(crate) struct TestCase { pub(crate) db: TestDb, - pub(crate) src: FileSystemPathBuf, - pub(crate) custom_typeshed: FileSystemPathBuf, - pub(crate) site_packages: FileSystemPathBuf, + pub(crate) src: SystemPathBuf, + pub(crate) custom_typeshed: SystemPathBuf, + pub(crate) site_packages: SystemPathBuf, } pub(crate) fn create_resolver_builder() -> std::io::Result { @@ -217,9 +189,9 @@ pub(crate) mod tests { let db = TestDb::new(); - let src = FileSystemPathBuf::from("src"); - let site_packages = FileSystemPathBuf::from("site_packages"); - let custom_typeshed = FileSystemPathBuf::from("typeshed"); + let src = SystemPathBuf::from("src"); + let site_packages = SystemPathBuf::from("site_packages"); + let custom_typeshed = SystemPathBuf::from("typeshed"); let fs = db.memory_file_system(); diff --git a/crates/red_knot_module_resolver/src/lib.rs b/crates/red_knot_module_resolver/src/lib.rs index d6ec501ccb799..cc85b7160aadc 100644 --- a/crates/red_knot_module_resolver/src/lib.rs +++ b/crates/red_knot_module_resolver/src/lib.rs @@ -12,4 +12,6 @@ pub use module::{Module, ModuleKind}; pub use module_name::ModuleName; pub use resolver::{resolve_module, set_module_resolution_settings, RawModuleResolutionSettings}; pub use supported_py_version::TargetVersion; -pub use typeshed::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind}; +pub use typeshed::{ + vendored_typeshed_stubs, TypeshedVersionsParseError, TypeshedVersionsParseErrorKind, +}; diff --git a/crates/red_knot_module_resolver/src/module.rs b/crates/red_knot_module_resolver/src/module.rs index bc2eb4358f0ab..9592cbe65df84 100644 --- a/crates/red_knot_module_resolver/src/module.rs +++ b/crates/red_knot_module_resolver/src/module.rs @@ -1,7 +1,7 @@ use std::fmt::Formatter; use std::sync::Arc; -use ruff_db::vfs::VfsFile; +use ruff_db::files::File; use crate::db::Db; use crate::module_name::ModuleName; @@ -18,7 +18,7 @@ impl Module { name: ModuleName, kind: ModuleKind, search_path: Arc, - file: VfsFile, + file: File, ) -> Self { Self { inner: Arc::new(ModuleInner { @@ -36,7 +36,7 @@ impl Module { } /// The file to the source code that defines this module - pub fn file(&self) -> VfsFile { + pub fn file(&self) -> File { self.inner.file } @@ -78,7 +78,7 @@ struct ModuleInner { name: ModuleName, kind: ModuleKind, search_path: Arc, - file: VfsFile, + file: File, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] diff --git a/crates/red_knot_module_resolver/src/path.rs b/crates/red_knot_module_resolver/src/path.rs index 70a8ea483297c..173697577812c 100644 --- a/crates/red_knot_module_resolver/src/path.rs +++ b/crates/red_knot_module_resolver/src/path.rs @@ -1,11 +1,12 @@ -/// Internal abstractions for differentiating between different kinds of search paths. -/// -/// TODO(Alex): Should we use different types for absolute vs relative paths? -/// +//! Internal abstractions for differentiating between different kinds of search paths. +//! +//! TODO(Alex): Should we use different types for absolute vs relative paths? +//! + use std::fmt; -use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf}; -use ruff_db::vfs::{system_path_to_file, VfsFile}; +use ruff_db::files::{system_path_to_file, File}; +use ruff_db::system::{SystemPath, SystemPathBuf}; use crate::module_name::ModuleName; use crate::state::ResolverState; @@ -20,10 +21,10 @@ use crate::typeshed::TypeshedVersionsQueryResult; /// [the order given in the typing spec]: https://typing.readthedocs.io/en/latest/spec/distributing.html#import-resolution-ordering #[derive(Debug, Clone, PartialEq, Eq, Hash)] enum ModuleResolutionPathBufInner { - Extra(FileSystemPathBuf), - FirstParty(FileSystemPathBuf), - StandardLibrary(FileSystemPathBuf), - SitePackages(FileSystemPathBuf), + Extra(SystemPathBuf), + FirstParty(SystemPathBuf), + StandardLibrary(SystemPathBuf), + SitePackages(SystemPathBuf), } impl ModuleResolutionPathBufInner { @@ -90,7 +91,7 @@ impl ModuleResolutionPathBuf { } #[must_use] - pub(crate) fn extra(path: impl Into) -> Option { + pub(crate) fn extra(path: impl Into) -> Option { let path = path.into(); path.extension() .map_or(true, |ext| matches!(ext, "py" | "pyi")) @@ -98,7 +99,7 @@ impl ModuleResolutionPathBuf { } #[must_use] - pub(crate) fn first_party(path: impl Into) -> Option { + pub(crate) fn first_party(path: impl Into) -> Option { let path = path.into(); path.extension() .map_or(true, |ext| matches!(ext, "pyi" | "py")) @@ -106,7 +107,7 @@ impl ModuleResolutionPathBuf { } #[must_use] - pub(crate) fn standard_library(path: impl Into) -> Option { + pub(crate) fn standard_library(path: impl Into) -> Option { let path = path.into(); path.extension() .map_or(true, |ext| ext == "pyi") @@ -114,12 +115,12 @@ impl ModuleResolutionPathBuf { } #[must_use] - pub(crate) fn stdlib_from_typeshed_root(typeshed_root: &FileSystemPath) -> Option { - Self::standard_library(typeshed_root.join(FileSystemPath::new("stdlib"))) + pub(crate) fn stdlib_from_typeshed_root(typeshed_root: &SystemPath) -> Option { + Self::standard_library(typeshed_root.join(SystemPath::new("stdlib"))) } #[must_use] - pub(crate) fn site_packages(path: impl Into) -> Option { + pub(crate) fn site_packages(path: impl Into) -> Option { let path = path.into(); path.extension() .map_or(true, |ext| matches!(ext, "pyi" | "py")) @@ -149,18 +150,14 @@ impl ModuleResolutionPathBuf { #[must_use] pub(crate) fn relativize_path<'a>( &'a self, - absolute_path: &'a (impl AsRef + ?Sized), + absolute_path: &'a (impl AsRef + ?Sized), ) -> Option> { ModuleResolutionPathRef::from(self).relativize_path(absolute_path.as_ref()) } /// Returns `None` if the path doesn't exist, isn't accessible, or if the path points to a directory. - pub(crate) fn to_vfs_file( - &self, - search_path: &Self, - resolver: &ResolverState, - ) -> Option { - ModuleResolutionPathRef::from(self).to_vfs_file(search_path, resolver) + pub(crate) fn to_file(&self, search_path: &Self, resolver: &ResolverState) -> Option { + ModuleResolutionPathRef::from(self).to_file(search_path, resolver) } } @@ -180,18 +177,18 @@ impl fmt::Debug for ModuleResolutionPathBuf { #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] enum ModuleResolutionPathRefInner<'a> { - Extra(&'a FileSystemPath), - FirstParty(&'a FileSystemPath), - StandardLibrary(&'a FileSystemPath), - SitePackages(&'a FileSystemPath), + Extra(&'a SystemPath), + FirstParty(&'a SystemPath), + StandardLibrary(&'a SystemPath), + SitePackages(&'a SystemPath), } impl<'a> ModuleResolutionPathRefInner<'a> { #[must_use] fn query_stdlib_version<'db>( - module_path: &'a FileSystemPath, + module_path: &'a SystemPath, stdlib_search_path: Self, - stdlib_root: &FileSystemPath, + stdlib_root: &SystemPath, resolver_state: &ResolverState<'db>, ) -> TypeshedVersionsQueryResult { let Some(module_name) = stdlib_search_path @@ -211,14 +208,14 @@ impl<'a> ModuleResolutionPathRefInner<'a> { #[must_use] fn is_directory(&self, search_path: Self, resolver: &ResolverState) -> bool { match (self, search_path) { - (Self::Extra(path), Self::Extra(_)) => resolver.file_system().is_directory(path), - (Self::FirstParty(path), Self::FirstParty(_)) => resolver.file_system().is_directory(path), - (Self::SitePackages(path), Self::SitePackages(_)) => resolver.file_system().is_directory(path), + (Self::Extra(path), Self::Extra(_)) => resolver.system().is_directory(path), + (Self::FirstParty(path), Self::FirstParty(_)) => resolver.system().is_directory(path), + (Self::SitePackages(path), Self::SitePackages(_)) => resolver.system().is_directory(path), (Self::StandardLibrary(path), Self::StandardLibrary(stdlib_root)) => { match Self::query_stdlib_version( path, search_path, stdlib_root, resolver) { TypeshedVersionsQueryResult::DoesNotExist => false, - TypeshedVersionsQueryResult::Exists => resolver.file_system().is_directory(path), - TypeshedVersionsQueryResult::MaybeExists => resolver.file_system().is_directory(path), + TypeshedVersionsQueryResult::Exists => resolver.system().is_directory(path), + TypeshedVersionsQueryResult::MaybeExists => resolver.system().is_directory(path), } } (path, root) => unreachable!( @@ -229,10 +226,10 @@ impl<'a> ModuleResolutionPathRefInner<'a> { #[must_use] fn is_regular_package(&self, search_path: Self, resolver: &ResolverState) -> bool { - fn is_non_stdlib_pkg(state: &ResolverState, path: &FileSystemPath) -> bool { - let file_system = state.file_system(); - file_system.exists(&path.join("__init__.py")) - || file_system.exists(&path.join("__init__.pyi")) + fn is_non_stdlib_pkg(state: &ResolverState, path: &SystemPath) -> bool { + let file_system = state.system(); + file_system.path_exists(&path.join("__init__.py")) + || file_system.path_exists(&path.join("__init__.pyi")) } match (self, search_path) { @@ -245,8 +242,8 @@ impl<'a> ModuleResolutionPathRefInner<'a> { (Self::StandardLibrary(path), Self::StandardLibrary(stdlib_root)) => { match Self::query_stdlib_version( path, search_path, stdlib_root, resolver) { TypeshedVersionsQueryResult::DoesNotExist => false, - TypeshedVersionsQueryResult::Exists => resolver.db.file_system().exists(&path.join("__init__.pyi")), - TypeshedVersionsQueryResult::MaybeExists => resolver.db.file_system().exists(&path.join("__init__.pyi")), + TypeshedVersionsQueryResult::Exists => resolver.db.system().path_exists(&path.join("__init__.pyi")), + TypeshedVersionsQueryResult::MaybeExists => resolver.db.system().path_exists(&path.join("__init__.pyi")), } } (path, root) => unreachable!( @@ -255,7 +252,7 @@ impl<'a> ModuleResolutionPathRefInner<'a> { } } - fn to_vfs_file(self, search_path: Self, resolver: &ResolverState) -> Option { + fn to_file(self, search_path: Self, resolver: &ResolverState) -> Option { match (self, search_path) { (Self::Extra(path), Self::Extra(_)) => system_path_to_file(resolver.db.upcast(), path), (Self::FirstParty(path), Self::FirstParty(_)) => system_path_to_file(resolver.db.upcast(), path), @@ -330,7 +327,7 @@ impl<'a> ModuleResolutionPathRefInner<'a> { } #[must_use] - fn relativize_path(&self, absolute_path: &'a FileSystemPath) -> Option { + fn relativize_path(&self, absolute_path: &'a SystemPath) -> Option { match self { Self::Extra(root) => absolute_path.strip_prefix(root).ok().and_then(|path| { path.extension() @@ -379,12 +376,12 @@ impl<'a> ModuleResolutionPathRef<'a> { } #[must_use] - pub(crate) fn to_vfs_file( + pub(crate) fn to_file( self, search_path: impl Into, resolver: &ResolverState, - ) -> Option { - self.0.to_vfs_file(search_path.into().0, resolver) + ) -> Option { + self.0.to_file(search_path.into().0, resolver) } #[must_use] @@ -403,7 +400,7 @@ impl<'a> ModuleResolutionPathRef<'a> { } #[must_use] - pub(crate) fn relativize_path(&self, absolute_path: &'a FileSystemPath) -> Option { + pub(crate) fn relativize_path(&self, absolute_path: &'a SystemPath) -> Option { self.0.relativize_path(absolute_path).map(Self) } } @@ -440,8 +437,8 @@ impl<'a> From<&'a ModuleResolutionPathBuf> for ModuleResolutionPathRef<'a> { } } -impl PartialEq for ModuleResolutionPathRef<'_> { - fn eq(&self, other: &FileSystemPath) -> bool { +impl PartialEq for ModuleResolutionPathRef<'_> { + fn eq(&self, other: &SystemPath) -> bool { let fs_path = match self.0 { ModuleResolutionPathRefInner::Extra(path) => path, ModuleResolutionPathRefInner::FirstParty(path) => path, @@ -452,19 +449,19 @@ impl PartialEq for ModuleResolutionPathRef<'_> { } } -impl PartialEq> for FileSystemPath { +impl PartialEq> for SystemPath { fn eq(&self, other: &ModuleResolutionPathRef) -> bool { other == self } } -impl PartialEq for ModuleResolutionPathRef<'_> { - fn eq(&self, other: &FileSystemPathBuf) -> bool { +impl PartialEq for ModuleResolutionPathRef<'_> { + fn eq(&self, other: &SystemPathBuf) -> bool { self == &**other } } -impl PartialEq> for FileSystemPathBuf { +impl PartialEq> for SystemPathBuf { fn eq(&self, other: &ModuleResolutionPathRef<'_>) -> bool { &**self == other } @@ -491,7 +488,7 @@ mod tests { #[must_use] fn join( &self, - component: &'a (impl AsRef + ?Sized), + component: &'a (impl AsRef + ?Sized), ) -> ModuleResolutionPathBuf { let mut result = self.to_path_buf(); result.push(component.as_ref().as_str()); @@ -547,7 +544,7 @@ mod tests { #[test] fn path_ref_debug_impl() { assert_debug_snapshot!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::Extra(FileSystemPath::new("foo/bar.py"))), + ModuleResolutionPathRef(ModuleResolutionPathRefInner::Extra(SystemPath::new("foo/bar.py"))), @r###" ModuleResolutionPathRef::Extra( "foo/bar.py", @@ -570,7 +567,7 @@ mod tests { .unwrap() .with_pyi_extension(), ModuleResolutionPathBuf(ModuleResolutionPathBufInner::StandardLibrary( - FileSystemPathBuf::from("foo.pyi") + SystemPathBuf::from("foo.pyi") )) ); @@ -580,7 +577,7 @@ mod tests { .with_py_extension() .unwrap(), ModuleResolutionPathBuf(ModuleResolutionPathBufInner::FirstParty( - FileSystemPathBuf::from("foo/bar.py") + SystemPathBuf::from("foo/bar.py") )) ); } @@ -588,25 +585,23 @@ mod tests { #[test] fn module_name_1_part() { assert_eq!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::Extra(FileSystemPath::new( - "foo" - ))) - .to_module_name(), + ModuleResolutionPathRef(ModuleResolutionPathRefInner::Extra(SystemPath::new("foo"))) + .to_module_name(), ModuleName::new_static("foo") ); assert_eq!( ModuleResolutionPathRef(ModuleResolutionPathRefInner::StandardLibrary( - FileSystemPath::new("foo.pyi") + SystemPath::new("foo.pyi") )) .to_module_name(), ModuleName::new_static("foo") ); assert_eq!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::FirstParty( - FileSystemPath::new("foo/__init__.py") - )) + ModuleResolutionPathRef(ModuleResolutionPathRefInner::FirstParty(SystemPath::new( + "foo/__init__.py" + ))) .to_module_name(), ModuleName::new_static("foo") ); @@ -616,14 +611,14 @@ mod tests { fn module_name_2_parts() { assert_eq!( ModuleResolutionPathRef(ModuleResolutionPathRefInner::StandardLibrary( - FileSystemPath::new("foo/bar") + SystemPath::new("foo/bar") )) .to_module_name(), ModuleName::new_static("foo.bar") ); assert_eq!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::Extra(FileSystemPath::new( + ModuleResolutionPathRef(ModuleResolutionPathRefInner::Extra(SystemPath::new( "foo/bar.pyi" ))) .to_module_name(), @@ -631,9 +626,9 @@ mod tests { ); assert_eq!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::SitePackages( - FileSystemPath::new("foo/bar/__init__.pyi") - )) + ModuleResolutionPathRef(ModuleResolutionPathRefInner::SitePackages(SystemPath::new( + "foo/bar/__init__.pyi" + ))) .to_module_name(), ModuleName::new_static("foo.bar") ); @@ -642,17 +637,17 @@ mod tests { #[test] fn module_name_3_parts() { assert_eq!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::SitePackages( - FileSystemPath::new("foo/bar/__init__.pyi") - )) + ModuleResolutionPathRef(ModuleResolutionPathRefInner::SitePackages(SystemPath::new( + "foo/bar/__init__.pyi" + ))) .to_module_name(), ModuleName::new_static("foo.bar") ); assert_eq!( - ModuleResolutionPathRef(ModuleResolutionPathRefInner::SitePackages( - FileSystemPath::new("foo/bar/baz") - )) + ModuleResolutionPathRef(ModuleResolutionPathRefInner::SitePackages(SystemPath::new( + "foo/bar/baz" + ))) .to_module_name(), ModuleName::new_static("foo.bar.baz") ); @@ -665,7 +660,7 @@ mod tests { .unwrap() .join("bar"), ModuleResolutionPathBuf(ModuleResolutionPathBufInner::StandardLibrary( - FileSystemPathBuf::from("foo/bar") + SystemPathBuf::from("foo/bar") )) ); assert_eq!( @@ -673,16 +668,16 @@ mod tests { .unwrap() .join("bar.pyi"), ModuleResolutionPathBuf(ModuleResolutionPathBufInner::StandardLibrary( - FileSystemPathBuf::from("foo/bar.pyi") + SystemPathBuf::from("foo/bar.pyi") )) ); assert_eq!( ModuleResolutionPathBuf::extra("foo") .unwrap() .join("bar.py"), - ModuleResolutionPathBuf(ModuleResolutionPathBufInner::Extra( - FileSystemPathBuf::from("foo/bar.py") - )) + ModuleResolutionPathBuf(ModuleResolutionPathBufInner::Extra(SystemPathBuf::from( + "foo/bar.py" + ))) ); } @@ -723,13 +718,13 @@ mod tests { let root = ModuleResolutionPathBuf::standard_library("foo/stdlib").unwrap(); // Must have a `.pyi` extension or no extension: - let bad_absolute_path = FileSystemPath::new("foo/stdlib/x.py"); + let bad_absolute_path = SystemPath::new("foo/stdlib/x.py"); assert_eq!(root.relativize_path(bad_absolute_path), None); - let second_bad_absolute_path = FileSystemPath::new("foo/stdlib/x.rs"); + let second_bad_absolute_path = SystemPath::new("foo/stdlib/x.rs"); assert_eq!(root.relativize_path(second_bad_absolute_path), None); // Must be a path that is a child of `root`: - let third_bad_absolute_path = FileSystemPath::new("bar/stdlib/x.pyi"); + let third_bad_absolute_path = SystemPath::new("bar/stdlib/x.pyi"); assert_eq!(root.relativize_path(third_bad_absolute_path), None); } @@ -737,10 +732,10 @@ mod tests { fn relativize_non_stdlib_path_errors() { let root = ModuleResolutionPathBuf::extra("foo/stdlib").unwrap(); // Must have a `.py` extension, a `.pyi` extension, or no extension: - let bad_absolute_path = FileSystemPath::new("foo/stdlib/x.rs"); + let bad_absolute_path = SystemPath::new("foo/stdlib/x.rs"); assert_eq!(root.relativize_path(bad_absolute_path), None); // Must be a path that is a child of `root`: - let second_bad_absolute_path = FileSystemPath::new("bar/stdlib/x.pyi"); + let second_bad_absolute_path = SystemPath::new("bar/stdlib/x.pyi"); assert_eq!(root.relativize_path(second_bad_absolute_path), None); } @@ -752,7 +747,7 @@ mod tests { .relativize_path("foo/baz/eggs/__init__.pyi") .unwrap(), ModuleResolutionPathRef(ModuleResolutionPathRefInner::StandardLibrary( - FileSystemPath::new("eggs/__init__.pyi") + SystemPath::new("eggs/__init__.pyi") )) ); } @@ -782,21 +777,18 @@ mod tests { assert!(asyncio_regular_package.is_regular_package(&stdlib_path, &resolver)); // Paths to directories don't resolve to VfsFiles assert_eq!( - asyncio_regular_package.to_vfs_file(&stdlib_path, &resolver), + asyncio_regular_package.to_file(&stdlib_path, &resolver), None ); assert!(asyncio_regular_package .join("__init__.pyi") - .to_vfs_file(&stdlib_path, &resolver) + .to_file(&stdlib_path, &resolver) .is_some()); // The `asyncio` package exists on Python 3.8, but the `asyncio.tasks` submodule does not, // according to the `VERSIONS` file in our typeshed mock: let asyncio_tasks_module = stdlib_path.join("asyncio/tasks.pyi"); - assert_eq!( - asyncio_tasks_module.to_vfs_file(&stdlib_path, &resolver), - None - ); + assert_eq!(asyncio_tasks_module.to_file(&stdlib_path, &resolver), None); assert!(!asyncio_tasks_module.is_directory(&stdlib_path, &resolver)); assert!(!asyncio_tasks_module.is_regular_package(&stdlib_path, &resolver)); } @@ -813,15 +805,12 @@ mod tests { let xml_namespace_package = stdlib_path.join("xml"); assert!(xml_namespace_package.is_directory(&stdlib_path, &resolver)); // Paths to directories don't resolve to VfsFiles - assert_eq!( - xml_namespace_package.to_vfs_file(&stdlib_path, &resolver), - None - ); + assert_eq!(xml_namespace_package.to_file(&stdlib_path, &resolver), None); assert!(!xml_namespace_package.is_regular_package(&stdlib_path, &resolver)); let xml_etree = stdlib_path.join("xml/etree.pyi"); assert!(!xml_etree.is_directory(&stdlib_path, &resolver)); - assert!(xml_etree.to_vfs_file(&stdlib_path, &resolver).is_some()); + assert!(xml_etree.to_file(&stdlib_path, &resolver).is_some()); assert!(!xml_etree.is_regular_package(&stdlib_path, &resolver)); } @@ -835,9 +824,7 @@ mod tests { }; let functools_module = stdlib_path.join("functools.pyi"); - assert!(functools_module - .to_vfs_file(&stdlib_path, &resolver) - .is_some()); + assert!(functools_module.to_file(&stdlib_path, &resolver).is_some()); assert!(!functools_module.is_directory(&stdlib_path, &resolver)); assert!(!functools_module.is_regular_package(&stdlib_path, &resolver)); } @@ -853,7 +840,7 @@ mod tests { let collections_regular_package = stdlib_path.join("collections"); assert_eq!( - collections_regular_package.to_vfs_file(&stdlib_path, &resolver), + collections_regular_package.to_file(&stdlib_path, &resolver), None ); assert!(!collections_regular_package.is_directory(&stdlib_path, &resolver)); @@ -871,14 +858,14 @@ mod tests { let importlib_namespace_package = stdlib_path.join("importlib"); assert_eq!( - importlib_namespace_package.to_vfs_file(&stdlib_path, &resolver), + importlib_namespace_package.to_file(&stdlib_path, &resolver), None ); assert!(!importlib_namespace_package.is_directory(&stdlib_path, &resolver)); assert!(!importlib_namespace_package.is_regular_package(&stdlib_path, &resolver)); let importlib_abc = stdlib_path.join("importlib/abc.pyi"); - assert_eq!(importlib_abc.to_vfs_file(&stdlib_path, &resolver), None); + assert_eq!(importlib_abc.to_file(&stdlib_path, &resolver), None); assert!(!importlib_abc.is_directory(&stdlib_path, &resolver)); assert!(!importlib_abc.is_regular_package(&stdlib_path, &resolver)); } @@ -893,7 +880,7 @@ mod tests { }; let non_existent = stdlib_path.join("doesnt_even_exist"); - assert_eq!(non_existent.to_vfs_file(&stdlib_path, &resolver), None); + assert_eq!(non_existent.to_file(&stdlib_path, &resolver), None); assert!(!non_existent.is_directory(&stdlib_path, &resolver)); assert!(!non_existent.is_regular_package(&stdlib_path, &resolver)); } @@ -928,18 +915,18 @@ mod tests { assert!(collections_regular_package.is_regular_package(&stdlib_path, &resolver)); // (This is still `None`, as directories don't resolve to `Vfs` files) assert_eq!( - collections_regular_package.to_vfs_file(&stdlib_path, &resolver), + collections_regular_package.to_file(&stdlib_path, &resolver), None ); assert!(collections_regular_package .join("__init__.pyi") - .to_vfs_file(&stdlib_path, &resolver) + .to_file(&stdlib_path, &resolver) .is_some()); // ...and so should the `asyncio.tasks` submodule (though it's still not a directory): let asyncio_tasks_module = stdlib_path.join("asyncio/tasks.pyi"); assert!(asyncio_tasks_module - .to_vfs_file(&stdlib_path, &resolver) + .to_file(&stdlib_path, &resolver) .is_some()); assert!(!asyncio_tasks_module.is_directory(&stdlib_path, &resolver)); assert!(!asyncio_tasks_module.is_regular_package(&stdlib_path, &resolver)); @@ -960,7 +947,7 @@ mod tests { assert!(!importlib_namespace_package.is_regular_package(&stdlib_path, &resolver)); // (This is still `None`, as directories don't resolve to `Vfs` files) assert_eq!( - importlib_namespace_package.to_vfs_file(&stdlib_path, &resolver), + importlib_namespace_package.to_file(&stdlib_path, &resolver), None ); @@ -968,7 +955,7 @@ mod tests { let importlib_abc = importlib_namespace_package.join("abc.pyi"); assert!(!importlib_abc.is_directory(&stdlib_path, &resolver)); assert!(!importlib_abc.is_regular_package(&stdlib_path, &resolver)); - assert!(importlib_abc.to_vfs_file(&stdlib_path, &resolver).is_some()); + assert!(importlib_abc.to_file(&stdlib_path, &resolver).is_some()); } #[test] @@ -982,15 +969,12 @@ mod tests { // The `xml` package no longer exists on py39: let xml_namespace_package = stdlib_path.join("xml"); - assert_eq!( - xml_namespace_package.to_vfs_file(&stdlib_path, &resolver), - None - ); + assert_eq!(xml_namespace_package.to_file(&stdlib_path, &resolver), None); assert!(!xml_namespace_package.is_directory(&stdlib_path, &resolver)); assert!(!xml_namespace_package.is_regular_package(&stdlib_path, &resolver)); let xml_etree = xml_namespace_package.join("etree.pyi"); - assert_eq!(xml_etree.to_vfs_file(&stdlib_path, &resolver), None); + assert_eq!(xml_etree.to_file(&stdlib_path, &resolver), None); assert!(!xml_etree.is_directory(&stdlib_path, &resolver)); assert!(!xml_etree.is_regular_package(&stdlib_path, &resolver)); } diff --git a/crates/red_knot_module_resolver/src/resolver.rs b/crates/red_knot_module_resolver/src/resolver.rs index 08438472cfadb..56f1137925ca4 100644 --- a/crates/red_knot_module_resolver/src/resolver.rs +++ b/crates/red_knot_module_resolver/src/resolver.rs @@ -1,8 +1,8 @@ use std::ops::Deref; use std::sync::Arc; -use ruff_db::file_system::FileSystemPathBuf; -use ruff_db::vfs::{vfs_path_to_file, VfsFile, VfsPath}; +use ruff_db::files::{File, FilePath}; +use ruff_db::system::SystemPathBuf; use crate::db::Db; use crate::module::{Module, ModuleKind}; @@ -58,7 +58,7 @@ pub(crate) fn resolve_module_query<'db>( /// /// Returns `None` if the path is not a module locatable via any of the known search paths. #[allow(unused)] -pub(crate) fn path_to_module(db: &dyn Db, path: &VfsPath) -> Option { +pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option { // It's not entirely clear on first sight why this method calls `file_to_module` instead of // it being the other way round, considering that the first thing that `file_to_module` does // is to retrieve the file's path. @@ -67,7 +67,7 @@ pub(crate) fn path_to_module(db: &dyn Db, path: &VfsPath) -> Option { // all arguments are Salsa ingredients (something stored in Salsa). `Path`s aren't salsa ingredients but // `VfsFile` is. So what we do here is to retrieve the `path`'s `VfsFile` so that we can make // use of Salsa's caching and invalidation. - let file = vfs_path_to_file(db.upcast(), path)?; + let file = path.to_file(db.upcast())?; file_to_module(db, file) } @@ -75,10 +75,10 @@ pub(crate) fn path_to_module(db: &dyn Db, path: &VfsPath) -> Option { /// /// Returns `None` if the file is not a module locatable via any of the known search paths. #[salsa::tracked] -pub(crate) fn file_to_module(db: &dyn Db, file: VfsFile) -> Option { +pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option { let _span = tracing::trace_span!("file_to_module", ?file).entered(); - let VfsPath::FileSystem(path) = file.path(db.upcast()) else { + let FilePath::System(path) = file.path(db.upcast()) else { todo!("VendoredPaths are not yet supported") }; @@ -120,18 +120,18 @@ pub struct RawModuleResolutionSettings { /// List of user-provided paths that should take first priority in the module resolution. /// Examples in other type checkers are mypy's MYPYPATH environment variable, /// or pyright's stubPath configuration setting. - pub extra_paths: Vec, + pub extra_paths: Vec, /// The root of the workspace, used for finding first-party modules. - pub workspace_root: FileSystemPathBuf, + pub workspace_root: SystemPathBuf, /// Optional (already validated) path to standard-library typeshed stubs. /// If this is not provided, we will fallback to our vendored typeshed stubs /// bundled as a zip file in the binary - pub custom_typeshed: Option, + pub custom_typeshed: Option, /// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed. - pub site_packages: Option, + pub site_packages: Option, } impl RawModuleResolutionSettings { @@ -243,7 +243,7 @@ fn module_resolver_settings(db: &dyn Db) -> &ModuleResolutionSettings { fn resolve_name( db: &dyn Db, name: &ModuleName, -) -> Option<(Arc, VfsFile, ModuleKind)> { +) -> Option<(Arc, File, ModuleKind)> { let resolver_settings = module_resolver_settings(db); let resolver_state = ResolverState::new(db, resolver_settings.target_version()); @@ -268,14 +268,14 @@ fn resolve_name( // TODO Implement full https://peps.python.org/pep-0561/#type-checker-module-resolution-order resolution if let Some(stub) = package_path .with_pyi_extension() - .to_vfs_file(search_path, &resolver_state) + .to_file(search_path, &resolver_state) { return Some((search_path.clone(), stub, kind)); } if let Some(module) = package_path .with_py_extension() - .and_then(|path| path.to_vfs_file(search_path, &resolver_state)) + .and_then(|path| path.to_file(search_path, &resolver_state)) { return Some((search_path.clone(), module, kind)); } @@ -386,8 +386,8 @@ impl PackageKind { #[cfg(test)] mod tests { - use ruff_db::file_system::FileSystemPath; - use ruff_db::vfs::{system_path_to_file, VfsFile, VfsPath}; + use ruff_db::files::{system_path_to_file, File, FilePath}; + use ruff_db::system::{DbWithTestSystem, SystemPath}; use crate::db::tests::{create_resolver_builder, TestCase}; use crate::module::ModuleKind; @@ -401,12 +401,11 @@ mod tests { #[test] fn first_party_module() -> anyhow::Result<()> { - let TestCase { db, src, .. } = setup_resolver_test(); + let TestCase { mut db, src, .. } = setup_resolver_test(); let foo_module_name = ModuleName::new_static("foo").unwrap(); let foo_path = src.join("foo.py"); - db.memory_file_system() - .write_file(&foo_path, "print('Hello, world!')")?; + db.write_file(&foo_path, "print('Hello, world!')")?; let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap(); @@ -422,7 +421,7 @@ mod tests { assert_eq!(&foo_path, foo_module.file().path(&db)); assert_eq!( Some(foo_module), - path_to_module(&db, &VfsPath::FileSystem(foo_path)) + path_to_module(&db, &FilePath::System(foo_path)) ); Ok(()) @@ -450,7 +449,7 @@ mod tests { assert_eq!(ModuleKind::Module, functools_module.kind()); let expected_functools_path = - VfsPath::FileSystem(custom_typeshed.join("stdlib/functools.pyi")); + FilePath::System(custom_typeshed.join("stdlib/functools.pyi")); assert_eq!(&expected_functools_path, functools_module.file().path(&db)); assert_eq!( @@ -562,11 +561,10 @@ mod tests { #[test] fn first_party_precedence_over_stdlib() -> anyhow::Result<()> { - let TestCase { db, src, .. } = setup_resolver_test(); + let TestCase { mut db, src, .. } = setup_resolver_test(); let first_party_functools_path = src.join("functools.py"); - db.memory_file_system() - .write_file(&first_party_functools_path, "def update_wrapper(): ...")?; + db.write_file(&first_party_functools_path, "def update_wrapper(): ...")?; let functools_module_name = ModuleName::new_static("functools").unwrap(); let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap(); @@ -584,7 +582,7 @@ mod tests { assert_eq!( Some(functools_module), - path_to_module(&db, &VfsPath::FileSystem(first_party_functools_path)) + path_to_module(&db, &FilePath::System(first_party_functools_path)) ); Ok(()) @@ -592,13 +590,12 @@ mod tests { #[test] fn resolve_package() -> anyhow::Result<()> { - let TestCase { src, db, .. } = setup_resolver_test(); + let TestCase { src, mut db, .. } = setup_resolver_test(); let foo_dir = src.join("foo"); let foo_path = foo_dir.join("__init__.py"); - db.memory_file_system() - .write_file(&foo_path, "print('Hello, world!')")?; + db.write_file(&foo_path, "print('Hello, world!')")?; let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap(); @@ -608,28 +605,26 @@ mod tests { assert_eq!( Some(&foo_module), - path_to_module(&db, &VfsPath::FileSystem(foo_path)).as_ref() + path_to_module(&db, &FilePath::System(foo_path)).as_ref() ); // Resolving by directory doesn't resolve to the init file. - assert_eq!(None, path_to_module(&db, &VfsPath::FileSystem(foo_dir))); + assert_eq!(None, path_to_module(&db, &FilePath::System(foo_dir))); Ok(()) } #[test] fn package_priority_over_module() -> anyhow::Result<()> { - let TestCase { db, src, .. } = setup_resolver_test(); + let TestCase { mut db, src, .. } = setup_resolver_test(); let foo_dir = src.join("foo"); let foo_init = foo_dir.join("__init__.py"); - db.memory_file_system() - .write_file(&foo_init, "print('Hello, world!')")?; + db.write_file(&foo_init, "print('Hello, world!')")?; let foo_py = src.join("foo.py"); - db.memory_file_system() - .write_file(&foo_py, "print('Hello, world!')")?; + db.write_file(&foo_py, "print('Hello, world!')")?; let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap(); @@ -639,45 +634,41 @@ mod tests { assert_eq!( Some(foo_module), - path_to_module(&db, &VfsPath::FileSystem(foo_init)) + path_to_module(&db, &FilePath::System(foo_init)) ); - assert_eq!(None, path_to_module(&db, &VfsPath::FileSystem(foo_py))); + assert_eq!(None, path_to_module(&db, &FilePath::System(foo_py))); Ok(()) } #[test] fn typing_stub_over_module() -> anyhow::Result<()> { - let TestCase { db, src, .. } = setup_resolver_test(); + let TestCase { mut db, src, .. } = setup_resolver_test(); let foo_stub = src.join("foo.pyi"); let foo_py = src.join("foo.py"); - db.memory_file_system() - .write_files([(&foo_stub, "x: int"), (&foo_py, "print('Hello, world!')")])?; + db.write_files([(&foo_stub, "x: int"), (&foo_py, "print('Hello, world!')")])?; let foo = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap(); assert_eq!(&src, &foo.search_path()); assert_eq!(&foo_stub, foo.file().path(&db)); - assert_eq!( - Some(foo), - path_to_module(&db, &VfsPath::FileSystem(foo_stub)) - ); - assert_eq!(None, path_to_module(&db, &VfsPath::FileSystem(foo_py))); + assert_eq!(Some(foo), path_to_module(&db, &FilePath::System(foo_stub))); + assert_eq!(None, path_to_module(&db, &FilePath::System(foo_py))); Ok(()) } #[test] fn sub_packages() -> anyhow::Result<()> { - let TestCase { db, src, .. } = setup_resolver_test(); + let TestCase { mut db, src, .. } = setup_resolver_test(); let foo = src.join("foo"); let bar = foo.join("bar"); let baz = bar.join("baz.py"); - db.memory_file_system().write_files([ + db.write_files([ (&foo.join("__init__.py"), ""), (&bar.join("__init__.py"), ""), (&baz, "print('Hello, world!')"), @@ -691,7 +682,7 @@ mod tests { assert_eq!( Some(baz_module), - path_to_module(&db, &VfsPath::FileSystem(baz)) + path_to_module(&db, &FilePath::System(baz)) ); Ok(()) @@ -700,7 +691,7 @@ mod tests { #[test] fn namespace_package() -> anyhow::Result<()> { let TestCase { - db, + mut db, src, site_packages, .. @@ -727,7 +718,7 @@ mod tests { let child2 = parent2.join("child"); let two = child2.join("two.py"); - db.memory_file_system().write_files([ + db.write_files([ (&one, "print('Hello, world!')"), (&two, "print('Hello, world!')"), ])?; @@ -737,14 +728,14 @@ mod tests { assert_eq!( Some(one_module), - path_to_module(&db, &VfsPath::FileSystem(one)) + path_to_module(&db, &FilePath::System(one)) ); let two_module = resolve_module(&db, ModuleName::new_static("parent.child.two").unwrap()).unwrap(); assert_eq!( Some(two_module), - path_to_module(&db, &VfsPath::FileSystem(two)) + path_to_module(&db, &FilePath::System(two)) ); Ok(()) @@ -753,7 +744,7 @@ mod tests { #[test] fn regular_package_in_namespace_package() -> anyhow::Result<()> { let TestCase { - db, + mut db, src, site_packages, .. @@ -780,7 +771,7 @@ mod tests { let child2 = parent2.join("child"); let two = child2.join("two.py"); - db.memory_file_system().write_files([ + db.write_files([ (&child1.join("__init__.py"), "print('Hello, world!')"), (&one, "print('Hello, world!')"), (&two, "print('Hello, world!')"), @@ -791,7 +782,7 @@ mod tests { assert_eq!( Some(one_module), - path_to_module(&db, &VfsPath::FileSystem(one)) + path_to_module(&db, &FilePath::System(one)) ); assert_eq!( @@ -804,7 +795,7 @@ mod tests { #[test] fn module_search_path_priority() -> anyhow::Result<()> { let TestCase { - db, + mut db, src, site_packages, .. @@ -813,8 +804,7 @@ mod tests { let foo_src = src.join("foo.py"); let foo_site_packages = site_packages.join("foo.py"); - db.memory_file_system() - .write_files([(&foo_src, ""), (&foo_site_packages, "")])?; + db.write_files([(&foo_src, ""), (&foo_site_packages, "")])?; let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap(); @@ -823,11 +813,11 @@ mod tests { assert_eq!( Some(foo_module), - path_to_module(&db, &VfsPath::FileSystem(foo_src)) + path_to_module(&db, &FilePath::System(foo_src)) ); assert_eq!( None, - path_to_module(&db, &VfsPath::FileSystem(foo_site_packages)) + path_to_module(&db, &FilePath::System(foo_site_packages)) ); Ok(()) @@ -843,10 +833,10 @@ mod tests { custom_typeshed, } = setup_resolver_test(); - db.with_os_file_system(); + db.use_os_system(); let temp_dir = tempfile::tempdir()?; - let root = FileSystemPath::from_std_path(temp_dir.path()).unwrap(); + let root = SystemPath::from_std_path(temp_dir.path()).unwrap(); let src = root.join(src); let site_packages = root.join(site_packages); @@ -890,11 +880,11 @@ mod tests { assert_eq!( Some(foo_module), - path_to_module(&db, &VfsPath::FileSystem(foo)) + path_to_module(&db, &FilePath::System(foo)) ); assert_eq!( Some(bar_module), - path_to_module(&db, &VfsPath::FileSystem(bar)) + path_to_module(&db, &FilePath::System(bar)) ); Ok(()) @@ -907,8 +897,7 @@ mod tests { let foo_path = src.join("foo.py"); let bar_path = src.join("bar.py"); - db.memory_file_system() - .write_files([(&foo_path, "x = 1"), (&bar_path, "y = 2")])?; + db.write_files([(&foo_path, "x = 1"), (&bar_path, "y = 2")])?; let foo_module_name = ModuleName::new_static("foo").unwrap(); let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap(); @@ -946,8 +935,8 @@ mod tests { assert_eq!(resolve_module(&db, foo_module_name.clone()), None); // Now write the foo file - db.memory_file_system().write_file(&foo_path, "x = 1")?; - VfsFile::touch_path(&mut db, &VfsPath::FileSystem(foo_path.clone())); + db.write_file(&foo_path, "x = 1")?; + let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist"); let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve"); @@ -963,8 +952,7 @@ mod tests { let foo_path = src.join("foo.py"); let foo_init_path = src.join("foo/__init__.py"); - db.memory_file_system() - .write_files([(&foo_path, "x = 1"), (&foo_init_path, "x = 2")])?; + db.write_files([(&foo_path, "x = 1"), (&foo_init_path, "x = 2")])?; let foo_module_name = ModuleName::new_static("foo").unwrap(); let foo_module = resolve_module(&db, foo_module_name.clone()).expect("foo module to exist"); @@ -975,7 +963,7 @@ mod tests { db.memory_file_system().remove_file(&foo_init_path)?; db.memory_file_system() .remove_directory(foo_init_path.parent().unwrap())?; - VfsFile::touch_path(&mut db, &VfsPath::FileSystem(foo_init_path)); + File::touch_path(&mut db, &FilePath::System(foo_init_path)); let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve"); assert_eq!(&foo_path, foo_module.file().path(&db)); diff --git a/crates/red_knot_module_resolver/src/state.rs b/crates/red_knot_module_resolver/src/state.rs index ad9a7329a89ba..0a0763840dcf4 100644 --- a/crates/red_knot_module_resolver/src/state.rs +++ b/crates/red_knot_module_resolver/src/state.rs @@ -1,4 +1,4 @@ -use ruff_db::file_system::FileSystem; +use ruff_db::system::System; use crate::db::Db; use crate::supported_py_version::TargetVersion; @@ -19,7 +19,7 @@ impl<'db> ResolverState<'db> { } } - pub(crate) fn file_system(&self) -> &dyn FileSystem { - self.db.file_system() + pub(crate) fn system(&self) -> &dyn System { + self.db.system() } } diff --git a/crates/red_knot_module_resolver/src/typeshed.rs b/crates/red_knot_module_resolver/src/typeshed.rs index c8a36b46260c8..f73e870268b0f 100644 --- a/crates/red_knot_module_resolver/src/typeshed.rs +++ b/crates/red_knot_module_resolver/src/typeshed.rs @@ -1,22 +1,33 @@ -mod versions; +use once_cell::sync::Lazy; + +use ruff_db::vendored::VendoredFileSystem; -pub(crate) use versions::{ +pub(crate) use self::versions::{ parse_typeshed_versions, LazyTypeshedVersions, TypeshedVersionsQueryResult, }; -pub use versions::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind}; +pub use self::versions::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind}; + +mod versions; + +// The file path here is hardcoded in this crate's `build.rs` script. +// Luckily this crate will fail to build if this file isn't available at build time. +static TYPESHED_ZIP_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip")); + +pub fn vendored_typeshed_stubs() -> &'static VendoredFileSystem { + static VENDORED_TYPESHED_STUBS: Lazy = + Lazy::new(|| VendoredFileSystem::new_static(TYPESHED_ZIP_BYTES).unwrap()); + &VENDORED_TYPESHED_STUBS +} #[cfg(test)] mod tests { use std::io::{self, Read}; use std::path::Path; - use ruff_db::vendored::VendoredFileSystem; - use ruff_db::vfs::VendoredPath; + use ruff_db::vendored::VendoredPath; - // The file path here is hardcoded in this crate's `build.rs` script. - // Luckily this crate will fail to build if this file isn't available at build time. - const TYPESHED_ZIP_BYTES: &[u8] = - include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip")); + use crate::typeshed::TYPESHED_ZIP_BYTES; + use crate::vendored_typeshed_stubs; #[test] fn typeshed_zip_created_at_build_time() { @@ -39,7 +50,7 @@ mod tests { #[test] fn typeshed_vfs_consistent_with_vendored_stubs() { let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap(); - let vendored_typeshed_stubs = VendoredFileSystem::new(TYPESHED_ZIP_BYTES).unwrap(); + let vendored_typeshed_stubs = vendored_typeshed_stubs(); let mut empty_iterator = true; for entry in walkdir::WalkDir::new(&vendored_typeshed_dir).min_depth(1) { @@ -69,7 +80,7 @@ mod tests { let vendored_path_kind = vendored_typeshed_stubs .metadata(vendored_path) - .unwrap_or_else(|| { + .unwrap_or_else(|_| { panic!( "Expected metadata for {vendored_path:?} to be retrievable from the `VendoredFileSystem! diff --git a/crates/red_knot_module_resolver/src/typeshed/versions.rs b/crates/red_knot_module_resolver/src/typeshed/versions.rs index 61ef0249cfecb..4600ddf0bd069 100644 --- a/crates/red_knot_module_resolver/src/typeshed/versions.rs +++ b/crates/red_knot_module_resolver/src/typeshed/versions.rs @@ -5,11 +5,12 @@ use std::num::{NonZeroU16, NonZeroUsize}; use std::ops::{RangeFrom, RangeInclusive}; use std::str::FromStr; -use ruff_db::file_system::FileSystemPath; -use ruff_db::source::source_text; -use ruff_db::vfs::{system_path_to_file, VfsFile}; use rustc_hash::FxHashMap; +use ruff_db::files::{system_path_to_file, File}; +use ruff_db::source::source_text; +use ruff_db::system::SystemPath; + use crate::db::Db; use crate::module_name::ModuleName; use crate::supported_py_version::TargetVersion; @@ -40,7 +41,7 @@ impl<'db> LazyTypeshedVersions<'db> { &self, module: &ModuleName, db: &'db dyn Db, - stdlib_root: &FileSystemPath, + stdlib_root: &SystemPath, target_version: TargetVersion, ) -> TypeshedVersionsQueryResult { let versions = self.0.get_or_init(|| { @@ -64,7 +65,7 @@ impl<'db> LazyTypeshedVersions<'db> { #[salsa::tracked(return_ref)] pub(crate) fn parse_typeshed_versions( db: &dyn Db, - versions_file: VfsFile, + versions_file: File, ) -> Result { let file_content = source_text(db.upcast(), versions_file); file_content.parse() @@ -429,10 +430,10 @@ mod tests { use std::num::{IntErrorKind, NonZeroU16}; use std::path::Path; - use super::*; - use insta::assert_snapshot; + use super::*; + const TYPESHED_STDLIB_DIR: &str = "stdlib"; #[allow(unsafe_code)] diff --git a/crates/red_knot_python_semantic/src/db.rs b/crates/red_knot_python_semantic/src/db.rs index 2ac63f2b4553d..9a543f74c5a72 100644 --- a/crates/red_knot_python_semantic/src/db.rs +++ b/crates/red_knot_python_semantic/src/db.rs @@ -1,8 +1,7 @@ use salsa::DbWithJar; -use ruff_db::{Db as SourceDb, Upcast}; - use red_knot_module_resolver::Db as ResolverDb; +use ruff_db::{Db as SourceDb, Upcast}; use crate::semantic_index::definition::Definition; use crate::semantic_index::symbol::{public_symbols_map, PublicSymbolId, ScopeId}; @@ -45,9 +44,10 @@ pub(crate) mod tests { use salsa::storage::HasIngredientsFor; use salsa::DebugWithDb; - use red_knot_module_resolver::{Db as ResolverDb, Jar as ResolverJar}; - use ruff_db::file_system::{FileSystem, MemoryFileSystem, OsFileSystem}; - use ruff_db::vfs::Vfs; + use red_knot_module_resolver::{vendored_typeshed_stubs, Db as ResolverDb, Jar as ResolverJar}; + use ruff_db::files::Files; + use ruff_db::system::{DbWithTestSystem, System, TestSystem}; + use ruff_db::vendored::VendoredFileSystem; use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast}; use super::{Db, Jar}; @@ -55,8 +55,9 @@ pub(crate) mod tests { #[salsa::db(Jar, ResolverJar, SourceJar)] pub(crate) struct TestDb { storage: salsa::Storage, - vfs: Vfs, - file_system: TestFileSystem, + files: Files, + system: TestSystem, + vendored: VendoredFileSystem, events: std::sync::Arc>>, } @@ -64,29 +65,13 @@ pub(crate) mod tests { pub(crate) fn new() -> Self { Self { storage: salsa::Storage::default(), - file_system: TestFileSystem::Memory(MemoryFileSystem::default()), + system: TestSystem::default(), + vendored: vendored_typeshed_stubs().snapshot(), events: std::sync::Arc::default(), - vfs: Vfs::with_stubbed_vendored(), + files: Files::default(), } } - /// Returns the memory file system. - /// - /// ## Panics - /// If this test db isn't using a memory file system. - pub(crate) fn memory_file_system(&self) -> &MemoryFileSystem { - if let TestFileSystem::Memory(fs) = &self.file_system { - fs - } else { - panic!("The test db is not using a memory file system"); - } - } - - #[allow(unused)] - pub(crate) fn vfs_mut(&mut self) -> &mut Vfs { - &mut self.vfs - } - /// Takes the salsa events. /// /// ## Panics @@ -107,16 +92,27 @@ pub(crate) mod tests { } } + impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system + } + + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system + } + } + impl SourceDb for TestDb { - fn file_system(&self) -> &dyn FileSystem { - match &self.file_system { - TestFileSystem::Memory(fs) => fs, - TestFileSystem::Os(fs) => fs, - } + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system } - fn vfs(&self) -> &Vfs { - &self.vfs + fn files(&self) -> &Files { + &self.files } } @@ -147,22 +143,14 @@ pub(crate) mod tests { fn snapshot(&self) -> salsa::Snapshot { salsa::Snapshot::new(Self { storage: self.storage.snapshot(), - vfs: self.vfs.snapshot(), - file_system: match &self.file_system { - TestFileSystem::Memory(memory) => TestFileSystem::Memory(memory.snapshot()), - TestFileSystem::Os(fs) => TestFileSystem::Os(fs.snapshot()), - }, + files: self.files.snapshot(), + system: self.system.snapshot(), + vendored: self.vendored.snapshot(), events: self.events.clone(), }) } } - enum TestFileSystem { - Memory(MemoryFileSystem), - #[allow(dead_code)] - Os(OsFileSystem), - } - pub(crate) fn assert_will_run_function_query<'db, C, Db, Jar>( db: &'db Db, to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient, diff --git a/crates/red_knot_python_semantic/src/semantic_index.rs b/crates/red_knot_python_semantic/src/semantic_index.rs index cb55587646307..354b5d382527d 100644 --- a/crates/red_knot_python_semantic/src/semantic_index.rs +++ b/crates/red_knot_python_semantic/src/semantic_index.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use rustc_hash::FxHashMap; +use ruff_db::files::File; use ruff_db::parsed::parsed_module; -use ruff_db::vfs::VfsFile; use ruff_index::{IndexSlice, IndexVec}; use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey; @@ -28,7 +28,7 @@ type SymbolMap = hashbrown::HashMap; /// /// Prefer using [`symbol_table`] when working with symbols from a single scope. #[salsa::tracked(return_ref, no_eq)] -pub(crate) fn semantic_index(db: &dyn Db, file: VfsFile) -> SemanticIndex<'_> { +pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> { let _span = tracing::trace_span!("semantic_index", ?file).entered(); let parsed = parsed_module(db.upcast(), file); @@ -51,7 +51,7 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc ScopeId<'_> { +pub(crate) fn root_scope(db: &dyn Db, file: File) -> ScopeId<'_> { let _span = tracing::trace_span!("root_scope", ?file).entered(); FileScopeId::root().to_scope_id(db, file) @@ -61,7 +61,7 @@ pub(crate) fn root_scope(db: &dyn Db, file: VfsFile) -> ScopeId<'_> { /// no symbol with the given name exists. pub(crate) fn public_symbol<'db>( db: &'db dyn Db, - file: VfsFile, + file: File, name: &str, ) -> Option> { let root_scope = root_scope(db, file); @@ -272,8 +272,9 @@ impl FusedIterator for ChildrenIter<'_> {} #[cfg(test)] mod tests { + use ruff_db::files::{system_path_to_file, File}; use ruff_db::parsed::parsed_module; - use ruff_db::vfs::{system_path_to_file, VfsFile}; + use ruff_db::system::DbWithTestSystem; use crate::db::tests::TestDb; use crate::semantic_index::symbol::{FileScopeId, Scope, ScopeKind, SymbolTable}; @@ -282,14 +283,12 @@ mod tests { struct TestCase { db: TestDb, - file: VfsFile, + file: File, } fn test_case(content: impl ToString) -> TestCase { - let db = TestDb::new(); - db.memory_file_system() - .write_file("test.py", content) - .unwrap(); + let mut db = TestDb::new(); + db.write_file("test.py", content).unwrap(); let file = system_path_to_file(&db, "test.py").unwrap(); @@ -631,7 +630,7 @@ class C[T]: fn scope_names<'a>( scopes: impl Iterator, db: &'a dyn Db, - file: VfsFile, + file: File, ) -> Vec<&'a str> { scopes .into_iter() diff --git a/crates/red_knot_python_semantic/src/semantic_index/builder.rs b/crates/red_knot_python_semantic/src/semantic_index/builder.rs index e4a2d60184ab1..e492098a7ee2d 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/builder.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/builder.rs @@ -2,8 +2,8 @@ use std::sync::Arc; use rustc_hash::FxHashMap; +use ruff_db::files::File; use ruff_db::parsed::ParsedModule; -use ruff_db::vfs::VfsFile; use ruff_index::IndexVec; use ruff_python_ast as ast; use ruff_python_ast::name::Name; @@ -22,7 +22,7 @@ use crate::Db; pub(super) struct SemanticIndexBuilder<'db, 'ast> { // Builder state db: &'db dyn Db, - file: VfsFile, + file: File, module: &'db ParsedModule, scope_stack: Vec, /// the target we're currently inferring @@ -42,7 +42,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast> where 'db: 'ast, { - pub(super) fn new(db: &'db dyn Db, file: VfsFile, parsed: &'db ParsedModule) -> Self { + pub(super) fn new(db: &'db dyn Db, file: File, parsed: &'db ParsedModule) -> Self { let mut builder = Self { db, file, diff --git a/crates/red_knot_python_semantic/src/semantic_index/definition.rs b/crates/red_knot_python_semantic/src/semantic_index/definition.rs index 90081435be0eb..a9cf7cf1f0770 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/definition.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/definition.rs @@ -1,5 +1,5 @@ +use ruff_db::files::File; use ruff_db::parsed::ParsedModule; -use ruff_db::vfs::VfsFile; use ruff_python_ast as ast; use crate::ast_node_ref::AstNodeRef; @@ -10,7 +10,7 @@ use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId}; pub struct Definition<'db> { /// The file in which the definition is defined. #[id] - pub(super) file: VfsFile, + pub(super) file: File, /// The scope in which the definition is defined. #[id] diff --git a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs index 00e73788ddadc..ce4edecf3593a 100644 --- a/crates/red_knot_python_semantic/src/semantic_index/symbol.rs +++ b/crates/red_knot_python_semantic/src/semantic_index/symbol.rs @@ -3,8 +3,8 @@ use std::ops::Range; use bitflags::bitflags; use hashbrown::hash_map::RawEntryMut; +use ruff_db::files::File; use ruff_db::parsed::ParsedModule; -use ruff_db::vfs::VfsFile; use ruff_index::{newtype_index, IndexVec}; use ruff_python_ast::name::Name; use ruff_python_ast::{self as ast}; @@ -79,7 +79,7 @@ bitflags! { #[salsa::tracked] pub struct PublicSymbolId<'db> { #[id] - pub(crate) file: VfsFile, + pub(crate) file: File, #[id] pub(crate) scoped_symbol_id: ScopedSymbolId, } @@ -116,14 +116,14 @@ impl ScopedSymbolId { /// /// # Panics /// May panic if the symbol does not belong to `file` or is not a symbol of `file`'s root scope. - pub(crate) fn to_public_symbol(self, db: &dyn Db, file: VfsFile) -> PublicSymbolId { + pub(crate) fn to_public_symbol(self, db: &dyn Db, file: File) -> PublicSymbolId { let symbols = public_symbols_map(db, file); symbols.public(self) } } #[salsa::tracked(return_ref)] -pub(crate) fn public_symbols_map(db: &dyn Db, file: VfsFile) -> PublicSymbolsMap<'_> { +pub(crate) fn public_symbols_map(db: &dyn Db, file: File) -> PublicSymbolsMap<'_> { let _span = tracing::trace_span!("public_symbols_map", ?file).entered(); let module_scope = root_scope(db, file); @@ -156,7 +156,7 @@ impl<'db> PublicSymbolsMap<'db> { #[salsa::tracked] pub struct ScopeId<'db> { #[id] - pub file: VfsFile, + pub file: File, #[id] pub file_scope_id: FileScopeId, @@ -190,7 +190,7 @@ impl FileScopeId { FileScopeId::from_u32(0) } - pub fn to_scope_id(self, db: &dyn Db, file: VfsFile) -> ScopeId<'_> { + pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> { let index = semantic_index(db, file); index.scope_ids_by_scope[self] } diff --git a/crates/red_knot_python_semantic/src/semantic_model.rs b/crates/red_knot_python_semantic/src/semantic_model.rs index 9e2afb8728738..29433ba4ee7e9 100644 --- a/crates/red_knot_python_semantic/src/semantic_model.rs +++ b/crates/red_knot_python_semantic/src/semantic_model.rs @@ -1,5 +1,5 @@ use red_knot_module_resolver::{resolve_module, Module, ModuleName}; -use ruff_db::vfs::VfsFile; +use ruff_db::files::File; use ruff_python_ast as ast; use ruff_python_ast::{Expr, ExpressionRef, StmtClassDef}; @@ -11,11 +11,11 @@ use crate::Db; pub struct SemanticModel<'db> { db: &'db dyn Db, - file: VfsFile, + file: File, } impl<'db> SemanticModel<'db> { - pub fn new(db: &'db dyn Db, file: VfsFile) -> Self { + pub fn new(db: &'db dyn Db, file: File) -> Self { Self { db, file } } @@ -182,9 +182,9 @@ mod tests { use red_knot_module_resolver::{ set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion, }; - use ruff_db::file_system::FileSystemPathBuf; + use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; - use ruff_db::vfs::system_path_to_file; + use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use crate::db::tests::TestDb; use crate::types::Type; @@ -196,7 +196,7 @@ mod tests { &mut db, RawModuleResolutionSettings { extra_paths: vec![], - workspace_root: FileSystemPathBuf::from("/src"), + workspace_root: SystemPathBuf::from("/src"), site_packages: None, custom_typeshed: None, target_version: TargetVersion::Py38, @@ -208,10 +208,9 @@ mod tests { #[test] fn function_ty() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system() - .write_file("/src/foo.py", "def test(): pass")?; + db.write_file("/src/foo.py", "def test(): pass")?; let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); let ast = parsed_module(&db, foo); @@ -227,10 +226,9 @@ mod tests { #[test] fn class_ty() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system() - .write_file("/src/foo.py", "class Test: pass")?; + db.write_file("/src/foo.py", "class Test: pass")?; let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); let ast = parsed_module(&db, foo); @@ -246,9 +244,9 @@ mod tests { #[test] fn alias_ty() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_files([ + db.write_files([ ("/src/foo.py", "class Test: pass"), ("/src/bar.py", "from foo import Test"), ])?; diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 30deaf15df269..535123e3ca1cc 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1,5 +1,5 @@ +use ruff_db::files::File; use ruff_db::parsed::parsed_module; -use ruff_db::vfs::VfsFile; use ruff_python_ast::name::Name; use crate::semantic_index::symbol::{NodeWithScopeKind, PublicSymbolId, ScopeId}; @@ -49,7 +49,7 @@ pub(crate) fn public_symbol_ty<'db>(db: &'db dyn Db, symbol: PublicSymbolId<'db> /// Shorthand for `public_symbol_ty` that takes a symbol name instead of a [`PublicSymbolId`]. pub(crate) fn public_symbol_ty_by_name<'db>( db: &'db dyn Db, - file: VfsFile, + file: File, name: &str, ) -> Option> { let symbol = public_symbol(db, file, name)?; @@ -105,7 +105,7 @@ pub enum Type<'db> { /// a specific function object Function(FunctionType<'db>), /// a specific module object - Module(VfsFile), + Module(File), /// a specific class object Class(ClassType<'db>), /// the set of Python objects with the given class in their __class__'s method resolution order @@ -274,9 +274,9 @@ mod tests { use red_knot_module_resolver::{ set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion, }; - use ruff_db::file_system::FileSystemPathBuf; + use ruff_db::files::system_path_to_file; use ruff_db::parsed::parsed_module; - use ruff_db::vfs::system_path_to_file; + use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use crate::db::tests::{ assert_will_not_run_function_query, assert_will_run_function_query, TestDb, @@ -292,7 +292,7 @@ mod tests { RawModuleResolutionSettings { target_version: TargetVersion::Py38, extra_paths: vec![], - workspace_root: FileSystemPathBuf::from("/src"), + workspace_root: SystemPathBuf::from("/src"), site_packages: None, custom_typeshed: None, }, @@ -303,9 +303,9 @@ mod tests { #[test] fn local_inference() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file("/src/a.py", "x = 10")?; + db.write_file("/src/a.py", "x = 10")?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); let parsed = parsed_module(&db, a); @@ -324,7 +324,7 @@ mod tests { fn dependency_public_symbol_type_change() -> anyhow::Result<()> { let mut db = setup_db(); - db.memory_file_system().write_files([ + db.write_files([ ("/src/a.py", "from foo import x"), ("/src/foo.py", "x = 10\ndef foo(): ..."), ])?; @@ -335,11 +335,7 @@ mod tests { assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); // Change `x` to a different value - db.memory_file_system() - .write_file("/src/foo.py", "x = 20\ndef foo(): ...")?; - - let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - foo.touch(&mut db); + db.write_file("/src/foo.py", "x = 20\ndef foo(): ...")?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); @@ -365,7 +361,7 @@ mod tests { fn dependency_non_public_symbol_change() -> anyhow::Result<()> { let mut db = setup_db(); - db.memory_file_system().write_files([ + db.write_files([ ("/src/a.py", "from foo import x"), ("/src/foo.py", "x = 10\ndef foo(): y = 1"), ])?; @@ -375,13 +371,9 @@ mod tests { assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - db.memory_file_system() - .write_file("/src/foo.py", "x = 10\ndef foo(): pass")?; + db.write_file("/src/foo.py", "x = 10\ndef foo(): pass")?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - - foo.touch(&mut db); db.clear_salsa_events(); @@ -407,7 +399,7 @@ mod tests { fn dependency_unrelated_public_symbol() -> anyhow::Result<()> { let mut db = setup_db(); - db.memory_file_system().write_files([ + db.write_files([ ("/src/a.py", "from foo import x"), ("/src/foo.py", "x = 10\ny = 20"), ])?; @@ -417,13 +409,9 @@ mod tests { assert_eq!(x_ty.display(&db).to_string(), "Literal[10]"); - db.memory_file_system() - .write_file("/src/foo.py", "x = 10\ny = 30")?; + db.write_file("/src/foo.py", "x = 10\ny = 30")?; let a = system_path_to_file(&db, "/src/a.py").unwrap(); - let foo = system_path_to_file(&db, "/src/foo.py").unwrap(); - - foo.touch(&mut db); db.clear_salsa_events(); diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 173ac48431be7..f8623ae37d699 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use std::sync::Arc; use red_knot_module_resolver::{resolve_module, ModuleName}; -use ruff_db::vfs::VfsFile; +use ruff_db::files::File; use ruff_index::IndexVec; use ruff_python_ast as ast; use ruff_python_ast::{ExprContext, TypeParams}; @@ -58,7 +58,7 @@ pub(super) struct TypeInferenceBuilder<'db> { // Cached lookups index: &'db SemanticIndex<'db>, file_scope_id: FileScopeId, - file_id: VfsFile, + file_id: File, symbol_table: Arc>, /// The type inference results @@ -601,8 +601,8 @@ mod tests { use red_knot_module_resolver::{ set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion, }; - use ruff_db::file_system::FileSystemPathBuf; - use ruff_db::vfs::system_path_to_file; + use ruff_db::files::system_path_to_file; + use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use ruff_python_ast::name::Name; use crate::db::tests::TestDb; @@ -616,7 +616,7 @@ mod tests { RawModuleResolutionSettings { target_version: TargetVersion::Py38, extra_paths: Vec::new(), - workspace_root: FileSystemPathBuf::from("/src"), + workspace_root: SystemPathBuf::from("/src"), site_packages: None, custom_typeshed: None, }, @@ -634,9 +634,9 @@ mod tests { #[test] fn follow_import_to_class() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_files([ + db.write_files([ ("src/a.py", "from b import C as D; E = D"), ("src/b.py", "class C: pass"), ])?; @@ -648,9 +648,9 @@ mod tests { #[test] fn resolve_base_class_by_name() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file( + db.write_file( "src/mod.py", r#" class Base: @@ -680,9 +680,9 @@ class Sub(Base): #[test] fn resolve_method() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file( + db.write_file( "src/mod.py", " class C: @@ -710,9 +710,9 @@ class C: #[test] fn resolve_module_member() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_files([ + db.write_files([ ("src/a.py", "import b; D = b.C"), ("src/b.py", "class C: pass"), ])?; @@ -724,9 +724,9 @@ class C: #[test] fn resolve_literal() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file("src/a.py", "x = 1")?; + db.write_file("src/a.py", "x = 1")?; assert_public_ty(&db, "src/a.py", "x", "Literal[1]"); @@ -735,9 +735,9 @@ class C: #[test] fn resolve_union() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file( + db.write_file( "src/a.py", " if flag: @@ -754,9 +754,9 @@ else: #[test] fn literal_int_arithmetic() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file( + db.write_file( "src/a.py", " a = 2 + 1 @@ -778,10 +778,9 @@ e = 5 % 3 #[test] fn walrus() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system() - .write_file("src/a.py", "x = (y := 1) + 1")?; + db.write_file("src/a.py", "x = (y := 1) + 1")?; assert_public_ty(&db, "src/a.py", "x", "Literal[2]"); assert_public_ty(&db, "src/a.py", "y", "Literal[1]"); @@ -791,10 +790,9 @@ e = 5 % 3 #[test] fn ifexpr() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system() - .write_file("src/a.py", "x = 1 if flag else 2")?; + db.write_file("src/a.py", "x = 1 if flag else 2")?; assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2]"); @@ -803,9 +801,9 @@ e = 5 % 3 #[test] fn ifexpr_walrus() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system().write_file( + db.write_file( "src/a.py", " y = z = 0 @@ -824,10 +822,9 @@ b = z #[test] fn ifexpr_nested() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system() - .write_file("src/a.py", "x = 1 if flag else 2 if flag2 else 3")?; + db.write_file("src/a.py", "x = 1 if flag else 2 if flag2 else 3")?; assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2, 3]"); @@ -836,10 +833,9 @@ b = z #[test] fn none() -> anyhow::Result<()> { - let db = setup_db(); + let mut db = setup_db(); - db.memory_file_system() - .write_file("src/a.py", "x = 1 if flag else None")?; + db.write_file("src/a.py", "x = 1 if flag else None")?; assert_public_ty(&db, "src/a.py", "x", "Literal[1] | None"); Ok(()) diff --git a/crates/ruff_benchmark/benches/red_knot.rs b/crates/ruff_benchmark/benches/red_knot.rs index cab02e64aa2e1..40882a82b29d9 100644 --- a/crates/ruff_benchmark/benches/red_knot.rs +++ b/crates/ruff_benchmark/benches/red_knot.rs @@ -8,9 +8,9 @@ use red_knot_module_resolver::{ use ruff_benchmark::criterion::{ criterion_group, criterion_main, BatchSize, Criterion, Throughput, }; -use ruff_db::file_system::{FileSystemPath, MemoryFileSystem}; +use ruff_db::files::{system_path_to_file, File}; use ruff_db::parsed::parsed_module; -use ruff_db::vfs::{system_path_to_file, VfsFile}; +use ruff_db::system::{MemoryFileSystem, SystemPath, TestSystem}; use ruff_db::Upcast; static FOO_CODE: &str = r#" @@ -47,16 +47,17 @@ def override(): ... struct Case { program: Program, fs: MemoryFileSystem, - foo: VfsFile, - bar: VfsFile, - typing: VfsFile, + foo: File, + bar: File, + typing: File, } fn setup_case() -> Case { - let fs = MemoryFileSystem::new(); - let foo_path = FileSystemPath::new("/src/foo.py"); - let bar_path = FileSystemPath::new("/src/bar.py"); - let typing_path = FileSystemPath::new("/src/typing.pyi"); + let system = TestSystem::default(); + let fs = system.memory_file_system().clone(); + let foo_path = SystemPath::new("/src/foo.py"); + let bar_path = SystemPath::new("/src/bar.py"); + let typing_path = SystemPath::new("/src/typing.pyi"); fs.write_files([ (foo_path, FOO_CODE), (bar_path, BAR_CODE), @@ -64,10 +65,10 @@ fn setup_case() -> Case { ]) .unwrap(); - let workspace_root = FileSystemPath::new("/src"); + let workspace_root = SystemPath::new("/src"); let workspace = Workspace::new(workspace_root.to_path_buf()); - let mut program = Program::new(workspace, fs.clone()); + let mut program = Program::new(workspace, system); let foo = system_path_to_file(&program, foo_path).unwrap(); set_module_resolution_settings( @@ -134,7 +135,7 @@ fn benchmark_incremental(criterion: &mut Criterion) { case.fs .write_file( - FileSystemPath::new("/src/foo.py"), + SystemPath::new("/src/foo.py"), format!("{BAR_CODE}\n# A comment\n"), ) .unwrap(); diff --git a/crates/ruff_db/Cargo.toml b/crates/ruff_db/Cargo.toml index 2c56e1ce451ff..1cfb7e88062a3 100644 --- a/crates/ruff_db/Cargo.toml +++ b/crates/ruff_db/Cargo.toml @@ -27,4 +27,3 @@ zip = { workspace = true } [dev-dependencies] insta = { workspace = true } -once_cell = { workspace = true } diff --git a/crates/ruff_db/src/vfs.rs b/crates/ruff_db/src/files.rs similarity index 50% rename from crates/ruff_db/src/vfs.rs rename to crates/ruff_db/src/files.rs index 4725f3aa5020a..8c5abac934893 100644 --- a/crates/ruff_db/src/vfs.rs +++ b/crates/ruff_db/src/files.rs @@ -3,13 +3,12 @@ use std::sync::Arc; use countme::Count; use dashmap::mapref::entry::Entry; -pub use crate::vendored::{VendoredPath, VendoredPathBuf}; -pub use path::VfsPath; +pub use path::FilePath; use crate::file_revision::FileRevision; -use crate::file_system::FileSystemPath; -use crate::vendored::VendoredFileSystem; -use crate::vfs::private::FileStatus; +use crate::files::private::FileStatus; +use crate::system::SystemPath; +use crate::vendored::VendoredPath; use crate::{Db, FxDashMap}; mod path; @@ -18,8 +17,8 @@ mod path; /// /// Returns `None` if the path doesn't exist, isn't accessible, or if the path points to a directory. #[inline] -pub fn system_path_to_file(db: &dyn Db, path: impl AsRef) -> Option { - let file = db.vfs().file_system(db, path.as_ref()); +pub fn system_path_to_file(db: &dyn Db, path: impl AsRef) -> Option { + let file = db.files().system(db, path.as_ref()); // It's important that `vfs.file_system` creates a `VfsFile` even for files that don't exist or don't // exist anymore so that Salsa can track that the caller of this function depends on the existence of @@ -33,98 +32,53 @@ pub fn system_path_to_file(db: &dyn Db, path: impl AsRef) -> Opt /// Interns a vendored file path. Returns `Some` if the vendored file for `path` exists and `None` otherwise. #[inline] -pub fn vendored_path_to_file(db: &dyn Db, path: impl AsRef) -> Option { - db.vfs().vendored(db, path.as_ref()) +pub fn vendored_path_to_file(db: &dyn Db, path: impl AsRef) -> Option { + db.files().vendored(db, path.as_ref()) } -/// Interns a virtual file system path and returns a salsa [`VfsFile`] ingredient. -/// -/// Returns `Some` if a file for `path` exists and is accessible by the user. Returns `None` otherwise. -/// -/// See [`system_path_to_file`] and [`vendored_path_to_file`] if you always have either a file system or vendored path. -#[inline] -pub fn vfs_path_to_file(db: &dyn Db, path: &VfsPath) -> Option { - match path { - VfsPath::FileSystem(path) => system_path_to_file(db, path), - VfsPath::Vendored(path) => vendored_path_to_file(db, path), - } -} - -/// Virtual file system that supports files from different sources. -/// -/// The [`Vfs`] supports accessing files from: -/// -/// * The file system -/// * Vendored files that are part of the distributed Ruff binary -/// -/// ## Why do both the [`Vfs`] and [`FileSystem`](crate::FileSystem) trait exist? -/// -/// It would have been an option to define [`FileSystem`](crate::FileSystem) in a way that all its operation accept -/// a [`VfsPath`]. This would have allowed to unify most of [`Vfs`] and [`FileSystem`](crate::FileSystem). The reason why they are -/// separate is that not all operations are supported for all [`VfsPath`]s: -/// -/// * The only relevant operations for [`VendoredPath`]s are testing for existence and reading the content. -/// * The vendored file system is immutable and doesn't support writing nor does it require watching for changes. -/// * There's no requirement to walk the vendored typesystem. -/// -/// The other reason is that most operations know if they are working with vendored or file system paths. -/// Requiring them to convert the path to an `VfsPath` to test if the file exist is cumbersome. -/// -/// The main downside of the approach is that vendored files needs their own stubbing mechanism. +/// Lookup table that maps [file paths](`FilePath`) to salsa interned [`File`] instances. #[derive(Default)] -pub struct Vfs { - inner: Arc, +pub struct Files { + inner: Arc, } #[derive(Default)] -struct VfsInner { - /// Lookup table that maps [`VfsPath`]s to salsa interned [`VfsFile`] instances. +struct FilesInner { + /// Lookup table that maps [`FilePath`]s to salsa interned [`File`] instances. /// /// The map also stores entries for files that don't exist on the file system. This is necessary /// so that queries that depend on the existence of a file are re-executed when the file is created. - /// - files_by_path: FxDashMap, - vendored: VendoredVfs, + files_by_path: FxDashMap, } -impl Vfs { - /// Creates a new [`Vfs`] instance where the vendored files are stubbed out. - pub fn with_stubbed_vendored() -> Self { - Self { - inner: Arc::new(VfsInner { - vendored: VendoredVfs::Stubbed(FxDashMap::default()), - ..VfsInner::default() - }), - } - } - - /// Looks up a file by its path. +impl Files { + /// Looks up a file by its `path`. /// - /// For a non-existing file, creates a new salsa [`VfsFile`] ingredient and stores it for future lookups. + /// For a non-existing file, creates a new salsa [`File`] ingredient and stores it for future lookups. /// /// The operation always succeeds even if the path doesn't exist on disk, isn't accessible or if the path points to a directory. /// In these cases, a file with status [`FileStatus::Deleted`] is returned. #[tracing::instrument(level = "debug", skip(self, db))] - fn file_system(&self, db: &dyn Db, path: &FileSystemPath) -> VfsFile { + fn system(&self, db: &dyn Db, path: &SystemPath) -> File { *self .inner .files_by_path - .entry(VfsPath::FileSystem(path.to_path_buf())) + .entry(FilePath::System(path.to_path_buf())) .or_insert_with(|| { - let metadata = db.file_system().metadata(path); + let metadata = db.system().path_metadata(path); match metadata { - Ok(metadata) if metadata.file_type().is_file() => VfsFile::new( + Ok(metadata) if metadata.file_type().is_file() => File::new( db, - VfsPath::FileSystem(path.to_path_buf()), + FilePath::System(path.to_path_buf()), metadata.permissions(), metadata.revision(), FileStatus::Exists, Count::default(), ), - _ => VfsFile::new( + _ => File::new( db, - VfsPath::FileSystem(path.to_path_buf()), + FilePath::System(path.to_path_buf()), None, FileRevision::zero(), FileStatus::Deleted, @@ -134,24 +88,32 @@ impl Vfs { }) } + /// Tries to look up the file for the given system path, returns `None` if no such file exists yet + fn try_system(&self, path: &SystemPath) -> Option { + self.inner + .files_by_path + .get(&FilePath::System(path.to_path_buf())) + .map(|entry| *entry.value()) + } + /// Looks up a vendored file by its path. Returns `Some` if a vendored file for the given path /// exists and `None` otherwise. #[tracing::instrument(level = "debug", skip(self, db))] - fn vendored(&self, db: &dyn Db, path: &VendoredPath) -> Option { + fn vendored(&self, db: &dyn Db, path: &VendoredPath) -> Option { let file = match self .inner .files_by_path - .entry(VfsPath::Vendored(path.to_path_buf())) + .entry(FilePath::Vendored(path.to_path_buf())) { Entry::Occupied(entry) => *entry.get(), Entry::Vacant(entry) => { - let revision = self.inner.vendored.revision(path)?; + let metadata = db.vendored().metadata(path).ok()?; - let file = VfsFile::new( + let file = File::new( db, - VfsPath::Vendored(path.to_path_buf()), + FilePath::Vendored(path.to_path_buf()), Some(0o444), - revision, + metadata.revision(), FileStatus::Exists, Count::default(), ); @@ -165,49 +127,16 @@ impl Vfs { Some(file) } - /// Stubs out the vendored files with the given content. - /// - /// ## Panics - /// If there are pending snapshots referencing this `Vfs` instance. - pub fn stub_vendored(&mut self, vendored: impl IntoIterator) - where - P: AsRef, - S: ToString, - { - let inner = Arc::get_mut(&mut self.inner).unwrap(); - - let stubbed = FxDashMap::default(); - - for (path, content) in vendored { - stubbed.insert(path.as_ref().to_path_buf(), content.to_string()); - } - - inner.vendored = VendoredVfs::Stubbed(stubbed); - } - - /// Creates a salsa like snapshot of the files. The instances share + /// Creates a salsa like snapshot. The instances share /// the same path-to-file mapping. pub fn snapshot(&self) -> Self { Self { inner: self.inner.clone(), } } - - fn read(&self, db: &dyn Db, path: &VfsPath) -> String { - match path { - VfsPath::FileSystem(path) => db.file_system().read(path).unwrap_or_default(), - - VfsPath::Vendored(vendored) => db - .vfs() - .inner - .vendored - .read(vendored) - .expect("Vendored file to exist"), - } - } } -impl std::fmt::Debug for Vfs { +impl std::fmt::Debug for Files { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut map = f.debug_map(); @@ -218,12 +147,13 @@ impl std::fmt::Debug for Vfs { } } +/// A file that's either stored on the host system's file system or in the vendored file system. #[salsa::input] -pub struct VfsFile { +pub struct File { /// The path of the file. #[id] #[return_ref] - pub path: VfsPath, + pub path: FilePath, /// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows /// or when the file has been deleted. @@ -234,17 +164,17 @@ pub struct VfsFile { /// The status of the file. /// - /// Salsa doesn't support deleting inputs. The only way to signal to the depending queries that + /// Salsa doesn't support deleting inputs. The only way to signal dependent queries that /// the file has been deleted is to change the status to `Deleted`. status: FileStatus, /// Counter that counts the number of created file instances and active file instances. /// Only enabled in debug builds. #[allow(unused)] - count: Count, + count: Count, } -impl VfsFile { +impl File { /// Reads the content of the file into a [`String`]. /// /// Reading the same file multiple times isn't guaranteed to return the same content. It's possible @@ -253,21 +183,26 @@ impl VfsFile { /// an empty string, which is the closest to the content that the file contains now. Returning /// an empty string shouldn't be a problem because the query will be re-executed as soon as the /// changes are applied to the database. - pub(crate) fn read(&self, db: &dyn Db) -> String { + pub(crate) fn read_to_string(&self, db: &dyn Db) -> String { let path = self.path(db); - if path.is_file_system_path() { - // Add a dependency on the revision to ensure the operation gets re-executed when the file changes. - let _ = self.revision(db); - } + let result = match path { + FilePath::System(system) => { + // Add a dependency on the revision to ensure the operation gets re-executed when the file changes. + let _ = self.revision(db); - db.vfs().read(db, path) + db.system().read_to_string(system) + } + FilePath::Vendored(vendored) => db.vendored().read_to_string(vendored), + }; + + result.unwrap_or_default() } /// Refreshes the file metadata by querying the file system if needed. /// TODO: The API should instead take all observed changes from the file system directly /// and then apply the VfsFile status accordingly. But for now, this is sufficient. - pub fn touch_path(db: &mut dyn Db, path: &VfsPath) { + pub fn touch_path(db: &mut dyn Db, path: &FilePath) { Self::touch_impl(db, path, None); } @@ -277,10 +212,10 @@ impl VfsFile { } /// Private method providing the implementation for [`Self::touch_path`] and [`Self::touch`]. - fn touch_impl(db: &mut dyn Db, path: &VfsPath, file: Option) { + fn touch_impl(db: &mut dyn Db, path: &FilePath, file: Option) { match path { - VfsPath::FileSystem(path) => { - let metadata = db.file_system().metadata(path); + FilePath::System(path) => { + let metadata = db.system().path_metadata(path); let (status, revision) = match metadata { Ok(metadata) if metadata.file_type().is_file() => { @@ -289,59 +224,20 @@ impl VfsFile { _ => (FileStatus::Deleted, FileRevision::zero()), }; - let file = file.unwrap_or_else(|| db.vfs().file_system(db, path)); + let Some(file) = file.or_else(|| db.files().try_system(path)) else { + return; + }; + file.set_status(db).to(status); file.set_revision(db).to(revision); } - VfsPath::Vendored(_) => { + FilePath::Vendored(_) => { // Readonly, can never be out of date. } } } } -#[derive(Debug)] -enum VendoredVfs { - #[allow(unused)] - Real(VendoredFileSystem), - Stubbed(FxDashMap), -} - -impl Default for VendoredVfs { - fn default() -> Self { - Self::Stubbed(FxDashMap::default()) - } -} - -impl VendoredVfs { - fn revision(&self, path: &VendoredPath) -> Option { - match self { - VendoredVfs::Real(file_system) => file_system - .metadata(path) - .map(|metadata| metadata.revision()), - VendoredVfs::Stubbed(stubbed) => stubbed - .contains_key(&path.to_path_buf()) - .then_some(FileRevision::new(1)), - } - } - - fn read(&self, path: &VendoredPath) -> std::io::Result { - match self { - VendoredVfs::Real(file_system) => file_system.read(path), - VendoredVfs::Stubbed(stubbed) => { - if let Some(contents) = stubbed.get(&path.to_path_buf()).as_deref().cloned() { - Ok(contents) - } else { - Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - format!("Could not find file {path:?}"), - )) - } - } - } - } -} - // The types in here need to be public because they're salsa ingredients but we // don't want them to be publicly accessible. That's why we put them into a private module. mod private { @@ -358,21 +254,22 @@ mod private { #[cfg(test)] mod tests { use crate::file_revision::FileRevision; + use crate::files::{system_path_to_file, vendored_path_to_file}; + use crate::system::DbWithTestSystem; use crate::tests::TestDb; - use crate::vfs::{system_path_to_file, vendored_path_to_file}; + use crate::vendored::tests::VendoredFileSystemBuilder; #[test] - fn file_system_existing_file() -> crate::file_system::Result<()> { + fn file_system_existing_file() -> crate::system::Result<()> { let mut db = TestDb::new(); - db.file_system_mut() - .write_files([("test.py", "print('Hello world')")])?; + db.write_file("test.py", "print('Hello world')")?; let test = system_path_to_file(&db, "test.py").expect("File to exist."); assert_eq!(test.permissions(&db), Some(0o755)); assert_ne!(test.revision(&db), FileRevision::zero()); - assert_eq!(&test.read(&db), "print('Hello world')"); + assert_eq!(&test.read_to_string(&db), "print('Hello world')"); Ok(()) } @@ -390,14 +287,18 @@ mod tests { fn stubbed_vendored_file() { let mut db = TestDb::new(); - db.vfs_mut() - .stub_vendored([("test.py", "def foo() -> str")]); + let mut vendored_builder = VendoredFileSystemBuilder::new(); + vendored_builder + .add_file("test.pyi", "def foo() -> str") + .unwrap(); + let vendored = vendored_builder.finish().unwrap(); + db.with_vendored(vendored); - let test = vendored_path_to_file(&db, "test.py").expect("Vendored file to exist."); + let test = vendored_path_to_file(&db, "test.pyi").expect("Vendored file to exist."); assert_eq!(test.permissions(&db), Some(0o444)); assert_ne!(test.revision(&db), FileRevision::zero()); - assert_eq!(&test.read(&db), "def foo() -> str"); + assert_eq!(&test.read_to_string(&db), "def foo() -> str"); } #[test] diff --git a/crates/ruff_db/src/files/path.rs b/crates/ruff_db/src/files/path.rs new file mode 100644 index 0000000000000..8def5dec869d1 --- /dev/null +++ b/crates/ruff_db/src/files/path.rs @@ -0,0 +1,176 @@ +use crate::files::{system_path_to_file, vendored_path_to_file, File}; +use crate::system::{SystemPath, SystemPathBuf}; +use crate::vendored::{VendoredPath, VendoredPathBuf}; +use crate::Db; + +/// Path to a file. +/// +/// The path abstracts that files in Ruff can come from different sources: +/// +/// * a file stored on the [host system](crate::system::System). +/// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem). +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub enum FilePath { + /// Path to a file on the [host system](crate::system::System). + System(SystemPathBuf), + /// Path to a file vendored as part of Ruff. Stored in the [vendored file system](crate::vendored::VendoredFileSystem). + Vendored(VendoredPathBuf), +} + +impl FilePath { + /// Create a new path to a file on the file system. + #[must_use] + pub fn system(path: impl AsRef) -> Self { + FilePath::System(path.as_ref().to_path_buf()) + } + + /// Returns `Some` if the path is a file system path that points to a path on disk. + #[must_use] + #[inline] + pub fn into_system_path_buf(self) -> Option { + match self { + FilePath::System(path) => Some(path), + FilePath::Vendored(_) => None, + } + } + + #[must_use] + #[inline] + pub fn as_system_path(&self) -> Option<&SystemPath> { + match self { + FilePath::System(path) => Some(path.as_path()), + FilePath::Vendored(_) => None, + } + } + + /// Returns `true` if the path is a file system path that points to a path on disk. + #[must_use] + #[inline] + pub const fn is_system_path(&self) -> bool { + matches!(self, FilePath::System(_)) + } + + /// Returns `true` if the path is a vendored path. + #[must_use] + #[inline] + pub const fn is_vendored_path(&self) -> bool { + matches!(self, FilePath::Vendored(_)) + } + + #[must_use] + #[inline] + pub fn as_vendored_path(&self) -> Option<&VendoredPath> { + match self { + FilePath::Vendored(path) => Some(path.as_path()), + FilePath::System(_) => None, + } + } + + /// Yields the underlying [`str`] slice. + pub fn as_str(&self) -> &str { + match self { + FilePath::System(path) => path.as_str(), + FilePath::Vendored(path) => path.as_str(), + } + } + + /// Interns a virtual file system path and returns a salsa [`File`] ingredient. + /// + /// Returns `Some` if a file for `path` exists and is accessible by the user. Returns `None` otherwise. + /// + /// See [`system_path_to_file`] and [`vendored_path_to_file`] if you always have either a file system or vendored path. + #[inline] + pub fn to_file(&self, db: &dyn Db) -> Option { + match self { + FilePath::System(path) => system_path_to_file(db, path), + FilePath::Vendored(path) => vendored_path_to_file(db, path), + } + } +} + +impl AsRef for FilePath { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl From for FilePath { + fn from(value: SystemPathBuf) -> Self { + Self::System(value) + } +} + +impl From<&SystemPath> for FilePath { + fn from(value: &SystemPath) -> Self { + FilePath::System(value.to_path_buf()) + } +} + +impl From for FilePath { + fn from(value: VendoredPathBuf) -> Self { + Self::Vendored(value) + } +} + +impl From<&VendoredPath> for FilePath { + fn from(value: &VendoredPath) -> Self { + Self::Vendored(value.to_path_buf()) + } +} + +impl PartialEq for FilePath { + #[inline] + fn eq(&self, other: &SystemPath) -> bool { + self.as_system_path() + .is_some_and(|self_path| self_path == other) + } +} + +impl PartialEq for SystemPath { + #[inline] + fn eq(&self, other: &FilePath) -> bool { + other == self + } +} + +impl PartialEq for FilePath { + #[inline] + fn eq(&self, other: &SystemPathBuf) -> bool { + self == other.as_path() + } +} + +impl PartialEq for SystemPathBuf { + fn eq(&self, other: &FilePath) -> bool { + other == self + } +} + +impl PartialEq for FilePath { + #[inline] + fn eq(&self, other: &VendoredPath) -> bool { + self.as_vendored_path() + .is_some_and(|self_path| self_path == other) + } +} + +impl PartialEq for VendoredPath { + #[inline] + fn eq(&self, other: &FilePath) -> bool { + other == self + } +} + +impl PartialEq for FilePath { + #[inline] + fn eq(&self, other: &VendoredPathBuf) -> bool { + other.as_path() == self + } +} + +impl PartialEq for VendoredPathBuf { + #[inline] + fn eq(&self, other: &FilePath) -> bool { + other == self + } +} diff --git a/crates/ruff_db/src/lib.rs b/crates/ruff_db/src/lib.rs index ac2891cabe829..cb8469315c51b 100644 --- a/crates/ruff_db/src/lib.rs +++ b/crates/ruff_db/src/lib.rs @@ -3,28 +3,29 @@ use std::hash::BuildHasherDefault; use rustc_hash::FxHasher; use salsa::DbWithJar; -use crate::file_system::FileSystem; +use crate::files::{File, Files}; use crate::parsed::parsed_module; use crate::source::{line_index, source_text}; -use crate::vfs::{Vfs, VfsFile}; +use crate::system::System; +use crate::vendored::VendoredFileSystem; pub mod file_revision; -pub mod file_system; +pub mod files; pub mod parsed; pub mod source; +pub mod system; pub mod vendored; -pub mod vfs; pub(crate) type FxDashMap = dashmap::DashMap>; #[salsa::jar(db=Db)] -pub struct Jar(VfsFile, source_text, line_index, parsed_module); +pub struct Jar(File, source_text, line_index, parsed_module); -/// Database that gives access to the virtual filesystem, source code, and parsed AST. +/// Most basic database that gives access to files, the host system, source code, and parsed AST. pub trait Db: DbWithJar { - fn file_system(&self) -> &dyn FileSystem; - - fn vfs(&self) -> &Vfs; + fn vendored(&self) -> &VendoredFileSystem; + fn system(&self) -> &dyn System; + fn files(&self) -> &Files; } /// Trait for upcasting a reference to a base trait object. @@ -38,39 +39,36 @@ mod tests { use salsa::DebugWithDb; - use crate::file_system::{FileSystem, MemoryFileSystem}; - use crate::vfs::{VendoredPathBuf, Vfs}; + use crate::files::Files; + use crate::system::TestSystem; + use crate::system::{DbWithTestSystem, System}; + use crate::vendored::VendoredFileSystem; use crate::{Db, Jar}; /// Database that can be used for testing. /// /// Uses an in memory filesystem and it stubs out the vendored files by default. + #[derive(Default)] #[salsa::db(Jar)] pub(crate) struct TestDb { storage: salsa::Storage, - vfs: Vfs, - file_system: MemoryFileSystem, + files: Files, + system: TestSystem, + vendored: VendoredFileSystem, events: std::sync::Arc>>, } impl TestDb { pub(crate) fn new() -> Self { - let mut vfs = Vfs::default(); - vfs.stub_vendored::([]); - Self { storage: salsa::Storage::default(), - file_system: MemoryFileSystem::default(), + system: TestSystem::default(), + vendored: VendoredFileSystem::default(), events: std::sync::Arc::default(), - vfs, + files: Files::default(), } } - #[allow(unused)] - pub(crate) fn file_system(&self) -> &MemoryFileSystem { - &self.file_system - } - /// Empties the internal store of salsa events that have been emitted, /// and returns them as a `Vec` (equivalent to [`std::mem::take`]). /// @@ -93,22 +91,32 @@ mod tests { self.take_salsa_events(); } - pub(crate) fn file_system_mut(&mut self) -> &mut MemoryFileSystem { - &mut self.file_system + pub(crate) fn with_vendored(&mut self, vendored_file_system: VendoredFileSystem) { + self.vendored = vendored_file_system; + } + } + + impl Db for TestDb { + fn vendored(&self) -> &VendoredFileSystem { + &self.vendored + } + + fn system(&self) -> &dyn System { + &self.system } - pub(crate) fn vfs_mut(&mut self) -> &mut Vfs { - &mut self.vfs + fn files(&self) -> &Files { + &self.files } } - impl Db for TestDb { - fn file_system(&self) -> &dyn FileSystem { - &self.file_system + impl DbWithTestSystem for TestDb { + fn test_system(&self) -> &TestSystem { + &self.system } - fn vfs(&self) -> &Vfs { - &self.vfs + fn test_system_mut(&mut self) -> &mut TestSystem { + &mut self.system } } @@ -124,9 +132,10 @@ mod tests { fn snapshot(&self) -> salsa::Snapshot { salsa::Snapshot::new(Self { storage: self.storage.snapshot(), - file_system: self.file_system.snapshot(), - vfs: self.vfs.snapshot(), + system: self.system.snapshot(), + files: self.files.snapshot(), events: self.events.clone(), + vendored: self.vendored.snapshot(), }) } } diff --git a/crates/ruff_db/src/parsed.rs b/crates/ruff_db/src/parsed.rs index 8eaf5506a77c1..14036ff1b4f71 100644 --- a/crates/ruff_db/src/parsed.rs +++ b/crates/ruff_db/src/parsed.rs @@ -5,13 +5,13 @@ use std::sync::Arc; use ruff_python_ast::{ModModule, PySourceType}; use ruff_python_parser::{parse_unchecked_source, Parsed}; +use crate::files::{File, FilePath}; use crate::source::source_text; -use crate::vfs::{VfsFile, VfsPath}; use crate::Db; /// Returns the parsed AST of `file`, including its token stream. /// -/// The query uses Ruff's error-resilient parser. That means that the parser always succeeds to produce a +/// The query uses Ruff's error-resilient parser. That means that the parser always succeeds to produce an /// AST even if the file contains syntax errors. The parse errors /// are then accessible through [`Parsed::errors`]. /// @@ -21,17 +21,17 @@ use crate::Db; /// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires /// for determining if a query result is unchanged. #[salsa::tracked(return_ref, no_eq)] -pub fn parsed_module(db: &dyn Db, file: VfsFile) -> ParsedModule { +pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule { let _span = tracing::trace_span!("parse_module", file = ?file).entered(); let source = source_text(db, file); let path = file.path(db); let ty = match path { - VfsPath::FileSystem(path) => path + FilePath::System(path) => path .extension() .map_or(PySourceType::Python, PySourceType::from_extension), - VfsPath::Vendored(_) => PySourceType::Stub, + FilePath::Vendored(_) => PySourceType::Stub, }; ParsedModule::new(parse_unchecked_source(&source, ty)) @@ -72,19 +72,18 @@ impl std::fmt::Debug for ParsedModule { #[cfg(test)] mod tests { - use crate::file_system::FileSystemPath; + use crate::files::{system_path_to_file, vendored_path_to_file}; use crate::parsed::parsed_module; + use crate::system::{DbWithTestSystem, SystemPath}; use crate::tests::TestDb; - use crate::vendored::VendoredPath; - use crate::vfs::{system_path_to_file, vendored_path_to_file}; + use crate::vendored::{tests::VendoredFileSystemBuilder, VendoredPath}; #[test] - fn python_file() -> crate::file_system::Result<()> { + fn python_file() -> crate::system::Result<()> { let mut db = TestDb::new(); let path = "test.py"; - db.file_system_mut() - .write_file(path, "x = 10".to_string())?; + db.write_file(path, "x = 10".to_string())?; let file = system_path_to_file(&db, path).unwrap(); @@ -96,12 +95,11 @@ mod tests { } #[test] - fn python_ipynb_file() -> crate::file_system::Result<()> { + fn python_ipynb_file() -> crate::system::Result<()> { let mut db = TestDb::new(); - let path = FileSystemPath::new("test.ipynb"); + let path = SystemPath::new("test.ipynb"); - db.file_system_mut() - .write_file(path, "%timeit a = b".to_string())?; + db.write_file(path, "%timeit a = b".to_string())?; let file = system_path_to_file(&db, path).unwrap(); @@ -115,9 +113,12 @@ mod tests { #[test] fn vendored_file() { let mut db = TestDb::new(); - db.vfs_mut().stub_vendored([( - "path.pyi", - r#" + + let mut vendored_builder = VendoredFileSystemBuilder::new(); + vendored_builder + .add_file( + "path.pyi", + r#" import sys if sys.platform == "win32": @@ -126,7 +127,10 @@ if sys.platform == "win32": else: from posixpath import * from posixpath import __all__ as __all__"#, - )]); + ) + .unwrap(); + let vendored = vendored_builder.finish().unwrap(); + db.with_vendored(vendored); let file = vendored_path_to_file(&db, VendoredPath::new("path.pyi")).unwrap(); diff --git a/crates/ruff_db/src/source.rs b/crates/ruff_db/src/source.rs index 321311a1d0a65..1ce69ff04e25e 100644 --- a/crates/ruff_db/src/source.rs +++ b/crates/ruff_db/src/source.rs @@ -4,15 +4,15 @@ use salsa::DebugWithDb; use std::ops::Deref; use std::sync::Arc; -use crate::vfs::VfsFile; +use crate::files::File; use crate::Db; /// Reads the content of file. #[salsa::tracked] -pub fn source_text(db: &dyn Db, file: VfsFile) -> SourceText { +pub fn source_text(db: &dyn Db, file: File) -> SourceText { let _span = tracing::trace_span!("source_text", ?file).entered(); - let content = file.read(db); + let content = file.read_to_string(db); SourceText { inner: Arc::from(content), @@ -22,7 +22,7 @@ pub fn source_text(db: &dyn Db, file: VfsFile) -> SourceText { /// Computes the [`LineIndex`] for `file`. #[salsa::tracked] -pub fn line_index(db: &dyn Db, file: VfsFile) -> LineIndex { +pub fn line_index(db: &dyn Db, file: File) -> LineIndex { let _span = tracing::trace_span!("line_index", file = ?file.debug(db)).entered(); let source = source_text(db, file); @@ -30,7 +30,7 @@ pub fn line_index(db: &dyn Db, file: VfsFile) -> LineIndex { LineIndex::from_source_text(&source) } -/// The source text of a [`VfsFile`]. +/// The source text of a [`File`]. /// /// Cheap cloneable in `O(1)`. #[derive(Clone, Eq, PartialEq)] @@ -63,30 +63,25 @@ impl std::fmt::Debug for SourceText { mod tests { use salsa::EventKind; - use ruff_source_file::OneIndexed; - use ruff_text_size::TextSize; - - use crate::file_system::FileSystemPath; + use crate::files::system_path_to_file; use crate::source::{line_index, source_text}; + use crate::system::{DbWithTestSystem, SystemPath}; use crate::tests::TestDb; - use crate::vfs::system_path_to_file; + use ruff_source_file::OneIndexed; + use ruff_text_size::TextSize; #[test] - fn re_runs_query_when_file_revision_changes() -> crate::file_system::Result<()> { + fn re_runs_query_when_file_revision_changes() -> crate::system::Result<()> { let mut db = TestDb::new(); - let path = FileSystemPath::new("test.py"); + let path = SystemPath::new("test.py"); - db.file_system_mut() - .write_file(path, "x = 10".to_string())?; + db.write_file(path, "x = 10".to_string())?; let file = system_path_to_file(&db, path).unwrap(); assert_eq!(&*source_text(&db, file), "x = 10"); - db.file_system_mut() - .write_file(path, "x = 20".to_string()) - .unwrap(); - file.touch(&mut db); + db.write_file(path, "x = 20".to_string()).unwrap(); assert_eq!(&*source_text(&db, file), "x = 20"); @@ -94,12 +89,11 @@ mod tests { } #[test] - fn text_is_cached_if_revision_is_unchanged() -> crate::file_system::Result<()> { + fn text_is_cached_if_revision_is_unchanged() -> crate::system::Result<()> { let mut db = TestDb::new(); - let path = FileSystemPath::new("test.py"); + let path = SystemPath::new("test.py"); - db.file_system_mut() - .write_file(path, "x = 10".to_string())?; + db.write_file(path, "x = 10".to_string())?; let file = system_path_to_file(&db, path).unwrap(); @@ -121,12 +115,11 @@ mod tests { } #[test] - fn line_index_for_source() -> crate::file_system::Result<()> { + fn line_index_for_source() -> crate::system::Result<()> { let mut db = TestDb::new(); - let path = FileSystemPath::new("test.py"); + let path = SystemPath::new("test.py"); - db.file_system_mut() - .write_file(path, "x = 10\ny = 20".to_string())?; + db.write_file(path, "x = 10\ny = 20".to_string())?; let file = system_path_to_file(&db, path).unwrap(); let index = line_index(&db, file); diff --git a/crates/ruff_db/src/system.rs b/crates/ruff_db/src/system.rs new file mode 100644 index 0000000000000..92637f5457c5f --- /dev/null +++ b/crates/ruff_db/src/system.rs @@ -0,0 +1,97 @@ +pub use memory_fs::MemoryFileSystem; +pub use os::OsSystem; +pub use test::{DbWithTestSystem, TestSystem}; + +use crate::file_revision::FileRevision; + +pub use self::path::{SystemPath, SystemPathBuf}; + +mod memory_fs; +mod os; +mod path; +mod test; + +pub type Result = std::io::Result; + +/// The system on which Ruff runs. +/// +/// Ruff supports running on the CLI, in a language server, and in a browser (WASM). Each of these +/// host-systems differ in what system operations they support and how they interact with the file system: +/// * Language server: +/// * Reading a file's content should take into account that it might have unsaved changes because it's open in the editor. +/// * Use structured representations for notebooks, making deserializing a notebook from a string unnecessary. +/// * Use their own file watching infrastructure. +/// * WASM (Browser): +/// * There are ways to emulate a file system in WASM but a native memory-filesystem is more efficient. +/// * Doesn't support a current working directory +/// * File watching isn't supported. +/// +/// Abstracting the system also enables tests to use a more efficient in-memory file system. +pub trait System { + /// Reads the metadata of the file or directory at `path`. + fn path_metadata(&self, path: &SystemPath) -> Result; + + /// Reads the content of the file at `path` into a [`String`]. + fn read_to_string(&self, path: &SystemPath) -> Result; + + /// Returns `true` if `path` exists. + fn path_exists(&self, path: &SystemPath) -> bool { + self.path_metadata(path).is_ok() + } + + /// Returns `true` if `path` exists and is a directory. + fn is_directory(&self, path: &SystemPath) -> bool { + self.path_metadata(path) + .map_or(false, |metadata| metadata.file_type.is_directory()) + } + + /// Returns `true` if `path` exists and is a file. + fn is_file(&self, path: &SystemPath) -> bool { + self.path_metadata(path) + .map_or(false, |metadata| metadata.file_type.is_file()) + } + + fn as_any(&self) -> &dyn std::any::Any; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Metadata { + revision: FileRevision, + permissions: Option, + file_type: FileType, +} + +impl Metadata { + pub fn revision(&self) -> FileRevision { + self.revision + } + + pub fn permissions(&self) -> Option { + self.permissions + } + + pub fn file_type(&self) -> FileType { + self.file_type + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] +pub enum FileType { + File, + Directory, + Symlink, +} + +impl FileType { + pub const fn is_file(self) -> bool { + matches!(self, FileType::File) + } + + pub const fn is_directory(self) -> bool { + matches!(self, FileType::Directory) + } + + pub const fn is_symlink(self) -> bool { + matches!(self, FileType::Symlink) + } +} diff --git a/crates/ruff_db/src/file_system/memory.rs b/crates/ruff_db/src/system/memory_fs.rs similarity index 66% rename from crates/ruff_db/src/file_system/memory.rs rename to crates/ruff_db/src/system/memory_fs.rs index debe236e4f0e7..286af8f8e22e4 100644 --- a/crates/ruff_db/src/file_system/memory.rs +++ b/crates/ruff_db/src/system/memory_fs.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, RwLock, RwLockWriteGuard}; use camino::{Utf8Path, Utf8PathBuf}; use filetime::FileTime; -use crate::file_system::{FileSystem, FileSystemPath, FileType, Metadata, Result}; +use crate::system::{FileType, Metadata, Result, SystemPath}; /// File system that stores all content in memory. /// @@ -16,9 +16,7 @@ use crate::file_system::{FileSystem, FileSystemPath, FileType, Metadata, Result} /// * hardlinks /// * permissions: All files and directories have the permission 0755. /// -/// Use a tempdir with the real file system to test these advanced file system features and complex file system behavior. -/// -/// Only intended for testing purposes. +/// Use a tempdir with the real file system to test these advanced file system features and behavior. #[derive(Clone)] pub struct MemoryFileSystem { inner: Arc, @@ -32,7 +30,8 @@ impl MemoryFileSystem { Self::with_cwd("/") } - pub fn with_cwd(cwd: impl AsRef) -> Self { + /// Creates a new, empty in memory file system with the given current working directory. + pub fn with_cwd(cwd: impl AsRef) -> Self { let cwd = Utf8PathBuf::from(cwd.as_ref().as_str()); assert!( @@ -47,7 +46,7 @@ impl MemoryFileSystem { }), }; - fs.create_directory_all(FileSystemPath::new(&cwd)).unwrap(); + fs.create_directory_all(SystemPath::new(&cwd)).unwrap(); fs } @@ -59,6 +58,69 @@ impl MemoryFileSystem { } } + pub fn metadata(&self, path: impl AsRef) -> Result { + fn metadata(fs: &MemoryFileSystemInner, path: &SystemPath) -> Result { + let by_path = fs.by_path.read().unwrap(); + let normalized = normalize_path(path, &fs.cwd); + + let entry = by_path.get(&normalized).ok_or_else(not_found)?; + + let metadata = match entry { + Entry::File(file) => Metadata { + revision: file.last_modified.into(), + permissions: Some(MemoryFileSystem::PERMISSION), + file_type: FileType::File, + }, + Entry::Directory(directory) => Metadata { + revision: directory.last_modified.into(), + permissions: Some(MemoryFileSystem::PERMISSION), + file_type: FileType::Directory, + }, + }; + + Ok(metadata) + } + + metadata(&self.inner, path.as_ref()) + } + + pub fn is_file(&self, path: impl AsRef) -> bool { + let by_path = self.inner.by_path.read().unwrap(); + let normalized = normalize_path(path.as_ref(), &self.inner.cwd); + + matches!(by_path.get(&normalized), Some(Entry::File(_))) + } + + pub fn is_directory(&self, path: impl AsRef) -> bool { + let by_path = self.inner.by_path.read().unwrap(); + let normalized = normalize_path(path.as_ref(), &self.inner.cwd); + + matches!(by_path.get(&normalized), Some(Entry::Directory(_))) + } + + pub fn read_to_string(&self, path: impl AsRef) -> Result { + fn read_to_string(fs: &MemoryFileSystemInner, path: &SystemPath) -> Result { + let by_path = fs.by_path.read().unwrap(); + let normalized = normalize_path(path, &fs.cwd); + + let entry = by_path.get(&normalized).ok_or_else(not_found)?; + + match entry { + Entry::File(file) => Ok(file.content.clone()), + Entry::Directory(_) => Err(is_a_directory()), + } + } + + read_to_string(&self.inner, path.as_ref()) + } + + pub fn exists(&self, path: &SystemPath) -> bool { + let by_path = self.inner.by_path.read().unwrap(); + let normalized = normalize_path(path, &self.inner.cwd); + + by_path.contains_key(&normalized) + } + /// Writes the files to the file system. /// /// The operation overrides existing files with the same normalized path. @@ -66,7 +128,7 @@ impl MemoryFileSystem { /// Enclosing directories are automatically created if they don't exist. pub fn write_files(&self, files: impl IntoIterator) -> Result<()> where - P: AsRef, + P: AsRef, C: ToString, { for (path, content) in files { @@ -81,11 +143,7 @@ impl MemoryFileSystem { /// The operation overrides the content for an existing file with the same normalized `path`. /// /// Enclosing directories are automatically created if they don't exist. - pub fn write_file( - &self, - path: impl AsRef, - content: impl ToString, - ) -> Result<()> { + pub fn write_file(&self, path: impl AsRef, content: impl ToString) -> Result<()> { let mut by_path = self.inner.by_path.write().unwrap(); let normalized = normalize_path(path.as_ref(), &self.inner.cwd); @@ -95,26 +153,30 @@ impl MemoryFileSystem { Ok(()) } - pub fn remove_file(&self, path: impl AsRef) -> Result<()> { - let mut by_path = self.inner.by_path.write().unwrap(); - let normalized = normalize_path(path.as_ref(), &self.inner.cwd); - - match by_path.entry(normalized) { - std::collections::btree_map::Entry::Occupied(entry) => match entry.get() { - Entry::File(_) => { - entry.remove(); - Ok(()) - } - Entry::Directory(_) => Err(is_a_directory()), - }, - std::collections::btree_map::Entry::Vacant(_) => Err(not_found()), + pub fn remove_file(&self, path: impl AsRef) -> Result<()> { + fn remove_file(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> { + let mut by_path = fs.inner.by_path.write().unwrap(); + let normalized = normalize_path(path, &fs.inner.cwd); + + match by_path.entry(normalized) { + std::collections::btree_map::Entry::Occupied(entry) => match entry.get() { + Entry::File(_) => { + entry.remove(); + Ok(()) + } + Entry::Directory(_) => Err(is_a_directory()), + }, + std::collections::btree_map::Entry::Vacant(_) => Err(not_found()), + } } + + remove_file(self, path.as_ref()) } /// Sets the last modified timestamp of the file stored at `path` to now. /// /// Creates a new file if the file at `path` doesn't exist. - pub fn touch(&self, path: impl AsRef) -> Result<()> { + pub fn touch(&self, path: impl AsRef) -> Result<()> { let mut by_path = self.inner.by_path.write().unwrap(); let normalized = normalize_path(path.as_ref(), &self.inner.cwd); @@ -124,7 +186,7 @@ impl MemoryFileSystem { } /// Creates a directory at `path`. All enclosing directories are created if they don't exist. - pub fn create_directory_all(&self, path: impl AsRef) -> Result<()> { + pub fn create_directory_all(&self, path: impl AsRef) -> Result<()> { let mut by_path = self.inner.by_path.write().unwrap(); let normalized = normalize_path(path.as_ref(), &self.inner.cwd); @@ -137,73 +199,34 @@ impl MemoryFileSystem { /// * If the directory is not empty /// * The `path` is not a directory /// * The `path` does not exist - pub fn remove_directory(&self, path: impl AsRef) -> Result<()> { - let mut by_path = self.inner.by_path.write().unwrap(); - let normalized = normalize_path(path.as_ref(), &self.inner.cwd); - - // Test if the directory is empty - // Skip the directory path itself - for (maybe_child, _) in by_path.range(normalized.clone()..).skip(1) { - if maybe_child.starts_with(&normalized) { - return Err(directory_not_empty()); - } else if !maybe_child.as_str().starts_with(normalized.as_str()) { - break; - } - } - - match by_path.entry(normalized.clone()) { - std::collections::btree_map::Entry::Occupied(entry) => match entry.get() { - Entry::Directory(_) => { - entry.remove(); - Ok(()) + pub fn remove_directory(&self, path: impl AsRef) -> Result<()> { + fn remove_directory(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> { + let mut by_path = fs.inner.by_path.write().unwrap(); + let normalized = normalize_path(path, &fs.inner.cwd); + + // Test if the directory is empty + // Skip the directory path itself + for (maybe_child, _) in by_path.range(normalized.clone()..).skip(1) { + if maybe_child.starts_with(&normalized) { + return Err(directory_not_empty()); + } else if !maybe_child.as_str().starts_with(normalized.as_str()) { + break; } - Entry::File(_) => Err(not_a_directory()), - }, - std::collections::btree_map::Entry::Vacant(_) => Err(not_found()), - } - } -} - -impl FileSystem for MemoryFileSystem { - fn metadata(&self, path: &FileSystemPath) -> Result { - let by_path = self.inner.by_path.read().unwrap(); - let normalized = normalize_path(path, &self.inner.cwd); - - let entry = by_path.get(&normalized).ok_or_else(not_found)?; - - let metadata = match entry { - Entry::File(file) => Metadata { - revision: file.last_modified.into(), - permissions: Some(Self::PERMISSION), - file_type: FileType::File, - }, - Entry::Directory(directory) => Metadata { - revision: directory.last_modified.into(), - permissions: Some(Self::PERMISSION), - file_type: FileType::Directory, - }, - }; - - Ok(metadata) - } - - fn read(&self, path: &FileSystemPath) -> Result { - let by_path = self.inner.by_path.read().unwrap(); - let normalized = normalize_path(path, &self.inner.cwd); - - let entry = by_path.get(&normalized).ok_or_else(not_found)?; + } - match entry { - Entry::File(file) => Ok(file.content.clone()), - Entry::Directory(_) => Err(is_a_directory()), + match by_path.entry(normalized.clone()) { + std::collections::btree_map::Entry::Occupied(entry) => match entry.get() { + Entry::Directory(_) => { + entry.remove(); + Ok(()) + } + Entry::File(_) => Err(not_a_directory()), + }, + std::collections::btree_map::Entry::Vacant(_) => Err(not_found()), + } } - } - - fn exists(&self, path: &FileSystemPath) -> bool { - let by_path = self.inner.by_path.read().unwrap(); - let normalized = normalize_path(path, &self.inner.cwd); - by_path.contains_key(&normalized) + remove_directory(self, path.as_ref()) } } @@ -272,7 +295,7 @@ fn directory_not_empty() -> std::io::Error { /// Normalizes the path by removing `.` and `..` components and transform the path into an absolute path. /// /// Adapted from https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61 -fn normalize_path(path: &FileSystemPath, cwd: &Utf8Path) -> Utf8PathBuf { +fn normalize_path(path: &SystemPath, cwd: &Utf8Path) -> Utf8PathBuf { let path = camino::Utf8Path::new(path.as_str()); let mut components = path.components().peekable(); @@ -353,14 +376,14 @@ mod tests { use std::io::ErrorKind; use std::time::Duration; - use crate::file_system::{FileSystem, FileSystemPath, MemoryFileSystem, Result}; + use crate::system::{MemoryFileSystem, Result, SystemPath}; /// Creates a file system with the given files. /// /// The content of all files will be empty. fn with_files

(files: impl IntoIterator) -> super::MemoryFileSystem where - P: AsRef, + P: AsRef, { let fs = MemoryFileSystem::new(); fs.write_files(files.into_iter().map(|path| (path, ""))) @@ -371,7 +394,7 @@ mod tests { #[test] fn is_file() { - let path = FileSystemPath::new("a.py"); + let path = SystemPath::new("a.py"); let fs = with_files([path]); assert!(fs.is_file(path)); @@ -382,26 +405,26 @@ mod tests { fn exists() { let fs = with_files(["a.py"]); - assert!(fs.exists(FileSystemPath::new("a.py"))); - assert!(!fs.exists(FileSystemPath::new("b.py"))); + assert!(fs.exists(SystemPath::new("a.py"))); + assert!(!fs.exists(SystemPath::new("b.py"))); } #[test] fn exists_directories() { let fs = with_files(["a/b/c.py"]); - assert!(fs.exists(FileSystemPath::new("a"))); - assert!(fs.exists(FileSystemPath::new("a/b"))); - assert!(fs.exists(FileSystemPath::new("a/b/c.py"))); + assert!(fs.exists(SystemPath::new("a"))); + assert!(fs.exists(SystemPath::new("a/b"))); + assert!(fs.exists(SystemPath::new("a/b/c.py"))); } #[test] fn path_normalization() { let fs = with_files(["a.py"]); - assert!(fs.exists(FileSystemPath::new("a.py"))); - assert!(fs.exists(FileSystemPath::new("/a.py"))); - assert!(fs.exists(FileSystemPath::new("/b/./../a.py"))); + assert!(fs.exists(SystemPath::new("a.py"))); + assert!(fs.exists(SystemPath::new("/a.py"))); + assert!(fs.exists(SystemPath::new("/b/./../a.py"))); } #[test] @@ -410,7 +433,7 @@ mod tests { // The default permissions match the default on Linux: 0755 assert_eq!( - fs.metadata(FileSystemPath::new("a.py"))?.permissions(), + fs.metadata(SystemPath::new("a.py"))?.permissions(), Some(MemoryFileSystem::PERMISSION) ); @@ -420,7 +443,7 @@ mod tests { #[test] fn touch() -> Result<()> { let fs = MemoryFileSystem::new(); - let path = FileSystemPath::new("a.py"); + let path = SystemPath::new("a.py"); // Creates a file if it doesn't exist fs.touch(path)?; @@ -445,16 +468,14 @@ mod tests { fn create_dir_all() { let fs = MemoryFileSystem::new(); - fs.create_directory_all(FileSystemPath::new("a/b/c")) - .unwrap(); + fs.create_directory_all(SystemPath::new("a/b/c")).unwrap(); - assert!(fs.is_directory(FileSystemPath::new("a"))); - assert!(fs.is_directory(FileSystemPath::new("a/b"))); - assert!(fs.is_directory(FileSystemPath::new("a/b/c"))); + assert!(fs.is_directory(SystemPath::new("a"))); + assert!(fs.is_directory(SystemPath::new("a/b"))); + assert!(fs.is_directory(SystemPath::new("a/b/c"))); // Should not fail if the directory already exists - fs.create_directory_all(FileSystemPath::new("a/b/c")) - .unwrap(); + fs.create_directory_all(SystemPath::new("a/b/c")).unwrap(); } #[test] @@ -462,7 +483,7 @@ mod tests { let fs = with_files(["a/b.py"]); let error = fs - .create_directory_all(FileSystemPath::new("a/b.py/c")) + .create_directory_all(SystemPath::new("a/b.py/c")) .unwrap_err(); assert_eq!(error.kind(), ErrorKind::Other); } @@ -472,7 +493,7 @@ mod tests { let fs = with_files(["a/b.py"]); let error = fs - .write_file(FileSystemPath::new("a/b.py/c"), "content".to_string()) + .write_file(SystemPath::new("a/b.py/c"), "content".to_string()) .unwrap_err(); assert_eq!(error.kind(), ErrorKind::Other); @@ -485,7 +506,7 @@ mod tests { fs.create_directory_all("a")?; let error = fs - .write_file(FileSystemPath::new("a"), "content".to_string()) + .write_file(SystemPath::new("a"), "content".to_string()) .unwrap_err(); assert_eq!(error.kind(), ErrorKind::Other); @@ -496,11 +517,11 @@ mod tests { #[test] fn read() -> Result<()> { let fs = MemoryFileSystem::new(); - let path = FileSystemPath::new("a.py"); + let path = SystemPath::new("a.py"); fs.write_file(path, "Test content".to_string())?; - assert_eq!(fs.read(path)?, "Test content"); + assert_eq!(fs.read_to_string(path)?, "Test content"); Ok(()) } @@ -511,7 +532,7 @@ mod tests { fs.create_directory_all("a")?; - let error = fs.read(FileSystemPath::new("a")).unwrap_err(); + let error = fs.read_to_string(SystemPath::new("a")).unwrap_err(); assert_eq!(error.kind(), ErrorKind::Other); @@ -522,7 +543,7 @@ mod tests { fn read_fails_if_path_doesnt_exist() -> Result<()> { let fs = MemoryFileSystem::new(); - let error = fs.read(FileSystemPath::new("a")).unwrap_err(); + let error = fs.read_to_string(SystemPath::new("a")).unwrap_err(); assert_eq!(error.kind(), ErrorKind::NotFound); @@ -535,13 +556,13 @@ mod tests { fs.remove_file("a/a.py")?; - assert!(!fs.exists(FileSystemPath::new("a/a.py"))); + assert!(!fs.exists(SystemPath::new("a/a.py"))); // It doesn't delete the enclosing directories - assert!(fs.exists(FileSystemPath::new("a"))); + assert!(fs.exists(SystemPath::new("a"))); // It doesn't delete unrelated files. - assert!(fs.exists(FileSystemPath::new("b.py"))); + assert!(fs.exists(SystemPath::new("b.py"))); Ok(()) } @@ -573,10 +594,10 @@ mod tests { fs.remove_directory("a")?; - assert!(!fs.exists(FileSystemPath::new("a"))); + assert!(!fs.exists(SystemPath::new("a"))); // It doesn't delete unrelated files. - assert!(fs.exists(FileSystemPath::new("b.py"))); + assert!(fs.exists(SystemPath::new("b.py"))); Ok(()) } @@ -596,9 +617,9 @@ mod tests { fs.remove_directory("foo").unwrap(); - assert!(!fs.exists(FileSystemPath::new("foo"))); - assert!(fs.exists(FileSystemPath::new("foo_bar.py"))); - assert!(fs.exists(FileSystemPath::new("foob.py"))); + assert!(!fs.exists(SystemPath::new("foo"))); + assert!(fs.exists(SystemPath::new("foo_bar.py"))); + assert!(fs.exists(SystemPath::new("foob.py"))); Ok(()) } diff --git a/crates/ruff_db/src/file_system/os.rs b/crates/ruff_db/src/system/os.rs similarity index 70% rename from crates/ruff_db/src/file_system/os.rs rename to crates/ruff_db/src/system/os.rs index d3f5faf40e9ac..79c27c27ecd00 100644 --- a/crates/ruff_db/src/file_system/os.rs +++ b/crates/ruff_db/src/system/os.rs @@ -1,11 +1,12 @@ use filetime::FileTime; +use std::any::Any; -use crate::file_system::{FileSystem, FileSystemPath, FileType, Metadata, Result}; +use crate::system::{FileType, Metadata, Result, System, SystemPath}; #[derive(Default, Debug)] -pub struct OsFileSystem; +pub struct OsSystem; -impl OsFileSystem { +impl OsSystem { #[cfg(unix)] fn permissions(metadata: &std::fs::Metadata) -> Option { use std::os::unix::fs::PermissionsExt; @@ -23,8 +24,8 @@ impl OsFileSystem { } } -impl FileSystem for OsFileSystem { - fn metadata(&self, path: &FileSystemPath) -> Result { +impl System for OsSystem { + fn path_metadata(&self, path: &SystemPath) -> Result { let metadata = path.as_std_path().metadata()?; let last_modified = FileTime::from_last_modification_time(&metadata); @@ -35,13 +36,17 @@ impl FileSystem for OsFileSystem { }) } - fn read(&self, path: &FileSystemPath) -> Result { - std::fs::read_to_string(path) + fn read_to_string(&self, path: &SystemPath) -> Result { + std::fs::read_to_string(path.as_std_path()) } - fn exists(&self, path: &FileSystemPath) -> bool { + fn path_exists(&self, path: &SystemPath) -> bool { path.as_std_path().exists() } + + fn as_any(&self) -> &dyn Any { + self + } } impl From for FileType { diff --git a/crates/ruff_db/src/file_system.rs b/crates/ruff_db/src/system/path.rs similarity index 51% rename from crates/ruff_db/src/file_system.rs rename to crates/ruff_db/src/system/path.rs index 1e6e90059219b..8e370f75551eb 100644 --- a/crates/ruff_db/src/file_system.rs +++ b/crates/ruff_db/src/system/path.rs @@ -1,66 +1,25 @@ +// TODO support untitled files for the LSP use case. Wrap a `str` and `String` +// The main question is how `as_std_path` would work for untitled files, that can only exist in the LSP case +// but there's no compile time guarantee that a [`OsSystem`] never gets an untitled file path. + +use camino::{Utf8Path, Utf8PathBuf}; use std::fmt::Formatter; use std::ops::Deref; use std::path::{Path, StripPrefixError}; -use camino::{Utf8Path, Utf8PathBuf}; - -use crate::file_revision::FileRevision; -pub use memory::MemoryFileSystem; -pub use os::OsFileSystem; - -mod memory; -mod os; - -pub type Result = std::io::Result; - -/// An abstraction over `std::fs` with features tailored to Ruff's needs. -/// -/// Provides a file system agnostic API to interact with files and directories. -/// Abstracting the file system operations enables: -/// -/// * Accessing unsaved or even untitled files in the LSP use case -/// * Testing with an in-memory file system -/// * Running Ruff in a WASM environment without needing to stub out the full `std::fs` API. -pub trait FileSystem: std::fmt::Debug { - /// Reads the metadata of the file or directory at `path`. - fn metadata(&self, path: &FileSystemPath) -> Result; - - /// Reads the content of the file at `path`. - fn read(&self, path: &FileSystemPath) -> Result; - - /// Returns `true` if `path` exists. - fn exists(&self, path: &FileSystemPath) -> bool; - - /// Returns `true` if `path` exists and is a directory. - fn is_directory(&self, path: &FileSystemPath) -> bool { - self.metadata(path) - .map_or(false, |metadata| metadata.file_type.is_directory()) - } - - /// Returns `true` if `path` exists and is a file. - fn is_file(&self, path: &FileSystemPath) -> bool { - self.metadata(path) - .map_or(false, |metadata| metadata.file_type.is_file()) - } -} - -// TODO support untitled files for the LSP use case. Wrap a `str` and `String` -// The main question is how `as_std_path` would work for untitled files, that can only exist in the LSP case -// but there's no compile time guarantee that a [`OsFileSystem`] never gets an untitled file path. - -/// Path to a file or directory stored in [`FileSystem`]. +/// A slice of a path on [`System`](super::System) (akin to [`str`]). /// /// The path is guaranteed to be valid UTF-8. #[repr(transparent)] #[derive(Eq, PartialEq, Hash, PartialOrd, Ord)] -pub struct FileSystemPath(Utf8Path); +pub struct SystemPath(Utf8Path); -impl FileSystemPath { +impl SystemPath { pub fn new(path: &(impl AsRef + ?Sized)) -> &Self { let path = path.as_ref(); // SAFETY: FsPath is marked as #[repr(transparent)] so the conversion from a // *const Utf8Path to a *const FsPath is valid. - unsafe { &*(path as *const Utf8Path as *const FileSystemPath) } + unsafe { &*(path as *const Utf8Path as *const SystemPath) } } /// Extracts the file extension, if possible. @@ -75,10 +34,10 @@ impl FileSystemPath { /// # Examples /// /// ``` - /// use ruff_db::file_system::FileSystemPath; + /// use ruff_db::system::SystemPath; /// - /// assert_eq!("rs", FileSystemPath::new("foo.rs").extension().unwrap()); - /// assert_eq!("gz", FileSystemPath::new("foo.tar.gz").extension().unwrap()); + /// assert_eq!("rs", SystemPath::new("foo.rs").extension().unwrap()); + /// assert_eq!("gz", SystemPath::new("foo.tar.gz").extension().unwrap()); /// ``` /// /// See [`Path::extension`] for more details. @@ -95,9 +54,9 @@ impl FileSystemPath { /// # Examples /// /// ``` - /// use ruff_db::file_system::FileSystemPath; + /// use ruff_db::system::SystemPath; /// - /// let path = FileSystemPath::new("/etc/passwd"); + /// let path = SystemPath::new("/etc/passwd"); /// /// assert!(path.starts_with("/etc")); /// assert!(path.starts_with("/etc/")); @@ -108,11 +67,11 @@ impl FileSystemPath { /// assert!(!path.starts_with("/e")); /// assert!(!path.starts_with("/etc/passwd.txt")); /// - /// assert!(!FileSystemPath::new("/etc/foo.rs").starts_with("/etc/foo")); + /// assert!(!SystemPath::new("/etc/foo.rs").starts_with("/etc/foo")); /// ``` #[inline] #[must_use] - pub fn starts_with(&self, base: impl AsRef) -> bool { + pub fn starts_with(&self, base: impl AsRef) -> bool { self.0.starts_with(base.as_ref()) } @@ -123,9 +82,9 @@ impl FileSystemPath { /// # Examples /// /// ``` - /// use ruff_db::file_system::FileSystemPath; + /// use ruff_db::system::SystemPath; /// - /// let path = FileSystemPath::new("/etc/resolv.conf"); + /// let path = SystemPath::new("/etc/resolv.conf"); /// /// assert!(path.ends_with("resolv.conf")); /// assert!(path.ends_with("etc/resolv.conf")); @@ -136,7 +95,7 @@ impl FileSystemPath { /// ``` #[inline] #[must_use] - pub fn ends_with(&self, child: impl AsRef) -> bool { + pub fn ends_with(&self, child: impl AsRef) -> bool { self.0.ends_with(child.as_ref()) } @@ -147,20 +106,20 @@ impl FileSystemPath { /// # Examples /// /// ``` - /// use ruff_db::file_system::FileSystemPath; + /// use ruff_db::system::SystemPath; /// - /// let path = FileSystemPath::new("/foo/bar"); + /// let path = SystemPath::new("/foo/bar"); /// let parent = path.parent().unwrap(); - /// assert_eq!(parent, FileSystemPath::new("/foo")); + /// assert_eq!(parent, SystemPath::new("/foo")); /// /// let grand_parent = parent.parent().unwrap(); - /// assert_eq!(grand_parent, FileSystemPath::new("/")); + /// assert_eq!(grand_parent, SystemPath::new("/")); /// assert_eq!(grand_parent.parent(), None); /// ``` #[inline] #[must_use] - pub fn parent(&self) -> Option<&FileSystemPath> { - self.0.parent().map(FileSystemPath::new) + pub fn parent(&self) -> Option<&SystemPath> { + self.0.parent().map(SystemPath::new) } /// Produces an iterator over the [`camino::Utf8Component`]s of the path. @@ -185,9 +144,9 @@ impl FileSystemPath { /// /// ``` /// use camino::{Utf8Component}; - /// use ruff_db::file_system::FileSystemPath; + /// use ruff_db::system::SystemPath; /// - /// let mut components = FileSystemPath::new("/tmp/foo.txt").components(); + /// let mut components = SystemPath::new("/tmp/foo.txt").components(); /// /// assert_eq!(components.next(), Some(Utf8Component::RootDir)); /// assert_eq!(components.next(), Some(Utf8Component::Normal("tmp"))); @@ -212,14 +171,14 @@ impl FileSystemPath { /// /// ``` /// use camino::Utf8Path; - /// use ruff_db::file_system::FileSystemPath; - /// - /// assert_eq!(Some("bin"), FileSystemPath::new("/usr/bin/").file_name()); - /// assert_eq!(Some("foo.txt"), FileSystemPath::new("tmp/foo.txt").file_name()); - /// assert_eq!(Some("foo.txt"), FileSystemPath::new("foo.txt/.").file_name()); - /// assert_eq!(Some("foo.txt"), FileSystemPath::new("foo.txt/.//").file_name()); - /// assert_eq!(None, FileSystemPath::new("foo.txt/..").file_name()); - /// assert_eq!(None, FileSystemPath::new("/").file_name()); + /// use ruff_db::system::SystemPath; + /// + /// assert_eq!(Some("bin"), SystemPath::new("/usr/bin/").file_name()); + /// assert_eq!(Some("foo.txt"), SystemPath::new("tmp/foo.txt").file_name()); + /// assert_eq!(Some("foo.txt"), SystemPath::new("foo.txt/.").file_name()); + /// assert_eq!(Some("foo.txt"), SystemPath::new("foo.txt/.//").file_name()); + /// assert_eq!(None, SystemPath::new("foo.txt/..").file_name()); + /// assert_eq!(None, SystemPath::new("/").file_name()); /// ``` #[inline] #[must_use] @@ -229,7 +188,7 @@ impl FileSystemPath { /// Extracts the stem (non-extension) portion of [`self.file_name`]. /// - /// [`self.file_name`]: FileSystemPath::file_name + /// [`self.file_name`]: SystemPath::file_name /// /// The stem is: /// @@ -241,10 +200,10 @@ impl FileSystemPath { /// # Examples /// /// ``` - /// use ruff_db::file_system::FileSystemPath; + /// use ruff_db::system::SystemPath; /// - /// assert_eq!("foo", FileSystemPath::new("foo.rs").file_stem().unwrap()); - /// assert_eq!("foo.tar", FileSystemPath::new("foo.tar.gz").file_stem().unwrap()); + /// assert_eq!("foo", SystemPath::new("foo.rs").file_stem().unwrap()); + /// assert_eq!("foo.tar", SystemPath::new("foo.tar.gz").file_stem().unwrap()); /// ``` #[inline] #[must_use] @@ -259,77 +218,77 @@ impl FileSystemPath { /// If `base` is not a prefix of `self` (i.e., [`starts_with`] /// returns `false`), returns [`Err`]. /// - /// [`starts_with`]: FileSystemPath::starts_with + /// [`starts_with`]: SystemPath::starts_with /// /// # Examples /// /// ``` - /// use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf}; + /// use ruff_db::system::{SystemPath, SystemPathBuf}; /// - /// let path = FileSystemPath::new("/test/haha/foo.txt"); + /// let path = SystemPath::new("/test/haha/foo.txt"); /// - /// assert_eq!(path.strip_prefix("/"), Ok(FileSystemPath::new("test/haha/foo.txt"))); - /// assert_eq!(path.strip_prefix("/test"), Ok(FileSystemPath::new("haha/foo.txt"))); - /// assert_eq!(path.strip_prefix("/test/"), Ok(FileSystemPath::new("haha/foo.txt"))); - /// assert_eq!(path.strip_prefix("/test/haha/foo.txt"), Ok(FileSystemPath::new(""))); - /// assert_eq!(path.strip_prefix("/test/haha/foo.txt/"), Ok(FileSystemPath::new(""))); + /// assert_eq!(path.strip_prefix("/"), Ok(SystemPath::new("test/haha/foo.txt"))); + /// assert_eq!(path.strip_prefix("/test"), Ok(SystemPath::new("haha/foo.txt"))); + /// assert_eq!(path.strip_prefix("/test/"), Ok(SystemPath::new("haha/foo.txt"))); + /// assert_eq!(path.strip_prefix("/test/haha/foo.txt"), Ok(SystemPath::new(""))); + /// assert_eq!(path.strip_prefix("/test/haha/foo.txt/"), Ok(SystemPath::new(""))); /// /// assert!(path.strip_prefix("test").is_err()); /// assert!(path.strip_prefix("/haha").is_err()); /// - /// let prefix = FileSystemPathBuf::from("/test/"); - /// assert_eq!(path.strip_prefix(prefix), Ok(FileSystemPath::new("haha/foo.txt"))); + /// let prefix = SystemPathBuf::from("/test/"); + /// assert_eq!(path.strip_prefix(prefix), Ok(SystemPath::new("haha/foo.txt"))); /// ``` #[inline] pub fn strip_prefix( &self, - base: impl AsRef, - ) -> std::result::Result<&FileSystemPath, StripPrefixError> { - self.0.strip_prefix(base.as_ref()).map(FileSystemPath::new) + base: impl AsRef, + ) -> std::result::Result<&SystemPath, StripPrefixError> { + self.0.strip_prefix(base.as_ref()).map(SystemPath::new) } - /// Creates an owned [`FileSystemPathBuf`] with `path` adjoined to `self`. + /// Creates an owned [`SystemPathBuf`] with `path` adjoined to `self`. /// /// See [`std::path::PathBuf::push`] for more details on what it means to adjoin a path. /// /// # Examples /// /// ``` - /// use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf}; + /// use ruff_db::system::{SystemPath, SystemPathBuf}; /// - /// assert_eq!(FileSystemPath::new("/etc").join("passwd"), FileSystemPathBuf::from("/etc/passwd")); + /// assert_eq!(SystemPath::new("/etc").join("passwd"), SystemPathBuf::from("/etc/passwd")); /// ``` #[inline] #[must_use] - pub fn join(&self, path: impl AsRef) -> FileSystemPathBuf { - FileSystemPathBuf::from_utf8_path_buf(self.0.join(&path.as_ref().0)) + pub fn join(&self, path: impl AsRef) -> SystemPathBuf { + SystemPathBuf::from_utf8_path_buf(self.0.join(&path.as_ref().0)) } - /// Creates an owned [`FileSystemPathBuf`] like `self` but with the given extension. + /// Creates an owned [`SystemPathBuf`] like `self` but with the given extension. /// /// See [`std::path::PathBuf::set_extension`] for more details. /// /// # Examples /// /// ``` - /// use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf}; + /// use ruff_db::system::{SystemPath, SystemPathBuf}; /// - /// let path = FileSystemPath::new("foo.rs"); - /// assert_eq!(path.with_extension("txt"), FileSystemPathBuf::from("foo.txt")); + /// let path = SystemPath::new("foo.rs"); + /// assert_eq!(path.with_extension("txt"), SystemPathBuf::from("foo.txt")); /// - /// let path = FileSystemPath::new("foo.tar.gz"); - /// assert_eq!(path.with_extension(""), FileSystemPathBuf::from("foo.tar")); - /// assert_eq!(path.with_extension("xz"), FileSystemPathBuf::from("foo.tar.xz")); - /// assert_eq!(path.with_extension("").with_extension("txt"), FileSystemPathBuf::from("foo.txt")); + /// let path = SystemPath::new("foo.tar.gz"); + /// assert_eq!(path.with_extension(""), SystemPathBuf::from("foo.tar")); + /// assert_eq!(path.with_extension("xz"), SystemPathBuf::from("foo.tar.xz")); + /// assert_eq!(path.with_extension("").with_extension("txt"), SystemPathBuf::from("foo.txt")); /// ``` #[inline] - pub fn with_extension(&self, extension: &str) -> FileSystemPathBuf { - FileSystemPathBuf::from_utf8_path_buf(self.0.with_extension(extension)) + pub fn with_extension(&self, extension: &str) -> SystemPathBuf { + SystemPathBuf::from_utf8_path_buf(self.0.with_extension(extension)) } - /// Converts the path to an owned [`FileSystemPathBuf`]. - pub fn to_path_buf(&self) -> FileSystemPathBuf { - FileSystemPathBuf(self.0.to_path_buf()) + /// Converts the path to an owned [`SystemPathBuf`]. + pub fn to_path_buf(&self) -> SystemPathBuf { + SystemPathBuf(self.0.to_path_buf()) } /// Returns the path as a string slice. @@ -344,19 +303,19 @@ impl FileSystemPath { self.0.as_std_path() } - pub fn from_std_path(path: &Path) -> Option<&FileSystemPath> { - Some(FileSystemPath::new(Utf8Path::from_path(path)?)) + pub fn from_std_path(path: &Path) -> Option<&SystemPath> { + Some(SystemPath::new(Utf8Path::from_path(path)?)) } } -/// Owned path to a file or directory stored in [`FileSystem`]. +/// An owned, mutable path on [`System`](`super::System`) (akin to [`String`]). /// /// The path is guaranteed to be valid UTF-8. #[repr(transparent)] #[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)] -pub struct FileSystemPathBuf(Utf8PathBuf); +pub struct SystemPathBuf(Utf8PathBuf); -impl FileSystemPathBuf { +impl SystemPathBuf { pub fn new() -> Self { Self(Utf8PathBuf::new()) } @@ -386,82 +345,82 @@ impl FileSystemPathBuf { /// Pushing a relative path extends the existing path: /// /// ``` - /// use ruff_db::file_system::FileSystemPathBuf; + /// use ruff_db::system::SystemPathBuf; /// - /// let mut path = FileSystemPathBuf::from("/tmp"); + /// let mut path = SystemPathBuf::from("/tmp"); /// path.push("file.bk"); - /// assert_eq!(path, FileSystemPathBuf::from("/tmp/file.bk")); + /// assert_eq!(path, SystemPathBuf::from("/tmp/file.bk")); /// ``` /// /// Pushing an absolute path replaces the existing path: /// /// ``` /// - /// use ruff_db::file_system::FileSystemPathBuf; + /// use ruff_db::system::SystemPathBuf; /// - /// let mut path = FileSystemPathBuf::from("/tmp"); + /// let mut path = SystemPathBuf::from("/tmp"); /// path.push("/etc"); - /// assert_eq!(path, FileSystemPathBuf::from("/etc")); + /// assert_eq!(path, SystemPathBuf::from("/etc")); /// ``` - pub fn push(&mut self, path: impl AsRef) { + pub fn push(&mut self, path: impl AsRef) { self.0.push(&path.as_ref().0); } #[inline] - pub fn as_path(&self) -> &FileSystemPath { - FileSystemPath::new(&self.0) + pub fn as_path(&self) -> &SystemPath { + SystemPath::new(&self.0) } } -impl From<&str> for FileSystemPathBuf { +impl From<&str> for SystemPathBuf { fn from(value: &str) -> Self { - FileSystemPathBuf::from_utf8_path_buf(Utf8PathBuf::from(value)) + SystemPathBuf::from_utf8_path_buf(Utf8PathBuf::from(value)) } } -impl Default for FileSystemPathBuf { +impl Default for SystemPathBuf { fn default() -> Self { Self::new() } } -impl AsRef for FileSystemPathBuf { +impl AsRef for SystemPathBuf { #[inline] - fn as_ref(&self) -> &FileSystemPath { + fn as_ref(&self) -> &SystemPath { self.as_path() } } -impl AsRef for FileSystemPath { +impl AsRef for SystemPath { #[inline] - fn as_ref(&self) -> &FileSystemPath { + fn as_ref(&self) -> &SystemPath { self } } -impl AsRef for str { +impl AsRef for str { #[inline] - fn as_ref(&self) -> &FileSystemPath { - FileSystemPath::new(self) + fn as_ref(&self) -> &SystemPath { + SystemPath::new(self) } } -impl AsRef for String { +impl AsRef for String { #[inline] - fn as_ref(&self) -> &FileSystemPath { - FileSystemPath::new(self) + fn as_ref(&self) -> &SystemPath { + SystemPath::new(self) } } -impl AsRef for FileSystemPath { +impl AsRef for SystemPath { #[inline] fn as_ref(&self) -> &Path { self.0.as_std_path() } } -impl Deref for FileSystemPathBuf { - type Target = FileSystemPath; +impl Deref for SystemPathBuf { + type Target = SystemPath; #[inline] fn deref(&self) -> &Self::Target { @@ -469,68 +428,26 @@ impl Deref for FileSystemPathBuf { } } -impl std::fmt::Debug for FileSystemPath { +impl std::fmt::Debug for SystemPath { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } -impl std::fmt::Display for FileSystemPath { +impl std::fmt::Display for SystemPath { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } -impl std::fmt::Debug for FileSystemPathBuf { +impl std::fmt::Debug for SystemPathBuf { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } -impl std::fmt::Display for FileSystemPathBuf { +impl std::fmt::Display for SystemPathBuf { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Metadata { - revision: FileRevision, - permissions: Option, - file_type: FileType, -} - -impl Metadata { - pub fn revision(&self) -> FileRevision { - self.revision - } - - pub fn permissions(&self) -> Option { - self.permissions - } - - pub fn file_type(&self) -> FileType { - self.file_type - } -} - -#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] -pub enum FileType { - File, - Directory, - Symlink, -} - -impl FileType { - pub const fn is_file(self) -> bool { - matches!(self, FileType::File) - } - - pub const fn is_directory(self) -> bool { - matches!(self, FileType::Directory) - } - - pub const fn is_symlink(self) -> bool { - matches!(self, FileType::Symlink) - } -} diff --git a/crates/ruff_db/src/system/test.rs b/crates/ruff_db/src/system/test.rs new file mode 100644 index 0000000000000..27b872e595f6b --- /dev/null +++ b/crates/ruff_db/src/system/test.rs @@ -0,0 +1,167 @@ +use crate::files::{File, FilePath}; +use crate::system::{MemoryFileSystem, Metadata, OsSystem, System, SystemPath}; +use crate::Db; +use std::any::Any; + +/// System implementation intended for testing. +/// +/// It uses a memory-file system by default, but can be switched to the real file system for tests +/// verifying more advanced file system features. +/// +/// ## Warning +/// Don't use this system for production code. It's intended for testing only. +#[derive(Default, Debug)] +pub struct TestSystem { + inner: TestFileSystem, +} + +impl TestSystem { + pub fn snapshot(&self) -> Self { + Self { + inner: self.inner.snapshot(), + } + } + + /// Returns the memory file system. + /// + /// ## Panics + /// If this test db isn't using a memory file system. + pub fn memory_file_system(&self) -> &MemoryFileSystem { + if let TestFileSystem::Stub(fs) = &self.inner { + fs + } else { + panic!("The test db is not using a memory file system"); + } + } + + fn use_os_system(&mut self) { + self.inner = TestFileSystem::Os(OsSystem); + } +} + +impl System for TestSystem { + fn path_metadata(&self, path: &SystemPath) -> crate::system::Result { + match &self.inner { + TestFileSystem::Stub(fs) => fs.metadata(path), + TestFileSystem::Os(fs) => fs.path_metadata(path), + } + } + + fn read_to_string(&self, path: &SystemPath) -> crate::system::Result { + match &self.inner { + TestFileSystem::Stub(fs) => fs.read_to_string(path), + TestFileSystem::Os(fs) => fs.read_to_string(path), + } + } + + fn path_exists(&self, path: &SystemPath) -> bool { + match &self.inner { + TestFileSystem::Stub(fs) => fs.exists(path), + TestFileSystem::Os(fs) => fs.path_exists(path), + } + } + + fn is_directory(&self, path: &SystemPath) -> bool { + match &self.inner { + TestFileSystem::Stub(fs) => fs.is_directory(path), + TestFileSystem::Os(fs) => fs.is_directory(path), + } + } + + fn is_file(&self, path: &SystemPath) -> bool { + match &self.inner { + TestFileSystem::Stub(fs) => fs.is_file(path), + TestFileSystem::Os(fs) => fs.is_file(path), + } + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +/// Extension trait for databases that use [`TestSystem`]. +/// +/// Provides various helper function that ease testing. +pub trait DbWithTestSystem: Db + Sized { + fn test_system(&self) -> &TestSystem; + + fn test_system_mut(&mut self) -> &mut TestSystem; + + /// Writes the content of the given file and notifies the Db about the change. + /// + /// # Panics + /// If the system isn't using the memory file system. + fn write_file( + &mut self, + path: impl AsRef, + content: impl ToString, + ) -> crate::system::Result<()> { + let path = path.as_ref().to_path_buf(); + let result = self + .test_system() + .memory_file_system() + .write_file(&path, content); + + if result.is_ok() { + File::touch_path(self, &FilePath::System(path)); + } + + result + } + + /// Writes the content of the given file and notifies the Db about the change. + /// + /// # Panics + /// If the system isn't using the memory file system for testing. + fn write_files(&mut self, files: I) -> crate::system::Result<()> + where + I: IntoIterator, + P: AsRef, + C: ToString, + { + for (path, content) in files { + self.write_file(path, content)?; + } + + Ok(()) + } + + /// Uses the real file system instead of the memory file system. + /// + /// This useful for testing advanced file system features like permissions, symlinks, etc. + /// + /// Note that any files written to the memory file system won't be copied over. + fn use_os_system(&mut self) { + self.test_system_mut().use_os_system(); + } + + /// Returns the memory file system. + /// + /// ## Panics + /// If this system isn't using a memory file system. + fn memory_file_system(&self) -> &MemoryFileSystem { + self.test_system().memory_file_system() + } +} + +#[derive(Debug)] +enum TestFileSystem { + Stub(MemoryFileSystem), + Os(OsSystem), +} + +impl TestFileSystem { + fn snapshot(&self) -> Self { + match self { + Self::Stub(fs) => Self::Stub(fs.snapshot()), + Self::Os(fs) => Self::Os(fs.snapshot()), + } + } +} + +impl Default for TestFileSystem { + fn default() -> Self { + Self::Stub(MemoryFileSystem::default()) + } +} diff --git a/crates/ruff_db/src/vendored.rs b/crates/ruff_db/src/vendored.rs index d1a4d2f083774..27f03163ef91c 100644 --- a/crates/ruff_db/src/vendored.rs +++ b/crates/ruff_db/src/vendored.rs @@ -2,14 +2,15 @@ use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt::{self, Debug}; use std::io::{self, Read}; -use std::sync::{Mutex, MutexGuard}; +use std::sync::{Arc, Mutex, MutexGuard}; -use zip::{read::ZipFile, ZipArchive}; +use zip::{read::ZipFile, ZipArchive, ZipWriter}; use crate::file_revision::FileRevision; -pub use path::{VendoredPath, VendoredPathBuf}; -pub mod path; +pub use self::path::{VendoredPath, VendoredPathBuf}; + +mod path; type Result = io::Result; type LockedZipArchive<'a> = MutexGuard<'a, VendoredZipArchive>; @@ -20,46 +21,75 @@ type LockedZipArchive<'a> = MutexGuard<'a, VendoredZipArchive>; /// "Files" in the `VendoredFileSystem` are read-only and immutable. /// Directories are supported, but symlinks and hardlinks cannot exist. pub struct VendoredFileSystem { - inner: Mutex, + inner: Arc>, } impl VendoredFileSystem { - pub fn new(raw_bytes: &'static [u8]) -> Result { + pub fn new_static(raw_bytes: &'static [u8]) -> Result { + Self::new_impl(Cow::Borrowed(raw_bytes)) + } + + pub fn new(raw_bytes: Vec) -> Result { + Self::new_impl(Cow::Owned(raw_bytes)) + } + + fn new_impl(data: Cow<'static, [u8]>) -> Result { Ok(Self { - inner: Mutex::new(VendoredZipArchive::new(raw_bytes)?), + inner: Arc::new(Mutex::new(VendoredZipArchive::new(data)?)), }) } - pub fn exists(&self, path: &VendoredPath) -> bool { - let normalized = NormalizedVendoredPath::from(path); - let mut archive = self.lock_archive(); - - // Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered - // different paths in a zip file, but we want to abstract over that difference here - // so that paths relative to the `VendoredFileSystem` - // work the same as other paths in Ruff. - archive.lookup_path(&normalized).is_ok() - || archive - .lookup_path(&normalized.with_trailing_slash()) - .is_ok() + pub fn snapshot(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } } - pub fn metadata(&self, path: &VendoredPath) -> Option { - let normalized = NormalizedVendoredPath::from(path); - let mut archive = self.lock_archive(); + pub fn exists(&self, path: impl AsRef) -> bool { + fn exists(fs: &VendoredFileSystem, path: &VendoredPath) -> bool { + let normalized = NormalizedVendoredPath::from(path); + let mut archive = fs.lock_archive(); - // Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered - // different paths in a zip file, but we want to abstract over that difference here - // so that paths relative to the `VendoredFileSystem` - // work the same as other paths in Ruff. - if let Ok(zip_file) = archive.lookup_path(&normalized) { - return Some(Metadata::from_zip_file(zip_file)); + // Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered + // different paths in a zip file, but we want to abstract over that difference here + // so that paths relative to the `VendoredFileSystem` + // work the same as other paths in Ruff. + archive.lookup_path(&normalized).is_ok() + || archive + .lookup_path(&normalized.with_trailing_slash()) + .is_ok() } - if let Ok(zip_file) = archive.lookup_path(&normalized.with_trailing_slash()) { - return Some(Metadata::from_zip_file(zip_file)); + + exists(self, path.as_ref()) + } + + pub fn metadata(&self, path: impl AsRef) -> Result { + fn metadata(fs: &VendoredFileSystem, path: &VendoredPath) -> Result { + let normalized = NormalizedVendoredPath::from(path); + let mut archive = fs.lock_archive(); + + // Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered + // different paths in a zip file, but we want to abstract over that difference here + // so that paths relative to the `VendoredFileSystem` + // work the same as other paths in Ruff. + if let Ok(zip_file) = archive.lookup_path(&normalized) { + return Ok(Metadata::from_zip_file(zip_file)); + } + let zip_file = archive.lookup_path(&normalized.with_trailing_slash())?; + Ok(Metadata::from_zip_file(zip_file)) } - None + metadata(self, path.as_ref()) + } + + pub fn is_directory(&self, path: impl AsRef) -> bool { + self.metadata(path) + .is_ok_and(|metadata| metadata.kind().is_directory()) + } + + pub fn is_file(&self, path: impl AsRef) -> bool { + self.metadata(path) + .is_ok_and(|metadata| metadata.kind().is_file()) } /// Read the entire contents of the zip file at `path` into a string @@ -68,12 +98,16 @@ impl VendoredFileSystem { /// - The path does not exist in the underlying zip archive /// - The path exists in the underlying zip archive, but represents a directory /// - The contents of the zip file at `path` contain invalid UTF-8 - pub fn read(&self, path: &VendoredPath) -> Result { - let mut archive = self.lock_archive(); - let mut zip_file = archive.lookup_path(&NormalizedVendoredPath::from(path))?; - let mut buffer = String::new(); - zip_file.read_to_string(&mut buffer)?; - Ok(buffer) + pub fn read_to_string(&self, path: impl AsRef) -> Result { + fn read_to_string(fs: &VendoredFileSystem, path: &VendoredPath) -> Result { + let mut archive = fs.lock_archive(); + let mut zip_file = archive.lookup_path(&NormalizedVendoredPath::from(path))?; + let mut buffer = String::new(); + zip_file.read_to_string(&mut buffer)?; + Ok(buffer) + } + + read_to_string(self, path.as_ref()) } /// Acquire a lock on the underlying zip archive. @@ -112,6 +146,20 @@ impl fmt::Debug for VendoredFileSystem { } } +impl Default for VendoredFileSystem { + fn default() -> Self { + let mut bytes: Vec = Vec::new(); + let mut cursor = io::Cursor::new(&mut bytes); + + { + let mut writer = ZipWriter::new(&mut cursor); + writer.finish().unwrap(); + } + + VendoredFileSystem::new(bytes).unwrap() + } +} + /// Private struct only used in `Debug` implementations /// /// This could possibly be unified with the `Metadata` struct, @@ -195,10 +243,10 @@ impl Metadata { /// Newtype wrapper around a ZipArchive. #[derive(Debug)] -struct VendoredZipArchive(ZipArchive>); +struct VendoredZipArchive(ZipArchive>>); impl VendoredZipArchive { - fn new(data: &'static [u8]) -> Result { + fn new(data: Cow<'static, [u8]>) -> Result { Ok(Self(ZipArchive::new(io::Cursor::new(data))?)) } @@ -290,11 +338,11 @@ impl<'a> From<&'a VendoredPath> for NormalizedVendoredPath<'a> { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use std::io::Write; use insta::assert_snapshot; - use once_cell::sync::Lazy; + use zip::result::ZipResult; use zip::write::FileOptions; use zip::{CompressionMethod, ZipWriter}; @@ -303,37 +351,66 @@ mod tests { const FUNCTOOLS_CONTENTS: &str = "def update_wrapper(): ..."; const ASYNCIO_TASKS_CONTENTS: &str = "class Task: ..."; - static MOCK_ZIP_ARCHIVE: Lazy> = Lazy::new(|| { - let mut typeshed_buffer = Vec::new(); - let typeshed = io::Cursor::new(&mut typeshed_buffer); + pub struct VendoredFileSystemBuilder { + writer: ZipWriter>>, + } - let options = FileOptions::default() - .compression_method(CompressionMethod::Zstd) - .unix_permissions(0o644); + impl Default for VendoredFileSystemBuilder { + fn default() -> Self { + Self::new() + } + } - { - let mut archive = ZipWriter::new(typeshed); + impl VendoredFileSystemBuilder { + pub fn new() -> Self { + let buffer = io::Cursor::new(Vec::new()); - archive.add_directory("stdlib/", options).unwrap(); - archive.start_file("stdlib/functools.pyi", options).unwrap(); - archive.write_all(FUNCTOOLS_CONTENTS.as_bytes()).unwrap(); + Self { + writer: ZipWriter::new(buffer), + } + } + + pub fn add_file( + &mut self, + path: impl AsRef, + content: &str, + ) -> std::io::Result<()> { + self.writer + .start_file(path.as_ref().as_str(), Self::options())?; + self.writer.write_all(content.as_bytes()) + } + + pub fn add_directory(&mut self, path: impl AsRef) -> ZipResult<()> { + self.writer + .add_directory(path.as_ref().as_str(), Self::options()) + } - archive.add_directory("stdlib/asyncio/", options).unwrap(); - archive - .start_file("stdlib/asyncio/tasks.pyi", options) - .unwrap(); - archive - .write_all(ASYNCIO_TASKS_CONTENTS.as_bytes()) - .unwrap(); + pub fn finish(mut self) -> Result { + let buffer = self.writer.finish()?; - archive.finish().unwrap(); + VendoredFileSystem::new(buffer.into_inner()) } - typeshed_buffer.into_boxed_slice() - }); + fn options() -> FileOptions { + FileOptions::default() + .compression_method(CompressionMethod::Zstd) + .unix_permissions(0o644) + } + } fn mock_typeshed() -> VendoredFileSystem { - VendoredFileSystem::new(&MOCK_ZIP_ARCHIVE).unwrap() + let mut builder = VendoredFileSystemBuilder::new(); + + builder.add_directory("stdlib/").unwrap(); + builder + .add_file("stdlib/functools.pyi", FUNCTOOLS_CONTENTS) + .unwrap(); + builder.add_directory("stdlib/asyncio/").unwrap(); + builder + .add_file("stdlib/asyncio/tasks.pyi", ASYNCIO_TASKS_CONTENTS) + .unwrap(); + + builder.finish().unwrap() } #[test] @@ -395,9 +472,9 @@ mod tests { let path = VendoredPath::new(dirname); assert!(mock_typeshed.exists(path)); - assert!(mock_typeshed.read(path).is_err()); + assert!(mock_typeshed.read_to_string(path).is_err()); let metadata = mock_typeshed.metadata(path).unwrap(); - assert!(metadata.kind.is_directory()); + assert!(metadata.kind().is_directory()); } #[test] @@ -434,9 +511,9 @@ mod tests { let mock_typeshed = mock_typeshed(); let path = VendoredPath::new(path); assert!(!mock_typeshed.exists(path)); - assert!(mock_typeshed.metadata(path).is_none()); + assert!(mock_typeshed.metadata(path).is_err()); assert!(mock_typeshed - .read(path) + .read_to_string(path) .is_err_and(|err| err.to_string().contains("file not found"))); } @@ -463,7 +540,7 @@ mod tests { fn test_file(mock_typeshed: &VendoredFileSystem, path: &VendoredPath) { assert!(mock_typeshed.exists(path)); let metadata = mock_typeshed.metadata(path).unwrap(); - assert!(metadata.kind.is_file()); + assert!(metadata.kind().is_file()); } #[test] @@ -471,11 +548,11 @@ mod tests { let mock_typeshed = mock_typeshed(); let path = VendoredPath::new("stdlib/functools.pyi"); test_file(&mock_typeshed, path); - let functools_stub = mock_typeshed.read(path).unwrap(); + let functools_stub = mock_typeshed.read_to_string(path).unwrap(); assert_eq!(functools_stub.as_str(), FUNCTOOLS_CONTENTS); // Test that using the RefCell doesn't mutate // the internal state of the underlying zip archive incorrectly: - let functools_stub_again = mock_typeshed.read(path).unwrap(); + let functools_stub_again = mock_typeshed.read_to_string(path).unwrap(); assert_eq!(functools_stub_again.as_str(), FUNCTOOLS_CONTENTS); } @@ -492,7 +569,7 @@ mod tests { let mock_typeshed = mock_typeshed(); let path = VendoredPath::new("stdlib/asyncio/tasks.pyi"); test_file(&mock_typeshed, path); - let asyncio_stub = mock_typeshed.read(path).unwrap(); + let asyncio_stub = mock_typeshed.read_to_string(path).unwrap(); assert_eq!(asyncio_stub.as_str(), ASYNCIO_TASKS_CONTENTS); } diff --git a/crates/ruff_db/src/vfs/path.rs b/crates/ruff_db/src/vfs/path.rs deleted file mode 100644 index a053ed4f52fb7..0000000000000 --- a/crates/ruff_db/src/vfs/path.rs +++ /dev/null @@ -1,161 +0,0 @@ -use crate::file_system::{FileSystemPath, FileSystemPathBuf}; -use crate::vendored::path::{VendoredPath, VendoredPathBuf}; - -/// Path to a file. -/// -/// The path abstracts that files in Ruff can come from different sources: -/// -/// * a file stored on disk -/// * a vendored file that ships as part of the ruff binary -/// * Future: A virtual file that references a slice of another file. For example, the CSS code in a python file. -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub enum VfsPath { - /// Path that points to a file on disk. - FileSystem(FileSystemPathBuf), - Vendored(VendoredPathBuf), -} - -impl VfsPath { - /// Create a new path to a file on the file system. - #[must_use] - pub fn file_system(path: impl AsRef) -> Self { - VfsPath::FileSystem(path.as_ref().to_path_buf()) - } - - /// Returns `Some` if the path is a file system path that points to a path on disk. - #[must_use] - #[inline] - pub fn into_file_system_path_buf(self) -> Option { - match self { - VfsPath::FileSystem(path) => Some(path), - VfsPath::Vendored(_) => None, - } - } - - #[must_use] - #[inline] - pub fn as_file_system_path(&self) -> Option<&FileSystemPath> { - match self { - VfsPath::FileSystem(path) => Some(path.as_path()), - VfsPath::Vendored(_) => None, - } - } - - /// Returns `true` if the path is a file system path that points to a path on disk. - #[must_use] - #[inline] - pub const fn is_file_system_path(&self) -> bool { - matches!(self, VfsPath::FileSystem(_)) - } - - /// Returns `true` if the path is a vendored path. - #[must_use] - #[inline] - pub const fn is_vendored_path(&self) -> bool { - matches!(self, VfsPath::Vendored(_)) - } - - #[must_use] - #[inline] - pub fn as_vendored_path(&self) -> Option<&VendoredPath> { - match self { - VfsPath::Vendored(path) => Some(path.as_path()), - VfsPath::FileSystem(_) => None, - } - } - - /// Yields the underlying [`str`] slice. - pub fn as_str(&self) -> &str { - match self { - VfsPath::FileSystem(path) => path.as_str(), - VfsPath::Vendored(path) => path.as_str(), - } - } -} - -impl AsRef for VfsPath { - fn as_ref(&self) -> &str { - self.as_str() - } -} - -impl From for VfsPath { - fn from(value: FileSystemPathBuf) -> Self { - Self::FileSystem(value) - } -} - -impl From<&FileSystemPath> for VfsPath { - fn from(value: &FileSystemPath) -> Self { - VfsPath::FileSystem(value.to_path_buf()) - } -} - -impl From for VfsPath { - fn from(value: VendoredPathBuf) -> Self { - Self::Vendored(value) - } -} - -impl From<&VendoredPath> for VfsPath { - fn from(value: &VendoredPath) -> Self { - Self::Vendored(value.to_path_buf()) - } -} - -impl PartialEq for VfsPath { - #[inline] - fn eq(&self, other: &FileSystemPath) -> bool { - self.as_file_system_path() - .is_some_and(|self_path| self_path == other) - } -} - -impl PartialEq for FileSystemPath { - #[inline] - fn eq(&self, other: &VfsPath) -> bool { - other == self - } -} - -impl PartialEq for VfsPath { - #[inline] - fn eq(&self, other: &FileSystemPathBuf) -> bool { - self == other.as_path() - } -} - -impl PartialEq for FileSystemPathBuf { - fn eq(&self, other: &VfsPath) -> bool { - other == self - } -} - -impl PartialEq for VfsPath { - #[inline] - fn eq(&self, other: &VendoredPath) -> bool { - self.as_vendored_path() - .is_some_and(|self_path| self_path == other) - } -} - -impl PartialEq for VendoredPath { - #[inline] - fn eq(&self, other: &VfsPath) -> bool { - other == self - } -} - -impl PartialEq for VfsPath { - #[inline] - fn eq(&self, other: &VendoredPathBuf) -> bool { - other.as_path() == self - } -} - -impl PartialEq for VendoredPathBuf { - #[inline] - fn eq(&self, other: &VfsPath) -> bool { - other == self - } -}