[red-knot] Case sensitive module resolver (#16521)

## Summary

This PR implements the first part of
https://github.com/astral-sh/ruff/discussions/16440. It ensures that Red
Knot's module resolver is case sensitive on all systems.

This PR combines a few approaches:

1. It uses `canonicalize` on non-case-sensitive systems to get the real
casing of a path. This works for as long as no symlinks or mapped
network drives (the windows `E:\` is mapped to `\\server\share` thingy).
This is the same as what Pyright does
2. If 1. fails, fall back to recursively list the parent directory and
test if the path's file name matches the casing exactly as listed in by
list dir. This is the same approach as CPython takes in its module
resolver. The main downside is that it requires more syscalls because,
unlike CPython, we Red Knot needs to invalidate its caches if a file
name gets renamed (CPython assumes that the folders are immutable).

It's worth noting that the file watching test that I added that renames
`lib.py` to `Lib.py` currently doesn't pass on case-insensitive systems.
Making it pass requires some more involved changes to `Files`. I plan to
work on this next. There's the argument that landing this PR on its own
isn't worth it without this issue being addressed. I think it's still a
good step in the right direction even when some of the details on how
and where the path case sensitive comparison is implemented.

## Test plan

I added multiple integration tests (including a failing one). I tested
that the `case-sensitivity` detection works as expected on Windows,
MacOS and Linux and that the fast-paths are taken accordingly.
This commit is contained in:
Micha Reiser 2025-03-14 20:16:44 +01:00 committed by GitHub
parent a128ca761f
commit a467e7c8d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 543 additions and 27 deletions

View file

@ -9,7 +9,7 @@ pub use os::OsSystem;
use ruff_notebook::{Notebook, NotebookError};
use std::error::Error;
use std::fmt::Debug;
use std::fmt::{Debug, Formatter};
use std::path::{Path, PathBuf};
use std::{fmt, io};
pub use test::{DbWithTestSystem, DbWithWritableSystem, InMemorySystem, TestSystem};
@ -89,6 +89,20 @@ pub trait System: Debug {
self.path_metadata(path).is_ok()
}
/// Returns `true` if `path` exists on disk using the exact casing as specified in `path` for the parts after `prefix`.
///
/// This is the same as [`Self::path_exists`] on case-sensitive systems.
///
/// ## The use of prefix
///
/// Prefix is only intended as an optimization for systems that can't efficiently check
/// if an entire path exists with the exact casing as specified in `path`. However,
/// implementations are allowed to check the casing of the entire path if they can do so efficiently.
fn path_exists_case_sensitive(&self, path: &SystemPath, prefix: &SystemPath) -> bool;
/// Returns the [`CaseSensitivity`] of the system's file system.
fn case_sensitivity(&self) -> CaseSensitivity;
/// Returns `true` if `path` exists and is a directory.
fn is_directory(&self, path: &SystemPath) -> bool {
self.path_metadata(path)
@ -161,6 +175,39 @@ pub trait System: Debug {
fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
}
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
pub enum CaseSensitivity {
/// The case sensitivity of the file system is unknown.
///
/// The file system is either case-sensitive or case-insensitive. A caller
/// should not assume either case.
#[default]
Unknown,
/// The file system is case-sensitive.
CaseSensitive,
/// The file system is case-insensitive.
CaseInsensitive,
}
impl CaseSensitivity {
/// Returns `true` if the file system is known to be case-sensitive.
pub const fn is_case_sensitive(self) -> bool {
matches!(self, Self::CaseSensitive)
}
}
impl fmt::Display for CaseSensitivity {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
CaseSensitivity::Unknown => f.write_str("unknown"),
CaseSensitivity::CaseSensitive => f.write_str("case-sensitive"),
CaseSensitivity::CaseInsensitive => f.write_str("case-insensitive"),
}
}
}
/// System trait for non-readonly systems.
pub trait WritableSystem: System {
/// Writes the given content to the file at the given path.