Skip to main content

ruff_db/
files.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::sync::Arc;
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use dashmap::mapref::entry::Entry;
7pub use directory::{DirectoryListing, DirectoryListingError, directory_listing};
8pub use file_root::{FileRoot, FileRootKind};
9pub use path::FilePath;
10use ruff_notebook::{Notebook, NotebookError};
11use ruff_python_ast::PySourceType;
12use ruff_text_size::{Ranged, TextRange};
13use salsa::plumbing::AsId;
14use salsa::{Durability, Setter};
15
16use crate::diagnostic::{Span, UnifiedFile};
17use crate::file_revision::FileRevision;
18use crate::files::file_root::FileRoots;
19use crate::files::private::FileStatus;
20use crate::source::SourceText;
21use crate::system::{
22    SystemPath, SystemPathBuf, SystemVirtualPath, SystemVirtualPathBuf, deduplicate_nested_paths,
23};
24use crate::vendored::{VendoredPath, VendoredPathBuf};
25use crate::{Db, FxDashMap, vendored};
26
27mod directory;
28mod file_root;
29mod path;
30
31/// Interns a file system path and returns a salsa `File` ingredient.
32///
33/// Returns `Err` if the path doesn't exist, isn't accessible, or if the path points to a directory.
34#[inline]
35pub fn system_path_to_file(db: &dyn Db, path: impl AsRef<SystemPath>) -> Result<File, FileError> {
36    let file = db.files().system(db, path.as_ref());
37
38    // It's important that `vfs.file_system` creates a `VfsFile` even for files that don't exist or don't
39    // exist anymore so that Salsa can track that the caller of this function depends on the existence of
40    // that file. This function filters out files that don't exist, but Salsa will know that it must
41    // re-run the calling query whenever the `file`'s status changes (because of the `.status` call here).
42    match file.status(db) {
43        FileStatus::Exists => Ok(file),
44        FileStatus::IsADirectory => Err(FileError::IsADirectory),
45        FileStatus::NotFound => Err(FileError::NotFound),
46    }
47}
48
49/// Interns a vendored file path. Returns `Some` if the vendored file for `path` exists and `None` otherwise.
50#[inline]
51pub fn vendored_path_to_file(
52    db: &dyn Db,
53    path: impl AsRef<VendoredPath>,
54) -> Result<File, FileError> {
55    db.files().vendored(db, path.as_ref())
56}
57
58/// Lookup table that maps [file paths](`FilePath`) to salsa interned [`File`] instances.
59#[derive(Default, Clone)]
60pub struct Files {
61    inner: Arc<FilesInner>,
62}
63
64#[derive(Default)]
65struct FilesInner {
66    /// Whether inputs on newly created files should be frozen.
67    frozen: AtomicBool,
68
69    /// Lookup table that maps [`SystemPathBuf`]s to salsa interned [`File`] instances.
70    ///
71    /// The map also stores entries for files that don't exist on the file system. This is necessary
72    /// so that queries that depend on the existence of a file are re-executed when the file is created.
73    system_by_path: FxDashMap<SystemPathBuf, File>,
74
75    /// Lookup table that maps [`SystemVirtualPathBuf`]s to [`VirtualFile`] instances.
76    system_virtual_by_path: FxDashMap<SystemVirtualPathBuf, VirtualFile>,
77
78    /// Lookup table that maps vendored files to the salsa [`File`] ingredients.
79    vendored_by_path: FxDashMap<VendoredPathBuf, File>,
80
81    /// Lookup table that maps file paths to their [`FileRoot`].
82    roots: std::sync::RwLock<FileRoots>,
83}
84
85impl Files {
86    /// Freezes all inputs on files created from now on.
87    ///
88    /// Existing files retain their current durability. Callers should therefore call this before
89    /// discovering any files if they need the freeze to apply to the entire project.
90    pub fn freeze(&self) {
91        self.inner.frozen.store(true, Ordering::Relaxed);
92    }
93
94    fn input_durability(&self, default: Durability) -> Durability {
95        if self.inner.frozen.load(Ordering::Relaxed) {
96            Durability::NEVER_CHANGE
97        } else {
98            default
99        }
100    }
101
102    /// Looks up a file by its `path`.
103    ///
104    /// For a non-existing file, creates a new salsa [`File`] ingredient and stores it for future lookups.
105    ///
106    /// The operation always succeeds even if the path doesn't exist on disk, isn't accessible or if the path points to a directory.
107    /// In these cases, a file with the appropriate [`FileStatus`] is returned.
108    fn system(&self, db: &dyn Db, path: &SystemPath) -> File {
109        // All cache keys are normalized, absolute paths. However, an absolute path does not need
110        // to be fully normalized for this lookup: Camino's equality and hashing ignore redundant
111        // separators and `.` components, so `/foo/bar.py`, `/foo//bar.py`, and `/foo/./bar.py`
112        // all match the same cached `File`. A `..` component is not ignored, so a path such as
113        // `/foo/baz/../bar.py` misses the cache and falls through to full normalization. Since
114        // this fast path only returns an existing entry and never inserts one, it cannot create
115        // separate `File` identities for different spellings of the same path.
116        if path.is_absolute()
117            && let Some(file) = self.inner.system_by_path.get(path)
118        {
119            return *file;
120        }
121
122        let absolute = SystemPath::absolute(path, db.system().current_directory());
123
124        // DashMap's entry API requires an owned key. Avoid cloning it for cached paths.
125        if let Some(file) = self.inner.system_by_path.get(absolute.as_path()) {
126            return *file;
127        }
128
129        *self
130            .inner
131            .system_by_path
132            .entry(absolute.clone())
133            .or_insert_with(|| {
134                let metadata = db.system().path_metadata(path);
135
136                tracing::trace!("Adding file '{absolute}'");
137
138                let durability = self.input_durability(
139                    self.root(db, &absolute)
140                        .map_or(Durability::default(), |root| root.durability(db)),
141                );
142
143                let builder = File::builder(FilePath::from(absolute))
144                    .durability(durability)
145                    .path_durability(Durability::NEVER_CHANGE);
146
147                let builder = match metadata {
148                    Ok(metadata) if metadata.file_type().is_file() => builder
149                        .permissions(metadata.permissions())
150                        .revision(metadata.revision()),
151                    Ok(metadata) if metadata.file_type().is_directory() => builder
152                        .durability(Durability::MEDIUM.max(durability))
153                        .status(FileStatus::IsADirectory)
154                        .permissions(metadata.permissions())
155                        .revision(metadata.revision()),
156                    _ => builder
157                        .status(FileStatus::NotFound)
158                        .status_durability(Durability::MEDIUM.max(durability)),
159                };
160
161                builder.new(db)
162            })
163    }
164
165    /// Tries to look up the file for the given system path, returns `None` if no such file exists yet
166    pub fn try_system(&self, db: &dyn Db, path: &SystemPath) -> Option<File> {
167        // As in `system`, path equality normalizes redundant separators and `.`, but not `..`.
168        if path.is_absolute()
169            && let Some(file) = self.inner.system_by_path.get(path)
170        {
171            return Some(*file);
172        }
173
174        let absolute = SystemPath::absolute(path, db.system().current_directory());
175        self.inner
176            .system_by_path
177            .get(&absolute)
178            .map(|entry| *entry.value())
179    }
180
181    /// Looks up a vendored file by its path. Returns `Some` if a vendored file for the given path
182    /// exists and `None` otherwise.
183    fn vendored(&self, db: &dyn Db, path: &VendoredPath) -> Result<File, FileError> {
184        if let Some(file) = self.inner.vendored_by_path.get(path) {
185            return Ok(*file);
186        }
187
188        let file = match self.inner.vendored_by_path.entry(path.to_path_buf()) {
189            Entry::Occupied(entry) => *entry.get(),
190            Entry::Vacant(entry) => {
191                let metadata = match db.vendored().metadata(path) {
192                    Ok(metadata) => match metadata.kind() {
193                        vendored::FileType::File => metadata,
194                        vendored::FileType::Directory => return Err(FileError::IsADirectory),
195                    },
196                    Err(_) => return Err(FileError::NotFound),
197                };
198
199                tracing::trace!("Adding vendored file `{}`", path);
200                let file = File::builder(FilePath::from(path))
201                    .permissions(Some(0o444))
202                    .revision(metadata.revision())
203                    .durability(Durability::NEVER_CHANGE)
204                    .new(db);
205
206                entry.insert(file);
207
208                file
209            }
210        };
211
212        Ok(file)
213    }
214
215    /// Create a new virtual file at the given path and store it for future lookups.
216    ///
217    /// This will always create a new file, overwriting any existing file at `path` in the internal
218    /// storage.
219    pub fn virtual_file(&self, db: &dyn Db, path: &SystemVirtualPath) -> VirtualFile {
220        tracing::trace!("Adding virtual file {}", path);
221        let virtual_file = VirtualFile(
222            File::builder(FilePath::from(path))
223                .durability(self.input_durability(Durability::LOW))
224                .path_durability(Durability::NEVER_CHANGE)
225                .status(FileStatus::Exists)
226                .revision(FileRevision::zero())
227                .permissions(None)
228                .permissions_durability(Durability::NEVER_CHANGE)
229                .new(db),
230        );
231        self.inner
232            .system_virtual_by_path
233            .insert(path.to_path_buf(), virtual_file);
234        virtual_file
235    }
236
237    /// Tries to look up a virtual file by its path. Returns `None` if no such file exists yet.
238    pub fn try_virtual_file(&self, path: &SystemVirtualPath) -> Option<VirtualFile> {
239        self.inner
240            .system_virtual_by_path
241            .get(path)
242            .map(|entry| *entry.value())
243    }
244
245    /// Looks up the closest  root for `path`. Returns `None` if `path` isn't enclosed by any source root.
246    ///
247    /// Roots can be nested, in which case the closest root is returned.
248    pub fn root(&self, db: &dyn Db, path: &SystemPath) -> Option<FileRoot> {
249        let roots = self.inner.roots.read().unwrap();
250
251        let absolute = SystemPath::absolute(path, db.system().current_directory());
252        roots.at(&absolute)
253    }
254
255    /// Adds a new root for `path` and returns the root.
256    ///
257    /// The root isn't added nor is the file root's kind updated if a root for `path` already exists.
258    pub fn try_add_root(&self, db: &dyn Db, path: &SystemPath, kind: FileRootKind) -> FileRoot {
259        let mut roots = self.inner.roots.write().unwrap();
260
261        let absolute = SystemPath::absolute(path, db.system().current_directory());
262        roots.try_add(db, absolute, kind)
263    }
264
265    /// Refreshes the state of all known files under `paths` recursively.
266    ///
267    /// The most common use case is to update the [`Files`] state after removing or moving directories.
268    ///
269    /// # Performance
270    /// Refreshing the state of files recursively is expensive. It requires iterating over all known files
271    /// and making system calls to get the latest status of matching files.
272    /// That's why [`File::sync_path`] is preferred if it is known that the path is a file.
273    pub fn sync_all_recursive<P, I>(db: &mut dyn Db, paths: I)
274    where
275        P: AsRef<SystemPath>,
276        I: IntoIterator<Item = P>,
277    {
278        let current_directory = db.system().current_directory();
279        let paths = deduplicate_nested_paths(
280            paths
281                .into_iter()
282                .map(|path| SystemPath::absolute(path.as_ref(), current_directory)),
283        )
284        .collect::<BTreeSet<_>>();
285
286        if paths.is_empty() {
287            return;
288        }
289
290        let parents = paths
291            .iter()
292            .filter_map(|path| path.parent().map(SystemPath::to_path_buf))
293            .collect::<BTreeSet<_>>();
294
295        let inner = Arc::clone(&db.files().inner);
296        for entry in inner.system_by_path.iter_mut() {
297            let path = entry.key();
298            if paths
299                .range(..=path.to_path_buf())
300                .next_back()
301                .is_some_and(|candidate| path.starts_with(candidate.as_path()))
302                || parents.contains(path)
303            {
304                File::sync_system_path(db, path, Some(*entry.value()));
305            }
306        }
307    }
308
309    /// Refreshes the state of all known files.
310    ///
311    /// This is a last-resort method that should only be used when more granular updates aren't possible
312    /// (for example, because the file watcher failed to observe some changes). Use responsibly!
313    ///
314    /// # Performance
315    /// Refreshing the state of every file is expensive. It requires iterating over all known files and
316    /// issuing a system call to get the latest status of each file.
317    pub fn sync_all(db: &mut dyn Db) {
318        tracing::debug!("Syncing all files");
319        let inner = Arc::clone(&db.files().inner);
320        for entry in inner.system_by_path.iter_mut() {
321            File::sync_system_path(db, entry.key(), Some(*entry.value()));
322        }
323    }
324}
325
326impl fmt::Debug for Files {
327    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328        if f.alternate() {
329            let mut map = f.debug_map();
330
331            for entry in self.inner.system_by_path.iter() {
332                map.entry(entry.key(), entry.value());
333            }
334            map.finish()
335        } else {
336            f.debug_struct("Files")
337                .field("system_by_path", &self.inner.system_by_path.len())
338                .field(
339                    "system_virtual_by_path",
340                    &self.inner.system_virtual_by_path.len(),
341                )
342                .field("vendored_by_path", &self.inner.vendored_by_path.len())
343                .finish()
344        }
345    }
346}
347
348impl std::panic::RefUnwindSafe for Files {}
349
350/// A file-system path that's either stored on the host system's file system or in the vendored file system.
351///
352/// # Ordering
353/// Ordering is based on the file's salsa-assigned id and not on its values.
354/// The id may change between runs.
355#[salsa::input(heap_size=ruff_memory_usage::heap_size)]
356#[derive(PartialOrd, Ord)]
357pub struct File {
358    /// The path of the file (immutable).
359    #[returns(ref)]
360    pub path: FilePath,
361
362    /// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows
363    /// or when the file has been deleted.
364    #[default]
365    #[returns(copy)]
366    pub permissions: Option<u32>,
367
368    /// The path revision. A file or directory has changed if the revisions don't compare equal.
369    #[default]
370    #[returns(copy)]
371    pub revision: FileRevision,
372
373    /// The status of the file.
374    ///
375    /// Salsa doesn't support deleting inputs. The only way to signal dependent queries that
376    /// the file has been deleted is to change the status to `Deleted`.
377    #[default]
378    #[returns(copy)]
379    pub status: FileStatus,
380
381    /// Overrides the result of [`source_text`](crate::source::source_text).
382    ///
383    /// This is useful when running queries after modifying a file's content but
384    /// before the content is written to disk. For example, to verify that the applied fixes
385    /// didn't introduce any new errors.
386    ///
387    /// The override gets automatically removed the next time the file changes.
388    #[default]
389    #[returns(ref)]
390    pub source_text_override: Option<SourceText>,
391}
392
393// The Salsa heap is tracked separately.
394impl get_size2::GetSize for File {}
395
396struct SyncPathResult {
397    status_changed: bool,
398}
399
400impl File {
401    /// Reads the content of the file into a [`String`].
402    ///
403    /// Reading the same file multiple times isn't guaranteed to return the same content. It's possible
404    /// that the file has been modified in between the reads.
405    pub fn read_to_string(&self, db: &dyn Db) -> crate::system::Result<String> {
406        let path = self.path(db);
407
408        match path {
409            FilePath::System(system) => {
410                // Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
411                let _ = self.revision(db);
412
413                db.system().read_to_string(system)
414            }
415            FilePath::Vendored(vendored) => db.vendored().read_to_string(vendored),
416            FilePath::SystemVirtual(system_virtual) => {
417                // Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
418                let _ = self.revision(db);
419
420                db.system().read_virtual_path_to_string(system_virtual)
421            }
422        }
423    }
424
425    /// Reads the content of the file into a [`Notebook`].
426    ///
427    /// Reading the same file multiple times isn't guaranteed to return the same content. It's possible
428    /// that the file has been modified in between the reads.
429    pub(crate) fn read_to_notebook(&self, db: &dyn Db) -> Result<Notebook, NotebookError> {
430        let path = self.path(db);
431
432        match path {
433            FilePath::System(system) => {
434                // Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
435                let _ = self.revision(db);
436
437                db.system().read_to_notebook(system)
438            }
439            FilePath::Vendored(_) => Err(NotebookError::Io(std::io::Error::new(
440                std::io::ErrorKind::InvalidInput,
441                "Reading a notebook from the vendored file system is not supported.",
442            ))),
443            FilePath::SystemVirtual(system_virtual) => {
444                // Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
445                let _ = self.revision(db);
446
447                db.system().read_virtual_path_to_notebook(system_virtual)
448            }
449        }
450    }
451
452    /// Refreshes the file metadata by querying the file system if needed.
453    ///
454    /// Directory listings are invalidated if the path's file status changed, its prior status is
455    /// unknown, or if `path` is itself a directory.
456    pub fn sync_path(db: &mut dyn Db, path: &SystemPath) {
457        let absolute = SystemPath::absolute(path, db.system().current_directory());
458        let result = Self::sync_system_path(db, &absolute, None);
459        Self::touch_parent_directory_after_sync(db, &absolute, result);
460    }
461
462    /// Refreshes *only* the file metadata by querying the file system if needed.
463    ///
464    /// This specifically does not invalidate any directory listings.
465    pub fn sync_path_only(db: &mut dyn Db, path: &SystemPath) {
466        let absolute = SystemPath::absolute(path, db.system().current_directory());
467        Self::sync_system_path(db, &absolute, None);
468    }
469
470    /// Increments the revision for the virtual file at `path`.
471    pub fn sync_virtual_path(db: &mut dyn Db, path: &SystemVirtualPath) {
472        if let Some(virtual_file) = db.files().try_virtual_file(path) {
473            virtual_file.sync(db);
474        }
475    }
476
477    /// Syncs the [`File`]'s state with the state of the file on the system.
478    pub fn sync(self, db: &mut dyn Db) {
479        let path = self.path(db).clone();
480
481        match path {
482            FilePath::System(system) => {
483                let result = Self::sync_system_path(db, &system, Some(self));
484                Self::touch_parent_directory_after_sync(db, &system, result);
485            }
486            FilePath::Vendored(_) => {
487                // Readonly, can never be out of date.
488            }
489            FilePath::SystemVirtual(_) => {
490                VirtualFile(self).sync(db);
491            }
492        }
493    }
494
495    /// Private method providing the implementation for [`Self::sync_path`] and [`Self::sync`] for
496    /// system paths.
497    fn sync_system_path(db: &mut dyn Db, path: &SystemPath, file: Option<File>) -> SyncPathResult {
498        let Some(file) = file.or_else(|| db.files().try_system(db, path)) else {
499            return SyncPathResult {
500                status_changed: true,
501            };
502        };
503
504        let (status, revision, permission) = match db.system().path_metadata(path) {
505            Ok(metadata) if metadata.file_type().is_file() => (
506                FileStatus::Exists,
507                metadata.revision(),
508                metadata.permissions(),
509            ),
510            Ok(metadata) if metadata.file_type().is_directory() => (
511                FileStatus::IsADirectory,
512                metadata.revision(),
513                metadata.permissions(),
514            ),
515            _ => (FileStatus::NotFound, FileRevision::zero(), None),
516        };
517
518        let mut clear_override = false;
519
520        let old_status = file.status(db);
521        let status_changed = old_status != status;
522
523        if status_changed {
524            tracing::debug!("Updating the status of `{}`", file.path(db));
525            file.set_status(db).to(status);
526            clear_override = true;
527        }
528
529        if file.revision(db) != revision {
530            tracing::debug!("Updating the revision of `{}`", file.path(db));
531            file.set_revision(db).to(revision);
532            clear_override = true;
533        }
534
535        if file.permissions(db) != permission {
536            tracing::debug!("Updating the permissions of `{}`", file.path(db));
537            file.set_permissions(db).to(permission);
538        }
539
540        if clear_override && file.source_text_override(db).is_some() {
541            file.set_source_text_override(db).to(None);
542        }
543
544        SyncPathResult { status_changed }
545    }
546
547    fn touch_parent_directory_after_sync(
548        db: &mut dyn Db,
549        path: &SystemPath,
550        result: SyncPathResult,
551    ) {
552        if result.status_changed
553            && let Some(parent) = path.parent()
554        {
555            Self::sync_system_path(db, parent, None);
556        }
557    }
558
559    /// Returns `true` if the file exists.
560    pub fn exists(self, db: &dyn Db) -> bool {
561        self.status(db) == FileStatus::Exists
562    }
563
564    /// Returns `true` if the file should be analyzed as a type stub.
565    pub fn is_stub(self, db: &dyn Db) -> bool {
566        self.source_type(db).is_stub()
567    }
568
569    /// Returns `true` if the file is an `__init__.pyi`
570    pub fn is_package(self, db: &dyn Db) -> bool {
571        let path = self.path(db).as_str();
572        path.ends_with("__init__.pyi") || path.ends_with("__init__.py")
573    }
574
575    pub fn source_type(self, db: &dyn Db) -> PySourceType {
576        match self.path(db) {
577            FilePath::System(path) => path
578                .extension()
579                .map_or(PySourceType::Python, PySourceType::from_extension),
580            FilePath::Vendored(_) => PySourceType::Stub,
581            FilePath::SystemVirtual(path) => path
582                .extension()
583                .map_or(PySourceType::Python, PySourceType::from_extension),
584        }
585    }
586}
587
588impl fmt::Debug for File {
589    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
590        salsa::with_attached_database(|db| {
591            if f.alternate() {
592                f.debug_struct("File")
593                    .field("path", &self.path(db))
594                    .field("status", &self.status(db))
595                    .field("permissions", &self.permissions(db))
596                    .field("revision", &self.revision(db))
597                    .finish()
598            } else {
599                f.debug_tuple("File").field(&self.path(db)).finish()
600            }
601        })
602        .unwrap_or_else(|| f.debug_tuple("file").field(&self.as_id()).finish())
603    }
604}
605
606/// A virtual file that doesn't exist on the file system.
607///
608/// This is a wrapper around a [`File`] that provides additional methods to interact with a virtual
609/// file.
610#[derive(Copy, Clone, Debug)]
611pub struct VirtualFile(File);
612
613impl VirtualFile {
614    /// Returns the underlying [`File`].
615    pub fn file(&self) -> File {
616        self.0
617    }
618
619    /// Increments the revision of the underlying [`File`].
620    pub fn sync(&self, db: &mut dyn Db) {
621        let file = self.0;
622        tracing::debug!("Updating the revision of `{}`", file.path(db));
623        let current_revision = file.revision(db);
624        file.set_revision(db)
625            .to(FileRevision::new(current_revision.as_u128() + 1));
626    }
627
628    /// Closes the virtual file.
629    pub fn close(&self, db: &mut dyn Db) {
630        tracing::debug!("Closing virtual file `{}`", self.0.path(db));
631        self.0.set_status(db).to(FileStatus::NotFound);
632    }
633}
634
635// The types in here need to be public because they're salsa ingredients but we
636// don't want them to be publicly accessible. That's why we put them into a private module.
637mod private {
638    #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
639    pub enum FileStatus {
640        /// The file exists.
641        #[default]
642        Exists,
643
644        /// The path isn't a file and instead points to a directory.
645        IsADirectory,
646
647        /// The path doesn't exist, isn't accessible, or no longer exists.
648        NotFound,
649    }
650}
651
652#[derive(Copy, Clone, Debug, Eq, PartialEq)]
653pub enum FileError {
654    IsADirectory,
655    NotFound,
656}
657
658impl fmt::Display for FileError {
659    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
660        match self {
661            FileError::IsADirectory => f.write_str("Is a directory"),
662            FileError::NotFound => f.write_str("Not found"),
663        }
664    }
665}
666
667impl std::error::Error for FileError {}
668
669/// Range with its corresponding file.
670#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
671pub struct FileRange {
672    file: File,
673    range: TextRange,
674}
675
676impl FileRange {
677    pub const fn new(file: File, range: TextRange) -> Self {
678        Self { file, range }
679    }
680
681    pub const fn file(&self) -> File {
682        self.file
683    }
684}
685
686impl Ranged for FileRange {
687    #[inline]
688    fn range(&self) -> TextRange {
689        self.range
690    }
691}
692
693impl TryFrom<&Span> for FileRange {
694    type Error = ();
695
696    fn try_from(value: &Span) -> Result<Self, Self::Error> {
697        let UnifiedFile::Ty(file) = value.file() else {
698            return Err(());
699        };
700
701        Ok(Self {
702            file: *file,
703            range: value.range().ok_or(())?,
704        })
705    }
706}
707
708impl TryFrom<Span> for FileRange {
709    type Error = ();
710
711    fn try_from(value: Span) -> Result<Self, Self::Error> {
712        Self::try_from(&value)
713    }
714}
715
716#[cfg(test)]
717mod tests {
718    use salsa::Setter;
719
720    use crate::Db as _;
721    use crate::file_revision::FileRevision;
722    use crate::files::{File, FileError, system_path_to_file, vendored_path_to_file};
723    use crate::source::source_text;
724    use crate::system::{DbWithWritableSystem as _, SystemPath};
725    use crate::tests::TestDb;
726    use crate::vendored::VendoredFileSystemBuilder;
727    use zip::CompressionMethod;
728
729    #[test]
730    fn system_existing_file() -> crate::system::Result<()> {
731        let mut db = TestDb::new();
732
733        db.write_file("test.py", "print('Hello world')")?;
734
735        let test = system_path_to_file(&db, "test.py").expect("File to exist.");
736
737        assert_eq!(test.permissions(&db), Some(0o755));
738        assert_ne!(test.revision(&db), FileRevision::zero());
739        assert_eq!(&test.read_to_string(&db)?, "print('Hello world')");
740
741        Ok(())
742    }
743
744    #[test]
745    fn system_non_existing_file() {
746        let db = TestDb::new();
747
748        let test = system_path_to_file(&db, "test.py");
749
750        assert_eq!(test, Err(FileError::NotFound));
751    }
752
753    #[test]
754    fn system_normalize_paths() {
755        #[track_caller]
756        fn assert_normalized_path(db: &TestDb, path: &str, canonical: File) {
757            assert_eq!(system_path_to_file(db, path), Ok(canonical));
758            assert_eq!(
759                db.files().try_system(db, SystemPath::new(path)),
760                Some(canonical)
761            );
762        }
763
764        let mut db = TestDb::new();
765        db.write_file("/foo/bar.py", "x = 1").unwrap();
766        db.write_file("/foo/baz/bar.py", "x = 2").unwrap();
767
768        let canonical = system_path_to_file(&db, "/foo/bar.py").unwrap();
769        assert_normalized_path(&db, "foo/bar.py", canonical);
770        assert_normalized_path(&db, "/foo//bar.py", canonical);
771        assert_normalized_path(&db, "/foo/./bar.py", canonical);
772        assert_normalized_path(&db, "/foo/baz/../bar.py", canonical);
773
774        let distinct = system_path_to_file(&db, "/foo/baz/bar.py").unwrap();
775        assert_ne!(canonical, distinct);
776    }
777
778    #[test]
779    #[should_panic]
780    fn freeze_applies_to_new_file_overrides() {
781        let mut db = TestDb::new();
782        db.write_file("test.py", "x = 1").unwrap();
783        db.files().freeze();
784
785        let file = system_path_to_file(&db, "test.py").unwrap();
786        let source = source_text(&db, file);
787        file.set_source_text_override(&mut db).to(Some(source));
788    }
789
790    #[test]
791    fn freeze_does_not_change_existing_files() {
792        let mut db = TestDb::new();
793        db.write_file("test.py", "x = 1").unwrap();
794
795        let file = system_path_to_file(&db, "test.py").unwrap();
796        db.files().freeze();
797
798        let source = source_text(&db, file);
799        file.set_source_text_override(&mut db)
800            .to(Some(source.clone()));
801        assert_eq!(file.source_text_override(&db).as_ref(), Some(&source));
802    }
803
804    #[test]
805    fn stubbed_vendored_file() -> crate::system::Result<()> {
806        let mut db = TestDb::new();
807
808        let mut vendored_builder = VendoredFileSystemBuilder::new(CompressionMethod::Stored);
809        vendored_builder
810            .add_file("test.pyi", "def foo() -> str")
811            .unwrap();
812        let vendored = vendored_builder.finish().unwrap();
813        db.with_vendored(vendored);
814
815        let test = vendored_path_to_file(&db, "test.pyi").expect("Vendored file to exist.");
816
817        assert_eq!(test.permissions(&db), Some(0o444));
818        assert_ne!(test.revision(&db), FileRevision::zero());
819        assert_eq!(&test.read_to_string(&db)?, "def foo() -> str");
820
821        Ok(())
822    }
823
824    #[test]
825    fn stubbed_vendored_file_non_existing() {
826        let db = TestDb::new();
827
828        assert_eq!(
829            vendored_path_to_file(&db, "test.py"),
830            Err(FileError::NotFound)
831        );
832    }
833}