ruff_db/system.rs
1pub use command::{Command, CommandExecutor};
2pub use memory_fs::MemoryFileSystem;
3
4#[cfg(all(feature = "testing", feature = "os"))]
5pub use os::testing::UserConfigDirectoryOverrideGuard;
6
7#[cfg(feature = "os")]
8pub use os::OsSystem;
9
10use filetime::FileTime;
11use ruff_notebook::{Notebook, NotebookError};
12use ruff_python_ast::PySourceType;
13use std::error::Error;
14use std::fmt;
15use std::fmt::Debug;
16use std::process::Output;
17pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
18use walk_directory::WalkDirectoryBuilder;
19
20pub use self::path::{
21 DeduplicatedNestedPathsIter, SystemPath, SystemPathBuf, SystemVirtualPath,
22 SystemVirtualPathBuf, deduplicate_nested_paths,
23};
24use crate::file_revision::FileRevision;
25
26mod command;
27mod memory_fs;
28#[cfg(feature = "os")]
29mod os;
30mod path;
31mod test;
32pub mod walk_directory;
33
34pub type Result<T> = std::io::Result<T>;
35pub type WhichResult = std::result::Result<SystemPathBuf, WhichError>;
36
37/// The system on which Ruff runs.
38///
39/// Ruff supports running on the CLI, in a language server, and in a browser (WASM). Each of these
40/// host-systems differ in what system operations they support and how they interact with the file system:
41/// * Language server:
42/// * Reading a file's content should take into account that it might have unsaved changes because it's open in the editor.
43/// * Use structured representations for notebooks, making deserializing a notebook from a string unnecessary.
44/// * Use their own file watching infrastructure.
45/// * WASM (Browser):
46/// * There are ways to emulate a file system in WASM but a native memory-filesystem is more efficient.
47/// * Doesn't support a current working directory
48/// * File watching isn't supported.
49///
50/// Abstracting the system also enables tests to use a more efficient in-memory file system.
51pub trait System: Debug + Sync + Send {
52 /// Reads the metadata of the file or directory at `path`.
53 ///
54 /// This function will traverse symbolic links to query information about the destination file.
55 fn path_metadata(&self, path: &SystemPath) -> Result<Metadata>;
56
57 /// Returns the canonical, absolute form of a path with all intermediate components normalized
58 /// and symbolic links resolved.
59 ///
60 /// # Errors
61 /// This function will return an error in the following situations, but is not limited to just these cases:
62 /// * `path` does not exist.
63 /// * A non-final component in `path` is not a directory.
64 /// * the symlink target path is not valid Unicode.
65 ///
66 /// ## Windows long-paths
67 /// Unlike `std::fs::canonicalize`, this function does remove UNC prefixes if possible.
68 /// See [dunce::canonicalize] for more information.
69 fn canonicalize_path(&self, path: &SystemPath) -> Result<SystemPathBuf>;
70
71 /// Returns `true` if both paths refer to the same file.
72 fn is_same_file(&self, first: &SystemPath, second: &SystemPath) -> Result<bool>;
73
74 /// Returns the source type for `path` if known or `None`.
75 ///
76 /// The default is to always return `None`, assuming the system
77 /// has no additional information and that the caller should
78 /// rely on the file extension instead.
79 ///
80 /// This is primarily used for the LSP integration to respect
81 /// the chosen language (or the fact that it is a notebook) in
82 /// the editor.
83 fn source_type(&self, path: &SystemPath) -> Option<PySourceType> {
84 let _ = path;
85 None
86 }
87
88 /// Returns the source type for `path` if known or `None`.
89 ///
90 /// The default is to always return `None`, assuming the system
91 /// has no additional information and that the caller should
92 /// rely on the file extension instead.
93 ///
94 /// This is primarily used for the LSP integration to respect
95 /// the chosen language (or the fact that it is a notebook) in
96 /// the editor.
97 fn virtual_path_source_type(&self, path: &SystemVirtualPath) -> Option<PySourceType> {
98 let _ = path;
99
100 None
101 }
102
103 /// Find an executable binary's path by name.
104 fn which(&self, binary_name: &str) -> WhichResult;
105
106 /// Runs `command` and captures its standard output and standard error.
107 fn run_command(&self, command: Command) -> Result<Output> {
108 let Some(executor) = self.command_executor() else {
109 return Err(std::io::Error::new(
110 std::io::ErrorKind::Unsupported,
111 "running commands is not supported by this system",
112 ));
113 };
114
115 executor.execute(command)
116 }
117
118 /// Returns the system's command executor, if it supports running commands.
119 fn command_executor(&self) -> Option<&dyn CommandExecutor> {
120 None
121 }
122
123 /// Reads the content of the file at `path` into a [`String`].
124 fn read_to_string(&self, path: &SystemPath) -> Result<String>;
125
126 /// Reads the content of the file at `path` as a Notebook.
127 ///
128 /// This method optimizes for the case where the system holds a structured representation of a [`Notebook`],
129 /// allowing to skip the notebook deserialization. Systems that don't use a structured
130 /// representation fall-back to deserializing the notebook from a string.
131 fn read_to_notebook(&self, path: &SystemPath) -> std::result::Result<Notebook, NotebookError>;
132
133 /// Reads the content of the virtual file at `path` into a [`String`].
134 fn read_virtual_path_to_string(&self, path: &SystemVirtualPath) -> Result<String>;
135
136 /// Reads the content of the virtual file at `path` as a [`Notebook`].
137 fn read_virtual_path_to_notebook(
138 &self,
139 path: &SystemVirtualPath,
140 ) -> std::result::Result<Notebook, NotebookError>;
141
142 /// Returns `true` if `path` exists.
143 fn path_exists(&self, path: &SystemPath) -> bool {
144 self.path_metadata(path).is_ok()
145 }
146
147 /// Returns `true` if `path` exists and is a directory.
148 fn is_directory(&self, path: &SystemPath) -> bool {
149 self.path_metadata(path)
150 .is_ok_and(|metadata| metadata.file_type.is_directory())
151 }
152
153 /// Returns `true` if `path` exists and is a file.
154 fn is_file(&self, path: &SystemPath) -> bool {
155 self.path_metadata(path)
156 .is_ok_and(|metadata| metadata.file_type.is_file())
157 }
158
159 /// Returns the current working directory
160 fn current_directory(&self) -> &SystemPath;
161
162 /// Returns the directory path where user configurations are stored.
163 ///
164 /// Returns `None` if no such convention exists for the system.
165 fn user_config_directory(&self) -> Option<SystemPathBuf>;
166
167 /// Returns the directory path where cached files are stored.
168 ///
169 /// Returns `None` if no such convention exists for the system.
170 fn cache_dir(&self) -> Option<SystemPathBuf>;
171
172 /// Iterate over the contents of the directory at `path`.
173 ///
174 /// The returned iterator must have the following properties:
175 /// - It only iterates over the top level of the directory,
176 /// i.e., it does not recurse into subdirectories.
177 /// - It skips the current and parent directories (`.` and `..`
178 /// respectively).
179 /// - The iterator yields `std::io::Result<DirEntry>` instances.
180 /// For each instance, an `Err` variant may signify that the path
181 /// of the entry was not valid UTF8, in which case it should be an
182 /// [`std::io::Error`] with the ErrorKind set to
183 /// [`std::io::ErrorKind::InvalidData`] and the payload set to a
184 /// [`camino::FromPathBufError`]. It may also indicate that
185 /// "some sort of intermittent IO error occurred during iteration"
186 /// (language taken from the [`std::fs::read_dir`] documentation).
187 ///
188 /// # Errors
189 /// Returns an error:
190 /// - if `path` does not exist in the system,
191 /// - if `path` does not point to a directory,
192 /// - if the process does not have sufficient permissions to
193 /// view the contents of the directory at `path`
194 /// - May also return an error in some other situations as well.
195 fn read_directory<'a>(
196 &'a self,
197 path: &SystemPath,
198 ) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>>;
199
200 /// Recursively walks the content of `path`.
201 ///
202 /// It is allowed to pass a `path` that points to a file. In this case, the walker
203 /// yields a single entry for that file.
204 fn walk_directory(&self, path: &SystemPath) -> WalkDirectoryBuilder;
205
206 /// Fetches the environment variable `key` from the current process.
207 ///
208 /// # Errors
209 ///
210 /// Returns [`std::env::VarError::NotPresent`] if:
211 /// - The variable is not set.
212 /// - The variable's name contains an equal sign or NUL (`'='` or `'\0'`).
213 ///
214 /// Returns [`std::env::VarError::NotUnicode`] if the variable's value is not valid
215 /// Unicode.
216 fn env_var(&self, name: &str) -> std::result::Result<String, std::env::VarError> {
217 let _ = name;
218 Err(std::env::VarError::NotPresent)
219 }
220
221 /// Returns a handle to a [`WritableSystem`] if this system is writable.
222 fn as_writable(&self) -> Option<&dyn WritableSystem>;
223
224 fn as_any(&self) -> &dyn std::any::Any;
225
226 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
227
228 fn dyn_clone(&self) -> Box<dyn System>;
229}
230
231/// System trait for non-readonly systems.
232pub trait WritableSystem: System {
233 /// Creates a file at the given path.
234 ///
235 /// Returns an error if the file already exists.
236 fn create_new_file(&self, path: &SystemPath) -> Result<()>;
237
238 /// Writes the given content to the file at the given path.
239 fn write_file(&self, path: &SystemPath, content: &str) -> Result<()> {
240 self.write_file_bytes(path, content.as_bytes())
241 }
242
243 /// Writes the given content to the file at the given path.
244 fn write_file_bytes(&self, path: &SystemPath, content: &[u8]) -> Result<()>;
245
246 /// Creates a directory at `path` as well as any intermediate directories.
247 fn create_directory_all(&self, path: &SystemPath) -> Result<()>;
248
249 /// Reads the provided file from the system cache, or creates the file if necessary.
250 ///
251 /// Returns `Ok(None)` if the system does not expose a suitable cache directory.
252 fn get_or_cache(
253 &self,
254 path: &SystemPath,
255 read_contents: &dyn Fn() -> Result<String>,
256 ) -> Result<Option<SystemPathBuf>> {
257 let Some(cache_dir) = self.cache_dir() else {
258 return Ok(None);
259 };
260
261 let cache_path = cache_dir.join(path);
262
263 // The file has already been cached.
264 if self.is_file(&cache_path) {
265 return Ok(Some(cache_path));
266 }
267
268 // Read the file contents.
269 let contents = read_contents()?;
270
271 // Create the parent directory.
272 self.create_directory_all(cache_path.parent().unwrap())?;
273
274 // Create and write to the file on the system.
275 //
276 // Note that `create_new_file` will fail if the file has already been created. This
277 // ensures that only one thread/process ever attempts to write to it to avoid corrupting
278 // the cache.
279 self.create_new_file(&cache_path)?;
280 self.write_file(&cache_path, &contents)?;
281
282 Ok(Some(cache_path))
283 }
284
285 fn dyn_clone(&self) -> Box<dyn WritableSystem>;
286}
287
288#[derive(Clone, Debug, Eq, PartialEq)]
289pub struct Metadata {
290 revision: FileRevision,
291 permissions: Option<u32>,
292 file_type: FileType,
293}
294
295impl Metadata {
296 pub fn new(revision: FileRevision, permissions: Option<u32>, file_type: FileType) -> Self {
297 Self {
298 revision,
299 permissions,
300 file_type,
301 }
302 }
303
304 pub fn revision(&self) -> FileRevision {
305 self.revision
306 }
307
308 pub fn permissions(&self) -> Option<u32> {
309 self.permissions
310 }
311
312 pub fn file_type(&self) -> FileType {
313 self.file_type
314 }
315}
316
317#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash, get_size2::GetSize)]
318pub enum FileType {
319 File,
320 Directory,
321 Symlink,
322}
323
324impl FileType {
325 pub const fn is_file(self) -> bool {
326 matches!(self, FileType::File)
327 }
328
329 pub const fn is_directory(self) -> bool {
330 matches!(self, FileType::Directory)
331 }
332
333 pub const fn is_symlink(self) -> bool {
334 matches!(self, FileType::Symlink)
335 }
336}
337
338#[derive(Debug, PartialEq, Eq)]
339pub struct DirectoryEntry {
340 path: SystemPathBuf,
341 file_type: FileType,
342}
343
344impl DirectoryEntry {
345 pub fn new(path: SystemPathBuf, file_type: FileType) -> Self {
346 Self { path, file_type }
347 }
348
349 pub fn into_path(self) -> SystemPathBuf {
350 self.path
351 }
352
353 pub fn path(&self) -> &SystemPath {
354 &self.path
355 }
356
357 pub fn file_type(&self) -> FileType {
358 self.file_type
359 }
360}
361
362#[cfg(not(target_arch = "wasm32"))]
363pub fn file_time_now() -> FileTime {
364 FileTime::now()
365}
366
367#[cfg(target_arch = "wasm32")]
368pub fn file_time_now() -> FileTime {
369 // Copied from FileTime::from_system_time()
370 let time = web_time::SystemTime::now();
371
372 time.duration_since(web_time::UNIX_EPOCH)
373 .map(|d| FileTime::from_unix_time(d.as_secs() as i64, d.subsec_nanos()))
374 .unwrap_or_else(|e| {
375 let until_epoch = e.duration();
376 let (sec_offset, nanos) = if until_epoch.subsec_nanos() == 0 {
377 (0, 0)
378 } else {
379 (-1, 1_000_000_000 - until_epoch.subsec_nanos())
380 };
381
382 FileTime::from_unix_time(-(until_epoch.as_secs() as i64) + sec_offset, nanos)
383 })
384}
385
386#[derive(Copy, Clone, Eq, PartialEq, Debug)]
387pub enum WhichError {
388 /// An executable binary with that name was not found
389 CannotFindBinaryPath,
390
391 /// There was nowhere to search and the provided name wasn't an absolute path
392 CannotGetCurrentDirAndPathListEmpty,
393
394 /// Failed to canonicalize the path found
395 CannotCanonicalize,
396
397 /// The executable exists but its path contains non UTF8 characters.
398 NonUtf8Path,
399}
400
401impl Error for WhichError {}
402
403impl fmt::Display for WhichError {
404 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405 match self {
406 WhichError::CannotFindBinaryPath => write!(f, "cannot find binary path"),
407 WhichError::CannotGetCurrentDirAndPathListEmpty => write!(
408 f,
409 "no path to search and provided name is not an absolute path"
410 ),
411 WhichError::CannotCanonicalize => write!(f, "cannot canonicalize path"),
412 WhichError::NonUtf8Path => write!(f, "non UTF-8 path"),
413 }
414 }
415}