[red-knot] Rename FileSystem to System (#12214)

This commit is contained in:
Micha Reiser 2024-07-09 09:20:51 +02:00 committed by GitHub
parent 16a63c88cf
commit ac04380f36
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1432 additions and 1291 deletions

310
crates/ruff_db/src/files.rs Normal file
View file

@ -0,0 +1,310 @@
use std::sync::Arc;
use countme::Count;
use dashmap::mapref::entry::Entry;
pub use path::FilePath;
use crate::file_revision::FileRevision;
use crate::files::private::FileStatus;
use crate::system::SystemPath;
use crate::vendored::VendoredPath;
use crate::{Db, FxDashMap};
mod path;
/// Interns a file system path and returns a salsa `File` ingredient.
///
/// 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<SystemPath>) -> Option<File> {
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
// that file. This function filters out files that don't exist, but Salsa will know that it must
// re-run the calling query whenever the `file`'s status changes (because of the `.status` call here).
match file.status(db) {
FileStatus::Exists => Some(file),
FileStatus::Deleted => None,
}
}
/// 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<VendoredPath>) -> Option<File> {
db.files().vendored(db, path.as_ref())
}
/// Lookup table that maps [file paths](`FilePath`) to salsa interned [`File`] instances.
#[derive(Default)]
pub struct Files {
inner: Arc<FilesInner>,
}
#[derive(Default)]
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<FilePath, File>,
}
impl Files {
/// Looks up a file by its `path`.
///
/// 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 system(&self, db: &dyn Db, path: &SystemPath) -> File {
*self
.inner
.files_by_path
.entry(FilePath::System(path.to_path_buf()))
.or_insert_with(|| {
let metadata = db.system().path_metadata(path);
match metadata {
Ok(metadata) if metadata.file_type().is_file() => File::new(
db,
FilePath::System(path.to_path_buf()),
metadata.permissions(),
metadata.revision(),
FileStatus::Exists,
Count::default(),
),
_ => File::new(
db,
FilePath::System(path.to_path_buf()),
None,
FileRevision::zero(),
FileStatus::Deleted,
Count::default(),
),
}
})
}
/// 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<File> {
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<File> {
let file = match self
.inner
.files_by_path
.entry(FilePath::Vendored(path.to_path_buf()))
{
Entry::Occupied(entry) => *entry.get(),
Entry::Vacant(entry) => {
let metadata = db.vendored().metadata(path).ok()?;
let file = File::new(
db,
FilePath::Vendored(path.to_path_buf()),
Some(0o444),
metadata.revision(),
FileStatus::Exists,
Count::default(),
);
entry.insert(file);
file
}
};
Some(file)
}
/// Creates a salsa like snapshot. The instances share
/// the same path-to-file mapping.
pub fn snapshot(&self) -> Self {
Self {
inner: self.inner.clone(),
}
}
}
impl std::fmt::Debug for Files {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut map = f.debug_map();
for entry in self.inner.files_by_path.iter() {
map.entry(entry.key(), entry.value());
}
map.finish()
}
}
/// A file that's either stored on the host system's file system or in the vendored file system.
#[salsa::input]
pub struct File {
/// The path of the file.
#[id]
#[return_ref]
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.
pub permissions: Option<u32>,
/// The file revision. A file has changed if the revisions don't compare equal.
pub revision: FileRevision,
/// The status of the file.
///
/// 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<File>,
}
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
/// that the file has been modified in between the reads. It's even possible that a file that
/// is considered to exist has been deleted in the meantime. If this happens, then the method returns
/// 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_to_string(&self, db: &dyn Db) -> String {
let path = self.path(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.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: &FilePath) {
Self::touch_impl(db, path, None);
}
pub fn touch(self, db: &mut dyn Db) {
let path = self.path(db).clone();
Self::touch_impl(db, &path, Some(self));
}
/// Private method providing the implementation for [`Self::touch_path`] and [`Self::touch`].
fn touch_impl(db: &mut dyn Db, path: &FilePath, file: Option<File>) {
match path {
FilePath::System(path) => {
let metadata = db.system().path_metadata(path);
let (status, revision) = match metadata {
Ok(metadata) if metadata.file_type().is_file() => {
(FileStatus::Exists, metadata.revision())
}
_ => (FileStatus::Deleted, FileRevision::zero()),
};
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);
}
FilePath::Vendored(_) => {
// Readonly, can never be out of date.
}
}
}
}
// 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 {
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum FileStatus {
/// The file exists.
Exists,
/// The file was deleted, didn't exist to begin with or the path isn't a file.
Deleted,
}
}
#[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::vendored::tests::VendoredFileSystemBuilder;
#[test]
fn file_system_existing_file() -> crate::system::Result<()> {
let mut db = TestDb::new();
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_to_string(&db), "print('Hello world')");
Ok(())
}
#[test]
fn file_system_non_existing_file() {
let db = TestDb::new();
let test = system_path_to_file(&db, "test.py");
assert_eq!(test, None);
}
#[test]
fn stubbed_vendored_file() {
let mut db = TestDb::new();
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.pyi").expect("Vendored file to exist.");
assert_eq!(test.permissions(&db), Some(0o444));
assert_ne!(test.revision(&db), FileRevision::zero());
assert_eq!(&test.read_to_string(&db), "def foo() -> str");
}
#[test]
fn stubbed_vendored_file_non_existing() {
let db = TestDb::new();
assert_eq!(vendored_path_to_file(&db, "test.py"), None);
}
}