Skip to content

Commit efbf7b1

Browse files
red-knot[salsa part 2]: Setup semantic DB and Jar (#11837)
Co-authored-by: Alex Waygood <[email protected]>
1 parent 9dc226b commit efbf7b1

File tree

7 files changed

+267
-15
lines changed

7 files changed

+267
-15
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ license = "MIT"
1414
[workspace.dependencies]
1515
ruff = { path = "crates/ruff" }
1616
ruff_cache = { path = "crates/ruff_cache" }
17+
ruff_db = { path = "crates/ruff_db" }
1718
ruff_diagnostics = { path = "crates/ruff_diagnostics" }
1819
ruff_formatter = { path = "crates/ruff_formatter" }
1920
ruff_index = { path = "crates/ruff_index" }
@@ -81,7 +82,7 @@ libcst = { version = "1.1.0", default-features = false }
8182
log = { version = "0.4.17" }
8283
lsp-server = { version = "0.7.6" }
8384
lsp-types = { git = "https://github.com/astral-sh/lsp-types.git", rev = "3512a9f", features = [
84-
"proposed",
85+
"proposed",
8586
] }
8687
matchit = { version = "0.8.1" }
8788
memchr = { version = "2.7.1" }
@@ -111,7 +112,7 @@ serde-wasm-bindgen = { version = "0.6.4" }
111112
serde_json = { version = "1.0.113" }
112113
serde_test = { version = "1.0.152" }
113114
serde_with = { version = "3.6.0", default-features = false, features = [
114-
"macros",
115+
"macros",
115116
] }
116117
shellexpand = { version = "3.0.0" }
117118
similar = { version = "2.4.0", features = ["inline"] }
@@ -138,10 +139,10 @@ unicode-normalization = { version = "0.1.23" }
138139
ureq = { version = "2.9.6" }
139140
url = { version = "2.5.0" }
140141
uuid = { version = "1.6.1", features = [
141-
"v4",
142-
"fast-rng",
143-
"macro-diagnostics",
144-
"js",
142+
"v4",
143+
"fast-rng",
144+
"macro-diagnostics",
145+
"js",
145146
] }
146147
walkdir = { version = "2.3.2" }
147148
wasm-bindgen = { version = "0.2.92" }

crates/ruff_db/src/lib.rs

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,22 @@ mod tests {
5050
use crate::file_system::{FileSystem, MemoryFileSystem};
5151
use crate::vfs::{VendoredPathBuf, Vfs};
5252
use crate::{Db, Jar};
53+
use salsa::DebugWithDb;
54+
use std::sync::Arc;
5355

5456
/// Database that can be used for testing.
5557
///
5658
/// Uses an in memory filesystem and it stubs out the vendored files by default.
5759
#[salsa::db(Jar)]
58-
pub struct TestDb {
60+
pub(crate) struct TestDb {
5961
storage: salsa::Storage<Self>,
6062
vfs: Vfs,
6163
file_system: MemoryFileSystem,
6264
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
6365
}
6466

6567
impl TestDb {
66-
#[allow(unused)]
67-
pub fn new() -> Self {
68+
pub(crate) fn new() -> Self {
6869
let mut vfs = Vfs::default();
6970
vfs.stub_vendored::<VendoredPathBuf, String>([]);
7071

@@ -77,20 +78,37 @@ mod tests {
7778
}
7879

7980
#[allow(unused)]
80-
pub fn file_system(&self) -> &MemoryFileSystem {
81+
pub(crate) fn file_system(&self) -> &MemoryFileSystem {
8182
&self.file_system
8283
}
8384

85+
/// Empties the internal store of salsa events that have been emitted,
86+
/// and returns them as a `Vec` (equivalent to [`std::mem::take`]).
87+
///
88+
/// ## Panics
89+
/// If there are pending database snapshots.
90+
#[allow(unused)]
91+
pub(crate) fn take_salsa_events(&mut self) -> Vec<salsa::Event> {
92+
let inner = Arc::get_mut(&mut self.events)
93+
.expect("expected no pending salsa database snapshots.");
94+
95+
std::mem::take(inner.get_mut().unwrap())
96+
}
97+
98+
/// Clears the emitted salsa events.
99+
///
100+
/// ## Panics
101+
/// If there are pending database snapshots.
84102
#[allow(unused)]
85-
pub fn events(&self) -> std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>> {
86-
self.events.clone()
103+
pub(crate) fn clear_salsa_events(&mut self) {
104+
self.take_salsa_events();
87105
}
88106

89-
pub fn file_system_mut(&mut self) -> &mut MemoryFileSystem {
107+
pub(crate) fn file_system_mut(&mut self) -> &mut MemoryFileSystem {
90108
&mut self.file_system
91109
}
92110

93-
pub fn vfs_mut(&mut self) -> &mut Vfs {
111+
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
94112
&mut self.vfs
95113
}
96114
}
@@ -107,7 +125,7 @@ mod tests {
107125

108126
impl salsa::Database for TestDb {
109127
fn salsa_event(&self, event: salsa::Event) {
110-
tracing::trace!("event: {:?}", event);
128+
tracing::trace!("event: {:?}", event.debug(self));
111129
let mut events = self.events.lock().unwrap();
112130
events.push(event);
113131
}

crates/ruff_db/src/source.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use std::ops::Deref;
2+
use std::sync::Arc;
3+
4+
use ruff_source_file::LineIndex;
5+
6+
use crate::vfs::VfsFile;
7+
use crate::Db;
8+
9+
/// Reads the content of file.
10+
#[salsa::tracked]
11+
pub fn source_text(db: &dyn Db, file: VfsFile) -> SourceText {
12+
let content = file.read(db);
13+
14+
SourceText {
15+
inner: Arc::from(content),
16+
}
17+
}
18+
19+
/// Computes the [`LineIndex`] for `file`.
20+
#[salsa::tracked]
21+
pub fn line_index(db: &dyn Db, file: VfsFile) -> LineIndex {
22+
let source = source_text(db, file);
23+
24+
LineIndex::from_source_text(&source)
25+
}
26+
27+
/// The source text of a [`VfsFile`].
28+
///
29+
/// Cheap cloneable in `O(1)`.
30+
#[derive(Clone, Eq, PartialEq)]
31+
pub struct SourceText {
32+
inner: Arc<str>,
33+
}
34+
35+
impl SourceText {
36+
pub fn as_str(&self) -> &str {
37+
&self.inner
38+
}
39+
}
40+
41+
impl Deref for SourceText {
42+
type Target = str;
43+
44+
fn deref(&self) -> &str {
45+
self.as_str()
46+
}
47+
}
48+
49+
impl std::fmt::Debug for SourceText {
50+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51+
f.debug_tuple("SourceText").field(&self.inner).finish()
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use filetime::FileTime;
58+
use salsa::EventKind;
59+
60+
use ruff_source_file::OneIndexed;
61+
use ruff_text_size::TextSize;
62+
63+
use crate::file_system::FileSystemPath;
64+
use crate::source::{line_index, source_text};
65+
use crate::tests::TestDb;
66+
use crate::Db;
67+
68+
#[test]
69+
fn re_runs_query_when_file_revision_changes() {
70+
let mut db = TestDb::new();
71+
let path = FileSystemPath::new("test.py");
72+
73+
db.file_system_mut().write_file(path, "x = 10".to_string());
74+
75+
let file = db.file(path);
76+
77+
assert_eq!(&*source_text(&db, file), "x = 10");
78+
79+
db.file_system_mut().write_file(path, "x = 20".to_string());
80+
file.set_revision(&mut db).to(FileTime::now().into());
81+
82+
assert_eq!(&*source_text(&db, file), "x = 20");
83+
}
84+
85+
#[test]
86+
fn text_is_cached_if_revision_is_unchanged() {
87+
let mut db = TestDb::new();
88+
let path = FileSystemPath::new("test.py");
89+
90+
db.file_system_mut().write_file(path, "x = 10".to_string());
91+
92+
let file = db.file(path);
93+
94+
assert_eq!(&*source_text(&db, file), "x = 10");
95+
96+
// Change the file permission only
97+
file.set_permissions(&mut db).to(Some(0o777));
98+
99+
db.events().lock().unwrap().clear();
100+
assert_eq!(&*source_text(&db, file), "x = 10");
101+
102+
let events = db.events();
103+
let events = events.lock().unwrap();
104+
105+
assert!(!events
106+
.iter()
107+
.any(|event| matches!(event.kind, EventKind::WillExecute { .. })));
108+
}
109+
110+
#[test]
111+
fn line_index_for_source() {
112+
let mut db = TestDb::new();
113+
let path = FileSystemPath::new("test.py");
114+
115+
db.file_system_mut()
116+
.write_file(path, "x = 10\ny = 20".to_string());
117+
118+
let file = db.file(path);
119+
let index = line_index(&db, file);
120+
let text = source_text(&db, file);
121+
122+
assert_eq!(index.line_count(), 2);
123+
assert_eq!(
124+
index.line_start(OneIndexed::from_zero_indexed(0), &text),
125+
TextSize::new(0)
126+
);
127+
}
128+
}

crates/ruff_python_semantic/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ license = { workspace = true }
1414
doctest = false
1515

1616
[dependencies]
17+
ruff_db = { workspace = true, optional = true }
1718
ruff_index = { workspace = true }
1819
ruff_python_ast = { workspace = true }
1920
ruff_python_stdlib = { workspace = true }
@@ -22,10 +23,15 @@ ruff_text_size = { workspace = true }
2223

2324
bitflags = { workspace = true }
2425
is-macro = { workspace = true }
26+
salsa = { workspace = true, optional = true }
27+
tracing = { workspace = true, optional = true }
2528
rustc-hash = { workspace = true }
2629

2730
[dev-dependencies]
2831
ruff_python_parser = { workspace = true }
2932

3033
[lints]
3134
workspace = true
35+
36+
[features]
37+
red_knot = ["dep:ruff_db", "dep:salsa", "dep:tracing"]

crates/ruff_python_semantic/src/db.rs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
use ruff_db::{Db as SourceDb, Upcast};
2+
use salsa::DbWithJar;
3+
4+
// Salsa doesn't support a struct without fields, so allow the clippy lint for now.
5+
#[allow(clippy::empty_structs_with_brackets)]
6+
#[salsa::jar(db=Db)]
7+
pub struct Jar();
8+
9+
/// Database giving access to semantic information about a Python program.
10+
pub trait Db: SourceDb + DbWithJar<Jar> + Upcast<dyn SourceDb> {}
11+
12+
#[cfg(test)]
13+
mod tests {
14+
use super::{Db, Jar};
15+
use ruff_db::file_system::{FileSystem, MemoryFileSystem};
16+
use ruff_db::vfs::Vfs;
17+
use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast};
18+
use salsa::DebugWithDb;
19+
20+
#[salsa::db(Jar, SourceJar)]
21+
pub(crate) struct TestDb {
22+
storage: salsa::Storage<Self>,
23+
vfs: Vfs,
24+
file_system: MemoryFileSystem,
25+
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
26+
}
27+
28+
impl TestDb {
29+
#[allow(unused)]
30+
pub(crate) fn new() -> Self {
31+
Self {
32+
storage: salsa::Storage::default(),
33+
file_system: MemoryFileSystem::default(),
34+
events: std::sync::Arc::default(),
35+
vfs: Vfs::with_stubbed_vendored(),
36+
}
37+
}
38+
39+
#[allow(unused)]
40+
pub(crate) fn memory_file_system(&self) -> &MemoryFileSystem {
41+
&self.file_system
42+
}
43+
44+
#[allow(unused)]
45+
pub(crate) fn memory_file_system_mut(&mut self) -> &mut MemoryFileSystem {
46+
&mut self.file_system
47+
}
48+
49+
#[allow(unused)]
50+
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
51+
&mut self.vfs
52+
}
53+
}
54+
55+
impl SourceDb for TestDb {
56+
fn file_system(&self) -> &dyn FileSystem {
57+
&self.file_system
58+
}
59+
60+
fn vfs(&self) -> &Vfs {
61+
&self.vfs
62+
}
63+
}
64+
65+
impl Upcast<dyn SourceDb> for TestDb {
66+
fn upcast(&self) -> &(dyn SourceDb + 'static) {
67+
self
68+
}
69+
}
70+
71+
impl Db for TestDb {}
72+
73+
impl salsa::Database for TestDb {
74+
fn salsa_event(&self, event: salsa::Event) {
75+
tracing::trace!("event: {:?}", event.debug(self));
76+
let mut events = self.events.lock().unwrap();
77+
events.push(event);
78+
}
79+
}
80+
81+
impl salsa::ParallelDatabase for TestDb {
82+
fn snapshot(&self) -> salsa::Snapshot<Self> {
83+
salsa::Snapshot::new(Self {
84+
storage: self.storage.snapshot(),
85+
vfs: self.vfs.snapshot(),
86+
file_system: self.file_system.snapshot(),
87+
events: self.events.clone(),
88+
})
89+
}
90+
}
91+
}

crates/ruff_python_semantic/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub mod analyze;
22
mod binding;
33
mod branches;
44
mod context;
5+
#[cfg(feature = "red_knot")]
6+
mod db;
57
mod definition;
68
mod globals;
79
mod model;
@@ -20,3 +22,6 @@ pub use nodes::*;
2022
pub use reference::*;
2123
pub use scope::*;
2224
pub use star_import::*;
25+
26+
#[cfg(feature = "red_knot")]
27+
pub use db::{Db, Jar};

0 commit comments

Comments
 (0)