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#[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 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#[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#[derive(Default, Clone)]
60pub struct Files {
61 inner: Arc<FilesInner>,
62}
63
64#[derive(Default)]
65struct FilesInner {
66 frozen: AtomicBool,
68
69 system_by_path: FxDashMap<SystemPathBuf, File>,
74
75 system_virtual_by_path: FxDashMap<SystemVirtualPathBuf, VirtualFile>,
77
78 vendored_by_path: FxDashMap<VendoredPathBuf, File>,
80
81 roots: std::sync::RwLock<FileRoots>,
83}
84
85impl Files {
86 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 fn system(&self, db: &dyn Db, path: &SystemPath) -> File {
109 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 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 pub fn try_system(&self, db: &dyn Db, path: &SystemPath) -> Option<File> {
167 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 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 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 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 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 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 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 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#[salsa::input(heap_size=ruff_memory_usage::heap_size)]
356#[derive(PartialOrd, Ord)]
357pub struct File {
358 #[returns(ref)]
360 pub path: FilePath,
361
362 #[default]
365 #[returns(copy)]
366 pub permissions: Option<u32>,
367
368 #[default]
370 #[returns(copy)]
371 pub revision: FileRevision,
372
373 #[default]
378 #[returns(copy)]
379 pub status: FileStatus,
380
381 #[default]
389 #[returns(ref)]
390 pub source_text_override: Option<SourceText>,
391}
392
393impl get_size2::GetSize for File {}
395
396struct SyncPathResult {
397 status_changed: bool,
398}
399
400impl File {
401 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 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 let _ = self.revision(db);
419
420 db.system().read_virtual_path_to_string(system_virtual)
421 }
422 }
423 }
424
425 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 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 let _ = self.revision(db);
446
447 db.system().read_virtual_path_to_notebook(system_virtual)
448 }
449 }
450 }
451
452 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 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 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 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 }
489 FilePath::SystemVirtual(_) => {
490 VirtualFile(self).sync(db);
491 }
492 }
493 }
494
495 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 pub fn exists(self, db: &dyn Db) -> bool {
561 self.status(db) == FileStatus::Exists
562 }
563
564 pub fn is_stub(self, db: &dyn Db) -> bool {
566 self.source_type(db).is_stub()
567 }
568
569 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#[derive(Copy, Clone, Debug)]
611pub struct VirtualFile(File);
612
613impl VirtualFile {
614 pub fn file(&self) -> File {
616 self.0
617 }
618
619 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 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
635mod private {
638 #[derive(Copy, Clone, Debug, Eq, PartialEq, Default, get_size2::GetSize)]
639 pub enum FileStatus {
640 #[default]
642 Exists,
643
644 IsADirectory,
646
647 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#[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}