Skip to main content

ruff_db/files/
file_root.rs

1use path_slash::PathExt;
2use salsa::Durability;
3
4use crate::Db;
5use crate::system::{SystemPath, SystemPathBuf};
6
7/// A root path for files tracked by the database.
8///
9/// We currently create roots for:
10/// * static module resolution paths
11/// * the project root
12///
13/// File roots determine the durability of files and directories.
14#[salsa::input(debug, heap_size=ruff_memory_usage::heap_size)]
15pub struct FileRoot {
16    /// The path of a root is guaranteed to never change.
17    #[returns(deref)]
18    pub path: Box<SystemPath>,
19
20    /// The kind of the root at the time of its creation.
21    #[returns(copy)]
22    pub kind_at_time_of_creation: FileRootKind,
23}
24
25impl FileRoot {
26    pub(crate) fn durability(self, db: &dyn Db) -> salsa::Durability {
27        self.kind_at_time_of_creation(db).durability()
28    }
29}
30
31#[derive(Copy, Clone, Debug, Eq, PartialEq, get_size2::GetSize)]
32pub enum FileRootKind {
33    /// The root of a project.
34    Project,
35
36    /// A non-project module resolution search path.
37    SearchPath,
38}
39
40impl FileRootKind {
41    const fn durability(self) -> Durability {
42        match self {
43            FileRootKind::Project => Durability::LOW,
44            FileRootKind::SearchPath => Durability::HIGH,
45        }
46    }
47}
48
49#[derive(Default)]
50pub(super) struct FileRoots {
51    by_path: matchit::Router<FileRoot>,
52}
53
54impl FileRoots {
55    /// Tries to add a new root for `path` and returns the root.
56    ///
57    /// The root isn't added nor is the file root's kind updated if a root for `path` already exists.
58    pub(super) fn try_add(
59        &mut self,
60        db: &dyn Db,
61        path: SystemPathBuf,
62        kind: FileRootKind,
63    ) -> FileRoot {
64        // SAFETY: Guaranteed to succeed because `path` is a UTF-8 that only contains Unicode characters.
65        let normalized_path = path.as_std_path().to_slash().unwrap();
66
67        if let Ok(existing) = self.by_path.at(&normalized_path) {
68            // Only if it is an exact match
69            if existing.value.path(db) == &*path {
70                return *existing.value;
71            }
72        }
73
74        tracing::debug!("Adding new file root '{path}' of kind {kind:?}");
75
76        // normalize the path to use `/` separators and escape the '{' and '}' characters,
77        // which matchit uses for routing parameters
78        let mut route = normalized_path.replace('{', "{{").replace('}', "}}");
79
80        // Insert a new source root
81        let root = FileRoot::builder(path.into(), kind)
82            .durability(Durability::NEVER_CHANGE)
83            .new(db);
84
85        // Insert a path that matches the root itself
86        self.by_path.insert(route.clone(), root).unwrap();
87
88        // Insert a path that matches all subdirectories and files
89        if !route.ends_with("/") {
90            route.push('/');
91        }
92        route.push_str("{*filepath}");
93
94        self.by_path.insert(route, root).unwrap();
95
96        root
97    }
98
99    /// Returns the closest root for `path` or `None` if no root contains `path`.
100    pub(super) fn at(&self, path: &SystemPath) -> Option<FileRoot> {
101        // SAFETY: Guaranteed to succeed because `path` is a UTF-8 that only contains Unicode characters.
102        let normalized_path = path.as_std_path().to_slash().unwrap();
103        let entry = self.by_path.at(&normalized_path).ok()?;
104        Some(*entry.value)
105    }
106}