mirror of https://github.com/astral-sh/ruff
red-knot[salsa part 2]: Setup semantic DB and Jar (#11837)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
9dc226be97
commit
efbf7b14b5
|
|
@ -2329,6 +2329,7 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"is-macro",
|
"is-macro",
|
||||||
|
"ruff_db",
|
||||||
"ruff_index",
|
"ruff_index",
|
||||||
"ruff_python_ast",
|
"ruff_python_ast",
|
||||||
"ruff_python_parser",
|
"ruff_python_parser",
|
||||||
|
|
@ -2336,6 +2337,8 @@ dependencies = [
|
||||||
"ruff_source_file",
|
"ruff_source_file",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"salsa-2022",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ license = "MIT"
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
ruff = { path = "crates/ruff" }
|
ruff = { path = "crates/ruff" }
|
||||||
ruff_cache = { path = "crates/ruff_cache" }
|
ruff_cache = { path = "crates/ruff_cache" }
|
||||||
|
ruff_db = { path = "crates/ruff_db" }
|
||||||
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
|
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
|
||||||
ruff_formatter = { path = "crates/ruff_formatter" }
|
ruff_formatter = { path = "crates/ruff_formatter" }
|
||||||
ruff_index = { path = "crates/ruff_index" }
|
ruff_index = { path = "crates/ruff_index" }
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,14 @@ mod tests {
|
||||||
use crate::file_system::{FileSystem, MemoryFileSystem};
|
use crate::file_system::{FileSystem, MemoryFileSystem};
|
||||||
use crate::vfs::{VendoredPathBuf, Vfs};
|
use crate::vfs::{VendoredPathBuf, Vfs};
|
||||||
use crate::{Db, Jar};
|
use crate::{Db, Jar};
|
||||||
|
use salsa::DebugWithDb;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Database that can be used for testing.
|
/// Database that can be used for testing.
|
||||||
///
|
///
|
||||||
/// Uses an in memory filesystem and it stubs out the vendored files by default.
|
/// Uses an in memory filesystem and it stubs out the vendored files by default.
|
||||||
#[salsa::db(Jar)]
|
#[salsa::db(Jar)]
|
||||||
pub struct TestDb {
|
pub(crate) struct TestDb {
|
||||||
storage: salsa::Storage<Self>,
|
storage: salsa::Storage<Self>,
|
||||||
vfs: Vfs,
|
vfs: Vfs,
|
||||||
file_system: MemoryFileSystem,
|
file_system: MemoryFileSystem,
|
||||||
|
|
@ -63,8 +65,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestDb {
|
impl TestDb {
|
||||||
#[allow(unused)]
|
pub(crate) fn new() -> Self {
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut vfs = Vfs::default();
|
let mut vfs = Vfs::default();
|
||||||
vfs.stub_vendored::<VendoredPathBuf, String>([]);
|
vfs.stub_vendored::<VendoredPathBuf, String>([]);
|
||||||
|
|
||||||
|
|
@ -77,20 +78,37 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn file_system(&self) -> &MemoryFileSystem {
|
pub(crate) fn file_system(&self) -> &MemoryFileSystem {
|
||||||
&self.file_system
|
&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`]).
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
/// If there are pending database snapshots.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn events(&self) -> std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>> {
|
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
|
||||||
self.events.clone()
|
let inner = Arc::get_mut(&mut self.events)
|
||||||
|
.expect("expected no pending salsa database snapshots.");
|
||||||
|
|
||||||
|
std::mem::take(inner.get_mut().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn file_system_mut(&mut self) -> &mut MemoryFileSystem {
|
/// Clears the emitted salsa events.
|
||||||
|
///
|
||||||
|
/// ## Panics
|
||||||
|
/// If there are pending database snapshots.
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn clear_salsa_events(&mut self) {
|
||||||
|
self.take_salsa_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn file_system_mut(&mut self) -> &mut MemoryFileSystem {
|
||||||
&mut self.file_system
|
&mut self.file_system
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn vfs_mut(&mut self) -> &mut Vfs {
|
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
|
||||||
&mut self.vfs
|
&mut self.vfs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,7 +125,7 @@ mod tests {
|
||||||
|
|
||||||
impl salsa::Database for TestDb {
|
impl salsa::Database for TestDb {
|
||||||
fn salsa_event(&self, event: salsa::Event) {
|
fn salsa_event(&self, event: salsa::Event) {
|
||||||
tracing::trace!("event: {:?}", event);
|
tracing::trace!("event: {:?}", event.debug(self));
|
||||||
let mut events = self.events.lock().unwrap();
|
let mut events = self.events.lock().unwrap();
|
||||||
events.push(event);
|
events.push(event);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
|
|
||||||
|
use crate::vfs::VfsFile;
|
||||||
|
use crate::Db;
|
||||||
|
|
||||||
|
/// Reads the content of file.
|
||||||
|
#[salsa::tracked]
|
||||||
|
pub fn source_text(db: &dyn Db, file: VfsFile) -> SourceText {
|
||||||
|
let content = file.read(db);
|
||||||
|
|
||||||
|
SourceText {
|
||||||
|
inner: Arc::from(content),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the [`LineIndex`] for `file`.
|
||||||
|
#[salsa::tracked]
|
||||||
|
pub fn line_index(db: &dyn Db, file: VfsFile) -> LineIndex {
|
||||||
|
let source = source_text(db, file);
|
||||||
|
|
||||||
|
LineIndex::from_source_text(&source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The source text of a [`VfsFile`].
|
||||||
|
///
|
||||||
|
/// Cheap cloneable in `O(1)`.
|
||||||
|
#[derive(Clone, Eq, PartialEq)]
|
||||||
|
pub struct SourceText {
|
||||||
|
inner: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceText {
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SourceText {
|
||||||
|
type Target = str;
|
||||||
|
|
||||||
|
fn deref(&self) -> &str {
|
||||||
|
self.as_str()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for SourceText {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_tuple("SourceText").field(&self.inner).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use filetime::FileTime;
|
||||||
|
use salsa::EventKind;
|
||||||
|
|
||||||
|
use ruff_source_file::OneIndexed;
|
||||||
|
use ruff_text_size::TextSize;
|
||||||
|
|
||||||
|
use crate::file_system::FileSystemPath;
|
||||||
|
use crate::source::{line_index, source_text};
|
||||||
|
use crate::tests::TestDb;
|
||||||
|
use crate::Db;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn re_runs_query_when_file_revision_changes() {
|
||||||
|
let mut db = TestDb::new();
|
||||||
|
let path = FileSystemPath::new("test.py");
|
||||||
|
|
||||||
|
db.file_system_mut().write_file(path, "x = 10".to_string());
|
||||||
|
|
||||||
|
let file = db.file(path);
|
||||||
|
|
||||||
|
assert_eq!(&*source_text(&db, file), "x = 10");
|
||||||
|
|
||||||
|
db.file_system_mut().write_file(path, "x = 20".to_string());
|
||||||
|
file.set_revision(&mut db).to(FileTime::now().into());
|
||||||
|
|
||||||
|
assert_eq!(&*source_text(&db, file), "x = 20");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_is_cached_if_revision_is_unchanged() {
|
||||||
|
let mut db = TestDb::new();
|
||||||
|
let path = FileSystemPath::new("test.py");
|
||||||
|
|
||||||
|
db.file_system_mut().write_file(path, "x = 10".to_string());
|
||||||
|
|
||||||
|
let file = db.file(path);
|
||||||
|
|
||||||
|
assert_eq!(&*source_text(&db, file), "x = 10");
|
||||||
|
|
||||||
|
// Change the file permission only
|
||||||
|
file.set_permissions(&mut db).to(Some(0o777));
|
||||||
|
|
||||||
|
db.events().lock().unwrap().clear();
|
||||||
|
assert_eq!(&*source_text(&db, file), "x = 10");
|
||||||
|
|
||||||
|
let events = db.events();
|
||||||
|
let events = events.lock().unwrap();
|
||||||
|
|
||||||
|
assert!(!events
|
||||||
|
.iter()
|
||||||
|
.any(|event| matches!(event.kind, EventKind::WillExecute { .. })));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn line_index_for_source() {
|
||||||
|
let mut db = TestDb::new();
|
||||||
|
let path = FileSystemPath::new("test.py");
|
||||||
|
|
||||||
|
db.file_system_mut()
|
||||||
|
.write_file(path, "x = 10\ny = 20".to_string());
|
||||||
|
|
||||||
|
let file = db.file(path);
|
||||||
|
let index = line_index(&db, file);
|
||||||
|
let text = source_text(&db, file);
|
||||||
|
|
||||||
|
assert_eq!(index.line_count(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
index.line_start(OneIndexed::from_zero_indexed(0), &text),
|
||||||
|
TextSize::new(0)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ license = { workspace = true }
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ruff_db = { workspace = true, optional = true }
|
||||||
ruff_index = { workspace = true }
|
ruff_index = { workspace = true }
|
||||||
ruff_python_ast = { workspace = true }
|
ruff_python_ast = { workspace = true }
|
||||||
ruff_python_stdlib = { workspace = true }
|
ruff_python_stdlib = { workspace = true }
|
||||||
|
|
@ -22,6 +23,8 @@ ruff_text_size = { workspace = true }
|
||||||
|
|
||||||
bitflags = { workspace = true }
|
bitflags = { workspace = true }
|
||||||
is-macro = { workspace = true }
|
is-macro = { workspace = true }
|
||||||
|
salsa = { workspace = true, optional = true }
|
||||||
|
tracing = { workspace = true, optional = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
@ -29,3 +32,6 @@ ruff_python_parser = { workspace = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
red_knot = ["dep:ruff_db", "dep:salsa", "dep:tracing"]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
use ruff_db::{Db as SourceDb, Upcast};
|
||||||
|
use salsa::DbWithJar;
|
||||||
|
|
||||||
|
// Salsa doesn't support a struct without fields, so allow the clippy lint for now.
|
||||||
|
#[allow(clippy::empty_structs_with_brackets)]
|
||||||
|
#[salsa::jar(db=Db)]
|
||||||
|
pub struct Jar();
|
||||||
|
|
||||||
|
/// Database giving access to semantic information about a Python program.
|
||||||
|
pub trait Db: SourceDb + DbWithJar<Jar> + Upcast<dyn SourceDb> {}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Db, Jar};
|
||||||
|
use ruff_db::file_system::{FileSystem, MemoryFileSystem};
|
||||||
|
use ruff_db::vfs::Vfs;
|
||||||
|
use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast};
|
||||||
|
use salsa::DebugWithDb;
|
||||||
|
|
||||||
|
#[salsa::db(Jar, SourceJar)]
|
||||||
|
pub(crate) struct TestDb {
|
||||||
|
storage: salsa::Storage<Self>,
|
||||||
|
vfs: Vfs,
|
||||||
|
file_system: MemoryFileSystem,
|
||||||
|
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestDb {
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
storage: salsa::Storage::default(),
|
||||||
|
file_system: MemoryFileSystem::default(),
|
||||||
|
events: std::sync::Arc::default(),
|
||||||
|
vfs: Vfs::with_stubbed_vendored(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||||
|
&self.file_system
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn memory_file_system_mut(&mut self) -> &mut MemoryFileSystem {
|
||||||
|
&mut self.file_system
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
|
||||||
|
&mut self.vfs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SourceDb for TestDb {
|
||||||
|
fn file_system(&self) -> &dyn FileSystem {
|
||||||
|
&self.file_system
|
||||||
|
}
|
||||||
|
|
||||||
|
fn vfs(&self) -> &Vfs {
|
||||||
|
&self.vfs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Upcast<dyn SourceDb> for TestDb {
|
||||||
|
fn upcast(&self) -> &(dyn SourceDb + 'static) {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Db for TestDb {}
|
||||||
|
|
||||||
|
impl salsa::Database for TestDb {
|
||||||
|
fn salsa_event(&self, event: salsa::Event) {
|
||||||
|
tracing::trace!("event: {:?}", event.debug(self));
|
||||||
|
let mut events = self.events.lock().unwrap();
|
||||||
|
events.push(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl salsa::ParallelDatabase for TestDb {
|
||||||
|
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||||
|
salsa::Snapshot::new(Self {
|
||||||
|
storage: self.storage.snapshot(),
|
||||||
|
vfs: self.vfs.snapshot(),
|
||||||
|
file_system: self.file_system.snapshot(),
|
||||||
|
events: self.events.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ pub mod analyze;
|
||||||
mod binding;
|
mod binding;
|
||||||
mod branches;
|
mod branches;
|
||||||
mod context;
|
mod context;
|
||||||
|
#[cfg(feature = "red_knot")]
|
||||||
|
mod db;
|
||||||
mod definition;
|
mod definition;
|
||||||
mod globals;
|
mod globals;
|
||||||
mod model;
|
mod model;
|
||||||
|
|
@ -20,3 +22,6 @@ pub use nodes::*;
|
||||||
pub use reference::*;
|
pub use reference::*;
|
||||||
pub use scope::*;
|
pub use scope::*;
|
||||||
pub use star_import::*;
|
pub use star_import::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "red_knot")]
|
||||||
|
pub use db::{Db, Jar};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue