Skip to main content

ruff_db/system/
walk_directory.rs

1use crate::system::SystemPathBuf;
2use std::fmt::{Display, Formatter};
3use std::path::PathBuf;
4
5use super::{FileType, SystemPath};
6
7/// A matcher for determining whether paths are ignored during incremental directory walking.
8pub trait IgnoreIncremental {
9    /// Returns whether the directory walker ignores `path`.
10    fn is_ignored(&mut self, path: &SystemPath, is_directory: bool) -> bool;
11}
12
13/// A builder for constructing a directory recursive traversal.
14pub struct WalkDirectoryBuilder {
15    /// The implementation that does the directory walking.
16    walker: Box<dyn DirectoryWalker>,
17
18    /// The paths that should be walked.
19    paths: Vec<SystemPathBuf>,
20
21    ignore_hidden: bool,
22
23    standard_filters: bool,
24}
25
26impl WalkDirectoryBuilder {
27    pub fn new<W>(path: impl AsRef<SystemPath>, walker: W) -> Self
28    where
29        W: DirectoryWalker + 'static,
30    {
31        Self {
32            walker: Box::new(walker),
33            paths: vec![path.as_ref().to_path_buf()],
34            ignore_hidden: true,
35            standard_filters: true,
36        }
37    }
38
39    /// Adds a path that should be traversed recursively.
40    ///
41    /// Each additional path is traversed recursively.
42    /// This should be preferred over building multiple
43    /// walkers since it enables reusing resources.
44    #[expect(clippy::should_implement_trait)]
45    pub fn add(mut self, path: impl AsRef<SystemPath>) -> Self {
46        self.paths.push(path.as_ref().to_path_buf());
47        self
48    }
49
50    /// Whether hidden files should be ignored.
51    ///
52    /// The definition of what a hidden file depends on the [`System`](super::System) and can be platform-dependent.
53    ///
54    /// This is enabled by default.
55    pub fn ignore_hidden(mut self, hidden: bool) -> Self {
56        self.ignore_hidden = hidden;
57        self
58    }
59
60    /// Enables all the standard ignore filters.
61    ///
62    /// This toggles, as a group, all the filters that are enabled by default:
63    /// * [`hidden`](Self::ignore_hidden)
64    /// * Any [`System`](super::System) specific filters according (e.g., respecting `.ignore`, `.gitignore`, files).
65    ///
66    /// Defaults to `true`.
67    pub fn standard_filters(mut self, standard_filters: bool) -> Self {
68        self.standard_filters = standard_filters;
69        self.ignore_hidden = standard_filters;
70
71        self
72    }
73
74    /// Creates a matcher for determining whether paths are ignored during incremental walking.
75    pub fn incremental_matcher(self) -> Box<dyn IgnoreIncremental> {
76        let configuration = WalkDirectoryConfiguration {
77            paths: self.paths,
78            ignore_hidden: self.ignore_hidden,
79            standard_filters: self.standard_filters,
80        };
81        self.walker.incremental_matcher(configuration)
82    }
83
84    /// Runs the directory traversal and calls the passed `builder` to create visitors
85    /// that do the visiting. The walker may run multiple threads to visit the directories.
86    pub fn run<'s, F>(self, builder: F)
87    where
88        F: FnMut() -> FnVisitor<'s>,
89    {
90        self.visit(&mut FnBuilder { builder });
91    }
92
93    /// Runs the directory traversal and calls the passed `builder` to create visitors
94    /// that do the visiting. The walker may run multiple threads to visit the directories.
95    pub fn visit(self, builder: &mut dyn WalkDirectoryVisitorBuilder) {
96        let configuration = WalkDirectoryConfiguration {
97            paths: self.paths,
98            ignore_hidden: self.ignore_hidden,
99            standard_filters: self.standard_filters,
100        };
101
102        self.walker.walk(builder, configuration);
103    }
104}
105
106/// Concrete walker that performs the directory walking.
107pub trait DirectoryWalker {
108    fn walk(
109        &self,
110        builder: &mut dyn WalkDirectoryVisitorBuilder,
111        configuration: WalkDirectoryConfiguration,
112    );
113
114    /// Creates a matcher for determining whether paths are ignored during incremental walking.
115    fn incremental_matcher(
116        &self,
117        configuration: WalkDirectoryConfiguration,
118    ) -> Box<dyn IgnoreIncremental>;
119}
120
121/// Creates a visitor for each thread that does the visiting.
122pub trait WalkDirectoryVisitorBuilder<'s> {
123    fn build(&mut self) -> Box<dyn WalkDirectoryVisitor + 's>;
124}
125
126/// Visitor handling the individual directory entries.
127pub trait WalkDirectoryVisitor: Send {
128    fn visit(&mut self, entry: std::result::Result<DirectoryEntry, Error>) -> WalkState;
129}
130
131struct FnBuilder<F> {
132    builder: F,
133}
134
135impl<'s, F> WalkDirectoryVisitorBuilder<'s> for FnBuilder<F>
136where
137    F: FnMut() -> FnVisitor<'s>,
138{
139    fn build(&mut self) -> Box<dyn WalkDirectoryVisitor + 's> {
140        let visitor = (self.builder)();
141        Box::new(FnVisitorImpl(visitor))
142    }
143}
144
145type FnVisitor<'s> =
146    Box<dyn FnMut(std::result::Result<DirectoryEntry, Error>) -> WalkState + Send + 's>;
147
148struct FnVisitorImpl<'s>(FnVisitor<'s>);
149
150impl WalkDirectoryVisitor for FnVisitorImpl<'_> {
151    fn visit(&mut self, entry: std::result::Result<DirectoryEntry, Error>) -> WalkState {
152        (self.0)(entry)
153    }
154}
155
156pub struct WalkDirectoryConfiguration {
157    pub paths: Vec<SystemPathBuf>,
158    pub ignore_hidden: bool,
159    pub standard_filters: bool,
160}
161
162/// An entry in a directory.
163#[derive(Debug, Clone)]
164pub struct DirectoryEntry {
165    pub(super) path: SystemPathBuf,
166    pub(super) file_type: FileType,
167    pub(super) depth: usize,
168}
169
170impl DirectoryEntry {
171    /// The full path that this entry represents.
172    pub fn path(&self) -> &SystemPath {
173        &self.path
174    }
175
176    /// The full path that this entry represents.
177    /// Analogous to [`DirectoryEntry::path`], but moves ownership of the path.
178    pub fn into_path(self) -> SystemPathBuf {
179        self.path
180    }
181
182    /// Return the file type for the file that this entry points to.
183    pub fn file_type(&self) -> FileType {
184        self.file_type
185    }
186
187    /// Returns the depth at which this entry was created relative to the root.
188    pub fn depth(&self) -> usize {
189        self.depth
190    }
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
194pub enum WalkState {
195    /// Continue walking as normal
196    Continue,
197
198    /// If the entry given is a directory, don't descend into it.
199    /// In all other cases, this has no effect.
200    Skip,
201
202    /// Quit the entire iterator as soon as possible.
203    ///
204    /// Note: This is an inherently asynchronous action. It's possible
205    /// for more entries to be yielded even after instructing the iterator to quit.
206    Quit,
207}
208
209pub struct Error {
210    pub(super) depth: Option<usize>,
211    pub(super) kind: ErrorKind,
212}
213
214impl Error {
215    pub fn depth(&self) -> Option<usize> {
216        self.depth
217    }
218
219    pub fn kind(&self) -> &ErrorKind {
220        &self.kind
221    }
222}
223
224impl Display for Error {
225    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
226        match &self.kind {
227            ErrorKind::Loop { ancestor, child } => {
228                write!(
229                    f,
230                    "File system loop found: {child} points to an ancestor {ancestor}",
231                )
232            }
233            ErrorKind::Io {
234                path: Some(path),
235                err,
236            } => {
237                write!(f, "IO error for operation on {path}: {err}")
238            }
239            ErrorKind::Io { path: None, err } => err.fmt(f),
240            ErrorKind::NonUtf8Path { path } => {
241                write!(f, "Non-UTF8 path: {}", path.display())
242            }
243        }
244    }
245}
246
247impl std::fmt::Debug for Error {
248    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
249        std::fmt::Display::fmt(self, f)
250    }
251}
252
253impl std::error::Error for Error {}
254
255#[derive(Debug)]
256pub enum ErrorKind {
257    /// An error that occurs when a file loop is detected when traversing
258    /// symbolic links.
259    Loop {
260        ancestor: SystemPathBuf,
261        child: SystemPathBuf,
262    },
263
264    /// An error that occurs when doing I/O
265    Io {
266        path: Option<SystemPathBuf>,
267        err: std::io::Error,
268    },
269
270    /// A path is not a valid UTF-8 path.
271    NonUtf8Path { path: PathBuf },
272}
273
274#[cfg(test)]
275pub(super) mod tests {
276    use crate::system::walk_directory::{DirectoryEntry, Error};
277    use crate::system::{FileType, SystemPathBuf};
278    use std::collections::BTreeMap;
279
280    /// Test helper that creates a visual representation of the visited directory entries.
281    pub(crate) struct DirectoryEntryToString {
282        root_path: SystemPathBuf,
283        inner: std::sync::Mutex<DirectoryEntryToStringInner>,
284    }
285
286    impl DirectoryEntryToString {
287        pub(crate) fn new(root_path: SystemPathBuf) -> Self {
288            Self {
289                root_path,
290                inner: std::sync::Mutex::new(DirectoryEntryToStringInner::default()),
291            }
292        }
293
294        pub(crate) fn write_entry(&self, entry: Result<DirectoryEntry, Error>) {
295            let mut inner = self.inner.lock().unwrap();
296            let DirectoryEntryToStringInner { errors, visited } = &mut *inner;
297
298            match entry {
299                Ok(entry) => {
300                    let relative_path = entry
301                        .path()
302                        .strip_prefix(&self.root_path)
303                        .unwrap_or(entry.path());
304
305                    let unix_path = relative_path
306                        .components()
307                        .map(|component| component.as_str())
308                        .collect::<Vec<_>>()
309                        .join("/");
310
311                    visited.insert(unix_path, (entry.file_type, entry.depth));
312                }
313                Err(error) => {
314                    errors.push_str(&error.to_string());
315                    errors.push('\n');
316                }
317            }
318        }
319    }
320
321    impl std::fmt::Display for DirectoryEntryToString {
322        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
323            let inner = self.inner.lock().unwrap();
324            write!(f, "{paths:#?}", paths = inner.visited)?;
325
326            if !inner.errors.is_empty() {
327                writeln!(f, "\n\n{errors}", errors = inner.errors).unwrap();
328            }
329
330            Ok(())
331        }
332    }
333
334    #[derive(Default)]
335    struct DirectoryEntryToStringInner {
336        errors: String,
337        /// Stores the visited path. The key is the relative path to the root, using `/` as path separator.
338        visited: BTreeMap<String, (FileType, usize)>,
339    }
340}