Compare commits
6 Commits
0.5.2
...
20240708-1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b974d1a746 | ||
|
|
b9438eb57a | ||
|
|
75e65254e5 | ||
|
|
225a4169c4 | ||
|
|
dc3773b878 | ||
|
|
60f18bd0ef |
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,51 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.5.2
|
||||
|
||||
### Preview features
|
||||
|
||||
- Use `space` separator before parenthesized expressions in comprehensions with leading comments ([#12282](https://github.com/astral-sh/ruff/pull/12282))
|
||||
- \[`flake8-async`\] Update `ASYNC100` to include `anyio` and `asyncio` ([#12221](https://github.com/astral-sh/ruff/pull/12221))
|
||||
- \[`flake8-async`\] Update `ASYNC109` to include `anyio` and `asyncio` ([#12236](https://github.com/astral-sh/ruff/pull/12236))
|
||||
- \[`flake8-async`\] Update `ASYNC110` to include `anyio` and `asyncio` ([#12261](https://github.com/astral-sh/ruff/pull/12261))
|
||||
- \[`flake8-async`\] Update `ASYNC115` to include `anyio` and `asyncio` ([#12262](https://github.com/astral-sh/ruff/pull/12262))
|
||||
- \[`flake8-async`\] Update `ASYNC116` to include `anyio` and `asyncio` ([#12266](https://github.com/astral-sh/ruff/pull/12266))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-return`\] Exempt properties from explicit return rule (`RET501`) ([#12243](https://github.com/astral-sh/ruff/pull/12243))
|
||||
- \[`numpy`\] Add `np.NAN`-to-`np.nan` diagnostic ([#12292](https://github.com/astral-sh/ruff/pull/12292))
|
||||
- \[`refurb`\] Make `list-reverse-copy` an unsafe fix ([#12303](https://github.com/astral-sh/ruff/pull/12303))
|
||||
|
||||
### Server
|
||||
|
||||
- Consider `include` and `extend-include` settings in native server ([#12252](https://github.com/astral-sh/ruff/pull/12252))
|
||||
- Include nested configurations in settings reloading ([#12253](https://github.com/astral-sh/ruff/pull/12253))
|
||||
|
||||
### CLI
|
||||
|
||||
- Omit code frames for fixes with empty ranges ([#12304](https://github.com/astral-sh/ruff/pull/12304))
|
||||
- Warn about formatter incompatibility for `D203` ([#12238](https://github.com/astral-sh/ruff/pull/12238))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Make cache-write failures non-fatal on Windows ([#12302](https://github.com/astral-sh/ruff/pull/12302))
|
||||
- Treat `not` operations as boolean tests ([#12301](https://github.com/astral-sh/ruff/pull/12301))
|
||||
- \[`flake8-bandit`\] Avoid `S310` violations for HTTP-safe f-strings ([#12305](https://github.com/astral-sh/ruff/pull/12305))
|
||||
- \[`flake8-bandit`\] Support explicit string concatenations in S310 HTTP detection ([#12315](https://github.com/astral-sh/ruff/pull/12315))
|
||||
- \[`flake8-bandit`\] fix S113 false positive for httpx without `timeout` argument ([#12213](https://github.com/astral-sh/ruff/pull/12213))
|
||||
- \[`pycodestyle`\] Remove "non-obvious" allowance for E721 ([#12300](https://github.com/astral-sh/ruff/pull/12300))
|
||||
- \[`pyflakes`\] Consider `with` blocks as single-item branches for redefinition analysis ([#12311](https://github.com/astral-sh/ruff/pull/12311))
|
||||
- \[`refurb`\] Restrict forwarding for `newline` argument in `open()` calls to Python versions >= 3.10 ([#12244](https://github.com/astral-sh/ruff/pull/12244))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update help and documentation to reflect `--output-format full` default ([#12248](https://github.com/astral-sh/ruff/pull/12248))
|
||||
|
||||
### Performance
|
||||
|
||||
- Use more threads when discovering Python files ([#12258](https://github.com/astral-sh/ruff/pull/12258))
|
||||
|
||||
## 0.5.1
|
||||
|
||||
### Preview features
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -1864,7 +1864,6 @@ name = "red_knot"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"countme",
|
||||
"crossbeam",
|
||||
"ctrlc",
|
||||
@@ -1889,7 +1888,6 @@ dependencies = [
|
||||
"camino",
|
||||
"compact_str",
|
||||
"insta",
|
||||
"once_cell",
|
||||
"path-slash",
|
||||
"ruff_db",
|
||||
"ruff_python_stdlib",
|
||||
@@ -2001,7 +1999,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2096,13 +2094,13 @@ dependencies = [
|
||||
"dashmap 6.0.1",
|
||||
"filetime",
|
||||
"insta",
|
||||
"once_cell",
|
||||
"ruff_python_ast",
|
||||
"ruff_python_parser",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash 2.0.0",
|
||||
"salsa",
|
||||
"tempfile",
|
||||
"tracing",
|
||||
"zip",
|
||||
]
|
||||
@@ -2183,7 +2181,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2449,6 +2447,7 @@ version = "0.2.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"crossbeam",
|
||||
"globset",
|
||||
"insta",
|
||||
"jod-thread",
|
||||
"libc",
|
||||
|
||||
@@ -136,8 +136,8 @@ curl -LsSf https://astral.sh/ruff/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/install.ps1 | iex"
|
||||
|
||||
# For a specific version.
|
||||
curl -LsSf https://astral.sh/ruff/0.5.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.5.1/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.5.1/install.ps1 | iex"
|
||||
```
|
||||
|
||||
You can also install Ruff via [Homebrew](https://formulae.brew.sh/formula/ruff), [Conda](https://anaconda.org/conda-forge/ruff),
|
||||
@@ -170,7 +170,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.5.2
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -19,7 +19,6 @@ ruff_db = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
|
||||
anyhow = { workspace = true }
|
||||
clap = { workspace = true, features = ["wrap_help"] }
|
||||
countme = { workspace = true, features = ["enable"] }
|
||||
crossbeam = { workspace = true }
|
||||
ctrlc = { version = "3.4.4" }
|
||||
|
||||
@@ -1,53 +1,52 @@
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf};
|
||||
use ruff_db::vfs::VfsFile;
|
||||
|
||||
use crate::db::Jar;
|
||||
|
||||
pub mod db;
|
||||
pub mod lint;
|
||||
pub mod program;
|
||||
pub mod target_version;
|
||||
pub mod watch;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Workspace {
|
||||
root: SystemPathBuf,
|
||||
root: FileSystemPathBuf,
|
||||
/// The files that are open in the workspace.
|
||||
///
|
||||
/// * Editor: The files that are actively being edited in the editor (the user has a tab open with the file).
|
||||
/// * CLI: The resolved files passed as arguments to the CLI.
|
||||
open_files: FxHashSet<File>,
|
||||
open_files: FxHashSet<VfsFile>,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn new(root: SystemPathBuf) -> Self {
|
||||
pub fn new(root: FileSystemPathBuf) -> Self {
|
||||
Self {
|
||||
root,
|
||||
open_files: FxHashSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root(&self) -> &SystemPath {
|
||||
pub fn root(&self) -> &FileSystemPath {
|
||||
self.root.as_path()
|
||||
}
|
||||
|
||||
// TODO having the content in workspace feels wrong.
|
||||
pub fn open_file(&mut self, file_id: File) {
|
||||
pub fn open_file(&mut self, file_id: VfsFile) {
|
||||
self.open_files.insert(file_id);
|
||||
}
|
||||
|
||||
pub fn close_file(&mut self, file_id: File) {
|
||||
pub fn close_file(&mut self, file_id: VfsFile) {
|
||||
self.open_files.remove(&file_id);
|
||||
}
|
||||
|
||||
// TODO introduce an `OpenFile` type instead of using an anonymous tuple.
|
||||
pub fn open_files(&self) -> impl Iterator<Item = File> + '_ {
|
||||
pub fn open_files(&self) -> impl Iterator<Item = VfsFile> + '_ {
|
||||
self.open_files.iter().copied()
|
||||
}
|
||||
|
||||
pub fn is_file_open(&self, file_id: File) -> bool {
|
||||
pub fn is_file_open(&self, file_id: VfsFile) -> bool {
|
||||
self.open_files.contains(&file_id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,9 @@ use tracing::trace_span;
|
||||
use red_knot_module_resolver::ModuleName;
|
||||
use red_knot_python_semantic::types::Type;
|
||||
use red_knot_python_semantic::{HasTy, SemanticModel};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::{parsed_module, ParsedModule};
|
||||
use ruff_db::source::{source_text, SourceText};
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::visitor::{walk_stmt, Visitor};
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::db::Db;
|
||||
pub(crate) fn unwind_if_cancelled(db: &dyn Db) {}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn lint_syntax(db: &dyn Db, file_id: File) -> Diagnostics {
|
||||
pub(crate) fn lint_syntax(db: &dyn Db, file_id: VfsFile) -> Diagnostics {
|
||||
#[allow(clippy::print_stdout)]
|
||||
if std::env::var("RED_KNOT_SLOW_LINT").is_ok() {
|
||||
for i in 0..10 {
|
||||
@@ -74,7 +74,7 @@ fn lint_lines(source: &str, diagnostics: &mut Vec<String>) {
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn lint_semantic(db: &dyn Db, file_id: File) -> Diagnostics {
|
||||
pub(crate) fn lint_semantic(db: &dyn Db, file_id: VfsFile) -> Diagnostics {
|
||||
let _span = trace_span!("lint_semantic", ?file_id).entered();
|
||||
|
||||
let source = source_text(db.upcast(), file_id);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::sync::Mutex;
|
||||
|
||||
use clap::Parser;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::ParallelDatabase;
|
||||
use tracing::subscriber::Interest;
|
||||
@@ -11,38 +10,13 @@ use tracing_subscriber::{Layer, Registry};
|
||||
use tracing_tree::time::Uptime;
|
||||
|
||||
use red_knot::program::{FileWatcherChange, Program};
|
||||
use red_knot::target_version::TargetVersion;
|
||||
use red_knot::watch::FileWatcher;
|
||||
use red_knot::Workspace;
|
||||
use red_knot_module_resolver::{set_module_resolution_settings, RawModuleResolutionSettings};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(
|
||||
author,
|
||||
name = "red-knot",
|
||||
about = "An experimental multifile analysis backend for Ruff"
|
||||
)]
|
||||
#[command(version)]
|
||||
struct Args {
|
||||
#[clap(help = "File to check", required = true, value_name = "FILE")]
|
||||
entry_point: SystemPathBuf,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "DIRECTORY",
|
||||
help = "Custom directory to use for stdlib typeshed stubs"
|
||||
)]
|
||||
custom_typeshed_dir: Option<SystemPathBuf>,
|
||||
#[arg(
|
||||
long,
|
||||
value_name = "PATH",
|
||||
help = "Additional path to use as a module-resolution source (can be passed multiple times)"
|
||||
)]
|
||||
extra_search_path: Vec<SystemPathBuf>,
|
||||
#[arg(long, help = "Python version to assume when resolving types", default_value_t = TargetVersion::default(), value_name="VERSION")]
|
||||
target_version: TargetVersion,
|
||||
}
|
||||
use red_knot_module_resolver::{
|
||||
set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion,
|
||||
};
|
||||
use ruff_db::file_system::{FileSystem, FileSystemPath, OsFileSystem};
|
||||
use ruff_db::vfs::system_path_to_file;
|
||||
|
||||
#[allow(
|
||||
clippy::print_stdout,
|
||||
@@ -54,50 +28,43 @@ pub fn main() -> anyhow::Result<()> {
|
||||
countme::enable(true);
|
||||
setup_tracing();
|
||||
|
||||
let Args {
|
||||
entry_point,
|
||||
custom_typeshed_dir,
|
||||
extra_search_path: extra_search_paths,
|
||||
target_version,
|
||||
} = Args::parse_from(std::env::args().collect::<Vec<_>>());
|
||||
let arguments: Vec<_> = std::env::args().collect();
|
||||
|
||||
tracing::trace!("Target version: {target_version}");
|
||||
if let Some(custom_typeshed) = custom_typeshed_dir.as_ref() {
|
||||
tracing::trace!("Custom typeshed directory: {custom_typeshed}");
|
||||
}
|
||||
if !extra_search_paths.is_empty() {
|
||||
tracing::trace!("extra search paths: {extra_search_paths:?}");
|
||||
if arguments.len() < 2 {
|
||||
eprintln!("Usage: red_knot <path>");
|
||||
return Err(anyhow::anyhow!("Invalid arguments"));
|
||||
}
|
||||
|
||||
let cwd = std::env::current_dir().unwrap();
|
||||
let cwd = SystemPath::from_std_path(&cwd).unwrap();
|
||||
let system = OsSystem::new(cwd);
|
||||
let fs = OsFileSystem;
|
||||
let entry_point = FileSystemPath::new(&arguments[1]);
|
||||
|
||||
if !system.path_exists(&entry_point) {
|
||||
if !fs.exists(entry_point) {
|
||||
eprintln!("The entry point does not exist.");
|
||||
return Err(anyhow::anyhow!("Invalid arguments"));
|
||||
}
|
||||
|
||||
if !system.is_file(&entry_point) {
|
||||
if !fs.is_file(entry_point) {
|
||||
eprintln!("The entry point is not a file.");
|
||||
return Err(anyhow::anyhow!("Invalid arguments"));
|
||||
}
|
||||
|
||||
let entry_point = entry_point.to_path_buf();
|
||||
|
||||
let workspace_folder = entry_point.parent().unwrap();
|
||||
let workspace = Workspace::new(workspace_folder.to_path_buf());
|
||||
|
||||
let workspace_search_path = workspace.root().to_path_buf();
|
||||
|
||||
let mut program = Program::new(workspace, system);
|
||||
let mut program = Program::new(workspace, fs);
|
||||
|
||||
set_module_resolution_settings(
|
||||
&mut program,
|
||||
RawModuleResolutionSettings {
|
||||
extra_paths: extra_search_paths,
|
||||
extra_paths: vec![],
|
||||
workspace_root: workspace_search_path,
|
||||
site_packages: None,
|
||||
custom_typeshed: custom_typeshed_dir,
|
||||
target_version: red_knot_module_resolver::TargetVersion::from(target_version),
|
||||
custom_typeshed: None,
|
||||
target_version: TargetVersion::Py38,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use salsa::Cancelled;
|
||||
|
||||
use crate::lint::{lint_semantic, lint_syntax, Diagnostics};
|
||||
@@ -19,11 +19,11 @@ impl Program {
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self))]
|
||||
pub fn check_file(&self, file: File) -> Result<Diagnostics, Cancelled> {
|
||||
pub fn check_file(&self, file: VfsFile) -> Result<Diagnostics, Cancelled> {
|
||||
self.with_db(|db| db.check_file_impl(file))
|
||||
}
|
||||
|
||||
fn check_file_impl(&self, file: File) -> Diagnostics {
|
||||
fn check_file_impl(&self, file: VfsFile) -> Diagnostics {
|
||||
let mut diagnostics = Vec::new();
|
||||
diagnostics.extend_from_slice(lint_syntax(self, file));
|
||||
diagnostics.extend_from_slice(lint_semantic(self, file));
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::panic::{AssertUnwindSafe, RefUnwindSafe};
|
||||
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::{Cancelled, Database};
|
||||
|
||||
use red_knot_module_resolver::{vendored_typeshed_stubs, Db as ResolverDb, Jar as ResolverJar};
|
||||
use red_knot_module_resolver::{Db as ResolverDb, Jar as ResolverJar};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Jar as SemanticJar};
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::{System, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::file_system::{FileSystem, FileSystemPathBuf};
|
||||
use ruff_db::vfs::{Vfs, VfsFile, VfsPath};
|
||||
use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast};
|
||||
|
||||
use crate::db::{Db, Jar};
|
||||
@@ -18,20 +17,20 @@ mod check;
|
||||
#[salsa::db(SourceJar, ResolverJar, SemanticJar, Jar)]
|
||||
pub struct Program {
|
||||
storage: salsa::Storage<Program>,
|
||||
files: Files,
|
||||
system: Arc<dyn System + Send + Sync + RefUnwindSafe>,
|
||||
vfs: Vfs,
|
||||
fs: Arc<dyn FileSystem + Send + Sync + RefUnwindSafe>,
|
||||
workspace: Workspace,
|
||||
}
|
||||
|
||||
impl Program {
|
||||
pub fn new<S>(workspace: Workspace, system: S) -> Self
|
||||
pub fn new<Fs>(workspace: Workspace, file_system: Fs) -> Self
|
||||
where
|
||||
S: System + 'static + Send + Sync + RefUnwindSafe,
|
||||
Fs: FileSystem + 'static + Send + Sync + RefUnwindSafe,
|
||||
{
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
files: Files::default(),
|
||||
system: Arc::new(system),
|
||||
vfs: Vfs::default(),
|
||||
fs: Arc::new(file_system),
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
@@ -41,7 +40,7 @@ impl Program {
|
||||
I: IntoIterator<Item = FileWatcherChange>,
|
||||
{
|
||||
for change in changes {
|
||||
File::touch_path(self, &change.path);
|
||||
VfsFile::touch_path(self, &VfsPath::file_system(change.path));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,31 +52,14 @@ impl Program {
|
||||
&mut self.workspace
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn with_db<F, T>(&self, f: F) -> Result<T, Cancelled>
|
||||
where
|
||||
F: FnOnce(&Program) -> T + std::panic::UnwindSafe,
|
||||
F: FnOnce(&Program) -> T + UnwindSafe,
|
||||
{
|
||||
// The `AssertUnwindSafe` here looks scary, but is a consequence of Salsa's design.
|
||||
// Salsa uses panics to implement cancellation and to recover from cycles. However, the Salsa
|
||||
// storage isn't `UnwindSafe` or `RefUnwindSafe` because its dependencies `DashMap` and `parking_lot::*` aren't
|
||||
// unwind safe.
|
||||
//
|
||||
// Having to use `AssertUnwindSafe` isn't as big as a deal as it might seem because
|
||||
// the `UnwindSafe` and `RefUnwindSafe` traits are designed to catch logical bugs.
|
||||
// They don't protect against [UB](https://internals.rust-lang.org/t/pre-rfc-deprecating-unwindsafe/15974).
|
||||
// On top of that, `Cancelled` only catches specific Salsa-panics and propagates all other panics.
|
||||
//
|
||||
// That still leaves us with possible logical bugs in two sources:
|
||||
// * In Salsa itself: This must be considered a bug in Salsa and needs fixing upstream.
|
||||
// Reviewing Salsa code specifically around unwind safety seems doable.
|
||||
// * Our code: This is the main concern. Luckily, it only involves code that uses internal mutability
|
||||
// and calls into Salsa queries when mutating the internal state. Using `AssertUnwindSafe`
|
||||
// certainly makes it harder to catch these issues in our user code.
|
||||
//
|
||||
// For now, this is the only solution at hand unless Salsa decides to change its design.
|
||||
// [Zulip support thread](https://salsa.zulipchat.com/#narrow/stream/145099-general/topic/How.20to.20use.20.60Cancelled.3A.3Acatch.60)
|
||||
let db = &AssertUnwindSafe(self);
|
||||
Cancelled::catch(|| f(db))
|
||||
// TODO: Catch in `Caancelled::catch`
|
||||
// See https://salsa.zulipchat.com/#narrow/stream/145099-general/topic/How.20to.20use.20.60Cancelled.3A.3Acatch.60
|
||||
Ok(f(self))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,16 +86,12 @@ impl ResolverDb for Program {}
|
||||
impl SemanticDb for Program {}
|
||||
|
||||
impl SourceDb for Program {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
vendored_typeshed_stubs()
|
||||
fn file_system(&self) -> &dyn FileSystem {
|
||||
&*self.fs
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&*self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
fn vfs(&self) -> &Vfs {
|
||||
&self.vfs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +103,8 @@ impl salsa::ParallelDatabase for Program {
|
||||
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||
salsa::Snapshot::new(Self {
|
||||
storage: self.storage.snapshot(),
|
||||
files: self.files.snapshot(),
|
||||
system: self.system.clone(),
|
||||
vfs: self.vfs.snapshot(),
|
||||
fs: self.fs.clone(),
|
||||
workspace: self.workspace.clone(),
|
||||
})
|
||||
}
|
||||
@@ -134,13 +112,13 @@ impl salsa::ParallelDatabase for Program {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileWatcherChange {
|
||||
path: SystemPathBuf,
|
||||
path: FileSystemPathBuf,
|
||||
#[allow(unused)]
|
||||
kind: FileChangeKind,
|
||||
}
|
||||
|
||||
impl FileWatcherChange {
|
||||
pub fn new(path: SystemPathBuf, kind: FileChangeKind) -> Self {
|
||||
pub fn new(path: FileSystemPathBuf, kind: FileChangeKind) -> Self {
|
||||
Self { path, kind }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
/// Enumeration of all supported Python versions
|
||||
///
|
||||
/// TODO: unify with the `PythonVersion` enum in the linter/formatter crates?
|
||||
#[derive(Copy, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Default, clap::ValueEnum)]
|
||||
pub enum TargetVersion {
|
||||
Py37,
|
||||
#[default]
|
||||
Py38,
|
||||
Py39,
|
||||
Py310,
|
||||
Py311,
|
||||
Py312,
|
||||
Py313,
|
||||
}
|
||||
|
||||
impl TargetVersion {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Py37 => "py37",
|
||||
Self::Py38 => "py38",
|
||||
Self::Py39 => "py39",
|
||||
Self::Py310 => "py310",
|
||||
Self::Py311 => "py311",
|
||||
Self::Py312 => "py312",
|
||||
Self::Py313 => "py313",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for TargetVersion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TargetVersion> for red_knot_module_resolver::TargetVersion {
|
||||
fn from(value: TargetVersion) -> Self {
|
||||
match value {
|
||||
TargetVersion::Py37 => red_knot_module_resolver::TargetVersion::Py37,
|
||||
TargetVersion::Py38 => red_knot_module_resolver::TargetVersion::Py38,
|
||||
TargetVersion::Py39 => red_knot_module_resolver::TargetVersion::Py39,
|
||||
TargetVersion::Py310 => red_knot_module_resolver::TargetVersion::Py310,
|
||||
TargetVersion::Py311 => red_knot_module_resolver::TargetVersion::Py311,
|
||||
TargetVersion::Py312 => red_knot_module_resolver::TargetVersion::Py312,
|
||||
TargetVersion::Py313 => red_knot_module_resolver::TargetVersion::Py313,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::program::{FileChangeKind, FileWatcherChange};
|
||||
use anyhow::Context;
|
||||
use notify::event::{CreateKind, RemoveKind};
|
||||
use notify::{recommended_watcher, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||
|
||||
use ruff_db::system::SystemPath;
|
||||
|
||||
use crate::program::{FileChangeKind, FileWatcherChange};
|
||||
use ruff_db::file_system::FileSystemPath;
|
||||
|
||||
pub struct FileWatcher {
|
||||
watcher: RecommendedWatcher,
|
||||
@@ -52,7 +50,7 @@ impl FileWatcher {
|
||||
|
||||
for path in event.paths {
|
||||
if path.is_file() {
|
||||
if let Some(fs_path) = SystemPath::from_std_path(&path) {
|
||||
if let Some(fs_path) = FileSystemPath::from_std_path(&path) {
|
||||
changes.push(FileWatcherChange::new(
|
||||
fs_path.to_path_buf(),
|
||||
change_kind,
|
||||
|
||||
@@ -16,7 +16,6 @@ ruff_python_stdlib = { workspace = true }
|
||||
|
||||
compact_str = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
@@ -24,34 +24,58 @@ pub(crate) mod tests {
|
||||
|
||||
use salsa::DebugWithDb;
|
||||
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::file_system::{FileSystem, FileSystemPathBuf, MemoryFileSystem, OsFileSystem};
|
||||
use ruff_db::vfs::Vfs;
|
||||
|
||||
use crate::vendored_typeshed_stubs;
|
||||
use crate::resolver::{set_module_resolution_settings, RawModuleResolutionSettings};
|
||||
use crate::supported_py_version::TargetVersion;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[salsa::db(Jar, ruff_db::Jar)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
files: Files,
|
||||
file_system: TestFileSystem,
|
||||
events: sync::Arc<sync::Mutex<Vec<salsa::Event>>>,
|
||||
vfs: Vfs,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: vendored_typeshed_stubs().snapshot(),
|
||||
file_system: TestFileSystem::Memory(MemoryFileSystem::default()),
|
||||
events: sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
vfs: Vfs::with_stubbed_vendored(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the memory file system.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If this test db isn't using a memory file system.
|
||||
pub(crate) fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||
if let TestFileSystem::Memory(fs) = &self.file_system {
|
||||
fs
|
||||
} else {
|
||||
panic!("The test db is not using a memory file system");
|
||||
}
|
||||
}
|
||||
|
||||
/// Uses the real file system instead of the memory file system.
|
||||
///
|
||||
/// This useful for testing advanced file system features like permissions, symlinks, etc.
|
||||
///
|
||||
/// Note that any files written to the memory file system won't be copied over.
|
||||
pub(crate) fn with_os_file_system(&mut self) {
|
||||
self.file_system = TestFileSystem::Os(OsFileSystem);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
|
||||
&mut self.vfs
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
@@ -79,31 +103,17 @@ pub(crate) mod tests {
|
||||
}
|
||||
|
||||
impl ruff_db::Db for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
fn file_system(&self) -> &dyn ruff_db::file_system::FileSystem {
|
||||
self.file_system.inner()
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn ruff_db::system::System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
fn vfs(&self) -> &ruff_db::vfs::Vfs {
|
||||
&self.vfs
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for TestDb {}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
impl salsa::Database for TestDb {
|
||||
fn salsa_event(&self, event: salsa::Event) {
|
||||
tracing::trace!("event: {:?}", event.debug(self));
|
||||
@@ -116,11 +126,139 @@ pub(crate) mod tests {
|
||||
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||
salsa::Snapshot::new(Self {
|
||||
storage: self.storage.snapshot(),
|
||||
system: self.system.snapshot(),
|
||||
vendored: self.vendored.snapshot(),
|
||||
files: self.files.snapshot(),
|
||||
file_system: self.file_system.snapshot(),
|
||||
events: self.events.clone(),
|
||||
vfs: self.vfs.snapshot(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum TestFileSystem {
|
||||
Memory(MemoryFileSystem),
|
||||
#[allow(unused)]
|
||||
Os(OsFileSystem),
|
||||
}
|
||||
|
||||
impl TestFileSystem {
|
||||
fn inner(&self) -> &dyn FileSystem {
|
||||
match self {
|
||||
Self::Memory(inner) => inner,
|
||||
Self::Os(inner) => inner,
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> Self {
|
||||
match self {
|
||||
Self::Memory(inner) => Self::Memory(inner.snapshot()),
|
||||
Self::Os(inner) => Self::Os(inner.snapshot()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestCaseBuilder {
|
||||
db: TestDb,
|
||||
src: FileSystemPathBuf,
|
||||
custom_typeshed: FileSystemPathBuf,
|
||||
site_packages: FileSystemPathBuf,
|
||||
target_version: Option<TargetVersion>,
|
||||
}
|
||||
|
||||
impl TestCaseBuilder {
|
||||
#[must_use]
|
||||
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
|
||||
self.target_version = Some(target_version);
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> TestCase {
|
||||
let TestCaseBuilder {
|
||||
mut db,
|
||||
src,
|
||||
custom_typeshed,
|
||||
site_packages,
|
||||
target_version,
|
||||
} = self;
|
||||
|
||||
let settings = RawModuleResolutionSettings {
|
||||
target_version: target_version.unwrap_or_default(),
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
custom_typeshed: Some(custom_typeshed.clone()),
|
||||
site_packages: Some(site_packages.clone()),
|
||||
};
|
||||
|
||||
set_module_resolution_settings(&mut db, settings);
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
custom_typeshed,
|
||||
site_packages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct TestCase {
|
||||
pub(crate) db: TestDb,
|
||||
pub(crate) src: FileSystemPathBuf,
|
||||
pub(crate) custom_typeshed: FileSystemPathBuf,
|
||||
pub(crate) site_packages: FileSystemPathBuf,
|
||||
}
|
||||
|
||||
pub(crate) fn create_resolver_builder() -> std::io::Result<TestCaseBuilder> {
|
||||
static VERSIONS_DATA: &str = "\
|
||||
asyncio: 3.8- # 'Regular' package on py38+
|
||||
asyncio.tasks: 3.9-3.11
|
||||
collections: 3.9- # 'Regular' package on py39+
|
||||
functools: 3.8-
|
||||
importlib: 3.9- # Namespace package on py39+
|
||||
xml: 3.8-3.8 # Namespace package on py38 only
|
||||
";
|
||||
|
||||
let db = TestDb::new();
|
||||
|
||||
let src = FileSystemPathBuf::from("src");
|
||||
let site_packages = FileSystemPathBuf::from("site_packages");
|
||||
let custom_typeshed = FileSystemPathBuf::from("typeshed");
|
||||
|
||||
let fs = db.memory_file_system();
|
||||
|
||||
fs.create_directory_all(&src)?;
|
||||
fs.create_directory_all(&site_packages)?;
|
||||
fs.create_directory_all(&custom_typeshed)?;
|
||||
fs.write_file(custom_typeshed.join("stdlib/VERSIONS"), VERSIONS_DATA)?;
|
||||
|
||||
// Regular package on py38+
|
||||
fs.create_directory_all(custom_typeshed.join("stdlib/asyncio"))?;
|
||||
fs.touch(custom_typeshed.join("stdlib/asyncio/__init__.pyi"))?;
|
||||
fs.write_file(
|
||||
custom_typeshed.join("stdlib/asyncio/tasks.pyi"),
|
||||
"class Task: ...",
|
||||
)?;
|
||||
|
||||
// Regular package on py39+
|
||||
fs.create_directory_all(custom_typeshed.join("stdlib/collections"))?;
|
||||
fs.touch(custom_typeshed.join("stdlib/collections/__init__.pyi"))?;
|
||||
|
||||
// Namespace package on py38 only
|
||||
fs.create_directory_all(custom_typeshed.join("stdlib/xml"))?;
|
||||
fs.touch(custom_typeshed.join("stdlib/xml/etree.pyi"))?;
|
||||
|
||||
// Namespace package on py39+
|
||||
fs.create_directory_all(custom_typeshed.join("stdlib/importlib"))?;
|
||||
fs.touch(custom_typeshed.join("stdlib/importlib/abc.pyi"))?;
|
||||
|
||||
fs.write_file(
|
||||
custom_typeshed.join("stdlib/functools.pyi"),
|
||||
"def update_wrapper(): ...",
|
||||
)?;
|
||||
|
||||
Ok(TestCaseBuilder {
|
||||
db,
|
||||
src,
|
||||
custom_typeshed,
|
||||
site_packages,
|
||||
target_version: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,14 +7,9 @@ mod state;
|
||||
mod supported_py_version;
|
||||
mod typeshed;
|
||||
|
||||
#[cfg(test)]
|
||||
mod testing;
|
||||
|
||||
pub use db::{Db, Jar};
|
||||
pub use module::{Module, ModuleKind};
|
||||
pub use module_name::ModuleName;
|
||||
pub use resolver::{resolve_module, set_module_resolution_settings, RawModuleResolutionSettings};
|
||||
pub use supported_py_version::TargetVersion;
|
||||
pub use typeshed::{
|
||||
vendored_typeshed_stubs, TypeshedVersionsParseError, TypeshedVersionsParseErrorKind,
|
||||
};
|
||||
pub use typeshed::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
@@ -18,7 +18,7 @@ impl Module {
|
||||
name: ModuleName,
|
||||
kind: ModuleKind,
|
||||
search_path: Arc<ModuleResolutionPathBuf>,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(ModuleInner {
|
||||
@@ -36,7 +36,7 @@ impl Module {
|
||||
}
|
||||
|
||||
/// The file to the source code that defines this module
|
||||
pub fn file(&self) -> File {
|
||||
pub fn file(&self) -> VfsFile {
|
||||
self.inner.file
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ struct ModuleInner {
|
||||
name: ModuleName,
|
||||
kind: ModuleKind,
|
||||
search_path: Arc<ModuleResolutionPathBuf>,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ruff_db::files::{File, FilePath};
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_db::file_system::FileSystemPathBuf;
|
||||
use ruff_db::vfs::{vfs_path_to_file, VfsFile, VfsPath};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module::{Module, ModuleKind};
|
||||
@@ -58,7 +58,7 @@ pub(crate) fn resolve_module_query<'db>(
|
||||
///
|
||||
/// Returns `None` if the path is not a module locatable via any of the known search paths.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option<Module> {
|
||||
pub(crate) fn path_to_module(db: &dyn Db, path: &VfsPath) -> Option<Module> {
|
||||
// It's not entirely clear on first sight why this method calls `file_to_module` instead of
|
||||
// it being the other way round, considering that the first thing that `file_to_module` does
|
||||
// is to retrieve the file's path.
|
||||
@@ -67,7 +67,7 @@ pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option<Module> {
|
||||
// all arguments are Salsa ingredients (something stored in Salsa). `Path`s aren't salsa ingredients but
|
||||
// `VfsFile` is. So what we do here is to retrieve the `path`'s `VfsFile` so that we can make
|
||||
// use of Salsa's caching and invalidation.
|
||||
let file = path.to_file(db.upcast())?;
|
||||
let file = vfs_path_to_file(db.upcast(), path)?;
|
||||
file_to_module(db, file)
|
||||
}
|
||||
|
||||
@@ -75,10 +75,12 @@ pub(crate) fn path_to_module(db: &dyn Db, path: &FilePath) -> Option<Module> {
|
||||
///
|
||||
/// Returns `None` if the file is not a module locatable via any of the known search paths.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn file_to_module(db: &dyn Db, file: File) -> Option<Module> {
|
||||
pub(crate) fn file_to_module(db: &dyn Db, file: VfsFile) -> Option<Module> {
|
||||
let _span = tracing::trace_span!("file_to_module", ?file).entered();
|
||||
|
||||
let path = file.path(db.upcast());
|
||||
let VfsPath::FileSystem(path) = file.path(db.upcast()) else {
|
||||
todo!("VendoredPaths are not yet supported")
|
||||
};
|
||||
|
||||
let resolver_settings = module_resolver_settings(db);
|
||||
|
||||
@@ -118,18 +120,18 @@ pub struct RawModuleResolutionSettings {
|
||||
/// List of user-provided paths that should take first priority in the module resolution.
|
||||
/// Examples in other type checkers are mypy's MYPYPATH environment variable,
|
||||
/// or pyright's stubPath configuration setting.
|
||||
pub extra_paths: Vec<SystemPathBuf>,
|
||||
pub extra_paths: Vec<FileSystemPathBuf>,
|
||||
|
||||
/// The root of the workspace, used for finding first-party modules.
|
||||
pub workspace_root: SystemPathBuf,
|
||||
pub workspace_root: FileSystemPathBuf,
|
||||
|
||||
/// Optional (already validated) path to standard-library typeshed stubs.
|
||||
/// If this is not provided, we will fallback to our vendored typeshed stubs
|
||||
/// bundled as a zip file in the binary
|
||||
pub custom_typeshed: Option<SystemPathBuf>,
|
||||
pub custom_typeshed: Option<FileSystemPathBuf>,
|
||||
|
||||
/// The path to the user's `site-packages` directory, where third-party packages from ``PyPI`` are installed.
|
||||
pub site_packages: Option<SystemPathBuf>,
|
||||
pub site_packages: Option<FileSystemPathBuf>,
|
||||
}
|
||||
|
||||
impl RawModuleResolutionSettings {
|
||||
@@ -159,11 +161,11 @@ impl RawModuleResolutionSettings {
|
||||
|
||||
paths.push(ModuleResolutionPathBuf::first_party(workspace_root).unwrap());
|
||||
|
||||
paths.push(
|
||||
custom_typeshed.map_or_else(ModuleResolutionPathBuf::vendored_stdlib, |custom| {
|
||||
ModuleResolutionPathBuf::stdlib_from_custom_typeshed_root(&custom).unwrap()
|
||||
}),
|
||||
);
|
||||
if let Some(custom_typeshed) = custom_typeshed {
|
||||
paths.push(
|
||||
ModuleResolutionPathBuf::stdlib_from_typeshed_root(&custom_typeshed).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
// TODO vendor typeshed's third-party stubs as well as the stdlib and fallback to them as a final step
|
||||
if let Some(site_packages) = site_packages {
|
||||
@@ -241,7 +243,7 @@ fn module_resolver_settings(db: &dyn Db) -> &ModuleResolutionSettings {
|
||||
fn resolve_name(
|
||||
db: &dyn Db,
|
||||
name: &ModuleName,
|
||||
) -> Option<(Arc<ModuleResolutionPathBuf>, File, ModuleKind)> {
|
||||
) -> Option<(Arc<ModuleResolutionPathBuf>, VfsFile, ModuleKind)> {
|
||||
let resolver_settings = module_resolver_settings(db);
|
||||
let resolver_state = ResolverState::new(db, resolver_settings.target_version());
|
||||
|
||||
@@ -266,14 +268,14 @@ fn resolve_name(
|
||||
// TODO Implement full https://peps.python.org/pep-0561/#type-checker-module-resolution-order resolution
|
||||
if let Some(stub) = package_path
|
||||
.with_pyi_extension()
|
||||
.to_file(search_path, &resolver_state)
|
||||
.to_vfs_file(search_path, &resolver_state)
|
||||
{
|
||||
return Some((search_path.clone(), stub, kind));
|
||||
}
|
||||
|
||||
if let Some(module) = package_path
|
||||
.with_py_extension()
|
||||
.and_then(|path| path.to_file(search_path, &resolver_state))
|
||||
.and_then(|path| path.to_vfs_file(search_path, &resolver_state))
|
||||
{
|
||||
return Some((search_path.clone(), module, kind));
|
||||
}
|
||||
@@ -384,25 +386,28 @@ impl PackageKind {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use internal::ModuleNameIngredient;
|
||||
use ruff_db::files::{system_path_to_file, File, FilePath};
|
||||
use ruff_db::system::{DbWithTestSystem, OsSystem, SystemPath};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
use ruff_db::file_system::FileSystemPath;
|
||||
use ruff_db::vfs::{system_path_to_file, VfsFile, VfsPath};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::db::tests::{create_resolver_builder, TestCase};
|
||||
use crate::module::ModuleKind;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::testing::{FileSpec, MockedTypeshed, TestCase, TestCaseBuilder};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn setup_resolver_test() -> TestCase {
|
||||
create_resolver_builder().unwrap().build()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_party_module() {
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "print('Hello, world!')")])
|
||||
.build();
|
||||
fn first_party_module() -> anyhow::Result<()> {
|
||||
let TestCase { db, src, .. } = setup_resolver_test();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_path = src.join("foo.py");
|
||||
db.memory_file_system()
|
||||
.write_file(&foo_path, "print('Hello, world!')")?;
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@@ -414,26 +419,25 @@ mod tests {
|
||||
assert_eq!(&src, &foo_module.search_path());
|
||||
assert_eq!(ModuleKind::Module, foo_module.kind());
|
||||
|
||||
let expected_foo_path = src.join("foo.py");
|
||||
assert_eq!(&expected_foo_path, foo_module.file().path(&db));
|
||||
assert_eq!(&foo_path, foo_module.file().path(&db));
|
||||
assert_eq!(
|
||||
Some(foo_module),
|
||||
path_to_module(&db, &FilePath::System(expected_foo_path))
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo_path))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdlib() {
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
|
||||
versions: "functools: 3.8-",
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
let TestCase {
|
||||
db,
|
||||
custom_typeshed,
|
||||
..
|
||||
} = setup_resolver_test();
|
||||
|
||||
let stdlib_dir =
|
||||
ModuleResolutionPathBuf::stdlib_from_typeshed_root(&custom_typeshed).unwrap();
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
|
||||
@@ -442,15 +446,16 @@ mod tests {
|
||||
resolve_module(&db, functools_module_name).as_ref()
|
||||
);
|
||||
|
||||
assert_eq!(&stdlib, &functools_module.search_path().to_path_buf());
|
||||
assert_eq!(stdlib_dir, functools_module.search_path().to_path_buf());
|
||||
assert_eq!(ModuleKind::Module, functools_module.kind());
|
||||
|
||||
let expected_functools_path = stdlib.join("functools.pyi");
|
||||
let expected_functools_path =
|
||||
VfsPath::FileSystem(custom_typeshed.join("stdlib/functools.pyi"));
|
||||
assert_eq!(&expected_functools_path, functools_module.file().path(&db));
|
||||
|
||||
assert_eq!(
|
||||
Some(functools_module),
|
||||
path_to_module(&db, &FilePath::System(expected_functools_path))
|
||||
path_to_module(&db, &expected_functools_path)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -463,29 +468,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stdlib_resolution_respects_versions_file_py38_existing_modules() {
|
||||
const VERSIONS: &str = "\
|
||||
asyncio: 3.8- # 'Regular' package on py38+
|
||||
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
|
||||
functools: 3.8- # Top-level single-file module
|
||||
xml: 3.8-3.8 # Namespace package on py38 only
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[
|
||||
("asyncio/__init__.pyi", ""),
|
||||
("asyncio/tasks.pyi", ""),
|
||||
("functools.pyi", ""),
|
||||
("xml/etree.pyi", ""),
|
||||
];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
stdlib_files: STDLIB,
|
||||
versions: VERSIONS,
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
let TestCase {
|
||||
db,
|
||||
custom_typeshed,
|
||||
..
|
||||
} = setup_resolver_test();
|
||||
|
||||
let existing_modules = create_module_names(&["asyncio", "functools", "xml.etree"]);
|
||||
for module_name in existing_modules {
|
||||
@@ -494,7 +481,8 @@ mod tests {
|
||||
});
|
||||
let search_path = resolved_module.search_path();
|
||||
assert_eq!(
|
||||
&stdlib, &search_path,
|
||||
&custom_typeshed.join("stdlib"),
|
||||
&search_path,
|
||||
"Search path for {module_name} was unexpectedly {search_path:?}"
|
||||
);
|
||||
assert!(
|
||||
@@ -506,32 +494,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stdlib_resolution_respects_versions_file_py38_nonexisting_modules() {
|
||||
const VERSIONS: &str = "\
|
||||
asyncio: 3.8- # 'Regular' package on py38+
|
||||
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
|
||||
collections: 3.9- # 'Regular' package on py39+
|
||||
importlib: 3.9- # Namespace package on py39+
|
||||
xml: 3.8-3.8 # Namespace package on 3.8 only
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[
|
||||
("collections/__init__.pyi", ""),
|
||||
("asyncio/__init__.pyi", ""),
|
||||
("asyncio/tasks.pyi", ""),
|
||||
("importlib/abc.pyi", ""),
|
||||
("xml/etree.pyi", ""),
|
||||
];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
stdlib_files: STDLIB,
|
||||
versions: VERSIONS,
|
||||
};
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
|
||||
let TestCase { db, .. } = setup_resolver_test();
|
||||
let nonexisting_modules = create_module_names(&[
|
||||
"collections",
|
||||
"importlib",
|
||||
@@ -539,7 +502,6 @@ mod tests {
|
||||
"xml",
|
||||
"asyncio.tasks",
|
||||
]);
|
||||
|
||||
for module_name in nonexisting_modules {
|
||||
assert!(
|
||||
resolve_module(&db, module_name.clone()).is_none(),
|
||||
@@ -550,29 +512,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn stdlib_resolution_respects_versions_file_py39_existing_modules() {
|
||||
const VERSIONS: &str = "\
|
||||
asyncio: 3.8- # 'Regular' package on py38+
|
||||
asyncio.tasks: 3.9-3.11 # Submodule on py39+ only
|
||||
collections: 3.9- # 'Regular' package on py39+
|
||||
functools: 3.8- # Top-level single-file module
|
||||
importlib: 3.9- # Namespace package on py39+
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[
|
||||
("asyncio/__init__.pyi", ""),
|
||||
("asyncio/tasks.pyi", ""),
|
||||
("collections/__init__.pyi", ""),
|
||||
("functools.pyi", ""),
|
||||
("importlib/abc.pyi", ""),
|
||||
];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
stdlib_files: STDLIB,
|
||||
versions: VERSIONS,
|
||||
};
|
||||
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
let TestCase {
|
||||
db,
|
||||
custom_typeshed,
|
||||
..
|
||||
} = create_resolver_builder()
|
||||
.unwrap()
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.build();
|
||||
|
||||
@@ -583,14 +528,14 @@ mod tests {
|
||||
"collections",
|
||||
"asyncio.tasks",
|
||||
]);
|
||||
|
||||
for module_name in existing_modules {
|
||||
let resolved_module = resolve_module(&db, module_name.clone()).unwrap_or_else(|| {
|
||||
panic!("Expected module {module_name} to exist in the mock stdlib")
|
||||
});
|
||||
let search_path = resolved_module.search_path();
|
||||
assert_eq!(
|
||||
&stdlib, &search_path,
|
||||
&custom_typeshed.join("stdlib"),
|
||||
&search_path,
|
||||
"Search path for {module_name} was unexpectedly {search_path:?}"
|
||||
);
|
||||
assert!(
|
||||
@@ -601,20 +546,8 @@ mod tests {
|
||||
}
|
||||
#[test]
|
||||
fn stdlib_resolution_respects_versions_file_py39_nonexisting_modules() {
|
||||
const VERSIONS: &str = "\
|
||||
importlib: 3.9- # Namespace package on py39+
|
||||
xml: 3.8-3.8 # Namespace package on 3.8 only
|
||||
";
|
||||
|
||||
const STDLIB: &[FileSpec] = &[("importlib/abc.pyi", ""), ("xml/etree.pyi", "")];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
stdlib_files: STDLIB,
|
||||
versions: VERSIONS,
|
||||
};
|
||||
|
||||
let TestCase { db, .. } = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
let TestCase { db, .. } = create_resolver_builder()
|
||||
.unwrap()
|
||||
.with_target_version(TargetVersion::Py39)
|
||||
.build();
|
||||
|
||||
@@ -628,19 +561,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_party_precedence_over_stdlib() {
|
||||
const SRC: &[FileSpec] = &[("functools.py", "def update_wrapper(): ...")];
|
||||
fn first_party_precedence_over_stdlib() -> anyhow::Result<()> {
|
||||
let TestCase { db, src, .. } = setup_resolver_test();
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
|
||||
versions: "functools: 3.8-",
|
||||
};
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
let first_party_functools_path = src.join("functools.py");
|
||||
db.memory_file_system()
|
||||
.write_file(&first_party_functools_path, "def update_wrapper(): ...")?;
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
@@ -651,39 +577,29 @@ mod tests {
|
||||
);
|
||||
assert_eq!(&src, &functools_module.search_path());
|
||||
assert_eq!(ModuleKind::Module, functools_module.kind());
|
||||
assert_eq!(&src.join("functools.py"), functools_module.file().path(&db));
|
||||
assert_eq!(
|
||||
&first_party_functools_path,
|
||||
functools_module.file().path(&db)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Some(functools_module),
|
||||
path_to_module(&db, &FilePath::System(src.join("functools.py")))
|
||||
path_to_module(&db, &VfsPath::FileSystem(first_party_functools_path))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdlib_uses_vendored_typeshed_when_no_custom_typeshed_supplied() {
|
||||
let TestCase { db, stdlib, .. } = TestCaseBuilder::new()
|
||||
.with_vendored_typeshed()
|
||||
.with_target_version(TargetVersion::default())
|
||||
.build();
|
||||
fn resolve_package() -> anyhow::Result<()> {
|
||||
let TestCase { src, db, .. } = setup_resolver_test();
|
||||
|
||||
let pydoc_data_topics_name = ModuleName::new_static("pydoc_data.topics").unwrap();
|
||||
let pydoc_data_topics = resolve_module(&db, pydoc_data_topics_name).unwrap();
|
||||
let foo_dir = src.join("foo");
|
||||
let foo_path = foo_dir.join("__init__.py");
|
||||
|
||||
assert_eq!("pydoc_data.topics", pydoc_data_topics.name());
|
||||
assert_eq!(pydoc_data_topics.search_path(), stdlib);
|
||||
assert_eq!(
|
||||
pydoc_data_topics.file().path(&db),
|
||||
&stdlib.join("pydoc_data/topics.pyi")
|
||||
);
|
||||
}
|
||||
db.memory_file_system()
|
||||
.write_file(&foo_path, "print('Hello, world!')")?;
|
||||
|
||||
#[test]
|
||||
fn resolve_package() {
|
||||
let TestCase { src, db, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo/__init__.py", "print('Hello, world!'")])
|
||||
.build();
|
||||
|
||||
let foo_path = src.join("foo/__init__.py");
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
|
||||
assert_eq!("foo", foo_module.name());
|
||||
@@ -692,88 +608,106 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
Some(&foo_module),
|
||||
path_to_module(&db, &FilePath::System(foo_path)).as_ref()
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo_path)).as_ref()
|
||||
);
|
||||
|
||||
// Resolving by directory doesn't resolve to the init file.
|
||||
assert_eq!(
|
||||
None,
|
||||
path_to_module(&db, &FilePath::System(src.join("foo")))
|
||||
);
|
||||
assert_eq!(None, path_to_module(&db, &VfsPath::FileSystem(foo_dir)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn package_priority_over_module() {
|
||||
const SRC: &[FileSpec] = &[
|
||||
("foo/__init__.py", "print('Hello, world!')"),
|
||||
("foo.py", "print('Hello, world!')"),
|
||||
];
|
||||
fn package_priority_over_module() -> anyhow::Result<()> {
|
||||
let TestCase { db, src, .. } = setup_resolver_test();
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
let foo_dir = src.join("foo");
|
||||
let foo_init = foo_dir.join("__init__.py");
|
||||
|
||||
db.memory_file_system()
|
||||
.write_file(&foo_init, "print('Hello, world!')")?;
|
||||
|
||||
let foo_py = src.join("foo.py");
|
||||
db.memory_file_system()
|
||||
.write_file(&foo_py, "print('Hello, world!')")?;
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
assert_eq!(&src, &foo_module.search_path());
|
||||
assert_eq!(&foo_init_path, foo_module.file().path(&db));
|
||||
assert_eq!(&foo_init, foo_module.file().path(&db));
|
||||
assert_eq!(ModuleKind::Package, foo_module.kind());
|
||||
|
||||
assert_eq!(
|
||||
Some(foo_module),
|
||||
path_to_module(&db, &FilePath::System(foo_init_path))
|
||||
);
|
||||
assert_eq!(
|
||||
None,
|
||||
path_to_module(&db, &FilePath::System(src.join("foo.py")))
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo_init))
|
||||
);
|
||||
assert_eq!(None, path_to_module(&db, &VfsPath::FileSystem(foo_py)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_stub_over_module() {
|
||||
const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];
|
||||
fn typing_stub_over_module() -> anyhow::Result<()> {
|
||||
let TestCase { db, src, .. } = setup_resolver_test();
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
let foo_stub = src.join("foo.pyi");
|
||||
let foo_py = src.join("foo.py");
|
||||
db.memory_file_system()
|
||||
.write_files([(&foo_stub, "x: int"), (&foo_py, "print('Hello, world!')")])?;
|
||||
|
||||
let foo = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_stub = src.join("foo.pyi");
|
||||
|
||||
assert_eq!(&src, &foo.search_path());
|
||||
assert_eq!(&foo_stub, foo.file().path(&db));
|
||||
|
||||
assert_eq!(Some(foo), path_to_module(&db, &FilePath::System(foo_stub)));
|
||||
assert_eq!(
|
||||
None,
|
||||
path_to_module(&db, &FilePath::System(src.join("foo.py")))
|
||||
Some(foo),
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo_stub))
|
||||
);
|
||||
assert_eq!(None, path_to_module(&db, &VfsPath::FileSystem(foo_py)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_packages() {
|
||||
const SRC: &[FileSpec] = &[
|
||||
("foo/__init__.py", ""),
|
||||
("foo/bar/__init__.py", ""),
|
||||
("foo/bar/baz.py", "print('Hello, world!)'"),
|
||||
];
|
||||
fn sub_packages() -> anyhow::Result<()> {
|
||||
let TestCase { db, src, .. } = setup_resolver_test();
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
let foo = src.join("foo");
|
||||
let bar = foo.join("bar");
|
||||
let baz = bar.join("baz.py");
|
||||
|
||||
db.memory_file_system().write_files([
|
||||
(&foo.join("__init__.py"), ""),
|
||||
(&bar.join("__init__.py"), ""),
|
||||
(&baz, "print('Hello, world!')"),
|
||||
])?;
|
||||
|
||||
let baz_module =
|
||||
resolve_module(&db, ModuleName::new_static("foo.bar.baz").unwrap()).unwrap();
|
||||
let baz_path = src.join("foo/bar/baz.py");
|
||||
|
||||
assert_eq!(&src, &baz_module.search_path());
|
||||
assert_eq!(&baz_path, baz_module.file().path(&db));
|
||||
assert_eq!(&baz, baz_module.file().path(&db));
|
||||
|
||||
assert_eq!(
|
||||
Some(baz_module),
|
||||
path_to_module(&db, &FilePath::System(baz_path))
|
||||
path_to_module(&db, &VfsPath::FileSystem(baz))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespace_package() {
|
||||
fn namespace_package() -> anyhow::Result<()> {
|
||||
let TestCase {
|
||||
db,
|
||||
src,
|
||||
site_packages,
|
||||
..
|
||||
} = setup_resolver_test();
|
||||
|
||||
// From [PEP420](https://peps.python.org/pep-0420/#nested-namespace-packages).
|
||||
// But uses `src` for `project1` and `site-packages` for `project2`.
|
||||
// But uses `src` for `project1` and `site_packages2` for `project2`.
|
||||
// ```
|
||||
// src
|
||||
// parent
|
||||
@@ -784,33 +718,47 @@ mod tests {
|
||||
// child
|
||||
// two.py
|
||||
// ```
|
||||
|
||||
let parent1 = src.join("parent");
|
||||
let child1 = parent1.join("child");
|
||||
let one = child1.join("one.py");
|
||||
|
||||
let parent2 = site_packages.join("parent");
|
||||
let child2 = parent2.join("child");
|
||||
let two = child2.join("two.py");
|
||||
|
||||
db.memory_file_system().write_files([
|
||||
(&one, "print('Hello, world!')"),
|
||||
(&two, "print('Hello, world!')"),
|
||||
])?;
|
||||
|
||||
let one_module =
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(one_module),
|
||||
path_to_module(&db, &VfsPath::FileSystem(one))
|
||||
);
|
||||
|
||||
let two_module =
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.two").unwrap()).unwrap();
|
||||
assert_eq!(
|
||||
Some(two_module),
|
||||
path_to_module(&db, &VfsPath::FileSystem(two))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_package_in_namespace_package() -> anyhow::Result<()> {
|
||||
let TestCase {
|
||||
db,
|
||||
src,
|
||||
site_packages,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(&[("parent/child/one.py", "print('Hello, world!')")])
|
||||
.with_site_packages_files(&[("parent/child/two.py", "print('Hello, world!')")])
|
||||
.build();
|
||||
} = setup_resolver_test();
|
||||
|
||||
let one_module_name = ModuleName::new_static("parent.child.one").unwrap();
|
||||
let one_module_path = FilePath::System(src.join("parent/child/one.py"));
|
||||
assert_eq!(
|
||||
resolve_module(&db, one_module_name),
|
||||
path_to_module(&db, &one_module_path)
|
||||
);
|
||||
|
||||
let two_module_name = ModuleName::new_static("parent.child.two").unwrap();
|
||||
let two_module_path = FilePath::System(site_packages.join("parent/child/two.py"));
|
||||
assert_eq!(
|
||||
resolve_module(&db, two_module_name),
|
||||
path_to_module(&db, &two_module_path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regular_package_in_namespace_package() {
|
||||
// Adopted test case from the [PEP420 examples](https://peps.python.org/pep-0420/#nested-namespace-packages).
|
||||
// The `src/parent/child` package is a regular package. Therefore, `site_packages/parent/child/two.py` should not be resolved.
|
||||
// ```
|
||||
@@ -823,69 +771,86 @@ mod tests {
|
||||
// child
|
||||
// two.py
|
||||
// ```
|
||||
const SRC: &[FileSpec] = &[
|
||||
("parent/child/__init__.py", "print('Hello, world!')"),
|
||||
("parent/child/one.py", "print('Hello, world!')"),
|
||||
];
|
||||
|
||||
const SITE_PACKAGES: &[FileSpec] = &[("parent/child/two.py", "print('Hello, world!')")];
|
||||
let parent1 = src.join("parent");
|
||||
let child1 = parent1.join("child");
|
||||
let one = child1.join("one.py");
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_site_packages_files(SITE_PACKAGES)
|
||||
.build();
|
||||
let parent2 = site_packages.join("parent");
|
||||
let child2 = parent2.join("child");
|
||||
let two = child2.join("two.py");
|
||||
|
||||
let one_module_path = FilePath::System(src.join("parent/child/one.py"));
|
||||
let one_module_name =
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap());
|
||||
assert_eq!(one_module_name, path_to_module(&db, &one_module_path));
|
||||
db.memory_file_system().write_files([
|
||||
(&child1.join("__init__.py"), "print('Hello, world!')"),
|
||||
(&one, "print('Hello, world!')"),
|
||||
(&two, "print('Hello, world!')"),
|
||||
])?;
|
||||
|
||||
let one_module =
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.one").unwrap()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Some(one_module),
|
||||
path_to_module(&db, &VfsPath::FileSystem(one))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
None,
|
||||
resolve_module(&db, ModuleName::new_static("parent.child.two").unwrap())
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn module_search_path_priority() {
|
||||
fn module_search_path_priority() -> anyhow::Result<()> {
|
||||
let TestCase {
|
||||
db,
|
||||
src,
|
||||
site_packages,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "")])
|
||||
.with_site_packages_files(&[("foo.py", "")])
|
||||
.build();
|
||||
} = setup_resolver_test();
|
||||
|
||||
let foo_src = src.join("foo.py");
|
||||
let foo_site_packages = site_packages.join("foo.py");
|
||||
|
||||
db.memory_file_system()
|
||||
.write_files([(&foo_src, ""), (&foo_site_packages, "")])?;
|
||||
|
||||
let foo_module = resolve_module(&db, ModuleName::new_static("foo").unwrap()).unwrap();
|
||||
let foo_src_path = src.join("foo.py");
|
||||
|
||||
assert_eq!(&src, &foo_module.search_path());
|
||||
assert_eq!(&foo_src_path, foo_module.file().path(&db));
|
||||
assert_eq!(
|
||||
Some(foo_module),
|
||||
path_to_module(&db, &FilePath::System(foo_src_path))
|
||||
);
|
||||
assert_eq!(&foo_src, foo_module.file().path(&db));
|
||||
|
||||
assert_eq!(
|
||||
None,
|
||||
path_to_module(&db, &FilePath::System(site_packages.join("foo.py")))
|
||||
Some(foo_module),
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo_src))
|
||||
);
|
||||
assert_eq!(
|
||||
None,
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo_site_packages))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(target_family = "unix")]
|
||||
fn symlink() -> anyhow::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let TestCase {
|
||||
mut db,
|
||||
src,
|
||||
site_packages,
|
||||
custom_typeshed,
|
||||
} = setup_resolver_test();
|
||||
|
||||
db.with_os_file_system();
|
||||
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let root = SystemPath::from_std_path(temp_dir.path()).unwrap();
|
||||
db.use_os_system(OsSystem::new(root));
|
||||
let root = FileSystemPath::from_std_path(temp_dir.path()).unwrap();
|
||||
|
||||
let src = root.join("src");
|
||||
let site_packages = root.join("site-packages");
|
||||
let custom_typeshed = root.join("typeshed");
|
||||
let src = root.join(src);
|
||||
let site_packages = root.join(site_packages);
|
||||
let custom_typeshed = root.join(custom_typeshed);
|
||||
|
||||
let foo = src.join("foo.py");
|
||||
let bar = src.join("bar.py");
|
||||
@@ -925,33 +890,35 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
Some(foo_module),
|
||||
path_to_module(&db, &FilePath::System(foo))
|
||||
path_to_module(&db, &VfsPath::FileSystem(foo))
|
||||
);
|
||||
assert_eq!(
|
||||
Some(bar_module),
|
||||
path_to_module(&db, &FilePath::System(bar))
|
||||
path_to_module(&db, &VfsPath::FileSystem(bar))
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new()
|
||||
.with_src_files(&[("foo.py", "x = 1"), ("bar.py", "x = 2")])
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
fn deleting_an_unrelated_file_doesnt_change_module_resolution() -> anyhow::Result<()> {
|
||||
let TestCase { mut db, src, .. } = setup_resolver_test();
|
||||
|
||||
let foo_path = src.join("foo.py");
|
||||
let bar_path = src.join("bar.py");
|
||||
|
||||
db.memory_file_system()
|
||||
.write_files([(&foo_path, "x = 1"), (&bar_path, "y = 2")])?;
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
|
||||
let bar_path = src.join("bar.py");
|
||||
let bar = system_path_to_file(&db, &bar_path).expect("bar.py to exist");
|
||||
|
||||
db.clear_salsa_events();
|
||||
|
||||
// Delete `bar.py`
|
||||
db.memory_file_system().remove_file(&bar_path).unwrap();
|
||||
db.memory_file_system().remove_file(&bar_path)?;
|
||||
bar.touch(&mut db);
|
||||
|
||||
// Re-query the foo module. The foo module should still be cached because `bar.py` isn't relevant
|
||||
@@ -965,20 +932,22 @@ mod tests {
|
||||
.any(|event| { matches!(event.kind, salsa::EventKind::WillExecute { .. }) }));
|
||||
|
||||
assert_eq!(Some(foo_module), foo_module2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_file_on_which_module_resolution_depends_invalidates_previously_failing_query_that_now_succeeds(
|
||||
fn adding_a_file_on_which_the_module_resolution_depends_on_invalidates_the_query(
|
||||
) -> anyhow::Result<()> {
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new().build();
|
||||
let TestCase { mut db, src, .. } = setup_resolver_test();
|
||||
let foo_path = src.join("foo.py");
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
assert_eq!(resolve_module(&db, foo_module_name.clone()), None);
|
||||
|
||||
// Now write the foo file
|
||||
db.write_file(&foo_path, "x = 1")?;
|
||||
|
||||
db.memory_file_system().write_file(&foo_path, "x = 1")?;
|
||||
VfsFile::touch_path(&mut db, &VfsPath::FileSystem(foo_path.clone()));
|
||||
let foo_file = system_path_to_file(&db, &foo_path).expect("foo.py to exist");
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve");
|
||||
@@ -988,15 +957,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_file_on_which_module_resolution_depends_invalidates_previously_successful_query_that_now_fails(
|
||||
fn removing_a_file_that_the_module_resolution_depends_on_invalidates_the_query(
|
||||
) -> anyhow::Result<()> {
|
||||
const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/__init__.py", "x = 2")];
|
||||
let TestCase { mut db, src, .. } = setup_resolver_test();
|
||||
let foo_path = src.join("foo.py");
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
let TestCase { mut db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
db.memory_file_system()
|
||||
.write_files([(&foo_path, "x = 1"), (&foo_init_path, "x = 2")])?;
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).expect("foo module to exist");
|
||||
let foo_init_path = src.join("foo/__init__.py");
|
||||
|
||||
assert_eq!(&foo_init_path, foo_module.file().path(&db));
|
||||
|
||||
@@ -1004,140 +975,11 @@ mod tests {
|
||||
db.memory_file_system().remove_file(&foo_init_path)?;
|
||||
db.memory_file_system()
|
||||
.remove_directory(foo_init_path.parent().unwrap())?;
|
||||
File::touch_path(&mut db, &foo_init_path);
|
||||
VfsFile::touch_path(&mut db, &VfsPath::FileSystem(foo_init_path));
|
||||
|
||||
let foo_module = resolve_module(&db, foo_module_name).expect("Foo module to resolve");
|
||||
assert_eq!(&src.join("foo.py"), foo_module.file().path(&db));
|
||||
assert_eq!(&foo_path, foo_module.file().path(&db));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_file_to_search_path_with_lower_priority_does_not_invalidate_query() {
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
versions: "functools: 3.8-",
|
||||
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
|
||||
};
|
||||
|
||||
let TestCase {
|
||||
mut db,
|
||||
stdlib,
|
||||
site_packages,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let stdlib_functools_path = stdlib.join("functools.pyi");
|
||||
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
system_path_to_file(&db, &stdlib_functools_path)
|
||||
);
|
||||
|
||||
// Adding a file to site-packages does not invalidate the query,
|
||||
// since site-packages takes lower priority in the module resolution
|
||||
db.clear_salsa_events();
|
||||
let site_packages_functools_path = site_packages.join("functools.py");
|
||||
db.write_file(&site_packages_functools_path, "f: int")
|
||||
.unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
let events = db.take_salsa_events();
|
||||
assert_function_query_was_not_run::<resolve_module_query, _, _>(
|
||||
&db,
|
||||
|res| &res.function,
|
||||
&ModuleNameIngredient::new(&db, functools_module_name.clone()),
|
||||
&events,
|
||||
);
|
||||
assert_eq!(functools_module.search_path(), stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
system_path_to_file(&db, &stdlib_functools_path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adding_file_to_search_path_with_higher_priority_invalidates_the_query() {
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
versions: "functools: 3.8-",
|
||||
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
|
||||
};
|
||||
|
||||
let TestCase {
|
||||
mut db,
|
||||
stdlib,
|
||||
src,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
system_path_to_file(&db, stdlib.join("functools.pyi"))
|
||||
);
|
||||
|
||||
// Adding a first-party file invalidates the query,
|
||||
// since first-party files take higher priority in module resolution:
|
||||
let src_functools_path = src.join("functools.py");
|
||||
db.write_file(&src_functools_path, "FOO: int").unwrap();
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), src);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
system_path_to_file(&db, &src_functools_path)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deleting_file_from_higher_priority_search_path_invalidates_the_query() {
|
||||
const SRC: &[FileSpec] = &[("functools.py", "FOO: int")];
|
||||
|
||||
const TYPESHED: MockedTypeshed = MockedTypeshed {
|
||||
versions: "functools: 3.8-",
|
||||
stdlib_files: &[("functools.pyi", "def update_wrapper(): ...")],
|
||||
};
|
||||
|
||||
let TestCase {
|
||||
mut db,
|
||||
stdlib,
|
||||
src,
|
||||
..
|
||||
} = TestCaseBuilder::new()
|
||||
.with_src_files(SRC)
|
||||
.with_custom_typeshed(TYPESHED)
|
||||
.with_target_version(TargetVersion::Py38)
|
||||
.build();
|
||||
|
||||
let functools_module_name = ModuleName::new_static("functools").unwrap();
|
||||
let src_functools_path = src.join("functools.py");
|
||||
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), src);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
system_path_to_file(&db, &src_functools_path)
|
||||
);
|
||||
|
||||
// If we now delete the first-party file,
|
||||
// it should resolve to the stdlib:
|
||||
db.memory_file_system()
|
||||
.remove_file(&src_functools_path)
|
||||
.unwrap();
|
||||
File::touch_path(&mut db, &src_functools_path);
|
||||
let functools_module = resolve_module(&db, functools_module_name.clone()).unwrap();
|
||||
assert_eq!(functools_module.search_path(), stdlib);
|
||||
assert_eq!(
|
||||
Some(functools_module.file()),
|
||||
system_path_to_file(&db, stdlib.join("functools.pyi"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::file_system::FileSystem;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::supported_py_version::TargetVersion;
|
||||
@@ -20,11 +19,7 @@ impl<'db> ResolverState<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn system(&self) -> &dyn System {
|
||||
self.db.system()
|
||||
}
|
||||
|
||||
pub(crate) fn vendored(&self) -> &VendoredFileSystem {
|
||||
self.db.vendored()
|
||||
pub(crate) fn file_system(&self) -> &dyn FileSystem {
|
||||
self.db.file_system()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath, SystemPathBuf};
|
||||
use ruff_db::vendored::VendoredPathBuf;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::resolver::{set_module_resolution_settings, RawModuleResolutionSettings};
|
||||
use crate::supported_py_version::TargetVersion;
|
||||
|
||||
/// A test case for the module resolver.
|
||||
///
|
||||
/// You generally shouldn't construct instances of this struct directly;
|
||||
/// instead, use the [`TestCaseBuilder`].
|
||||
pub(crate) struct TestCase<T> {
|
||||
pub(crate) db: TestDb,
|
||||
pub(crate) src: SystemPathBuf,
|
||||
pub(crate) stdlib: T,
|
||||
pub(crate) site_packages: SystemPathBuf,
|
||||
pub(crate) target_version: TargetVersion,
|
||||
}
|
||||
|
||||
/// A `(file_name, file_contents)` tuple
|
||||
pub(crate) type FileSpec = (&'static str, &'static str);
|
||||
|
||||
/// Specification for a typeshed mock to be created as part of a test
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub(crate) struct MockedTypeshed {
|
||||
/// The stdlib files to be created in the typeshed mock
|
||||
pub(crate) stdlib_files: &'static [FileSpec],
|
||||
|
||||
/// The contents of the `stdlib/VERSIONS` file
|
||||
/// to be created in the typeshed mock
|
||||
pub(crate) versions: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct VendoredTypeshed;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct UnspecifiedTypeshed;
|
||||
|
||||
/// A builder for a module-resolver test case.
|
||||
///
|
||||
/// The builder takes care of creating a [`TestDb`]
|
||||
/// instance, applying the module resolver settings,
|
||||
/// and creating mock directories for the stdlib, `site-packages`,
|
||||
/// first-party code, etc.
|
||||
///
|
||||
/// For simple tests that do not involve typeshed,
|
||||
/// test cases can be created as follows:
|
||||
///
|
||||
/// ```rs
|
||||
/// let test_case = TestCaseBuilder::new()
|
||||
/// .with_src_files(...)
|
||||
/// .build();
|
||||
///
|
||||
/// let test_case2 = TestCaseBuilder::new()
|
||||
/// .with_site_packages_files(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// Any tests can specify the target Python version that should be used
|
||||
/// in the module resolver settings:
|
||||
///
|
||||
/// ```rs
|
||||
/// let test_case = TestCaseBuilder::new()
|
||||
/// .with_src_files(...)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// For tests checking that standard-library module resolution is working
|
||||
/// correctly, you should usually create a [`MockedTypeshed`] instance
|
||||
/// and pass it to the [`TestCaseBuilder::with_custom_typeshed`] method.
|
||||
/// If you need to check something that involves the vendored typeshed stubs
|
||||
/// we include as part of the binary, you can instead use the
|
||||
/// [`TestCaseBuilder::with_vendored_typeshed`] method.
|
||||
/// For either of these, you should almost always try to be explicit
|
||||
/// about the Python version you want to be specified in the module-resolver
|
||||
/// settings for the test:
|
||||
///
|
||||
/// ```rs
|
||||
/// const TYPESHED = MockedTypeshed { ... };
|
||||
///
|
||||
/// let test_case = resolver_test_case()
|
||||
/// .with_custom_typeshed(TYPESHED)
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
///
|
||||
/// let test_case2 = resolver_test_case()
|
||||
/// .with_vendored_typeshed()
|
||||
/// .with_target_version(...)
|
||||
/// .build();
|
||||
/// ```
|
||||
///
|
||||
/// If you have not called one of those options, the `stdlib` field
|
||||
/// on the [`TestCase`] instance created from `.build()` will be set
|
||||
/// to `()`.
|
||||
pub(crate) struct TestCaseBuilder<T> {
|
||||
typeshed_option: T,
|
||||
target_version: TargetVersion,
|
||||
first_party_files: Vec<FileSpec>,
|
||||
site_packages_files: Vec<FileSpec>,
|
||||
}
|
||||
|
||||
impl<T> TestCaseBuilder<T> {
|
||||
/// Specify files to be created in the `src` mock directory
|
||||
pub(crate) fn with_src_files(mut self, files: &[FileSpec]) -> Self {
|
||||
self.first_party_files.extend(files.iter().copied());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify files to be created in the `site-packages` mock directory
|
||||
pub(crate) fn with_site_packages_files(mut self, files: &[FileSpec]) -> Self {
|
||||
self.site_packages_files.extend(files.iter().copied());
|
||||
self
|
||||
}
|
||||
|
||||
/// Specify the target Python version the module resolver should assume
|
||||
pub(crate) fn with_target_version(mut self, target_version: TargetVersion) -> Self {
|
||||
self.target_version = target_version;
|
||||
self
|
||||
}
|
||||
|
||||
fn write_mock_directory(
|
||||
db: &mut TestDb,
|
||||
location: impl AsRef<SystemPath>,
|
||||
files: impl IntoIterator<Item = FileSpec>,
|
||||
) -> SystemPathBuf {
|
||||
let root = location.as_ref().to_path_buf();
|
||||
db.write_files(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|(relative_path, contents)| (root.join(relative_path), contents)),
|
||||
)
|
||||
.unwrap();
|
||||
root
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
pub(crate) fn new() -> TestCaseBuilder<UnspecifiedTypeshed> {
|
||||
Self {
|
||||
typeshed_option: UnspecifiedTypeshed,
|
||||
target_version: TargetVersion::default(),
|
||||
first_party_files: vec![],
|
||||
site_packages_files: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Use the vendored stdlib stubs included in the Ruff binary for this test case
|
||||
pub(crate) fn with_vendored_typeshed(self) -> TestCaseBuilder<VendoredTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
}
|
||||
|
||||
/// Use a mock typeshed directory for this test case
|
||||
pub(crate) fn with_custom_typeshed(
|
||||
self,
|
||||
typeshed: MockedTypeshed,
|
||||
) -> TestCaseBuilder<MockedTypeshed> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: _,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
TestCaseBuilder {
|
||||
typeshed_option: typeshed,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build(self) -> TestCase<()> {
|
||||
let TestCase {
|
||||
db,
|
||||
src,
|
||||
stdlib: _,
|
||||
site_packages,
|
||||
target_version,
|
||||
} = self.with_custom_typeshed(MockedTypeshed::default()).build();
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
stdlib: (),
|
||||
site_packages,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCaseBuilder<MockedTypeshed> {
|
||||
pub(crate) fn build(self) -> TestCase<SystemPathBuf> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let site_packages =
|
||||
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
|
||||
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
|
||||
let typeshed = Self::build_typeshed_mock(&mut db, &typeshed_option);
|
||||
|
||||
set_module_resolution_settings(
|
||||
&mut db,
|
||||
RawModuleResolutionSettings {
|
||||
target_version,
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
custom_typeshed: Some(typeshed.clone()),
|
||||
site_packages: Some(site_packages.clone()),
|
||||
},
|
||||
);
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
stdlib: typeshed.join("stdlib"),
|
||||
site_packages,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_typeshed_mock(db: &mut TestDb, typeshed_to_build: &MockedTypeshed) -> SystemPathBuf {
|
||||
let typeshed = SystemPathBuf::from("/typeshed");
|
||||
let MockedTypeshed {
|
||||
stdlib_files,
|
||||
versions,
|
||||
} = typeshed_to_build;
|
||||
Self::write_mock_directory(
|
||||
db,
|
||||
typeshed.join("stdlib"),
|
||||
stdlib_files
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(std::iter::once(("VERSIONS", *versions))),
|
||||
);
|
||||
typeshed
|
||||
}
|
||||
}
|
||||
|
||||
impl TestCaseBuilder<VendoredTypeshed> {
|
||||
pub(crate) fn build(self) -> TestCase<VendoredPathBuf> {
|
||||
let TestCaseBuilder {
|
||||
typeshed_option: VendoredTypeshed,
|
||||
target_version,
|
||||
first_party_files,
|
||||
site_packages_files,
|
||||
} = self;
|
||||
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let site_packages =
|
||||
Self::write_mock_directory(&mut db, "/site-packages", site_packages_files);
|
||||
let src = Self::write_mock_directory(&mut db, "/src", first_party_files);
|
||||
|
||||
set_module_resolution_settings(
|
||||
&mut db,
|
||||
RawModuleResolutionSettings {
|
||||
target_version,
|
||||
extra_paths: vec![],
|
||||
workspace_root: src.clone(),
|
||||
custom_typeshed: None,
|
||||
site_packages: Some(site_packages.clone()),
|
||||
},
|
||||
);
|
||||
|
||||
TestCase {
|
||||
db,
|
||||
src,
|
||||
stdlib: VendoredPathBuf::from("stdlib"),
|
||||
site_packages,
|
||||
target_version,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,96 @@
|
||||
pub use self::vendored::vendored_typeshed_stubs;
|
||||
pub(crate) use self::versions::{
|
||||
mod versions;
|
||||
|
||||
pub(crate) use versions::{
|
||||
parse_typeshed_versions, LazyTypeshedVersions, TypeshedVersionsQueryResult,
|
||||
};
|
||||
pub use self::versions::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind};
|
||||
pub use versions::{TypeshedVersionsParseError, TypeshedVersionsParseErrorKind};
|
||||
|
||||
mod vendored;
|
||||
mod versions;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::vfs::VendoredPath;
|
||||
|
||||
// The file path here is hardcoded in this crate's `build.rs` script.
|
||||
// Luckily this crate will fail to build if this file isn't available at build time.
|
||||
const TYPESHED_ZIP_BYTES: &[u8] =
|
||||
include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip"));
|
||||
|
||||
#[test]
|
||||
fn typeshed_zip_created_at_build_time() {
|
||||
let mut typeshed_zip_archive =
|
||||
zip::ZipArchive::new(io::Cursor::new(TYPESHED_ZIP_BYTES)).unwrap();
|
||||
|
||||
let mut functools_module_stub = typeshed_zip_archive
|
||||
.by_name("stdlib/functools.pyi")
|
||||
.unwrap();
|
||||
assert!(functools_module_stub.is_file());
|
||||
|
||||
let mut functools_module_stub_source = String::new();
|
||||
functools_module_stub
|
||||
.read_to_string(&mut functools_module_stub_source)
|
||||
.unwrap();
|
||||
|
||||
assert!(functools_module_stub_source.contains("def update_wrapper("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeshed_vfs_consistent_with_vendored_stubs() {
|
||||
let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap();
|
||||
let vendored_typeshed_stubs = VendoredFileSystem::new(TYPESHED_ZIP_BYTES).unwrap();
|
||||
|
||||
let mut empty_iterator = true;
|
||||
for entry in walkdir::WalkDir::new(&vendored_typeshed_dir).min_depth(1) {
|
||||
empty_iterator = false;
|
||||
let entry = entry.unwrap();
|
||||
let absolute_path = entry.path();
|
||||
let file_type = entry.file_type();
|
||||
|
||||
let relative_path = absolute_path
|
||||
.strip_prefix(&vendored_typeshed_dir)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Expected {absolute_path:?} to be a child of {vendored_typeshed_dir:?}")
|
||||
});
|
||||
|
||||
let vendored_path = <&VendoredPath>::try_from(relative_path)
|
||||
.unwrap_or_else(|_| panic!("Expected {relative_path:?} to be valid UTF-8"));
|
||||
|
||||
assert!(
|
||||
vendored_typeshed_stubs.exists(vendored_path),
|
||||
"Expected {vendored_path:?} to exist in the `VendoredFileSystem`!
|
||||
|
||||
Vendored file system:
|
||||
|
||||
{vendored_typeshed_stubs:#?}
|
||||
"
|
||||
);
|
||||
|
||||
let vendored_path_kind = vendored_typeshed_stubs
|
||||
.metadata(vendored_path)
|
||||
.unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Expected metadata for {vendored_path:?} to be retrievable from the `VendoredFileSystem!
|
||||
|
||||
Vendored file system:
|
||||
|
||||
{vendored_typeshed_stubs:#?}
|
||||
"
|
||||
)
|
||||
})
|
||||
.kind();
|
||||
|
||||
assert_eq!(
|
||||
vendored_path_kind.is_directory(),
|
||||
file_type.is_dir(),
|
||||
"{vendored_path:?} had type {vendored_path_kind:?}, inconsistent with fs path {relative_path:?}: {file_type:?}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
!empty_iterator,
|
||||
"Expected there to be at least one file or directory in the vendored typeshed stubs!"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
|
||||
// The file path here is hardcoded in this crate's `build.rs` script.
|
||||
// Luckily this crate will fail to build if this file isn't available at build time.
|
||||
static TYPESHED_ZIP_BYTES: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/zipped_typeshed.zip"));
|
||||
|
||||
pub fn vendored_typeshed_stubs() -> &'static VendoredFileSystem {
|
||||
static VENDORED_TYPESHED_STUBS: Lazy<VendoredFileSystem> =
|
||||
Lazy::new(|| VendoredFileSystem::new_static(TYPESHED_ZIP_BYTES).unwrap());
|
||||
&VENDORED_TYPESHED_STUBS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::{self, Read};
|
||||
use std::path::Path;
|
||||
|
||||
use ruff_db::vendored::VendoredPath;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn typeshed_zip_created_at_build_time() {
|
||||
let mut typeshed_zip_archive =
|
||||
zip::ZipArchive::new(io::Cursor::new(TYPESHED_ZIP_BYTES)).unwrap();
|
||||
|
||||
let mut functools_module_stub = typeshed_zip_archive
|
||||
.by_name("stdlib/functools.pyi")
|
||||
.unwrap();
|
||||
assert!(functools_module_stub.is_file());
|
||||
|
||||
let mut functools_module_stub_source = String::new();
|
||||
functools_module_stub
|
||||
.read_to_string(&mut functools_module_stub_source)
|
||||
.unwrap();
|
||||
|
||||
assert!(functools_module_stub_source.contains("def update_wrapper("));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typeshed_vfs_consistent_with_vendored_stubs() {
|
||||
let vendored_typeshed_dir = Path::new("vendor/typeshed").canonicalize().unwrap();
|
||||
let vendored_typeshed_stubs = vendored_typeshed_stubs();
|
||||
|
||||
let mut empty_iterator = true;
|
||||
for entry in walkdir::WalkDir::new(&vendored_typeshed_dir).min_depth(1) {
|
||||
empty_iterator = false;
|
||||
let entry = entry.unwrap();
|
||||
let absolute_path = entry.path();
|
||||
let file_type = entry.file_type();
|
||||
|
||||
let relative_path = absolute_path
|
||||
.strip_prefix(&vendored_typeshed_dir)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!("Expected {absolute_path:?} to be a child of {vendored_typeshed_dir:?}")
|
||||
});
|
||||
|
||||
let vendored_path = <&VendoredPath>::try_from(relative_path)
|
||||
.unwrap_or_else(|_| panic!("Expected {relative_path:?} to be valid UTF-8"));
|
||||
|
||||
assert!(
|
||||
vendored_typeshed_stubs.exists(vendored_path),
|
||||
"Expected {vendored_path:?} to exist in the `VendoredFileSystem`!
|
||||
|
||||
Vendored file system:
|
||||
|
||||
{vendored_typeshed_stubs:#?}
|
||||
"
|
||||
);
|
||||
|
||||
let vendored_path_kind = vendored_typeshed_stubs
|
||||
.metadata(vendored_path)
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"Expected metadata for {vendored_path:?} to be retrievable from the `VendoredFileSystem!
|
||||
|
||||
Vendored file system:
|
||||
|
||||
{vendored_typeshed_stubs:#?}
|
||||
"
|
||||
)
|
||||
})
|
||||
.kind();
|
||||
|
||||
assert_eq!(
|
||||
vendored_path_kind.is_directory(),
|
||||
file_type.is_dir(),
|
||||
"{vendored_path:?} had type {vendored_path_kind:?}, inconsistent with fs path {relative_path:?}: {file_type:?}"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(
|
||||
!empty_iterator,
|
||||
"Expected there to be at least one file or directory in the vendored typeshed stubs!"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,19 +5,15 @@ use std::num::{NonZeroU16, NonZeroUsize};
|
||||
use std::ops::{RangeFrom, RangeInclusive};
|
||||
use std::str::FromStr;
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use ruff_db::system::SystemPath;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::file_system::FileSystemPath;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::vfs::{system_path_to_file, VfsFile};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::supported_py_version::TargetVersion;
|
||||
|
||||
use super::vendored::vendored_typeshed_stubs;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct LazyTypeshedVersions<'db>(OnceCell<&'db TypeshedVersions>);
|
||||
|
||||
@@ -42,17 +38,13 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
#[must_use]
|
||||
pub(crate) fn query_module(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
module: &ModuleName,
|
||||
stdlib_root: Option<&SystemPath>,
|
||||
db: &'db dyn Db,
|
||||
stdlib_root: &FileSystemPath,
|
||||
target_version: TargetVersion,
|
||||
) -> TypeshedVersionsQueryResult {
|
||||
let versions = self.0.get_or_init(|| {
|
||||
let versions_path = if let Some(system_path) = stdlib_root {
|
||||
system_path.join("VERSIONS")
|
||||
} else {
|
||||
return &VENDORED_VERSIONS;
|
||||
};
|
||||
let versions_path = stdlib_root.join("VERSIONS");
|
||||
let Some(versions_file) = system_path_to_file(db.upcast(), &versions_path) else {
|
||||
todo!(
|
||||
"Still need to figure out how to handle VERSIONS files being deleted \
|
||||
@@ -72,21 +64,12 @@ impl<'db> LazyTypeshedVersions<'db> {
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn parse_typeshed_versions(
|
||||
db: &dyn Db,
|
||||
versions_file: File,
|
||||
versions_file: VfsFile,
|
||||
) -> Result<TypeshedVersions, TypeshedVersionsParseError> {
|
||||
let file_content = source_text(db.upcast(), versions_file);
|
||||
file_content.parse()
|
||||
}
|
||||
|
||||
static VENDORED_VERSIONS: Lazy<TypeshedVersions> = Lazy::new(|| {
|
||||
TypeshedVersions::from_str(
|
||||
&vendored_typeshed_stubs()
|
||||
.read_to_string("stdlib/VERSIONS")
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct TypeshedVersionsParseError {
|
||||
line_number: Option<NonZeroU16>,
|
||||
@@ -131,6 +114,7 @@ pub enum TypeshedVersionsParseErrorKind {
|
||||
version: String,
|
||||
err: std::num::ParseIntError,
|
||||
},
|
||||
EmptyVersionsFile,
|
||||
}
|
||||
|
||||
impl fmt::Display for TypeshedVersionsParseErrorKind {
|
||||
@@ -159,6 +143,7 @@ impl fmt::Display for TypeshedVersionsParseErrorKind {
|
||||
f,
|
||||
"Failed to convert '{version}' to a pair of integers due to {err}",
|
||||
),
|
||||
Self::EmptyVersionsFile => f.write_str("Versions file was empty!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,7 +290,14 @@ impl FromStr for TypeshedVersions {
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self(map))
|
||||
if map.is_empty() {
|
||||
Err(TypeshedVersionsParseError {
|
||||
line_number: None,
|
||||
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile,
|
||||
})
|
||||
} else {
|
||||
Ok(Self(map))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,10 +429,10 @@ mod tests {
|
||||
use std::num::{IntErrorKind, NonZeroU16};
|
||||
use std::path::Path;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
use super::*;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
|
||||
const TYPESHED_STDLIB_DIR: &str = "stdlib";
|
||||
|
||||
#[allow(unsafe_code)]
|
||||
@@ -676,6 +668,31 @@ foo: 3.8- # trailing comment
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_empty_versions_file() {
|
||||
assert_eq!(
|
||||
TypeshedVersions::from_str(""),
|
||||
Err(TypeshedVersionsParseError {
|
||||
line_number: None,
|
||||
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
TypeshedVersions::from_str(" "),
|
||||
Err(TypeshedVersionsParseError {
|
||||
line_number: None,
|
||||
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
TypeshedVersions::from_str(" \n \n \n "),
|
||||
Err(TypeshedVersionsParseError {
|
||||
line_number: None,
|
||||
reason: TypeshedVersionsParseErrorKind::EmptyVersionsFile
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_huge_versions_file() {
|
||||
let offset = 100;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use salsa::DbWithJar;
|
||||
|
||||
use red_knot_module_resolver::Db as ResolverDb;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use red_knot_module_resolver::Db as ResolverDb;
|
||||
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::symbol::{public_symbols_map, PublicSymbolId, ScopeId};
|
||||
use crate::semantic_index::{root_scope, semantic_index, symbol_table};
|
||||
@@ -35,14 +36,18 @@ pub trait Db:
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use std::fmt::Formatter;
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use salsa::id::AsId;
|
||||
use salsa::ingredient::Ingredient;
|
||||
use salsa::storage::HasIngredientsFor;
|
||||
use salsa::DebugWithDb;
|
||||
|
||||
use red_knot_module_resolver::{vendored_typeshed_stubs, Db as ResolverDb, Jar as ResolverJar};
|
||||
use ruff_db::files::Files;
|
||||
use ruff_db::system::{DbWithTestSystem, System, TestSystem};
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use red_knot_module_resolver::{Db as ResolverDb, Jar as ResolverJar};
|
||||
use ruff_db::file_system::{FileSystem, MemoryFileSystem, OsFileSystem};
|
||||
use ruff_db::vfs::Vfs;
|
||||
use ruff_db::{Db as SourceDb, Jar as SourceJar, Upcast};
|
||||
|
||||
use super::{Db, Jar};
|
||||
@@ -50,9 +55,8 @@ pub(crate) mod tests {
|
||||
#[salsa::db(Jar, ResolverJar, SourceJar)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
vfs: Vfs,
|
||||
file_system: TestFileSystem,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
}
|
||||
|
||||
@@ -60,13 +64,29 @@ pub(crate) mod tests {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: vendored_typeshed_stubs().snapshot(),
|
||||
file_system: TestFileSystem::Memory(MemoryFileSystem::default()),
|
||||
events: std::sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
vfs: Vfs::with_stubbed_vendored(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the memory file system.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If this test db isn't using a memory file system.
|
||||
pub(crate) fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||
if let TestFileSystem::Memory(fs) = &self.file_system {
|
||||
fs
|
||||
} else {
|
||||
panic!("The test db is not using a memory file system");
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
|
||||
&mut self.vfs
|
||||
}
|
||||
|
||||
/// Takes the salsa events.
|
||||
///
|
||||
/// ## Panics
|
||||
@@ -87,27 +107,16 @@ pub(crate) mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceDb for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
fn file_system(&self) -> &dyn FileSystem {
|
||||
match &self.file_system {
|
||||
TestFileSystem::Memory(fs) => fs,
|
||||
TestFileSystem::Os(fs) => fs,
|
||||
}
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
fn vfs(&self) -> &Vfs {
|
||||
&self.vfs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,11 +147,121 @@ pub(crate) mod tests {
|
||||
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||
salsa::Snapshot::new(Self {
|
||||
storage: self.storage.snapshot(),
|
||||
files: self.files.snapshot(),
|
||||
system: self.system.snapshot(),
|
||||
vendored: self.vendored.snapshot(),
|
||||
vfs: self.vfs.snapshot(),
|
||||
file_system: match &self.file_system {
|
||||
TestFileSystem::Memory(memory) => TestFileSystem::Memory(memory.snapshot()),
|
||||
TestFileSystem::Os(fs) => TestFileSystem::Os(fs.snapshot()),
|
||||
},
|
||||
events: self.events.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
enum TestFileSystem {
|
||||
Memory(MemoryFileSystem),
|
||||
#[allow(dead_code)]
|
||||
Os(OsFileSystem),
|
||||
}
|
||||
|
||||
pub(crate) fn assert_will_run_function_query<'db, C, Db, Jar>(
|
||||
db: &'db Db,
|
||||
to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient<C>,
|
||||
input: &C::Input<'db>,
|
||||
events: &[salsa::Event],
|
||||
) where
|
||||
C: salsa::function::Configuration<Jar = Jar>
|
||||
+ salsa::storage::IngredientsFor<Jar = Jar, Ingredients = C>,
|
||||
Jar: HasIngredientsFor<C>,
|
||||
Db: salsa::DbWithJar<Jar>,
|
||||
C::Input<'db>: AsId,
|
||||
{
|
||||
will_run_function_query(db, to_function, input, events, true);
|
||||
}
|
||||
|
||||
pub(crate) fn assert_will_not_run_function_query<'db, C, Db, Jar>(
|
||||
db: &'db Db,
|
||||
to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient<C>,
|
||||
input: &C::Input<'db>,
|
||||
events: &[salsa::Event],
|
||||
) where
|
||||
C: salsa::function::Configuration<Jar = Jar>
|
||||
+ salsa::storage::IngredientsFor<Jar = Jar, Ingredients = C>,
|
||||
Jar: HasIngredientsFor<C>,
|
||||
Db: salsa::DbWithJar<Jar>,
|
||||
C::Input<'db>: AsId,
|
||||
{
|
||||
will_run_function_query(db, to_function, input, events, false);
|
||||
}
|
||||
|
||||
fn will_run_function_query<'db, C, Db, Jar>(
|
||||
db: &'db Db,
|
||||
to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient<C>,
|
||||
input: &C::Input<'db>,
|
||||
events: &[salsa::Event],
|
||||
should_run: bool,
|
||||
) where
|
||||
C: salsa::function::Configuration<Jar = Jar>
|
||||
+ salsa::storage::IngredientsFor<Jar = Jar, Ingredients = C>,
|
||||
Jar: HasIngredientsFor<C>,
|
||||
Db: salsa::DbWithJar<Jar>,
|
||||
C::Input<'db>: AsId,
|
||||
{
|
||||
let (jar, _) =
|
||||
<_ as salsa::storage::HasJar<<C as salsa::storage::IngredientsFor>::Jar>>::jar(db);
|
||||
let ingredient = jar.ingredient();
|
||||
|
||||
let function_ingredient = to_function(ingredient);
|
||||
|
||||
let ingredient_index =
|
||||
<salsa::function::FunctionIngredient<C> as Ingredient<Db>>::ingredient_index(
|
||||
function_ingredient,
|
||||
);
|
||||
|
||||
let did_run = events.iter().any(|event| {
|
||||
if let salsa::EventKind::WillExecute { database_key } = event.kind {
|
||||
database_key.ingredient_index() == ingredient_index
|
||||
&& database_key.key_index() == input.as_id()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if should_run && !did_run {
|
||||
panic!(
|
||||
"Expected query {:?} to run but it didn't",
|
||||
DebugIdx {
|
||||
db: PhantomData::<Db>,
|
||||
value_id: input.as_id(),
|
||||
ingredient: function_ingredient,
|
||||
}
|
||||
);
|
||||
} else if !should_run && did_run {
|
||||
panic!(
|
||||
"Expected query {:?} not to run but it did",
|
||||
DebugIdx {
|
||||
db: PhantomData::<Db>,
|
||||
value_id: input.as_id(),
|
||||
ingredient: function_ingredient,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugIdx<'a, I, Db>
|
||||
where
|
||||
I: Ingredient<Db>,
|
||||
{
|
||||
value_id: salsa::Id,
|
||||
ingredient: &'a I,
|
||||
db: PhantomData<Db>,
|
||||
}
|
||||
|
||||
impl<'a, I, Db> std::fmt::Debug for DebugIdx<'a, I, Db>
|
||||
where
|
||||
I: Ingredient<Db>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.ingredient.fmt_index(Some(self.value_id), f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_index::{IndexSlice, IndexVec};
|
||||
|
||||
use crate::semantic_index::ast_ids::node_key::ExpressionNodeKey;
|
||||
@@ -28,7 +28,7 @@ type SymbolMap = hashbrown::HashMap<ScopedSymbolId, (), ()>;
|
||||
///
|
||||
/// Prefer using [`symbol_table`] when working with symbols from a single scope.
|
||||
#[salsa::tracked(return_ref, no_eq)]
|
||||
pub(crate) fn semantic_index(db: &dyn Db, file: File) -> SemanticIndex<'_> {
|
||||
pub(crate) fn semantic_index(db: &dyn Db, file: VfsFile) -> SemanticIndex<'_> {
|
||||
let _span = tracing::trace_span!("semantic_index", ?file).entered();
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
@@ -51,7 +51,7 @@ pub(crate) fn symbol_table<'db>(db: &'db dyn Db, scope: ScopeId<'db>) -> Arc<Sym
|
||||
|
||||
/// Returns the root scope of `file`.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn root_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
pub(crate) fn root_scope(db: &dyn Db, file: VfsFile) -> ScopeId<'_> {
|
||||
let _span = tracing::trace_span!("root_scope", ?file).entered();
|
||||
|
||||
FileScopeId::root().to_scope_id(db, file)
|
||||
@@ -61,7 +61,7 @@ pub(crate) fn root_scope(db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
/// no symbol with the given name exists.
|
||||
pub(crate) fn public_symbol<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
name: &str,
|
||||
) -> Option<PublicSymbolId<'db>> {
|
||||
let root_scope = root_scope(db, file);
|
||||
@@ -272,9 +272,8 @@ impl FusedIterator for ChildrenIter<'_> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::DbWithTestSystem;
|
||||
use ruff_db::vfs::{system_path_to_file, VfsFile};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::semantic_index::symbol::{FileScopeId, Scope, ScopeKind, SymbolTable};
|
||||
@@ -283,12 +282,14 @@ mod tests {
|
||||
|
||||
struct TestCase {
|
||||
db: TestDb,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
}
|
||||
|
||||
fn test_case(content: impl ToString) -> TestCase {
|
||||
let mut db = TestDb::new();
|
||||
db.write_file("test.py", content).unwrap();
|
||||
let db = TestDb::new();
|
||||
db.memory_file_system()
|
||||
.write_file("test.py", content)
|
||||
.unwrap();
|
||||
|
||||
let file = system_path_to_file(&db, "test.py").unwrap();
|
||||
|
||||
@@ -630,7 +631,7 @@ class C[T]:
|
||||
fn scope_names<'a>(
|
||||
scopes: impl Iterator<Item = (FileScopeId, &'a Scope)>,
|
||||
db: &'a dyn Db,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
) -> Vec<&'a str> {
|
||||
scopes
|
||||
.into_iter()
|
||||
|
||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
||||
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::name::Name;
|
||||
@@ -22,7 +22,7 @@ use crate::Db;
|
||||
pub(super) struct SemanticIndexBuilder<'db, 'ast> {
|
||||
// Builder state
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
module: &'db ParsedModule,
|
||||
scope_stack: Vec<FileScopeId>,
|
||||
/// the target we're currently inferring
|
||||
@@ -42,7 +42,7 @@ impl<'db, 'ast> SemanticIndexBuilder<'db, 'ast>
|
||||
where
|
||||
'db: 'ast,
|
||||
{
|
||||
pub(super) fn new(db: &'db dyn Db, file: File, parsed: &'db ParsedModule) -> Self {
|
||||
pub(super) fn new(db: &'db dyn Db, file: VfsFile, parsed: &'db ParsedModule) -> Self {
|
||||
let mut builder = Self {
|
||||
db,
|
||||
file,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
@@ -10,7 +10,7 @@ use crate::semantic_index::symbol::{FileScopeId, ScopedSymbolId};
|
||||
pub struct Definition<'db> {
|
||||
/// The file in which the definition is defined.
|
||||
#[id]
|
||||
pub(super) file: File,
|
||||
pub(super) file: VfsFile,
|
||||
|
||||
/// The scope in which the definition is defined.
|
||||
#[id]
|
||||
|
||||
@@ -3,8 +3,8 @@ use std::ops::Range;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use hashbrown::hash_map::RawEntryMut;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::ParsedModule;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast};
|
||||
@@ -79,7 +79,7 @@ bitflags! {
|
||||
#[salsa::tracked]
|
||||
pub struct PublicSymbolId<'db> {
|
||||
#[id]
|
||||
pub(crate) file: File,
|
||||
pub(crate) file: VfsFile,
|
||||
#[id]
|
||||
pub(crate) scoped_symbol_id: ScopedSymbolId,
|
||||
}
|
||||
@@ -116,14 +116,14 @@ impl ScopedSymbolId {
|
||||
///
|
||||
/// # Panics
|
||||
/// May panic if the symbol does not belong to `file` or is not a symbol of `file`'s root scope.
|
||||
pub(crate) fn to_public_symbol(self, db: &dyn Db, file: File) -> PublicSymbolId {
|
||||
pub(crate) fn to_public_symbol(self, db: &dyn Db, file: VfsFile) -> PublicSymbolId {
|
||||
let symbols = public_symbols_map(db, file);
|
||||
symbols.public(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
pub(crate) fn public_symbols_map(db: &dyn Db, file: File) -> PublicSymbolsMap<'_> {
|
||||
pub(crate) fn public_symbols_map(db: &dyn Db, file: VfsFile) -> PublicSymbolsMap<'_> {
|
||||
let _span = tracing::trace_span!("public_symbols_map", ?file).entered();
|
||||
|
||||
let module_scope = root_scope(db, file);
|
||||
@@ -156,7 +156,7 @@ impl<'db> PublicSymbolsMap<'db> {
|
||||
#[salsa::tracked]
|
||||
pub struct ScopeId<'db> {
|
||||
#[id]
|
||||
pub file: File,
|
||||
pub file: VfsFile,
|
||||
#[id]
|
||||
pub file_scope_id: FileScopeId,
|
||||
|
||||
@@ -190,7 +190,7 @@ impl FileScopeId {
|
||||
FileScopeId::from_u32(0)
|
||||
}
|
||||
|
||||
pub fn to_scope_id(self, db: &dyn Db, file: File) -> ScopeId<'_> {
|
||||
pub fn to_scope_id(self, db: &dyn Db, file: VfsFile) -> ScopeId<'_> {
|
||||
let index = semantic_index(db, file);
|
||||
index.scope_ids_by_scope[self]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use red_knot_module_resolver::{resolve_module, Module, ModuleName};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{Expr, ExpressionRef, StmtClassDef};
|
||||
|
||||
@@ -11,11 +11,11 @@ use crate::Db;
|
||||
|
||||
pub struct SemanticModel<'db> {
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
}
|
||||
|
||||
impl<'db> SemanticModel<'db> {
|
||||
pub fn new(db: &'db dyn Db, file: File) -> Self {
|
||||
pub fn new(db: &'db dyn Db, file: VfsFile) -> Self {
|
||||
Self { db, file }
|
||||
}
|
||||
|
||||
@@ -182,9 +182,9 @@ mod tests {
|
||||
use red_knot_module_resolver::{
|
||||
set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion,
|
||||
};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::file_system::FileSystemPathBuf;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::vfs::system_path_to_file;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::types::Type;
|
||||
@@ -196,7 +196,7 @@ mod tests {
|
||||
&mut db,
|
||||
RawModuleResolutionSettings {
|
||||
extra_paths: vec![],
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
workspace_root: FileSystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
custom_typeshed: None,
|
||||
target_version: TargetVersion::Py38,
|
||||
@@ -208,9 +208,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn function_ty() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("/src/foo.py", "def test(): pass")?;
|
||||
db.memory_file_system()
|
||||
.write_file("/src/foo.py", "def test(): pass")?;
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
let ast = parsed_module(&db, foo);
|
||||
@@ -226,9 +227,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn class_ty() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("/src/foo.py", "class Test: pass")?;
|
||||
db.memory_file_system()
|
||||
.write_file("/src/foo.py", "class Test: pass")?;
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
let ast = parsed_module(&db, foo);
|
||||
@@ -244,9 +246,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn alias_ty() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
db.memory_file_system().write_files([
|
||||
("/src/foo.py", "class Test: pass"),
|
||||
("/src/bar.py", "from foo import Test"),
|
||||
])?;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use crate::semantic_index::symbol::{NodeWithScopeKind, PublicSymbolId, ScopeId};
|
||||
@@ -49,7 +49,7 @@ pub(crate) fn public_symbol_ty<'db>(db: &'db dyn Db, symbol: PublicSymbolId<'db>
|
||||
/// Shorthand for `public_symbol_ty` that takes a symbol name instead of a [`PublicSymbolId`].
|
||||
pub(crate) fn public_symbol_ty_by_name<'db>(
|
||||
db: &'db dyn Db,
|
||||
file: File,
|
||||
file: VfsFile,
|
||||
name: &str,
|
||||
) -> Option<Type<'db>> {
|
||||
let symbol = public_symbol(db, file, name)?;
|
||||
@@ -105,7 +105,7 @@ pub enum Type<'db> {
|
||||
/// a specific function object
|
||||
Function(FunctionType<'db>),
|
||||
/// a specific module object
|
||||
Module(File),
|
||||
Module(VfsFile),
|
||||
/// a specific class object
|
||||
Class(ClassType<'db>),
|
||||
/// the set of Python objects with the given class in their __class__'s method resolution order
|
||||
@@ -274,12 +274,13 @@ mod tests {
|
||||
use red_knot_module_resolver::{
|
||||
set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion,
|
||||
};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::file_system::FileSystemPathBuf;
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::testing::{assert_function_query_was_not_run, assert_function_query_was_run};
|
||||
use ruff_db::vfs::system_path_to_file;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::db::tests::{
|
||||
assert_will_not_run_function_query, assert_will_run_function_query, TestDb,
|
||||
};
|
||||
use crate::semantic_index::root_scope;
|
||||
use crate::types::{infer_types, public_symbol_ty_by_name};
|
||||
use crate::{HasTy, SemanticModel};
|
||||
@@ -291,7 +292,7 @@ mod tests {
|
||||
RawModuleResolutionSettings {
|
||||
target_version: TargetVersion::Py38,
|
||||
extra_paths: vec![],
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
workspace_root: FileSystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
custom_typeshed: None,
|
||||
},
|
||||
@@ -302,9 +303,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn local_inference() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("/src/a.py", "x = 10")?;
|
||||
db.memory_file_system().write_file("/src/a.py", "x = 10")?;
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
|
||||
let parsed = parsed_module(&db, a);
|
||||
@@ -323,7 +324,7 @@ mod tests {
|
||||
fn dependency_public_symbol_type_change() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
db.memory_file_system().write_files([
|
||||
("/src/a.py", "from foo import x"),
|
||||
("/src/foo.py", "x = 10\ndef foo(): ..."),
|
||||
])?;
|
||||
@@ -334,7 +335,11 @@ mod tests {
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
// Change `x` to a different value
|
||||
db.write_file("/src/foo.py", "x = 20\ndef foo(): ...")?;
|
||||
db.memory_file_system()
|
||||
.write_file("/src/foo.py", "x = 20\ndef foo(): ...")?;
|
||||
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
foo.touch(&mut db);
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
|
||||
@@ -346,7 +351,7 @@ mod tests {
|
||||
let events = db.take_salsa_events();
|
||||
|
||||
let a_root_scope = root_scope(&db, a);
|
||||
assert_function_query_was_run::<infer_types, _, _>(
|
||||
assert_will_run_function_query::<infer_types, _, _>(
|
||||
&db,
|
||||
|ty| &ty.function,
|
||||
&a_root_scope,
|
||||
@@ -360,7 +365,7 @@ mod tests {
|
||||
fn dependency_non_public_symbol_change() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
db.memory_file_system().write_files([
|
||||
("/src/a.py", "from foo import x"),
|
||||
("/src/foo.py", "x = 10\ndef foo(): y = 1"),
|
||||
])?;
|
||||
@@ -370,9 +375,13 @@ mod tests {
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
db.write_file("/src/foo.py", "x = 10\ndef foo(): pass")?;
|
||||
db.memory_file_system()
|
||||
.write_file("/src/foo.py", "x = 10\ndef foo(): pass")?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
foo.touch(&mut db);
|
||||
|
||||
db.clear_salsa_events();
|
||||
|
||||
@@ -384,7 +393,7 @@ mod tests {
|
||||
|
||||
let a_root_scope = root_scope(&db, a);
|
||||
|
||||
assert_function_query_was_not_run::<infer_types, _, _>(
|
||||
assert_will_not_run_function_query::<infer_types, _, _>(
|
||||
&db,
|
||||
|ty| &ty.function,
|
||||
&a_root_scope,
|
||||
@@ -398,7 +407,7 @@ mod tests {
|
||||
fn dependency_unrelated_public_symbol() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
db.memory_file_system().write_files([
|
||||
("/src/a.py", "from foo import x"),
|
||||
("/src/foo.py", "x = 10\ny = 20"),
|
||||
])?;
|
||||
@@ -408,9 +417,13 @@ mod tests {
|
||||
|
||||
assert_eq!(x_ty.display(&db).to_string(), "Literal[10]");
|
||||
|
||||
db.write_file("/src/foo.py", "x = 10\ny = 30")?;
|
||||
db.memory_file_system()
|
||||
.write_file("/src/foo.py", "x = 10\ny = 30")?;
|
||||
|
||||
let a = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let foo = system_path_to_file(&db, "/src/foo.py").unwrap();
|
||||
|
||||
foo.touch(&mut db);
|
||||
|
||||
db.clear_salsa_events();
|
||||
|
||||
@@ -421,7 +434,7 @@ mod tests {
|
||||
let events = db.take_salsa_events();
|
||||
|
||||
let a_root_scope = root_scope(&db, a);
|
||||
assert_function_query_was_not_run::<infer_types, _, _>(
|
||||
assert_will_not_run_function_query::<infer_types, _, _>(
|
||||
&db,
|
||||
|ty| &ty.function,
|
||||
&a_root_scope,
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use red_knot_module_resolver::{resolve_module, ModuleName};
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::vfs::VfsFile;
|
||||
use ruff_index::IndexVec;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{ExprContext, TypeParams};
|
||||
@@ -58,7 +58,7 @@ pub(super) struct TypeInferenceBuilder<'db> {
|
||||
// Cached lookups
|
||||
index: &'db SemanticIndex<'db>,
|
||||
file_scope_id: FileScopeId,
|
||||
file_id: File,
|
||||
file_id: VfsFile,
|
||||
symbol_table: Arc<SymbolTable<'db>>,
|
||||
|
||||
/// The type inference results
|
||||
@@ -601,8 +601,8 @@ mod tests {
|
||||
use red_knot_module_resolver::{
|
||||
set_module_resolution_settings, RawModuleResolutionSettings, TargetVersion,
|
||||
};
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_db::file_system::FileSystemPathBuf;
|
||||
use ruff_db::vfs::system_path_to_file;
|
||||
use ruff_python_ast::name::Name;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
@@ -616,7 +616,7 @@ mod tests {
|
||||
RawModuleResolutionSettings {
|
||||
target_version: TargetVersion::Py38,
|
||||
extra_paths: Vec::new(),
|
||||
workspace_root: SystemPathBuf::from("/src"),
|
||||
workspace_root: FileSystemPathBuf::from("/src"),
|
||||
site_packages: None,
|
||||
custom_typeshed: None,
|
||||
},
|
||||
@@ -634,9 +634,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn follow_import_to_class() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
db.memory_file_system().write_files([
|
||||
("src/a.py", "from b import C as D; E = D"),
|
||||
("src/b.py", "class C: pass"),
|
||||
])?;
|
||||
@@ -648,9 +648,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn resolve_base_class_by_name() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file(
|
||||
db.memory_file_system().write_file(
|
||||
"src/mod.py",
|
||||
r#"
|
||||
class Base:
|
||||
@@ -680,9 +680,9 @@ class Sub(Base):
|
||||
|
||||
#[test]
|
||||
fn resolve_method() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file(
|
||||
db.memory_file_system().write_file(
|
||||
"src/mod.py",
|
||||
"
|
||||
class C:
|
||||
@@ -710,9 +710,9 @@ class C:
|
||||
|
||||
#[test]
|
||||
fn resolve_module_member() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_files([
|
||||
db.memory_file_system().write_files([
|
||||
("src/a.py", "import b; D = b.C"),
|
||||
("src/b.py", "class C: pass"),
|
||||
])?;
|
||||
@@ -724,9 +724,9 @@ class C:
|
||||
|
||||
#[test]
|
||||
fn resolve_literal() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("src/a.py", "x = 1")?;
|
||||
db.memory_file_system().write_file("src/a.py", "x = 1")?;
|
||||
|
||||
assert_public_ty(&db, "src/a.py", "x", "Literal[1]");
|
||||
|
||||
@@ -735,9 +735,9 @@ class C:
|
||||
|
||||
#[test]
|
||||
fn resolve_union() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file(
|
||||
db.memory_file_system().write_file(
|
||||
"src/a.py",
|
||||
"
|
||||
if flag:
|
||||
@@ -754,9 +754,9 @@ else:
|
||||
|
||||
#[test]
|
||||
fn literal_int_arithmetic() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file(
|
||||
db.memory_file_system().write_file(
|
||||
"src/a.py",
|
||||
"
|
||||
a = 2 + 1
|
||||
@@ -778,9 +778,10 @@ e = 5 % 3
|
||||
|
||||
#[test]
|
||||
fn walrus() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("src/a.py", "x = (y := 1) + 1")?;
|
||||
db.memory_file_system()
|
||||
.write_file("src/a.py", "x = (y := 1) + 1")?;
|
||||
|
||||
assert_public_ty(&db, "src/a.py", "x", "Literal[2]");
|
||||
assert_public_ty(&db, "src/a.py", "y", "Literal[1]");
|
||||
@@ -790,9 +791,10 @@ e = 5 % 3
|
||||
|
||||
#[test]
|
||||
fn ifexpr() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("src/a.py", "x = 1 if flag else 2")?;
|
||||
db.memory_file_system()
|
||||
.write_file("src/a.py", "x = 1 if flag else 2")?;
|
||||
|
||||
assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2]");
|
||||
|
||||
@@ -801,9 +803,9 @@ e = 5 % 3
|
||||
|
||||
#[test]
|
||||
fn ifexpr_walrus() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file(
|
||||
db.memory_file_system().write_file(
|
||||
"src/a.py",
|
||||
"
|
||||
y = z = 0
|
||||
@@ -822,9 +824,10 @@ b = z
|
||||
|
||||
#[test]
|
||||
fn ifexpr_nested() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("src/a.py", "x = 1 if flag else 2 if flag2 else 3")?;
|
||||
db.memory_file_system()
|
||||
.write_file("src/a.py", "x = 1 if flag else 2 if flag2 else 3")?;
|
||||
|
||||
assert_public_ty(&db, "src/a.py", "x", "Literal[1, 2, 3]");
|
||||
|
||||
@@ -833,9 +836,10 @@ b = z
|
||||
|
||||
#[test]
|
||||
fn none() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
let db = setup_db();
|
||||
|
||||
db.write_file("src/a.py", "x = 1 if flag else None")?;
|
||||
db.memory_file_system()
|
||||
.write_file("src/a.py", "x = 1 if flag else None")?;
|
||||
|
||||
assert_public_ty(&db, "src/a.py", "x", "Literal[1] | None");
|
||||
Ok(())
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -182,7 +182,8 @@ pub struct CheckCommand {
|
||||
ignore_noqa: bool,
|
||||
|
||||
/// Output serialization format for violations.
|
||||
/// The default serialization format is "full".
|
||||
/// The default serialization format is "concise".
|
||||
/// In preview mode, the default serialization format is "full".
|
||||
#[arg(long, value_enum, env = "RUFF_OUTPUT_FORMAT")]
|
||||
pub output_format: Option<OutputFormat>,
|
||||
|
||||
|
||||
@@ -180,24 +180,12 @@ impl Cache {
|
||||
.write_all(&serialized)
|
||||
.context("Failed to write serialized cache to temporary file.")?;
|
||||
|
||||
if let Err(err) = temp_file.persist(&self.path) {
|
||||
// On Windows, writing to the cache file can fail if the file is still open (e.g., if
|
||||
// the user is running Ruff from multiple processes over the same directory).
|
||||
if cfg!(windows) && err.error.kind() == io::ErrorKind::PermissionDenied {
|
||||
warn_user!(
|
||||
"Failed to write cache file '{}': {}",
|
||||
self.path.display(),
|
||||
err.error
|
||||
);
|
||||
} else {
|
||||
return Err(err).with_context(|| {
|
||||
format!(
|
||||
"Failed to rename temporary cache file to {}",
|
||||
self.path.display()
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
temp_file.persist(&self.path).with_context(|| {
|
||||
format!(
|
||||
"Failed to rename temporary cache file to {}",
|
||||
self.path.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -176,7 +176,6 @@ pub(crate) fn format(
|
||||
duration
|
||||
);
|
||||
|
||||
// Store the caches.
|
||||
caches.persist()?;
|
||||
|
||||
// Report on any errors.
|
||||
|
||||
@@ -1259,6 +1259,9 @@ fn redirect_direct() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
||||
|
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1291,6 +1294,9 @@ fn redirect_prefix() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
||||
|
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1308,6 +1314,9 @@ fn deprecated_direct() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
|
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1325,7 +1334,13 @@ fn deprecated_multiple_direct() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF921 Hey this is another deprecated test rule.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1344,7 +1359,13 @@ fn deprecated_indirect() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF921 Hey this is another deprecated test rule.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1523,7 +1544,13 @@ fn check_hints_hidden_unsafe_fixes() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -1541,6 +1568,11 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF902
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -1559,7 +1591,13 @@ fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the --fix option.
|
||||
|
||||
@@ -1579,6 +1617,11 @@ fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF902
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
|
||||
----- stderr -----
|
||||
@@ -1596,7 +1639,13 @@ fn check_shows_unsafe_fixes_with_opt_in() {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the --fix option.
|
||||
|
||||
@@ -1618,6 +1667,11 @@ fn fix_applies_safe_fixes_by_default() {
|
||||
|
||||
----- stderr -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
1 | # fix from stable-test-rule-safe-fix
|
||||
| RUF902
|
||||
|
|
||||
|
||||
Found 2 errors (1 fixed, 1 remaining).
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
"###);
|
||||
@@ -1655,6 +1709,11 @@ fn fix_does_not_apply_display_only_fixes() {
|
||||
def add_to_list(item, some_list=[]): ...
|
||||
----- stderr -----
|
||||
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
||||
|
|
||||
1 | def add_to_list(item, some_list=[]): ...
|
||||
| RUF903
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
"###);
|
||||
}
|
||||
@@ -1673,6 +1732,11 @@ fn fix_does_not_apply_display_only_fixes_with_unsafe_fixes_enabled() {
|
||||
def add_to_list(item, some_list=[]): ...
|
||||
----- stderr -----
|
||||
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
||||
|
|
||||
1 | def add_to_list(item, some_list=[]): ...
|
||||
| RUF903
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
"###);
|
||||
}
|
||||
@@ -1690,6 +1754,9 @@ fn fix_only_unsafe_fixes_available() {
|
||||
|
||||
----- stderr -----
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 1 error.
|
||||
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
"###);
|
||||
@@ -1826,7 +1893,13 @@ extend-unsafe-fixes = ["RUF901"]
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
No fixes available (2 hidden fixes can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -1858,7 +1931,13 @@ extend-safe-fixes = ["RUF902"]
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
[*] 2 fixable with the `--fix` option.
|
||||
|
||||
@@ -1892,7 +1971,13 @@ extend-safe-fixes = ["RUF902"]
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF901 [*] Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
|
|
||||
|
||||
-:1:1: RUF902 Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
|
|
||||
|
||||
Found 2 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
@@ -1928,12 +2013,61 @@ extend-safe-fixes = ["RUF9"]
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
-:1:1: RUF900 Hey this is a stable test rule.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF900
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
-:1:1: RUF901 Hey this is a stable test rule with a safe fix.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF901
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
-:1:1: RUF902 [*] Hey this is a stable test rule with an unsafe fix.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF902
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
-:1:1: RUF903 Hey this is a stable test rule with a display only fix.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF903
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
-:1:1: RUF920 Hey this is a deprecated test rule.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF920
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
-:1:1: RUF921 Hey this is another deprecated test rule.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF921
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
-:1:1: RUF950 Hey this is a test rule that was redirected from another.
|
||||
|
|
||||
1 | x = {'a': 1, 'a': 1}
|
||||
| RUF950
|
||||
2 | print(('foo'))
|
||||
3 | print(str('foo'))
|
||||
|
|
||||
|
||||
Found 7 errors.
|
||||
[*] 1 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ use red_knot_module_resolver::{
|
||||
use ruff_benchmark::criterion::{
|
||||
criterion_group, criterion_main, BatchSize, Criterion, Throughput,
|
||||
};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::file_system::{FileSystemPath, MemoryFileSystem};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{MemoryFileSystem, SystemPath, TestSystem};
|
||||
use ruff_db::vfs::{system_path_to_file, VfsFile};
|
||||
use ruff_db::Upcast;
|
||||
|
||||
static FOO_CODE: &str = r#"
|
||||
@@ -47,17 +47,16 @@ def override(): ...
|
||||
struct Case {
|
||||
program: Program,
|
||||
fs: MemoryFileSystem,
|
||||
foo: File,
|
||||
bar: File,
|
||||
typing: File,
|
||||
foo: VfsFile,
|
||||
bar: VfsFile,
|
||||
typing: VfsFile,
|
||||
}
|
||||
|
||||
fn setup_case() -> Case {
|
||||
let system = TestSystem::default();
|
||||
let fs = system.memory_file_system().clone();
|
||||
let foo_path = SystemPath::new("/src/foo.py");
|
||||
let bar_path = SystemPath::new("/src/bar.py");
|
||||
let typing_path = SystemPath::new("/src/typing.pyi");
|
||||
let fs = MemoryFileSystem::new();
|
||||
let foo_path = FileSystemPath::new("/src/foo.py");
|
||||
let bar_path = FileSystemPath::new("/src/bar.py");
|
||||
let typing_path = FileSystemPath::new("/src/typing.pyi");
|
||||
fs.write_files([
|
||||
(foo_path, FOO_CODE),
|
||||
(bar_path, BAR_CODE),
|
||||
@@ -65,10 +64,10 @@ fn setup_case() -> Case {
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let workspace_root = SystemPath::new("/src");
|
||||
let workspace_root = FileSystemPath::new("/src");
|
||||
let workspace = Workspace::new(workspace_root.to_path_buf());
|
||||
|
||||
let mut program = Program::new(workspace, system);
|
||||
let mut program = Program::new(workspace, fs.clone());
|
||||
let foo = system_path_to_file(&program, foo_path).unwrap();
|
||||
|
||||
set_module_resolution_settings(
|
||||
@@ -135,7 +134,7 @@ fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
|
||||
case.fs
|
||||
.write_file(
|
||||
SystemPath::new("/src/foo.py"),
|
||||
FileSystemPath::new("/src/foo.py"),
|
||||
format!("{BAR_CODE}\n# A comment\n"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -27,4 +27,4 @@ zip = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
|
||||
536
crates/ruff_db/src/file_system.rs
Normal file
536
crates/ruff_db/src/file_system.rs
Normal file
@@ -0,0 +1,536 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, StripPrefixError};
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
pub use memory::MemoryFileSystem;
|
||||
pub use os::OsFileSystem;
|
||||
|
||||
mod memory;
|
||||
mod os;
|
||||
|
||||
pub type Result<T> = std::io::Result<T>;
|
||||
|
||||
/// An abstraction over `std::fs` with features tailored to Ruff's needs.
|
||||
///
|
||||
/// Provides a file system agnostic API to interact with files and directories.
|
||||
/// Abstracting the file system operations enables:
|
||||
///
|
||||
/// * Accessing unsaved or even untitled files in the LSP use case
|
||||
/// * Testing with an in-memory file system
|
||||
/// * Running Ruff in a WASM environment without needing to stub out the full `std::fs` API.
|
||||
pub trait FileSystem: std::fmt::Debug {
|
||||
/// Reads the metadata of the file or directory at `path`.
|
||||
fn metadata(&self, path: &FileSystemPath) -> Result<Metadata>;
|
||||
|
||||
/// Reads the content of the file at `path`.
|
||||
fn read(&self, path: &FileSystemPath) -> Result<String>;
|
||||
|
||||
/// Returns `true` if `path` exists.
|
||||
fn exists(&self, path: &FileSystemPath) -> bool;
|
||||
|
||||
/// Returns `true` if `path` exists and is a directory.
|
||||
fn is_directory(&self, path: &FileSystemPath) -> bool {
|
||||
self.metadata(path)
|
||||
.map_or(false, |metadata| metadata.file_type.is_directory())
|
||||
}
|
||||
|
||||
/// Returns `true` if `path` exists and is a file.
|
||||
fn is_file(&self, path: &FileSystemPath) -> bool {
|
||||
self.metadata(path)
|
||||
.map_or(false, |metadata| metadata.file_type.is_file())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO support untitled files for the LSP use case. Wrap a `str` and `String`
|
||||
// The main question is how `as_std_path` would work for untitled files, that can only exist in the LSP case
|
||||
// but there's no compile time guarantee that a [`OsFileSystem`] never gets an untitled file path.
|
||||
|
||||
/// Path to a file or directory stored in [`FileSystem`].
|
||||
///
|
||||
/// The path is guaranteed to be valid UTF-8.
|
||||
#[repr(transparent)]
|
||||
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct FileSystemPath(Utf8Path);
|
||||
|
||||
impl FileSystemPath {
|
||||
pub fn new(path: &(impl AsRef<Utf8Path> + ?Sized)) -> &Self {
|
||||
let path = path.as_ref();
|
||||
// SAFETY: FsPath is marked as #[repr(transparent)] so the conversion from a
|
||||
// *const Utf8Path to a *const FsPath is valid.
|
||||
unsafe { &*(path as *const Utf8Path as *const FileSystemPath) }
|
||||
}
|
||||
|
||||
/// Extracts the file extension, if possible.
|
||||
///
|
||||
/// The extension is:
|
||||
///
|
||||
/// * [`None`], if there is no file name;
|
||||
/// * [`None`], if there is no embedded `.`;
|
||||
/// * [`None`], if the file name begins with `.` and has no other `.`s within;
|
||||
/// * Otherwise, the portion of the file name after the final `.`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// assert_eq!("rs", FileSystemPath::new("foo.rs").extension().unwrap());
|
||||
/// assert_eq!("gz", FileSystemPath::new("foo.tar.gz").extension().unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// See [`Path::extension`] for more details.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn extension(&self) -> Option<&str> {
|
||||
self.0.extension()
|
||||
}
|
||||
|
||||
/// Determines whether `base` is a prefix of `self`.
|
||||
///
|
||||
/// Only considers whole path components to match.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// let path = FileSystemPath::new("/etc/passwd");
|
||||
///
|
||||
/// assert!(path.starts_with("/etc"));
|
||||
/// assert!(path.starts_with("/etc/"));
|
||||
/// assert!(path.starts_with("/etc/passwd"));
|
||||
/// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay
|
||||
/// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay
|
||||
///
|
||||
/// assert!(!path.starts_with("/e"));
|
||||
/// assert!(!path.starts_with("/etc/passwd.txt"));
|
||||
///
|
||||
/// assert!(!FileSystemPath::new("/etc/foo.rs").starts_with("/etc/foo"));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn starts_with(&self, base: impl AsRef<FileSystemPath>) -> bool {
|
||||
self.0.starts_with(base.as_ref())
|
||||
}
|
||||
|
||||
/// Determines whether `child` is a suffix of `self`.
|
||||
///
|
||||
/// Only considers whole path components to match.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// let path = FileSystemPath::new("/etc/resolv.conf");
|
||||
///
|
||||
/// assert!(path.ends_with("resolv.conf"));
|
||||
/// assert!(path.ends_with("etc/resolv.conf"));
|
||||
/// assert!(path.ends_with("/etc/resolv.conf"));
|
||||
///
|
||||
/// assert!(!path.ends_with("/resolv.conf"));
|
||||
/// assert!(!path.ends_with("conf")); // use .extension() instead
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn ends_with(&self, child: impl AsRef<FileSystemPath>) -> bool {
|
||||
self.0.ends_with(child.as_ref())
|
||||
}
|
||||
|
||||
/// Returns the `FileSystemPath` without its final component, if there is one.
|
||||
///
|
||||
/// Returns [`None`] if the path terminates in a root or prefix.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// let path = FileSystemPath::new("/foo/bar");
|
||||
/// let parent = path.parent().unwrap();
|
||||
/// assert_eq!(parent, FileSystemPath::new("/foo"));
|
||||
///
|
||||
/// let grand_parent = parent.parent().unwrap();
|
||||
/// assert_eq!(grand_parent, FileSystemPath::new("/"));
|
||||
/// assert_eq!(grand_parent.parent(), None);
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn parent(&self) -> Option<&FileSystemPath> {
|
||||
self.0.parent().map(FileSystemPath::new)
|
||||
}
|
||||
|
||||
/// Produces an iterator over the [`camino::Utf8Component`]s of the path.
|
||||
///
|
||||
/// When parsing the path, there is a small amount of normalization:
|
||||
///
|
||||
/// * Repeated separators are ignored, so `a/b` and `a//b` both have
|
||||
/// `a` and `b` as components.
|
||||
///
|
||||
/// * Occurrences of `.` are normalized away, except if they are at the
|
||||
/// beginning of the path. For example, `a/./b`, `a/b/`, `a/b/.` and
|
||||
/// `a/b` all have `a` and `b` as components, but `./a/b` starts with
|
||||
/// an additional [`CurDir`] component.
|
||||
///
|
||||
/// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
|
||||
///
|
||||
/// Note that no other normalization takes place; in particular, `a/c`
|
||||
/// and `a/b/../c` are distinct, to account for the possibility that `b`
|
||||
/// is a symbolic link (so its parent isn't `a`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use camino::{Utf8Component};
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// let mut components = FileSystemPath::new("/tmp/foo.txt").components();
|
||||
///
|
||||
/// assert_eq!(components.next(), Some(Utf8Component::RootDir));
|
||||
/// assert_eq!(components.next(), Some(Utf8Component::Normal("tmp")));
|
||||
/// assert_eq!(components.next(), Some(Utf8Component::Normal("foo.txt")));
|
||||
/// assert_eq!(components.next(), None)
|
||||
/// ```
|
||||
///
|
||||
/// [`CurDir`]: camino::Utf8Component::CurDir
|
||||
#[inline]
|
||||
pub fn components(&self) -> camino::Utf8Components {
|
||||
self.0.components()
|
||||
}
|
||||
|
||||
/// Returns the final component of the `FileSystemPath`, if there is one.
|
||||
///
|
||||
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
|
||||
/// is the directory name.
|
||||
///
|
||||
/// Returns [`None`] if the path terminates in `..`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use camino::Utf8Path;
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// assert_eq!(Some("bin"), FileSystemPath::new("/usr/bin/").file_name());
|
||||
/// assert_eq!(Some("foo.txt"), FileSystemPath::new("tmp/foo.txt").file_name());
|
||||
/// assert_eq!(Some("foo.txt"), FileSystemPath::new("foo.txt/.").file_name());
|
||||
/// assert_eq!(Some("foo.txt"), FileSystemPath::new("foo.txt/.//").file_name());
|
||||
/// assert_eq!(None, FileSystemPath::new("foo.txt/..").file_name());
|
||||
/// assert_eq!(None, FileSystemPath::new("/").file_name());
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.0.file_name()
|
||||
}
|
||||
|
||||
/// Extracts the stem (non-extension) portion of [`self.file_name`].
|
||||
///
|
||||
/// [`self.file_name`]: FileSystemPath::file_name
|
||||
///
|
||||
/// The stem is:
|
||||
///
|
||||
/// * [`None`], if there is no file name;
|
||||
/// * The entire file name if there is no embedded `.`;
|
||||
/// * The entire file name if the file name begins with `.` and has no other `.`s within;
|
||||
/// * Otherwise, the portion of the file name before the final `.`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::FileSystemPath;
|
||||
///
|
||||
/// assert_eq!("foo", FileSystemPath::new("foo.rs").file_stem().unwrap());
|
||||
/// assert_eq!("foo.tar", FileSystemPath::new("foo.tar.gz").file_stem().unwrap());
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn file_stem(&self) -> Option<&str> {
|
||||
self.0.file_stem()
|
||||
}
|
||||
|
||||
/// Returns a path that, when joined onto `base`, yields `self`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `base` is not a prefix of `self` (i.e., [`starts_with`]
|
||||
/// returns `false`), returns [`Err`].
|
||||
///
|
||||
/// [`starts_with`]: FileSystemPath::starts_with
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf};
|
||||
///
|
||||
/// let path = FileSystemPath::new("/test/haha/foo.txt");
|
||||
///
|
||||
/// assert_eq!(path.strip_prefix("/"), Ok(FileSystemPath::new("test/haha/foo.txt")));
|
||||
/// assert_eq!(path.strip_prefix("/test"), Ok(FileSystemPath::new("haha/foo.txt")));
|
||||
/// assert_eq!(path.strip_prefix("/test/"), Ok(FileSystemPath::new("haha/foo.txt")));
|
||||
/// assert_eq!(path.strip_prefix("/test/haha/foo.txt"), Ok(FileSystemPath::new("")));
|
||||
/// assert_eq!(path.strip_prefix("/test/haha/foo.txt/"), Ok(FileSystemPath::new("")));
|
||||
///
|
||||
/// assert!(path.strip_prefix("test").is_err());
|
||||
/// assert!(path.strip_prefix("/haha").is_err());
|
||||
///
|
||||
/// let prefix = FileSystemPathBuf::from("/test/");
|
||||
/// assert_eq!(path.strip_prefix(prefix), Ok(FileSystemPath::new("haha/foo.txt")));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn strip_prefix(
|
||||
&self,
|
||||
base: impl AsRef<FileSystemPath>,
|
||||
) -> std::result::Result<&FileSystemPath, StripPrefixError> {
|
||||
self.0.strip_prefix(base.as_ref()).map(FileSystemPath::new)
|
||||
}
|
||||
|
||||
/// Creates an owned [`FileSystemPathBuf`] with `path` adjoined to `self`.
|
||||
///
|
||||
/// See [`std::path::PathBuf::push`] for more details on what it means to adjoin a path.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf};
|
||||
///
|
||||
/// assert_eq!(FileSystemPath::new("/etc").join("passwd"), FileSystemPathBuf::from("/etc/passwd"));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn join(&self, path: impl AsRef<FileSystemPath>) -> FileSystemPathBuf {
|
||||
FileSystemPathBuf::from_utf8_path_buf(self.0.join(&path.as_ref().0))
|
||||
}
|
||||
|
||||
/// Creates an owned [`FileSystemPathBuf`] like `self` but with the given extension.
|
||||
///
|
||||
/// See [`std::path::PathBuf::set_extension`] for more details.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::{FileSystemPath, FileSystemPathBuf};
|
||||
///
|
||||
/// let path = FileSystemPath::new("foo.rs");
|
||||
/// assert_eq!(path.with_extension("txt"), FileSystemPathBuf::from("foo.txt"));
|
||||
///
|
||||
/// let path = FileSystemPath::new("foo.tar.gz");
|
||||
/// assert_eq!(path.with_extension(""), FileSystemPathBuf::from("foo.tar"));
|
||||
/// assert_eq!(path.with_extension("xz"), FileSystemPathBuf::from("foo.tar.xz"));
|
||||
/// assert_eq!(path.with_extension("").with_extension("txt"), FileSystemPathBuf::from("foo.txt"));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn with_extension(&self, extension: &str) -> FileSystemPathBuf {
|
||||
FileSystemPathBuf::from_utf8_path_buf(self.0.with_extension(extension))
|
||||
}
|
||||
|
||||
/// Converts the path to an owned [`FileSystemPathBuf`].
|
||||
pub fn to_path_buf(&self) -> FileSystemPathBuf {
|
||||
FileSystemPathBuf(self.0.to_path_buf())
|
||||
}
|
||||
|
||||
/// Returns the path as a string slice.
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
/// Returns the std path for the file.
|
||||
#[inline]
|
||||
pub fn as_std_path(&self) -> &Path {
|
||||
self.0.as_std_path()
|
||||
}
|
||||
|
||||
pub fn from_std_path(path: &Path) -> Option<&FileSystemPath> {
|
||||
Some(FileSystemPath::new(Utf8Path::from_path(path)?))
|
||||
}
|
||||
}
|
||||
|
||||
/// Owned path to a file or directory stored in [`FileSystem`].
|
||||
///
|
||||
/// The path is guaranteed to be valid UTF-8.
|
||||
#[repr(transparent)]
|
||||
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
|
||||
pub struct FileSystemPathBuf(Utf8PathBuf);
|
||||
|
||||
impl FileSystemPathBuf {
|
||||
pub fn new() -> Self {
|
||||
Self(Utf8PathBuf::new())
|
||||
}
|
||||
|
||||
pub fn from_utf8_path_buf(path: Utf8PathBuf) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
|
||||
pub fn from_path_buf(
|
||||
path: std::path::PathBuf,
|
||||
) -> std::result::Result<Self, std::path::PathBuf> {
|
||||
Utf8PathBuf::from_path_buf(path).map(Self)
|
||||
}
|
||||
|
||||
/// Extends `self` with `path`.
|
||||
///
|
||||
/// If `path` is absolute, it replaces the current path.
|
||||
///
|
||||
/// On Windows:
|
||||
///
|
||||
/// * if `path` has a root but no prefix (e.g., `\windows`), it
|
||||
/// replaces everything except for the prefix (if any) of `self`.
|
||||
/// * if `path` has a prefix but no root, it replaces `self`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Pushing a relative path extends the existing path:
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::file_system::FileSystemPathBuf;
|
||||
///
|
||||
/// let mut path = FileSystemPathBuf::from("/tmp");
|
||||
/// path.push("file.bk");
|
||||
/// assert_eq!(path, FileSystemPathBuf::from("/tmp/file.bk"));
|
||||
/// ```
|
||||
///
|
||||
/// Pushing an absolute path replaces the existing path:
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// use ruff_db::file_system::FileSystemPathBuf;
|
||||
///
|
||||
/// let mut path = FileSystemPathBuf::from("/tmp");
|
||||
/// path.push("/etc");
|
||||
/// assert_eq!(path, FileSystemPathBuf::from("/etc"));
|
||||
/// ```
|
||||
pub fn push(&mut self, path: impl AsRef<FileSystemPath>) {
|
||||
self.0.push(&path.as_ref().0);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_path(&self) -> &FileSystemPath {
|
||||
FileSystemPath::new(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for FileSystemPathBuf {
|
||||
fn from(value: &str) -> Self {
|
||||
FileSystemPathBuf::from_utf8_path_buf(Utf8PathBuf::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FileSystemPathBuf {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<FileSystemPath> for FileSystemPathBuf {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &FileSystemPath {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<FileSystemPath> for FileSystemPath {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &FileSystemPath {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<FileSystemPath> for str {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &FileSystemPath {
|
||||
FileSystemPath::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<FileSystemPath> for String {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &FileSystemPath {
|
||||
FileSystemPath::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for FileSystemPath {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_std_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for FileSystemPathBuf {
|
||||
type Target = FileSystemPath;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FileSystemPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FileSystemPath {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for FileSystemPathBuf {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for FileSystemPathBuf {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
revision: FileRevision,
|
||||
permissions: Option<u32>,
|
||||
file_type: FileType,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub fn revision(&self) -> FileRevision {
|
||||
self.revision
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> Option<u32> {
|
||||
self.permissions
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> FileType {
|
||||
self.file_type
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
Symlink,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub const fn is_file(self) -> bool {
|
||||
matches!(self, FileType::File)
|
||||
}
|
||||
|
||||
pub const fn is_directory(self) -> bool {
|
||||
matches!(self, FileType::Directory)
|
||||
}
|
||||
|
||||
pub const fn is_symlink(self) -> bool {
|
||||
matches!(self, FileType::Symlink)
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ use std::sync::{Arc, RwLock, RwLockWriteGuard};
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use filetime::FileTime;
|
||||
|
||||
use crate::system::{DirectoryEntry, FileType, Metadata, Result, SystemPath, SystemPathBuf};
|
||||
use crate::file_system::{FileSystem, FileSystemPath, FileType, Metadata, Result};
|
||||
|
||||
/// File system that stores all content in memory.
|
||||
///
|
||||
@@ -16,7 +16,9 @@ use crate::system::{DirectoryEntry, FileType, Metadata, Result, SystemPath, Syst
|
||||
/// * hardlinks
|
||||
/// * permissions: All files and directories have the permission 0755.
|
||||
///
|
||||
/// Use a tempdir with the real file system to test these advanced file system features and behavior.
|
||||
/// Use a tempdir with the real file system to test these advanced file system features and complex file system behavior.
|
||||
///
|
||||
/// Only intended for testing purposes.
|
||||
#[derive(Clone)]
|
||||
pub struct MemoryFileSystem {
|
||||
inner: Arc<MemoryFileSystemInner>,
|
||||
@@ -27,12 +29,11 @@ impl MemoryFileSystem {
|
||||
const PERMISSION: u32 = 0o755;
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self::with_current_directory("/")
|
||||
Self::with_cwd("/")
|
||||
}
|
||||
|
||||
/// Creates a new, empty in memory file system with the given current working directory.
|
||||
pub fn with_current_directory(cwd: impl AsRef<SystemPath>) -> Self {
|
||||
let cwd = cwd.as_ref().to_path_buf();
|
||||
pub fn with_cwd(cwd: impl AsRef<FileSystemPath>) -> Self {
|
||||
let cwd = Utf8PathBuf::from(cwd.as_ref().as_str());
|
||||
|
||||
assert!(
|
||||
cwd.starts_with("/"),
|
||||
@@ -46,15 +47,11 @@ impl MemoryFileSystem {
|
||||
}),
|
||||
};
|
||||
|
||||
fs.create_directory_all(&cwd).unwrap();
|
||||
fs.create_directory_all(FileSystemPath::new(&cwd)).unwrap();
|
||||
|
||||
fs
|
||||
}
|
||||
|
||||
pub fn current_directory(&self) -> &SystemPath {
|
||||
&self.inner.cwd
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
@@ -62,69 +59,6 @@ impl MemoryFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata(&self, path: impl AsRef<SystemPath>) -> Result<Metadata> {
|
||||
fn metadata(fs: &MemoryFileSystem, path: &SystemPath) -> Result<Metadata> {
|
||||
let by_path = fs.inner.by_path.read().unwrap();
|
||||
let normalized = fs.normalize_path(path);
|
||||
|
||||
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
||||
|
||||
let metadata = match entry {
|
||||
Entry::File(file) => Metadata {
|
||||
revision: file.last_modified.into(),
|
||||
permissions: Some(MemoryFileSystem::PERMISSION),
|
||||
file_type: FileType::File,
|
||||
},
|
||||
Entry::Directory(directory) => Metadata {
|
||||
revision: directory.last_modified.into(),
|
||||
permissions: Some(MemoryFileSystem::PERMISSION),
|
||||
file_type: FileType::Directory,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
metadata(self, path.as_ref())
|
||||
}
|
||||
|
||||
pub fn is_file(&self, path: impl AsRef<SystemPath>) -> bool {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
|
||||
matches!(by_path.get(&normalized), Some(Entry::File(_)))
|
||||
}
|
||||
|
||||
pub fn is_directory(&self, path: impl AsRef<SystemPath>) -> bool {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
|
||||
matches!(by_path.get(&normalized), Some(Entry::Directory(_)))
|
||||
}
|
||||
|
||||
pub fn read_to_string(&self, path: impl AsRef<SystemPath>) -> Result<String> {
|
||||
fn read_to_string(fs: &MemoryFileSystem, path: &SystemPath) -> Result<String> {
|
||||
let by_path = fs.inner.by_path.read().unwrap();
|
||||
let normalized = fs.normalize_path(path);
|
||||
|
||||
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
||||
|
||||
match entry {
|
||||
Entry::File(file) => Ok(file.content.clone()),
|
||||
Entry::Directory(_) => Err(is_a_directory()),
|
||||
}
|
||||
}
|
||||
|
||||
read_to_string(self, path.as_ref())
|
||||
}
|
||||
|
||||
pub fn exists(&self, path: &SystemPath) -> bool {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = self.normalize_path(path);
|
||||
|
||||
by_path.contains_key(&normalized)
|
||||
}
|
||||
|
||||
/// Writes the files to the file system.
|
||||
///
|
||||
/// The operation overrides existing files with the same normalized path.
|
||||
@@ -132,7 +66,7 @@ impl MemoryFileSystem {
|
||||
/// Enclosing directories are automatically created if they don't exist.
|
||||
pub fn write_files<P, C>(&self, files: impl IntoIterator<Item = (P, C)>) -> Result<()>
|
||||
where
|
||||
P: AsRef<SystemPath>,
|
||||
P: AsRef<FileSystemPath>,
|
||||
C: ToString,
|
||||
{
|
||||
for (path, content) in files {
|
||||
@@ -147,42 +81,42 @@ impl MemoryFileSystem {
|
||||
/// The operation overrides the content for an existing file with the same normalized `path`.
|
||||
///
|
||||
/// Enclosing directories are automatically created if they don't exist.
|
||||
pub fn write_file(&self, path: impl AsRef<SystemPath>, content: impl ToString) -> Result<()> {
|
||||
pub fn write_file(
|
||||
&self,
|
||||
path: impl AsRef<FileSystemPath>,
|
||||
content: impl ToString,
|
||||
) -> Result<()> {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
let normalized = normalize_path(path.as_ref(), &self.inner.cwd);
|
||||
|
||||
get_or_create_file(&mut by_path, &normalized)?.content = content.to_string();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_file(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
||||
fn remove_file(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> {
|
||||
let mut by_path = fs.inner.by_path.write().unwrap();
|
||||
let normalized = fs.normalize_path(path);
|
||||
pub fn remove_file(&self, path: impl AsRef<FileSystemPath>) -> Result<()> {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
let normalized = normalize_path(path.as_ref(), &self.inner.cwd);
|
||||
|
||||
match by_path.entry(normalized) {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => match entry.get() {
|
||||
Entry::File(_) => {
|
||||
entry.remove();
|
||||
Ok(())
|
||||
}
|
||||
Entry::Directory(_) => Err(is_a_directory()),
|
||||
},
|
||||
std::collections::btree_map::Entry::Vacant(_) => Err(not_found()),
|
||||
}
|
||||
match by_path.entry(normalized) {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => match entry.get() {
|
||||
Entry::File(_) => {
|
||||
entry.remove();
|
||||
Ok(())
|
||||
}
|
||||
Entry::Directory(_) => Err(is_a_directory()),
|
||||
},
|
||||
std::collections::btree_map::Entry::Vacant(_) => Err(not_found()),
|
||||
}
|
||||
|
||||
remove_file(self, path.as_ref())
|
||||
}
|
||||
|
||||
/// Sets the last modified timestamp of the file stored at `path` to now.
|
||||
///
|
||||
/// Creates a new file if the file at `path` doesn't exist.
|
||||
pub fn touch(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
||||
pub fn touch(&self, path: impl AsRef<FileSystemPath>) -> Result<()> {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
let normalized = normalize_path(path.as_ref(), &self.inner.cwd);
|
||||
|
||||
get_or_create_file(&mut by_path, &normalized)?.last_modified = FileTime::now();
|
||||
|
||||
@@ -190,9 +124,9 @@ impl MemoryFileSystem {
|
||||
}
|
||||
|
||||
/// Creates a directory at `path`. All enclosing directories are created if they don't exist.
|
||||
pub fn create_directory_all(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
||||
pub fn create_directory_all(&self, path: impl AsRef<FileSystemPath>) -> Result<()> {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
let normalized = normalize_path(path.as_ref(), &self.inner.cwd);
|
||||
|
||||
create_dir_all(&mut by_path, &normalized)
|
||||
}
|
||||
@@ -203,67 +137,73 @@ impl MemoryFileSystem {
|
||||
/// * If the directory is not empty
|
||||
/// * The `path` is not a directory
|
||||
/// * The `path` does not exist
|
||||
pub fn remove_directory(&self, path: impl AsRef<SystemPath>) -> Result<()> {
|
||||
fn remove_directory(fs: &MemoryFileSystem, path: &SystemPath) -> Result<()> {
|
||||
let mut by_path = fs.inner.by_path.write().unwrap();
|
||||
let normalized = fs.normalize_path(path);
|
||||
pub fn remove_directory(&self, path: impl AsRef<FileSystemPath>) -> Result<()> {
|
||||
let mut by_path = self.inner.by_path.write().unwrap();
|
||||
let normalized = normalize_path(path.as_ref(), &self.inner.cwd);
|
||||
|
||||
// Test if the directory is empty
|
||||
// Skip the directory path itself
|
||||
for (maybe_child, _) in by_path.range(normalized.clone()..).skip(1) {
|
||||
if maybe_child.starts_with(&normalized) {
|
||||
return Err(directory_not_empty());
|
||||
} else if !maybe_child.as_str().starts_with(normalized.as_str()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match by_path.entry(normalized.clone()) {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => match entry.get() {
|
||||
Entry::Directory(_) => {
|
||||
entry.remove();
|
||||
Ok(())
|
||||
}
|
||||
Entry::File(_) => Err(not_a_directory()),
|
||||
},
|
||||
std::collections::btree_map::Entry::Vacant(_) => Err(not_found()),
|
||||
// Test if the directory is empty
|
||||
// Skip the directory path itself
|
||||
for (maybe_child, _) in by_path.range(normalized.clone()..).skip(1) {
|
||||
if maybe_child.starts_with(&normalized) {
|
||||
return Err(directory_not_empty());
|
||||
} else if !maybe_child.as_str().starts_with(normalized.as_str()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
remove_directory(self, path.as_ref())
|
||||
}
|
||||
|
||||
fn normalize_path(&self, path: impl AsRef<SystemPath>) -> Utf8PathBuf {
|
||||
let normalized = SystemPath::absolute(path, &self.inner.cwd);
|
||||
normalized.into_utf8_path_buf()
|
||||
}
|
||||
|
||||
pub fn read_directory(
|
||||
&self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
) -> Result<impl Iterator<Item = Result<DirectoryEntry>> + '_> {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = self.normalize_path(path.as_ref());
|
||||
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
||||
if entry.is_file() {
|
||||
return Err(not_a_directory());
|
||||
};
|
||||
Ok(by_path
|
||||
.range(normalized.clone()..)
|
||||
.skip(1)
|
||||
.take_while(|(path, _)| path.starts_with(&normalized))
|
||||
.filter_map(|(path, entry)| {
|
||||
if path.parent()? == normalized {
|
||||
Some(Ok(DirectoryEntry {
|
||||
path: SystemPathBuf::from_utf8_path_buf(path.to_owned()),
|
||||
file_type: Ok(entry.file_type()),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
match by_path.entry(normalized.clone()) {
|
||||
std::collections::btree_map::Entry::Occupied(entry) => match entry.get() {
|
||||
Entry::Directory(_) => {
|
||||
entry.remove();
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter())
|
||||
Entry::File(_) => Err(not_a_directory()),
|
||||
},
|
||||
std::collections::btree_map::Entry::Vacant(_) => Err(not_found()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for MemoryFileSystem {
|
||||
fn metadata(&self, path: &FileSystemPath) -> Result<Metadata> {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = normalize_path(path, &self.inner.cwd);
|
||||
|
||||
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
||||
|
||||
let metadata = match entry {
|
||||
Entry::File(file) => Metadata {
|
||||
revision: file.last_modified.into(),
|
||||
permissions: Some(Self::PERMISSION),
|
||||
file_type: FileType::File,
|
||||
},
|
||||
Entry::Directory(directory) => Metadata {
|
||||
revision: directory.last_modified.into(),
|
||||
permissions: Some(Self::PERMISSION),
|
||||
file_type: FileType::Directory,
|
||||
},
|
||||
};
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
fn read(&self, path: &FileSystemPath) -> Result<String> {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = normalize_path(path, &self.inner.cwd);
|
||||
|
||||
let entry = by_path.get(&normalized).ok_or_else(not_found)?;
|
||||
|
||||
match entry {
|
||||
Entry::File(file) => Ok(file.content.clone()),
|
||||
Entry::Directory(_) => Err(is_a_directory()),
|
||||
}
|
||||
}
|
||||
|
||||
fn exists(&self, path: &FileSystemPath) -> bool {
|
||||
let by_path = self.inner.by_path.read().unwrap();
|
||||
let normalized = normalize_path(path, &self.inner.cwd);
|
||||
|
||||
by_path.contains_key(&normalized)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -283,7 +223,7 @@ impl std::fmt::Debug for MemoryFileSystem {
|
||||
|
||||
struct MemoryFileSystemInner {
|
||||
by_path: RwLock<BTreeMap<Utf8PathBuf, Entry>>,
|
||||
cwd: SystemPathBuf,
|
||||
cwd: Utf8PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -296,13 +236,6 @@ impl Entry {
|
||||
const fn is_file(&self) -> bool {
|
||||
matches!(self, Entry::File(_))
|
||||
}
|
||||
|
||||
const fn file_type(&self) -> FileType {
|
||||
match self {
|
||||
Self::File(_) => FileType::File,
|
||||
Self::Directory(_) => FileType::Directory,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -336,6 +269,42 @@ fn directory_not_empty() -> std::io::Error {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, "directory not empty")
|
||||
}
|
||||
|
||||
/// Normalizes the path by removing `.` and `..` components and transform the path into an absolute path.
|
||||
///
|
||||
/// Adapted from https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61
|
||||
fn normalize_path(path: &FileSystemPath, cwd: &Utf8Path) -> Utf8PathBuf {
|
||||
let path = camino::Utf8Path::new(path.as_str());
|
||||
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret =
|
||||
if let Some(c @ (camino::Utf8Component::Prefix(..) | camino::Utf8Component::RootDir)) =
|
||||
components.peek().cloned()
|
||||
{
|
||||
components.next();
|
||||
Utf8PathBuf::from(c.as_str())
|
||||
} else {
|
||||
cwd.to_path_buf()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
camino::Utf8Component::Prefix(..) => unreachable!(),
|
||||
camino::Utf8Component::RootDir => {
|
||||
ret.push(component);
|
||||
}
|
||||
camino::Utf8Component::CurDir => {}
|
||||
camino::Utf8Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
camino::Utf8Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
fn create_dir_all(
|
||||
paths: &mut RwLockWriteGuard<BTreeMap<Utf8PathBuf, Entry>>,
|
||||
normalized: &Utf8Path,
|
||||
@@ -384,16 +353,14 @@ mod tests {
|
||||
use std::io::ErrorKind;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::system::{
|
||||
DirectoryEntry, FileType, MemoryFileSystem, Result, SystemPath, SystemPathBuf,
|
||||
};
|
||||
use crate::file_system::{FileSystem, FileSystemPath, MemoryFileSystem, Result};
|
||||
|
||||
/// Creates a file system with the given files.
|
||||
///
|
||||
/// The content of all files will be empty.
|
||||
fn with_files<P>(files: impl IntoIterator<Item = P>) -> super::MemoryFileSystem
|
||||
where
|
||||
P: AsRef<SystemPath>,
|
||||
P: AsRef<FileSystemPath>,
|
||||
{
|
||||
let fs = MemoryFileSystem::new();
|
||||
fs.write_files(files.into_iter().map(|path| (path, "")))
|
||||
@@ -404,7 +371,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn is_file() {
|
||||
let path = SystemPath::new("a.py");
|
||||
let path = FileSystemPath::new("a.py");
|
||||
let fs = with_files([path]);
|
||||
|
||||
assert!(fs.is_file(path));
|
||||
@@ -415,26 +382,26 @@ mod tests {
|
||||
fn exists() {
|
||||
let fs = with_files(["a.py"]);
|
||||
|
||||
assert!(fs.exists(SystemPath::new("a.py")));
|
||||
assert!(!fs.exists(SystemPath::new("b.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("a.py")));
|
||||
assert!(!fs.exists(FileSystemPath::new("b.py")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exists_directories() {
|
||||
let fs = with_files(["a/b/c.py"]);
|
||||
|
||||
assert!(fs.exists(SystemPath::new("a")));
|
||||
assert!(fs.exists(SystemPath::new("a/b")));
|
||||
assert!(fs.exists(SystemPath::new("a/b/c.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("a")));
|
||||
assert!(fs.exists(FileSystemPath::new("a/b")));
|
||||
assert!(fs.exists(FileSystemPath::new("a/b/c.py")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_normalization() {
|
||||
let fs = with_files(["a.py"]);
|
||||
|
||||
assert!(fs.exists(SystemPath::new("a.py")));
|
||||
assert!(fs.exists(SystemPath::new("/a.py")));
|
||||
assert!(fs.exists(SystemPath::new("/b/./../a.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("a.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("/a.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("/b/./../a.py")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -443,7 +410,7 @@ mod tests {
|
||||
|
||||
// The default permissions match the default on Linux: 0755
|
||||
assert_eq!(
|
||||
fs.metadata(SystemPath::new("a.py"))?.permissions(),
|
||||
fs.metadata(FileSystemPath::new("a.py"))?.permissions(),
|
||||
Some(MemoryFileSystem::PERMISSION)
|
||||
);
|
||||
|
||||
@@ -453,7 +420,7 @@ mod tests {
|
||||
#[test]
|
||||
fn touch() -> Result<()> {
|
||||
let fs = MemoryFileSystem::new();
|
||||
let path = SystemPath::new("a.py");
|
||||
let path = FileSystemPath::new("a.py");
|
||||
|
||||
// Creates a file if it doesn't exist
|
||||
fs.touch(path)?;
|
||||
@@ -478,14 +445,16 @@ mod tests {
|
||||
fn create_dir_all() {
|
||||
let fs = MemoryFileSystem::new();
|
||||
|
||||
fs.create_directory_all(SystemPath::new("a/b/c")).unwrap();
|
||||
fs.create_directory_all(FileSystemPath::new("a/b/c"))
|
||||
.unwrap();
|
||||
|
||||
assert!(fs.is_directory(SystemPath::new("a")));
|
||||
assert!(fs.is_directory(SystemPath::new("a/b")));
|
||||
assert!(fs.is_directory(SystemPath::new("a/b/c")));
|
||||
assert!(fs.is_directory(FileSystemPath::new("a")));
|
||||
assert!(fs.is_directory(FileSystemPath::new("a/b")));
|
||||
assert!(fs.is_directory(FileSystemPath::new("a/b/c")));
|
||||
|
||||
// Should not fail if the directory already exists
|
||||
fs.create_directory_all(SystemPath::new("a/b/c")).unwrap();
|
||||
fs.create_directory_all(FileSystemPath::new("a/b/c"))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -493,7 +462,7 @@ mod tests {
|
||||
let fs = with_files(["a/b.py"]);
|
||||
|
||||
let error = fs
|
||||
.create_directory_all(SystemPath::new("a/b.py/c"))
|
||||
.create_directory_all(FileSystemPath::new("a/b.py/c"))
|
||||
.unwrap_err();
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
}
|
||||
@@ -503,7 +472,7 @@ mod tests {
|
||||
let fs = with_files(["a/b.py"]);
|
||||
|
||||
let error = fs
|
||||
.write_file(SystemPath::new("a/b.py/c"), "content".to_string())
|
||||
.write_file(FileSystemPath::new("a/b.py/c"), "content".to_string())
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
@@ -516,7 +485,7 @@ mod tests {
|
||||
fs.create_directory_all("a")?;
|
||||
|
||||
let error = fs
|
||||
.write_file(SystemPath::new("a"), "content".to_string())
|
||||
.write_file(FileSystemPath::new("a"), "content".to_string())
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
@@ -527,11 +496,11 @@ mod tests {
|
||||
#[test]
|
||||
fn read() -> Result<()> {
|
||||
let fs = MemoryFileSystem::new();
|
||||
let path = SystemPath::new("a.py");
|
||||
let path = FileSystemPath::new("a.py");
|
||||
|
||||
fs.write_file(path, "Test content".to_string())?;
|
||||
|
||||
assert_eq!(fs.read_to_string(path)?, "Test content");
|
||||
assert_eq!(fs.read(path)?, "Test content");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -542,7 +511,7 @@ mod tests {
|
||||
|
||||
fs.create_directory_all("a")?;
|
||||
|
||||
let error = fs.read_to_string(SystemPath::new("a")).unwrap_err();
|
||||
let error = fs.read(FileSystemPath::new("a")).unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
|
||||
@@ -553,7 +522,7 @@ mod tests {
|
||||
fn read_fails_if_path_doesnt_exist() -> Result<()> {
|
||||
let fs = MemoryFileSystem::new();
|
||||
|
||||
let error = fs.read_to_string(SystemPath::new("a")).unwrap_err();
|
||||
let error = fs.read(FileSystemPath::new("a")).unwrap_err();
|
||||
|
||||
assert_eq!(error.kind(), ErrorKind::NotFound);
|
||||
|
||||
@@ -566,13 +535,13 @@ mod tests {
|
||||
|
||||
fs.remove_file("a/a.py")?;
|
||||
|
||||
assert!(!fs.exists(SystemPath::new("a/a.py")));
|
||||
assert!(!fs.exists(FileSystemPath::new("a/a.py")));
|
||||
|
||||
// It doesn't delete the enclosing directories
|
||||
assert!(fs.exists(SystemPath::new("a")));
|
||||
assert!(fs.exists(FileSystemPath::new("a")));
|
||||
|
||||
// It doesn't delete unrelated files.
|
||||
assert!(fs.exists(SystemPath::new("b.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("b.py")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -604,10 +573,10 @@ mod tests {
|
||||
|
||||
fs.remove_directory("a")?;
|
||||
|
||||
assert!(!fs.exists(SystemPath::new("a")));
|
||||
assert!(!fs.exists(FileSystemPath::new("a")));
|
||||
|
||||
// It doesn't delete unrelated files.
|
||||
assert!(fs.exists(SystemPath::new("b.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("b.py")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -627,9 +596,9 @@ mod tests {
|
||||
|
||||
fs.remove_directory("foo").unwrap();
|
||||
|
||||
assert!(!fs.exists(SystemPath::new("foo")));
|
||||
assert!(fs.exists(SystemPath::new("foo_bar.py")));
|
||||
assert!(fs.exists(SystemPath::new("foob.py")));
|
||||
assert!(!fs.exists(FileSystemPath::new("foo")));
|
||||
assert!(fs.exists(FileSystemPath::new("foo_bar.py")));
|
||||
assert!(fs.exists(FileSystemPath::new("foob.py")));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -649,39 +618,4 @@ mod tests {
|
||||
let error = fs.remove_directory("a").unwrap_err();
|
||||
assert_eq!(error.kind(), ErrorKind::Other);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory() {
|
||||
let fs = with_files(["b.ts", "a/bar.py", "d.rs", "a/foo/bar.py", "a/baz.pyi"]);
|
||||
let contents: Vec<DirectoryEntry> = fs
|
||||
.read_directory("a")
|
||||
.unwrap()
|
||||
.map(Result::unwrap)
|
||||
.collect();
|
||||
let expected_contents = vec![
|
||||
DirectoryEntry::new(SystemPathBuf::from("/a/bar.py"), Ok(FileType::File)),
|
||||
DirectoryEntry::new(SystemPathBuf::from("/a/baz.pyi"), Ok(FileType::File)),
|
||||
DirectoryEntry::new(SystemPathBuf::from("/a/foo"), Ok(FileType::Directory)),
|
||||
];
|
||||
assert_eq!(contents, expected_contents)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory_nonexistent() {
|
||||
let fs = MemoryFileSystem::new();
|
||||
let Err(error) = fs.read_directory("doesnt_exist") else {
|
||||
panic!("Expected this to fail");
|
||||
};
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::NotFound);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory_on_file() {
|
||||
let fs = with_files(["a.py"]);
|
||||
let Err(error) = fs.read_directory("a.py") else {
|
||||
panic!("Expected this to fail");
|
||||
};
|
||||
assert_eq!(error.kind(), std::io::ErrorKind::Other);
|
||||
assert!(error.to_string().contains("Not a directory"));
|
||||
}
|
||||
}
|
||||
57
crates/ruff_db/src/file_system/os.rs
Normal file
57
crates/ruff_db/src/file_system/os.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
use filetime::FileTime;
|
||||
|
||||
use crate::file_system::{FileSystem, FileSystemPath, FileType, Metadata, Result};
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct OsFileSystem;
|
||||
|
||||
impl OsFileSystem {
|
||||
#[cfg(unix)]
|
||||
fn permissions(metadata: &std::fs::Metadata) -> Option<u32> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
Some(metadata.permissions().mode())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn permissions(_metadata: &std::fs::Metadata) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
impl FileSystem for OsFileSystem {
|
||||
fn metadata(&self, path: &FileSystemPath) -> Result<Metadata> {
|
||||
let metadata = path.as_std_path().metadata()?;
|
||||
let last_modified = FileTime::from_last_modification_time(&metadata);
|
||||
|
||||
Ok(Metadata {
|
||||
revision: last_modified.into(),
|
||||
permissions: Self::permissions(&metadata),
|
||||
file_type: metadata.file_type().into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn read(&self, path: &FileSystemPath) -> Result<String> {
|
||||
std::fs::read_to_string(path)
|
||||
}
|
||||
|
||||
fn exists(&self, path: &FileSystemPath) -> bool {
|
||||
path.as_std_path().exists()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::fs::FileType> for FileType {
|
||||
fn from(file_type: std::fs::FileType) -> Self {
|
||||
if file_type.is_file() {
|
||||
FileType::File
|
||||
} else if file_type.is_dir() {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::Symlink
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use dashmap::mapref::entry::Entry;
|
||||
|
||||
pub use path::FilePath;
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
use crate::files::private::FileStatus;
|
||||
use crate::system::SystemPath;
|
||||
use crate::vendored::VendoredPath;
|
||||
use crate::{Db, FxDashMap};
|
||||
|
||||
mod path;
|
||||
|
||||
/// Interns a file system path and returns a salsa `File` ingredient.
|
||||
///
|
||||
/// Returns `None` if the path doesn't exist, isn't accessible, or if the path points to a directory.
|
||||
#[inline]
|
||||
pub fn system_path_to_file(db: &dyn Db, path: impl AsRef<SystemPath>) -> Option<File> {
|
||||
let file = db.files().system(db, path.as_ref());
|
||||
|
||||
// It's important that `vfs.file_system` creates a `VfsFile` even for files that don't exist or don't
|
||||
// exist anymore so that Salsa can track that the caller of this function depends on the existence of
|
||||
// that file. This function filters out files that don't exist, but Salsa will know that it must
|
||||
// re-run the calling query whenever the `file`'s status changes (because of the `.status` call here).
|
||||
match file.status(db) {
|
||||
FileStatus::Exists => Some(file),
|
||||
FileStatus::Deleted => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Interns a vendored file path. Returns `Some` if the vendored file for `path` exists and `None` otherwise.
|
||||
#[inline]
|
||||
pub fn vendored_path_to_file(db: &dyn Db, path: impl AsRef<VendoredPath>) -> Option<File> {
|
||||
db.files().vendored(db, path.as_ref())
|
||||
}
|
||||
|
||||
/// Lookup table that maps [file paths](`FilePath`) to salsa interned [`File`] instances.
|
||||
#[derive(Default)]
|
||||
pub struct Files {
|
||||
inner: Arc<FilesInner>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FilesInner {
|
||||
/// Lookup table that maps [`FilePath`]s to salsa interned [`File`] instances.
|
||||
///
|
||||
/// The map also stores entries for files that don't exist on the file system. This is necessary
|
||||
/// so that queries that depend on the existence of a file are re-executed when the file is created.
|
||||
files_by_path: FxDashMap<FilePath, File>,
|
||||
}
|
||||
|
||||
impl Files {
|
||||
/// Looks up a file by its `path`.
|
||||
///
|
||||
/// For a non-existing file, creates a new salsa [`File`] ingredient and stores it for future lookups.
|
||||
///
|
||||
/// The operation always succeeds even if the path doesn't exist on disk, isn't accessible or if the path points to a directory.
|
||||
/// In these cases, a file with status [`FileStatus::Deleted`] is returned.
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
fn system(&self, db: &dyn Db, path: &SystemPath) -> File {
|
||||
let absolute = SystemPath::absolute(path, db.system().current_directory());
|
||||
let absolute = FilePath::System(absolute);
|
||||
|
||||
*self
|
||||
.inner
|
||||
.files_by_path
|
||||
.entry(absolute.clone())
|
||||
.or_insert_with(|| {
|
||||
let metadata = db.system().path_metadata(path);
|
||||
|
||||
match metadata {
|
||||
Ok(metadata) if metadata.file_type().is_file() => File::new(
|
||||
db,
|
||||
absolute,
|
||||
metadata.permissions(),
|
||||
metadata.revision(),
|
||||
FileStatus::Exists,
|
||||
Count::default(),
|
||||
),
|
||||
_ => File::new(
|
||||
db,
|
||||
absolute,
|
||||
None,
|
||||
FileRevision::zero(),
|
||||
FileStatus::Deleted,
|
||||
Count::default(),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Tries to look up the file for the given system path, returns `None` if no such file exists yet
|
||||
fn try_system(&self, db: &dyn Db, path: &SystemPath) -> Option<File> {
|
||||
let absolute = SystemPath::absolute(path, db.system().current_directory());
|
||||
self.inner
|
||||
.files_by_path
|
||||
.get(&FilePath::System(absolute))
|
||||
.map(|entry| *entry.value())
|
||||
}
|
||||
|
||||
/// Looks up a vendored file by its path. Returns `Some` if a vendored file for the given path
|
||||
/// exists and `None` otherwise.
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
fn vendored(&self, db: &dyn Db, path: &VendoredPath) -> Option<File> {
|
||||
let file = match self
|
||||
.inner
|
||||
.files_by_path
|
||||
.entry(FilePath::Vendored(path.to_path_buf()))
|
||||
{
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => {
|
||||
let metadata = db.vendored().metadata(path).ok()?;
|
||||
|
||||
let file = File::new(
|
||||
db,
|
||||
FilePath::Vendored(path.to_path_buf()),
|
||||
Some(0o444),
|
||||
metadata.revision(),
|
||||
FileStatus::Exists,
|
||||
Count::default(),
|
||||
);
|
||||
|
||||
entry.insert(file);
|
||||
|
||||
file
|
||||
}
|
||||
};
|
||||
|
||||
Some(file)
|
||||
}
|
||||
|
||||
/// Creates a salsa like snapshot. The instances share
|
||||
/// the same path-to-file mapping.
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Files {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
|
||||
for entry in self.inner.files_by_path.iter() {
|
||||
map.entry(entry.key(), entry.value());
|
||||
}
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A file that's either stored on the host system's file system or in the vendored file system.
|
||||
#[salsa::input]
|
||||
pub struct File {
|
||||
/// The path of the file.
|
||||
#[id]
|
||||
#[return_ref]
|
||||
pub path: FilePath,
|
||||
|
||||
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows
|
||||
/// or when the file has been deleted.
|
||||
pub permissions: Option<u32>,
|
||||
|
||||
/// The file revision. A file has changed if the revisions don't compare equal.
|
||||
pub revision: FileRevision,
|
||||
|
||||
/// The status of the file.
|
||||
///
|
||||
/// Salsa doesn't support deleting inputs. The only way to signal dependent queries that
|
||||
/// the file has been deleted is to change the status to `Deleted`.
|
||||
status: FileStatus,
|
||||
|
||||
/// Counter that counts the number of created file instances and active file instances.
|
||||
/// Only enabled in debug builds.
|
||||
#[allow(unused)]
|
||||
count: Count<File>,
|
||||
}
|
||||
|
||||
impl File {
|
||||
/// Reads the content of the file into a [`String`].
|
||||
///
|
||||
/// Reading the same file multiple times isn't guaranteed to return the same content. It's possible
|
||||
/// that the file has been modified in between the reads. It's even possible that a file that
|
||||
/// is considered to exist has been deleted in the meantime. If this happens, then the method returns
|
||||
/// an empty string, which is the closest to the content that the file contains now. Returning
|
||||
/// an empty string shouldn't be a problem because the query will be re-executed as soon as the
|
||||
/// changes are applied to the database.
|
||||
pub(crate) fn read_to_string(&self, db: &dyn Db) -> String {
|
||||
let path = self.path(db);
|
||||
|
||||
let result = match path {
|
||||
FilePath::System(system) => {
|
||||
// Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
|
||||
let _ = self.revision(db);
|
||||
|
||||
db.system().read_to_string(system)
|
||||
}
|
||||
FilePath::Vendored(vendored) => db.vendored().read_to_string(vendored),
|
||||
};
|
||||
|
||||
result.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Refreshes the file metadata by querying the file system if needed.
|
||||
/// TODO: The API should instead take all observed changes from the file system directly
|
||||
/// and then apply the VfsFile status accordingly. But for now, this is sufficient.
|
||||
pub fn touch_path(db: &mut dyn Db, path: &SystemPath) {
|
||||
Self::touch_impl(db, path, None);
|
||||
}
|
||||
|
||||
pub fn touch(self, db: &mut dyn Db) {
|
||||
let path = self.path(db).clone();
|
||||
|
||||
match path {
|
||||
FilePath::System(system) => {
|
||||
Self::touch_impl(db, &system, Some(self));
|
||||
}
|
||||
FilePath::Vendored(_) => {
|
||||
// Readonly, can never be out of date.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Private method providing the implementation for [`Self::touch_path`] and [`Self::touch`].
|
||||
fn touch_impl(db: &mut dyn Db, path: &SystemPath, file: Option<File>) {
|
||||
let metadata = db.system().path_metadata(path);
|
||||
|
||||
let (status, revision) = match metadata {
|
||||
Ok(metadata) if metadata.file_type().is_file() => {
|
||||
(FileStatus::Exists, metadata.revision())
|
||||
}
|
||||
_ => (FileStatus::Deleted, FileRevision::zero()),
|
||||
};
|
||||
|
||||
let Some(file) = file.or_else(|| db.files().try_system(db, path)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
file.set_status(db).to(status);
|
||||
file.set_revision(db).to(revision);
|
||||
}
|
||||
}
|
||||
|
||||
// The types in here need to be public because they're salsa ingredients but we
|
||||
// don't want them to be publicly accessible. That's why we put them into a private module.
|
||||
mod private {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum FileStatus {
|
||||
/// The file exists.
|
||||
Exists,
|
||||
|
||||
/// The file was deleted, didn't exist to begin with or the path isn't a file.
|
||||
Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::file_revision::FileRevision;
|
||||
use crate::files::{system_path_to_file, vendored_path_to_file};
|
||||
use crate::system::DbWithTestSystem;
|
||||
use crate::tests::TestDb;
|
||||
use crate::vendored::tests::VendoredFileSystemBuilder;
|
||||
|
||||
#[test]
|
||||
fn system_existing_file() -> crate::system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
db.write_file("test.py", "print('Hello world')")?;
|
||||
|
||||
let test = system_path_to_file(&db, "test.py").expect("File to exist.");
|
||||
|
||||
assert_eq!(test.permissions(&db), Some(0o755));
|
||||
assert_ne!(test.revision(&db), FileRevision::zero());
|
||||
assert_eq!(&test.read_to_string(&db), "print('Hello world')");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_non_existing_file() {
|
||||
let db = TestDb::new();
|
||||
|
||||
let test = system_path_to_file(&db, "test.py");
|
||||
|
||||
assert_eq!(test, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn system_normalize_paths() {
|
||||
let db = TestDb::new();
|
||||
|
||||
assert_eq!(
|
||||
system_path_to_file(&db, "test.py"),
|
||||
system_path_to_file(&db, "/test.py")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
system_path_to_file(&db, "/root/.././test.py"),
|
||||
system_path_to_file(&db, "/root/test.py")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stubbed_vendored_file() {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let mut vendored_builder = VendoredFileSystemBuilder::new();
|
||||
vendored_builder
|
||||
.add_file("test.pyi", "def foo() -> str")
|
||||
.unwrap();
|
||||
let vendored = vendored_builder.finish().unwrap();
|
||||
db.with_vendored(vendored);
|
||||
|
||||
let test = vendored_path_to_file(&db, "test.pyi").expect("Vendored file to exist.");
|
||||
|
||||
assert_eq!(test.permissions(&db), Some(0o444));
|
||||
assert_ne!(test.revision(&db), FileRevision::zero());
|
||||
assert_eq!(&test.read_to_string(&db), "def foo() -> str");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stubbed_vendored_file_non_existing() {
|
||||
let db = TestDb::new();
|
||||
|
||||
assert_eq!(vendored_path_to_file(&db, "test.py"), None);
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
use crate::files::{system_path_to_file, vendored_path_to_file, File};
|
||||
use crate::system::{SystemPath, SystemPathBuf};
|
||||
use crate::vendored::{VendoredPath, VendoredPathBuf};
|
||||
use crate::Db;
|
||||
|
||||
/// Path to a file.
|
||||
///
|
||||
/// The path abstracts that files in Ruff can come from different sources:
|
||||
///
|
||||
/// * a file stored on the [host system](crate::system::System).
|
||||
/// * a vendored file stored in the [vendored file system](crate::vendored::VendoredFileSystem).
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum FilePath {
|
||||
/// Path to a file on the [host system](crate::system::System).
|
||||
System(SystemPathBuf),
|
||||
/// Path to a file vendored as part of Ruff. Stored in the [vendored file system](crate::vendored::VendoredFileSystem).
|
||||
Vendored(VendoredPathBuf),
|
||||
}
|
||||
|
||||
impl FilePath {
|
||||
/// Create a new path to a file on the file system.
|
||||
#[must_use]
|
||||
pub fn system(path: impl AsRef<SystemPath>) -> Self {
|
||||
FilePath::System(path.as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
/// Returns `Some` if the path is a file system path that points to a path on disk.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn into_system_path_buf(self) -> Option<SystemPathBuf> {
|
||||
match self {
|
||||
FilePath::System(path) => Some(path),
|
||||
FilePath::Vendored(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_system_path(&self) -> Option<&SystemPath> {
|
||||
match self {
|
||||
FilePath::System(path) => Some(path.as_path()),
|
||||
FilePath::Vendored(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a file system path that points to a path on disk.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn is_system_path(&self) -> bool {
|
||||
matches!(self, FilePath::System(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a vendored path.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn is_vendored_path(&self) -> bool {
|
||||
matches!(self, FilePath::Vendored(_))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_vendored_path(&self) -> Option<&VendoredPath> {
|
||||
match self {
|
||||
FilePath::Vendored(path) => Some(path.as_path()),
|
||||
FilePath::System(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields the underlying [`str`] slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
FilePath::System(path) => path.as_str(),
|
||||
FilePath::Vendored(path) => path.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Interns a virtual file system path and returns a salsa [`File`] ingredient.
|
||||
///
|
||||
/// Returns `Some` if a file for `path` exists and is accessible by the user. Returns `None` otherwise.
|
||||
///
|
||||
/// See [`system_path_to_file`] and [`vendored_path_to_file`] if you always have either a file system or vendored path.
|
||||
#[inline]
|
||||
pub fn to_file(&self, db: &dyn Db) -> Option<File> {
|
||||
match self {
|
||||
FilePath::System(path) => system_path_to_file(db, path),
|
||||
FilePath::Vendored(path) => vendored_path_to_file(db, path),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn extension(&self) -> Option<&str> {
|
||||
match self {
|
||||
FilePath::System(path) => path.extension(),
|
||||
FilePath::Vendored(path) => path.extension(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for FilePath {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SystemPathBuf> for FilePath {
|
||||
fn from(value: SystemPathBuf) -> Self {
|
||||
Self::System(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&SystemPath> for FilePath {
|
||||
fn from(value: &SystemPath) -> Self {
|
||||
FilePath::System(value.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VendoredPathBuf> for FilePath {
|
||||
fn from(value: VendoredPathBuf) -> Self {
|
||||
Self::Vendored(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&VendoredPath> for FilePath {
|
||||
fn from(value: &VendoredPath) -> Self {
|
||||
Self::Vendored(value.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPath> for FilePath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &SystemPath) -> bool {
|
||||
self.as_system_path()
|
||||
.is_some_and(|self_path| self_path == other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FilePath> for SystemPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &FilePath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<SystemPathBuf> for FilePath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &SystemPathBuf) -> bool {
|
||||
self == other.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FilePath> for SystemPathBuf {
|
||||
fn eq(&self, other: &FilePath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VendoredPath> for FilePath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VendoredPath) -> bool {
|
||||
self.as_vendored_path()
|
||||
.is_some_and(|self_path| self_path == other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FilePath> for VendoredPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &FilePath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VendoredPathBuf> for FilePath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VendoredPathBuf) -> bool {
|
||||
other.as_path() == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FilePath> for VendoredPathBuf {
|
||||
#[inline]
|
||||
fn eq(&self, other: &FilePath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
@@ -3,30 +3,28 @@ use std::hash::BuildHasherDefault;
|
||||
use rustc_hash::FxHasher;
|
||||
use salsa::DbWithJar;
|
||||
|
||||
use crate::files::{File, Files};
|
||||
use crate::file_system::FileSystem;
|
||||
use crate::parsed::parsed_module;
|
||||
use crate::source::{line_index, source_text};
|
||||
use crate::system::System;
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::vfs::{Vfs, VfsFile};
|
||||
|
||||
pub mod file_revision;
|
||||
pub mod files;
|
||||
pub mod file_system;
|
||||
pub mod parsed;
|
||||
pub mod source;
|
||||
pub mod system;
|
||||
pub mod testing;
|
||||
pub mod vendored;
|
||||
pub mod vfs;
|
||||
|
||||
pub(crate) type FxDashMap<K, V> = dashmap::DashMap<K, V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
#[salsa::jar(db=Db)]
|
||||
pub struct Jar(File, source_text, line_index, parsed_module);
|
||||
pub struct Jar(VfsFile, source_text, line_index, parsed_module);
|
||||
|
||||
/// Most basic database that gives access to files, the host system, source code, and parsed AST.
|
||||
/// Database that gives access to the virtual filesystem, source code, and parsed AST.
|
||||
pub trait Db: DbWithJar<Jar> {
|
||||
fn vendored(&self) -> &VendoredFileSystem;
|
||||
fn system(&self) -> &dyn System;
|
||||
fn files(&self) -> &Files;
|
||||
fn file_system(&self) -> &dyn FileSystem;
|
||||
|
||||
fn vfs(&self) -> &Vfs;
|
||||
}
|
||||
|
||||
/// Trait for upcasting a reference to a base trait object.
|
||||
@@ -40,36 +38,39 @@ mod tests {
|
||||
|
||||
use salsa::DebugWithDb;
|
||||
|
||||
use crate::files::Files;
|
||||
use crate::system::TestSystem;
|
||||
use crate::system::{DbWithTestSystem, System};
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::file_system::{FileSystem, MemoryFileSystem};
|
||||
use crate::vfs::{VendoredPathBuf, Vfs};
|
||||
use crate::{Db, Jar};
|
||||
|
||||
/// Database that can be used for testing.
|
||||
///
|
||||
/// Uses an in memory filesystem and it stubs out the vendored files by default.
|
||||
#[derive(Default)]
|
||||
#[salsa::db(Jar)]
|
||||
pub(crate) struct TestDb {
|
||||
storage: salsa::Storage<Self>,
|
||||
files: Files,
|
||||
system: TestSystem,
|
||||
vendored: VendoredFileSystem,
|
||||
vfs: Vfs,
|
||||
file_system: MemoryFileSystem,
|
||||
events: std::sync::Arc<std::sync::Mutex<Vec<salsa::Event>>>,
|
||||
}
|
||||
|
||||
impl TestDb {
|
||||
pub(crate) fn new() -> Self {
|
||||
let mut vfs = Vfs::default();
|
||||
vfs.stub_vendored::<VendoredPathBuf, String>([]);
|
||||
|
||||
Self {
|
||||
storage: salsa::Storage::default(),
|
||||
system: TestSystem::default(),
|
||||
vendored: VendoredFileSystem::default(),
|
||||
file_system: MemoryFileSystem::default(),
|
||||
events: std::sync::Arc::default(),
|
||||
files: Files::default(),
|
||||
vfs,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn file_system(&self) -> &MemoryFileSystem {
|
||||
&self.file_system
|
||||
}
|
||||
|
||||
/// Empties the internal store of salsa events that have been emitted,
|
||||
/// and returns them as a `Vec` (equivalent to [`std::mem::take`]).
|
||||
///
|
||||
@@ -92,32 +93,22 @@ mod tests {
|
||||
self.take_salsa_events();
|
||||
}
|
||||
|
||||
pub(crate) fn with_vendored(&mut self, vendored_file_system: VendoredFileSystem) {
|
||||
self.vendored = vendored_file_system;
|
||||
pub(crate) fn file_system_mut(&mut self) -> &mut MemoryFileSystem {
|
||||
&mut self.file_system
|
||||
}
|
||||
|
||||
pub(crate) fn vfs_mut(&mut self) -> &mut Vfs {
|
||||
&mut self.vfs
|
||||
}
|
||||
}
|
||||
|
||||
impl Db for TestDb {
|
||||
fn vendored(&self) -> &VendoredFileSystem {
|
||||
&self.vendored
|
||||
fn file_system(&self) -> &dyn FileSystem {
|
||||
&self.file_system
|
||||
}
|
||||
|
||||
fn system(&self) -> &dyn System {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn files(&self) -> &Files {
|
||||
&self.files
|
||||
}
|
||||
}
|
||||
|
||||
impl DbWithTestSystem for TestDb {
|
||||
fn test_system(&self) -> &TestSystem {
|
||||
&self.system
|
||||
}
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem {
|
||||
&mut self.system
|
||||
fn vfs(&self) -> &Vfs {
|
||||
&self.vfs
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,10 +124,9 @@ mod tests {
|
||||
fn snapshot(&self) -> salsa::Snapshot<Self> {
|
||||
salsa::Snapshot::new(Self {
|
||||
storage: self.storage.snapshot(),
|
||||
system: self.system.snapshot(),
|
||||
files: self.files.snapshot(),
|
||||
file_system: self.file_system.snapshot(),
|
||||
vfs: self.vfs.snapshot(),
|
||||
events: self.events.clone(),
|
||||
vendored: self.vendored.snapshot(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ use std::sync::Arc;
|
||||
use ruff_python_ast::{ModModule, PySourceType};
|
||||
use ruff_python_parser::{parse_unchecked_source, Parsed};
|
||||
|
||||
use crate::files::{File, FilePath};
|
||||
use crate::source::source_text;
|
||||
use crate::vfs::{VfsFile, VfsPath};
|
||||
use crate::Db;
|
||||
|
||||
/// Returns the parsed AST of `file`, including its token stream.
|
||||
///
|
||||
/// The query uses Ruff's error-resilient parser. That means that the parser always succeeds to produce an
|
||||
/// The query uses Ruff's error-resilient parser. That means that the parser always succeeds to produce a
|
||||
/// AST even if the file contains syntax errors. The parse errors
|
||||
/// are then accessible through [`Parsed::errors`].
|
||||
///
|
||||
@@ -21,17 +21,17 @@ use crate::Db;
|
||||
/// The other reason is that Ruff's AST doesn't implement `Eq` which Sala requires
|
||||
/// for determining if a query result is unchanged.
|
||||
#[salsa::tracked(return_ref, no_eq)]
|
||||
pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
|
||||
pub fn parsed_module(db: &dyn Db, file: VfsFile) -> ParsedModule {
|
||||
let _span = tracing::trace_span!("parse_module", file = ?file).entered();
|
||||
|
||||
let source = source_text(db, file);
|
||||
let path = file.path(db);
|
||||
|
||||
let ty = match path {
|
||||
FilePath::System(path) => path
|
||||
VfsPath::FileSystem(path) => path
|
||||
.extension()
|
||||
.map_or(PySourceType::Python, PySourceType::from_extension),
|
||||
FilePath::Vendored(_) => PySourceType::Stub,
|
||||
VfsPath::Vendored(_) => PySourceType::Stub,
|
||||
};
|
||||
|
||||
ParsedModule::new(parse_unchecked_source(&source, ty))
|
||||
@@ -72,18 +72,19 @@ impl std::fmt::Debug for ParsedModule {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::files::{system_path_to_file, vendored_path_to_file};
|
||||
use crate::file_system::FileSystemPath;
|
||||
use crate::parsed::parsed_module;
|
||||
use crate::system::{DbWithTestSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
use crate::vendored::{tests::VendoredFileSystemBuilder, VendoredPath};
|
||||
use crate::vendored::VendoredPath;
|
||||
use crate::vfs::{system_path_to_file, vendored_path_to_file};
|
||||
|
||||
#[test]
|
||||
fn python_file() -> crate::system::Result<()> {
|
||||
fn python_file() -> crate::file_system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = "test.py";
|
||||
|
||||
db.write_file(path, "x = 10".to_string())?;
|
||||
db.file_system_mut()
|
||||
.write_file(path, "x = 10".to_string())?;
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
@@ -95,11 +96,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn python_ipynb_file() -> crate::system::Result<()> {
|
||||
fn python_ipynb_file() -> crate::file_system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = SystemPath::new("test.ipynb");
|
||||
let path = FileSystemPath::new("test.ipynb");
|
||||
|
||||
db.write_file(path, "%timeit a = b".to_string())?;
|
||||
db.file_system_mut()
|
||||
.write_file(path, "%timeit a = b".to_string())?;
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
@@ -113,12 +115,9 @@ mod tests {
|
||||
#[test]
|
||||
fn vendored_file() {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
let mut vendored_builder = VendoredFileSystemBuilder::new();
|
||||
vendored_builder
|
||||
.add_file(
|
||||
"path.pyi",
|
||||
r#"
|
||||
db.vfs_mut().stub_vendored([(
|
||||
"path.pyi",
|
||||
r#"
|
||||
import sys
|
||||
|
||||
if sys.platform == "win32":
|
||||
@@ -127,10 +126,7 @@ if sys.platform == "win32":
|
||||
else:
|
||||
from posixpath import *
|
||||
from posixpath import __all__ as __all__"#,
|
||||
)
|
||||
.unwrap();
|
||||
let vendored = vendored_builder.finish().unwrap();
|
||||
db.with_vendored(vendored);
|
||||
)]);
|
||||
|
||||
let file = vendored_path_to_file(&db, VendoredPath::new("path.pyi")).unwrap();
|
||||
|
||||
|
||||
@@ -4,15 +4,15 @@ use salsa::DebugWithDb;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::files::File;
|
||||
use crate::vfs::VfsFile;
|
||||
use crate::Db;
|
||||
|
||||
/// Reads the content of file.
|
||||
#[salsa::tracked]
|
||||
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
pub fn source_text(db: &dyn Db, file: VfsFile) -> SourceText {
|
||||
let _span = tracing::trace_span!("source_text", ?file).entered();
|
||||
|
||||
let content = file.read_to_string(db);
|
||||
let content = file.read(db);
|
||||
|
||||
SourceText {
|
||||
inner: Arc::from(content),
|
||||
@@ -22,7 +22,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
|
||||
/// Computes the [`LineIndex`] for `file`.
|
||||
#[salsa::tracked]
|
||||
pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
|
||||
pub fn line_index(db: &dyn Db, file: VfsFile) -> LineIndex {
|
||||
let _span = tracing::trace_span!("line_index", file = ?file.debug(db)).entered();
|
||||
|
||||
let source = source_text(db, file);
|
||||
@@ -30,7 +30,7 @@ pub fn line_index(db: &dyn Db, file: File) -> LineIndex {
|
||||
LineIndex::from_source_text(&source)
|
||||
}
|
||||
|
||||
/// The source text of a [`File`].
|
||||
/// The source text of a [`VfsFile`].
|
||||
///
|
||||
/// Cheap cloneable in `O(1)`.
|
||||
#[derive(Clone, Eq, PartialEq)]
|
||||
@@ -63,25 +63,30 @@ impl std::fmt::Debug for SourceText {
|
||||
mod tests {
|
||||
use salsa::EventKind;
|
||||
|
||||
use crate::files::system_path_to_file;
|
||||
use crate::source::{line_index, source_text};
|
||||
use crate::system::{DbWithTestSystem, SystemPath};
|
||||
use crate::tests::TestDb;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::TextSize;
|
||||
|
||||
#[test]
|
||||
fn re_runs_query_when_file_revision_changes() -> crate::system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = SystemPath::new("test.py");
|
||||
use crate::file_system::FileSystemPath;
|
||||
use crate::source::{line_index, source_text};
|
||||
use crate::tests::TestDb;
|
||||
use crate::vfs::system_path_to_file;
|
||||
|
||||
db.write_file(path, "x = 10".to_string())?;
|
||||
#[test]
|
||||
fn re_runs_query_when_file_revision_changes() -> crate::file_system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = FileSystemPath::new("test.py");
|
||||
|
||||
db.file_system_mut()
|
||||
.write_file(path, "x = 10".to_string())?;
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
assert_eq!(&*source_text(&db, file), "x = 10");
|
||||
|
||||
db.write_file(path, "x = 20".to_string()).unwrap();
|
||||
db.file_system_mut()
|
||||
.write_file(path, "x = 20".to_string())
|
||||
.unwrap();
|
||||
file.touch(&mut db);
|
||||
|
||||
assert_eq!(&*source_text(&db, file), "x = 20");
|
||||
|
||||
@@ -89,11 +94,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_is_cached_if_revision_is_unchanged() -> crate::system::Result<()> {
|
||||
fn text_is_cached_if_revision_is_unchanged() -> crate::file_system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = SystemPath::new("test.py");
|
||||
let path = FileSystemPath::new("test.py");
|
||||
|
||||
db.write_file(path, "x = 10".to_string())?;
|
||||
db.file_system_mut()
|
||||
.write_file(path, "x = 10".to_string())?;
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
|
||||
@@ -115,11 +121,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_index_for_source() -> crate::system::Result<()> {
|
||||
fn line_index_for_source() -> crate::file_system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
let path = SystemPath::new("test.py");
|
||||
let path = FileSystemPath::new("test.py");
|
||||
|
||||
db.write_file(path, "x = 10\ny = 20".to_string())?;
|
||||
db.file_system_mut()
|
||||
.write_file(path, "x = 10\ny = 20".to_string())?;
|
||||
|
||||
let file = system_path_to_file(&db, path).unwrap();
|
||||
let index = line_index(&db, file);
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
pub use memory_fs::MemoryFileSystem;
|
||||
pub use os::OsSystem;
|
||||
pub use test::{DbWithTestSystem, TestSystem};
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
|
||||
pub use self::path::{SystemPath, SystemPathBuf};
|
||||
|
||||
mod memory_fs;
|
||||
mod os;
|
||||
mod path;
|
||||
mod test;
|
||||
|
||||
pub type Result<T> = std::io::Result<T>;
|
||||
|
||||
/// The system on which Ruff runs.
|
||||
///
|
||||
/// Ruff supports running on the CLI, in a language server, and in a browser (WASM). Each of these
|
||||
/// host-systems differ in what system operations they support and how they interact with the file system:
|
||||
/// * Language server:
|
||||
/// * Reading a file's content should take into account that it might have unsaved changes because it's open in the editor.
|
||||
/// * Use structured representations for notebooks, making deserializing a notebook from a string unnecessary.
|
||||
/// * Use their own file watching infrastructure.
|
||||
/// * WASM (Browser):
|
||||
/// * There are ways to emulate a file system in WASM but a native memory-filesystem is more efficient.
|
||||
/// * Doesn't support a current working directory
|
||||
/// * File watching isn't supported.
|
||||
///
|
||||
/// Abstracting the system also enables tests to use a more efficient in-memory file system.
|
||||
pub trait System {
|
||||
/// Reads the metadata of the file or directory at `path`.
|
||||
fn path_metadata(&self, path: &SystemPath) -> Result<Metadata>;
|
||||
|
||||
/// Reads the content of the file at `path` into a [`String`].
|
||||
fn read_to_string(&self, path: &SystemPath) -> Result<String>;
|
||||
|
||||
/// Returns `true` if `path` exists.
|
||||
fn path_exists(&self, path: &SystemPath) -> bool {
|
||||
self.path_metadata(path).is_ok()
|
||||
}
|
||||
|
||||
/// Returns `true` if `path` exists and is a directory.
|
||||
fn is_directory(&self, path: &SystemPath) -> bool {
|
||||
self.path_metadata(path)
|
||||
.map_or(false, |metadata| metadata.file_type.is_directory())
|
||||
}
|
||||
|
||||
/// Returns `true` if `path` exists and is a file.
|
||||
fn is_file(&self, path: &SystemPath) -> bool {
|
||||
self.path_metadata(path)
|
||||
.map_or(false, |metadata| metadata.file_type.is_file())
|
||||
}
|
||||
|
||||
/// Returns the current working directory
|
||||
fn current_directory(&self) -> &SystemPath;
|
||||
|
||||
/// Iterate over the contents of the directory at `path`.
|
||||
///
|
||||
/// The returned iterator must have the following properties:
|
||||
/// - It only iterates over the top level of the directory,
|
||||
/// i.e., it does not recurse into subdirectories.
|
||||
/// - It skips the current and parent directories (`.` and `..`
|
||||
/// respectively).
|
||||
/// - The iterator yields `std::io::Result<DirEntry>` instances.
|
||||
/// For each instance, an `Err` variant may signify that the path
|
||||
/// of the entry was not valid UTF8, in which case it should be an
|
||||
/// [`std::io::Error`] with the ErrorKind set to
|
||||
/// [`std::io::ErrorKind::InvalidData`] and the payload set to a
|
||||
/// [`camino::FromPathBufError`]. It may also indicate that
|
||||
/// "some sort of intermittent IO error occurred during iteration"
|
||||
/// (language taken from the [`std::fs::read_dir`] documentation).
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns an error:
|
||||
/// - if `path` does not exist in the system,
|
||||
/// - if `path` does not point to a directory,
|
||||
/// - if the process does not have sufficient permissions to
|
||||
/// view the contents of the directory at `path`
|
||||
/// - May also return an error in some other situations as well.
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &SystemPath,
|
||||
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>>;
|
||||
|
||||
fn as_any(&self) -> &dyn std::any::Any;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Metadata {
|
||||
revision: FileRevision,
|
||||
permissions: Option<u32>,
|
||||
file_type: FileType,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
pub fn revision(&self) -> FileRevision {
|
||||
self.revision
|
||||
}
|
||||
|
||||
pub fn permissions(&self) -> Option<u32> {
|
||||
self.permissions
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> FileType {
|
||||
self.file_type
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
pub enum FileType {
|
||||
File,
|
||||
Directory,
|
||||
Symlink,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub const fn is_file(self) -> bool {
|
||||
matches!(self, FileType::File)
|
||||
}
|
||||
|
||||
pub const fn is_directory(self) -> bool {
|
||||
matches!(self, FileType::Directory)
|
||||
}
|
||||
|
||||
pub const fn is_symlink(self) -> bool {
|
||||
matches!(self, FileType::Symlink)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DirectoryEntry {
|
||||
path: SystemPathBuf,
|
||||
file_type: Result<FileType>,
|
||||
}
|
||||
|
||||
impl DirectoryEntry {
|
||||
pub fn new(path: SystemPathBuf, file_type: Result<FileType>) -> Self {
|
||||
Self { path, file_type }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &SystemPath {
|
||||
&self.path
|
||||
}
|
||||
|
||||
pub fn file_type(&self) -> &Result<FileType> {
|
||||
&self.file_type
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for DirectoryEntry {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.path == other.path
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
use crate::system::{
|
||||
DirectoryEntry, FileType, Metadata, Result, System, SystemPath, SystemPathBuf,
|
||||
};
|
||||
use filetime::FileTime;
|
||||
use std::any::Any;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct OsSystem {
|
||||
inner: Arc<OsSystemInner>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct OsSystemInner {
|
||||
cwd: SystemPathBuf,
|
||||
}
|
||||
|
||||
impl OsSystem {
|
||||
pub fn new(cwd: impl AsRef<SystemPath>) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(OsSystemInner {
|
||||
cwd: cwd.as_ref().to_path_buf(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn permissions(metadata: &std::fs::Metadata) -> Option<u32> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
Some(metadata.permissions().mode())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn permissions(_metadata: &std::fs::Metadata) -> Option<u32> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl System for OsSystem {
|
||||
fn path_metadata(&self, path: &SystemPath) -> Result<Metadata> {
|
||||
let metadata = path.as_std_path().metadata()?;
|
||||
let last_modified = FileTime::from_last_modification_time(&metadata);
|
||||
|
||||
Ok(Metadata {
|
||||
revision: last_modified.into(),
|
||||
permissions: Self::permissions(&metadata),
|
||||
file_type: metadata.file_type().into(),
|
||||
})
|
||||
}
|
||||
|
||||
fn read_to_string(&self, path: &SystemPath) -> Result<String> {
|
||||
std::fs::read_to_string(path.as_std_path())
|
||||
}
|
||||
|
||||
fn path_exists(&self, path: &SystemPath) -> bool {
|
||||
path.as_std_path().exists()
|
||||
}
|
||||
|
||||
fn current_directory(&self) -> &SystemPath {
|
||||
&self.inner.cwd
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn read_directory(
|
||||
&self,
|
||||
path: &SystemPath,
|
||||
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>>>> {
|
||||
Ok(Box::new(
|
||||
path.as_utf8_path()
|
||||
.read_dir_utf8()?
|
||||
.map(|res| res.map(DirectoryEntry::from)),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::fs::FileType> for FileType {
|
||||
fn from(file_type: std::fs::FileType) -> Self {
|
||||
if file_type.is_file() {
|
||||
FileType::File
|
||||
} else if file_type.is_dir() {
|
||||
FileType::Directory
|
||||
} else {
|
||||
FileType::Symlink
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<camino::Utf8DirEntry> for DirectoryEntry {
|
||||
fn from(value: camino::Utf8DirEntry) -> Self {
|
||||
let file_type = value.file_type().map(FileType::from);
|
||||
Self {
|
||||
path: SystemPathBuf::from_utf8_path_buf(value.into_path()),
|
||||
file_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tempfile::TempDir;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn read_directory() {
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = tempdir.path();
|
||||
std::fs::create_dir_all(tempdir_path.join("a/foo")).unwrap();
|
||||
let files = &["b.ts", "a/bar.py", "d.rs", "a/foo/bar.py", "a/baz.pyi"];
|
||||
for path in files {
|
||||
std::fs::File::create(tempdir_path.join(path)).unwrap();
|
||||
}
|
||||
|
||||
let tempdir_path = SystemPath::from_std_path(tempdir_path).unwrap();
|
||||
let fs = OsSystem::new(tempdir_path);
|
||||
|
||||
let mut sorted_contents: Vec<DirectoryEntry> = fs
|
||||
.read_directory(&tempdir_path.join("a"))
|
||||
.unwrap()
|
||||
.map(Result::unwrap)
|
||||
.collect();
|
||||
sorted_contents.sort_by(|a, b| a.path.cmp(&b.path));
|
||||
|
||||
let expected_contents = vec![
|
||||
DirectoryEntry::new(tempdir_path.join("a/bar.py"), Ok(FileType::File)),
|
||||
DirectoryEntry::new(tempdir_path.join("a/baz.pyi"), Ok(FileType::File)),
|
||||
DirectoryEntry::new(tempdir_path.join("a/foo"), Ok(FileType::Directory)),
|
||||
];
|
||||
assert_eq!(sorted_contents, expected_contents)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory_nonexistent() {
|
||||
let fs = OsSystem::new("");
|
||||
let result = fs.read_directory(SystemPath::new("doesnt_exist"));
|
||||
assert!(result.is_err_and(|error| error.kind() == std::io::ErrorKind::NotFound));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn read_directory_on_file() {
|
||||
let tempdir = TempDir::new().unwrap();
|
||||
let tempdir_path = tempdir.path();
|
||||
std::fs::File::create(tempdir_path.join("a.py")).unwrap();
|
||||
|
||||
let tempdir_path = SystemPath::from_std_path(tempdir_path).unwrap();
|
||||
let fs = OsSystem::new(tempdir_path);
|
||||
let result = fs.read_directory(&tempdir_path.join("a.py"));
|
||||
let Err(error) = result else {
|
||||
panic!("Expected the read_dir() call to fail!");
|
||||
};
|
||||
|
||||
// We can't assert the error kind here because it's apparently an unstable feature!
|
||||
// https://github.com/rust-lang/rust/issues/86442
|
||||
// assert_eq!(error.kind(), std::io::ErrorKind::NotADirectory);
|
||||
|
||||
// We can't even assert the error message on all platforms, as it's different on Windows,
|
||||
// where the message is "The directory name is invalid" rather than "Not a directory".
|
||||
if cfg!(unix) {
|
||||
assert!(error.to_string().contains("Not a directory"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,551 +0,0 @@
|
||||
// TODO support untitled files for the LSP use case. Wrap a `str` and `String`
|
||||
// The main question is how `as_std_path` would work for untitled files, that can only exist in the LSP case
|
||||
// but there's no compile time guarantee that a [`OsSystem`] never gets an untitled file path.
|
||||
|
||||
use camino::{Utf8Path, Utf8PathBuf};
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::path::{Path, StripPrefixError};
|
||||
|
||||
/// A slice of a path on [`System`](super::System) (akin to [`str`]).
|
||||
///
|
||||
/// The path is guaranteed to be valid UTF-8.
|
||||
#[repr(transparent)]
|
||||
#[derive(Eq, PartialEq, Hash, PartialOrd, Ord)]
|
||||
pub struct SystemPath(Utf8Path);
|
||||
|
||||
impl SystemPath {
|
||||
pub fn new(path: &(impl AsRef<Utf8Path> + ?Sized)) -> &Self {
|
||||
let path = path.as_ref();
|
||||
// SAFETY: FsPath is marked as #[repr(transparent)] so the conversion from a
|
||||
// *const Utf8Path to a *const FsPath is valid.
|
||||
unsafe { &*(path as *const Utf8Path as *const SystemPath) }
|
||||
}
|
||||
|
||||
/// Extracts the file extension, if possible.
|
||||
///
|
||||
/// The extension is:
|
||||
///
|
||||
/// * [`None`], if there is no file name;
|
||||
/// * [`None`], if there is no embedded `.`;
|
||||
/// * [`None`], if the file name begins with `.` and has no other `.`s within;
|
||||
/// * Otherwise, the portion of the file name after the final `.`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// assert_eq!("rs", SystemPath::new("foo.rs").extension().unwrap());
|
||||
/// assert_eq!("gz", SystemPath::new("foo.tar.gz").extension().unwrap());
|
||||
/// ```
|
||||
///
|
||||
/// See [`Path::extension`] for more details.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn extension(&self) -> Option<&str> {
|
||||
self.0.extension()
|
||||
}
|
||||
|
||||
/// Determines whether `base` is a prefix of `self`.
|
||||
///
|
||||
/// Only considers whole path components to match.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// let path = SystemPath::new("/etc/passwd");
|
||||
///
|
||||
/// assert!(path.starts_with("/etc"));
|
||||
/// assert!(path.starts_with("/etc/"));
|
||||
/// assert!(path.starts_with("/etc/passwd"));
|
||||
/// assert!(path.starts_with("/etc/passwd/")); // extra slash is okay
|
||||
/// assert!(path.starts_with("/etc/passwd///")); // multiple extra slashes are okay
|
||||
///
|
||||
/// assert!(!path.starts_with("/e"));
|
||||
/// assert!(!path.starts_with("/etc/passwd.txt"));
|
||||
///
|
||||
/// assert!(!SystemPath::new("/etc/foo.rs").starts_with("/etc/foo"));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn starts_with(&self, base: impl AsRef<SystemPath>) -> bool {
|
||||
self.0.starts_with(base.as_ref())
|
||||
}
|
||||
|
||||
/// Determines whether `child` is a suffix of `self`.
|
||||
///
|
||||
/// Only considers whole path components to match.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// let path = SystemPath::new("/etc/resolv.conf");
|
||||
///
|
||||
/// assert!(path.ends_with("resolv.conf"));
|
||||
/// assert!(path.ends_with("etc/resolv.conf"));
|
||||
/// assert!(path.ends_with("/etc/resolv.conf"));
|
||||
///
|
||||
/// assert!(!path.ends_with("/resolv.conf"));
|
||||
/// assert!(!path.ends_with("conf")); // use .extension() instead
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn ends_with(&self, child: impl AsRef<SystemPath>) -> bool {
|
||||
self.0.ends_with(child.as_ref())
|
||||
}
|
||||
|
||||
/// Returns the `FileSystemPath` without its final component, if there is one.
|
||||
///
|
||||
/// Returns [`None`] if the path terminates in a root or prefix.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// let path = SystemPath::new("/foo/bar");
|
||||
/// let parent = path.parent().unwrap();
|
||||
/// assert_eq!(parent, SystemPath::new("/foo"));
|
||||
///
|
||||
/// let grand_parent = parent.parent().unwrap();
|
||||
/// assert_eq!(grand_parent, SystemPath::new("/"));
|
||||
/// assert_eq!(grand_parent.parent(), None);
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn parent(&self) -> Option<&SystemPath> {
|
||||
self.0.parent().map(SystemPath::new)
|
||||
}
|
||||
|
||||
/// Produces an iterator over the [`camino::Utf8Component`]s of the path.
|
||||
///
|
||||
/// When parsing the path, there is a small amount of normalization:
|
||||
///
|
||||
/// * Repeated separators are ignored, so `a/b` and `a//b` both have
|
||||
/// `a` and `b` as components.
|
||||
///
|
||||
/// * Occurrences of `.` are normalized away, except if they are at the
|
||||
/// beginning of the path. For example, `a/./b`, `a/b/`, `a/b/.` and
|
||||
/// `a/b` all have `a` and `b` as components, but `./a/b` starts with
|
||||
/// an additional [`CurDir`] component.
|
||||
///
|
||||
/// * A trailing slash is normalized away, `/a/b` and `/a/b/` are equivalent.
|
||||
///
|
||||
/// Note that no other normalization takes place; in particular, `a/c`
|
||||
/// and `a/b/../c` are distinct, to account for the possibility that `b`
|
||||
/// is a symbolic link (so its parent isn't `a`).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use camino::{Utf8Component};
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// let mut components = SystemPath::new("/tmp/foo.txt").components();
|
||||
///
|
||||
/// assert_eq!(components.next(), Some(Utf8Component::RootDir));
|
||||
/// assert_eq!(components.next(), Some(Utf8Component::Normal("tmp")));
|
||||
/// assert_eq!(components.next(), Some(Utf8Component::Normal("foo.txt")));
|
||||
/// assert_eq!(components.next(), None)
|
||||
/// ```
|
||||
///
|
||||
/// [`CurDir`]: camino::Utf8Component::CurDir
|
||||
#[inline]
|
||||
pub fn components(&self) -> camino::Utf8Components {
|
||||
self.0.components()
|
||||
}
|
||||
|
||||
/// Returns the final component of the `FileSystemPath`, if there is one.
|
||||
///
|
||||
/// If the path is a normal file, this is the file name. If it's the path of a directory, this
|
||||
/// is the directory name.
|
||||
///
|
||||
/// Returns [`None`] if the path terminates in `..`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use camino::Utf8Path;
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// assert_eq!(Some("bin"), SystemPath::new("/usr/bin/").file_name());
|
||||
/// assert_eq!(Some("foo.txt"), SystemPath::new("tmp/foo.txt").file_name());
|
||||
/// assert_eq!(Some("foo.txt"), SystemPath::new("foo.txt/.").file_name());
|
||||
/// assert_eq!(Some("foo.txt"), SystemPath::new("foo.txt/.//").file_name());
|
||||
/// assert_eq!(None, SystemPath::new("foo.txt/..").file_name());
|
||||
/// assert_eq!(None, SystemPath::new("/").file_name());
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn file_name(&self) -> Option<&str> {
|
||||
self.0.file_name()
|
||||
}
|
||||
|
||||
/// Extracts the stem (non-extension) portion of [`self.file_name`].
|
||||
///
|
||||
/// [`self.file_name`]: SystemPath::file_name
|
||||
///
|
||||
/// The stem is:
|
||||
///
|
||||
/// * [`None`], if there is no file name;
|
||||
/// * The entire file name if there is no embedded `.`;
|
||||
/// * The entire file name if the file name begins with `.` and has no other `.`s within;
|
||||
/// * Otherwise, the portion of the file name before the final `.`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::SystemPath;
|
||||
///
|
||||
/// assert_eq!("foo", SystemPath::new("foo.rs").file_stem().unwrap());
|
||||
/// assert_eq!("foo.tar", SystemPath::new("foo.tar.gz").file_stem().unwrap());
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn file_stem(&self) -> Option<&str> {
|
||||
self.0.file_stem()
|
||||
}
|
||||
|
||||
/// Returns a path that, when joined onto `base`, yields `self`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `base` is not a prefix of `self` (i.e., [`starts_with`]
|
||||
/// returns `false`), returns [`Err`].
|
||||
///
|
||||
/// [`starts_with`]: SystemPath::starts_with
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
///
|
||||
/// let path = SystemPath::new("/test/haha/foo.txt");
|
||||
///
|
||||
/// assert_eq!(path.strip_prefix("/"), Ok(SystemPath::new("test/haha/foo.txt")));
|
||||
/// assert_eq!(path.strip_prefix("/test"), Ok(SystemPath::new("haha/foo.txt")));
|
||||
/// assert_eq!(path.strip_prefix("/test/"), Ok(SystemPath::new("haha/foo.txt")));
|
||||
/// assert_eq!(path.strip_prefix("/test/haha/foo.txt"), Ok(SystemPath::new("")));
|
||||
/// assert_eq!(path.strip_prefix("/test/haha/foo.txt/"), Ok(SystemPath::new("")));
|
||||
///
|
||||
/// assert!(path.strip_prefix("test").is_err());
|
||||
/// assert!(path.strip_prefix("/haha").is_err());
|
||||
///
|
||||
/// let prefix = SystemPathBuf::from("/test/");
|
||||
/// assert_eq!(path.strip_prefix(prefix), Ok(SystemPath::new("haha/foo.txt")));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn strip_prefix(
|
||||
&self,
|
||||
base: impl AsRef<SystemPath>,
|
||||
) -> std::result::Result<&SystemPath, StripPrefixError> {
|
||||
self.0.strip_prefix(base.as_ref()).map(SystemPath::new)
|
||||
}
|
||||
|
||||
/// Creates an owned [`SystemPathBuf`] with `path` adjoined to `self`.
|
||||
///
|
||||
/// See [`std::path::PathBuf::push`] for more details on what it means to adjoin a path.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
///
|
||||
/// assert_eq!(SystemPath::new("/etc").join("passwd"), SystemPathBuf::from("/etc/passwd"));
|
||||
/// ```
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn join(&self, path: impl AsRef<SystemPath>) -> SystemPathBuf {
|
||||
SystemPathBuf::from_utf8_path_buf(self.0.join(&path.as_ref().0))
|
||||
}
|
||||
|
||||
/// Creates an owned [`SystemPathBuf`] like `self` but with the given extension.
|
||||
///
|
||||
/// See [`std::path::PathBuf::set_extension`] for more details.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
///
|
||||
/// let path = SystemPath::new("foo.rs");
|
||||
/// assert_eq!(path.with_extension("txt"), SystemPathBuf::from("foo.txt"));
|
||||
///
|
||||
/// let path = SystemPath::new("foo.tar.gz");
|
||||
/// assert_eq!(path.with_extension(""), SystemPathBuf::from("foo.tar"));
|
||||
/// assert_eq!(path.with_extension("xz"), SystemPathBuf::from("foo.tar.xz"));
|
||||
/// assert_eq!(path.with_extension("").with_extension("txt"), SystemPathBuf::from("foo.txt"));
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn with_extension(&self, extension: &str) -> SystemPathBuf {
|
||||
SystemPathBuf::from_utf8_path_buf(self.0.with_extension(extension))
|
||||
}
|
||||
|
||||
/// Converts the path to an owned [`SystemPathBuf`].
|
||||
pub fn to_path_buf(&self) -> SystemPathBuf {
|
||||
SystemPathBuf(self.0.to_path_buf())
|
||||
}
|
||||
|
||||
/// Returns the path as a string slice.
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
|
||||
/// Returns the std path for the file.
|
||||
#[inline]
|
||||
pub fn as_std_path(&self) -> &Path {
|
||||
self.0.as_std_path()
|
||||
}
|
||||
|
||||
/// Returns the [`Utf8Path`] for the file.
|
||||
#[inline]
|
||||
pub fn as_utf8_path(&self) -> &Utf8Path {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn from_std_path(path: &Path) -> Option<&SystemPath> {
|
||||
Some(SystemPath::new(Utf8Path::from_path(path)?))
|
||||
}
|
||||
|
||||
/// Makes a path absolute and normalizes it without accessing the file system.
|
||||
///
|
||||
/// Adapted from [cargo](https://github.com/rust-lang/cargo/blob/fede83ccf973457de319ba6fa0e36ead454d2e20/src/cargo/util/paths.rs#L61)
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Posix paths
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(unix)]
|
||||
/// # fn main() {
|
||||
/// use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
///
|
||||
/// // Relative to absolute
|
||||
/// let absolute = SystemPath::absolute("foo/./bar", "/tmp");
|
||||
/// assert_eq!(absolute, SystemPathBuf::from("/tmp/foo/bar"));
|
||||
///
|
||||
/// // Path's going past the root are normalized to the root
|
||||
/// let absolute = SystemPath::absolute("../../../", "/tmp");
|
||||
/// assert_eq!(absolute, SystemPathBuf::from("/"));
|
||||
///
|
||||
/// // Absolute to absolute
|
||||
/// let absolute = SystemPath::absolute("/foo//test/.././bar.rs", "/tmp");
|
||||
/// assert_eq!(absolute, SystemPathBuf::from("/foo/bar.rs"));
|
||||
/// # }
|
||||
/// # #[cfg(not(unix))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
///
|
||||
/// ## Windows paths
|
||||
///
|
||||
/// ```
|
||||
/// # #[cfg(windows)]
|
||||
/// # fn main() {
|
||||
/// use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
///
|
||||
/// // Relative to absolute
|
||||
/// let absolute = SystemPath::absolute(r"foo\.\bar", r"C:\tmp");
|
||||
/// assert_eq!(absolute, SystemPathBuf::from(r"C:\tmp\foo\bar"));
|
||||
///
|
||||
/// // Path's going past the root are normalized to the root
|
||||
/// let absolute = SystemPath::absolute(r"..\..\..\", r"C:\tmp");
|
||||
/// assert_eq!(absolute, SystemPathBuf::from(r"C:\"));
|
||||
///
|
||||
/// // Absolute to absolute
|
||||
/// let absolute = SystemPath::absolute(r"C:\foo//test\..\./bar.rs", r"C:\tmp");
|
||||
/// assert_eq!(absolute, SystemPathBuf::from(r"C:\foo\bar.rs"));
|
||||
/// # }
|
||||
/// # #[cfg(not(windows))]
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
pub fn absolute(path: impl AsRef<SystemPath>, cwd: impl AsRef<SystemPath>) -> SystemPathBuf {
|
||||
fn absolute(path: &SystemPath, cwd: &SystemPath) -> SystemPathBuf {
|
||||
let path = &path.0;
|
||||
|
||||
let mut components = path.components().peekable();
|
||||
let mut ret = if let Some(
|
||||
c @ (camino::Utf8Component::Prefix(..) | camino::Utf8Component::RootDir),
|
||||
) = components.peek().cloned()
|
||||
{
|
||||
components.next();
|
||||
Utf8PathBuf::from(c.as_str())
|
||||
} else {
|
||||
cwd.0.to_path_buf()
|
||||
};
|
||||
|
||||
for component in components {
|
||||
match component {
|
||||
camino::Utf8Component::Prefix(..) => unreachable!(),
|
||||
camino::Utf8Component::RootDir => {
|
||||
ret.push(component);
|
||||
}
|
||||
camino::Utf8Component::CurDir => {}
|
||||
camino::Utf8Component::ParentDir => {
|
||||
ret.pop();
|
||||
}
|
||||
camino::Utf8Component::Normal(c) => {
|
||||
ret.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemPathBuf::from_utf8_path_buf(ret)
|
||||
}
|
||||
|
||||
absolute(path.as_ref(), cwd.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned, mutable path on [`System`](`super::System`) (akin to [`String`]).
|
||||
///
|
||||
/// The path is guaranteed to be valid UTF-8.
|
||||
#[repr(transparent)]
|
||||
#[derive(Eq, PartialEq, Clone, Hash, PartialOrd, Ord)]
|
||||
pub struct SystemPathBuf(Utf8PathBuf);
|
||||
|
||||
impl SystemPathBuf {
|
||||
pub fn new() -> Self {
|
||||
Self(Utf8PathBuf::new())
|
||||
}
|
||||
|
||||
pub fn from_utf8_path_buf(path: Utf8PathBuf) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
|
||||
pub fn from_path_buf(
|
||||
path: std::path::PathBuf,
|
||||
) -> std::result::Result<Self, std::path::PathBuf> {
|
||||
Utf8PathBuf::from_path_buf(path).map(Self)
|
||||
}
|
||||
|
||||
/// Extends `self` with `path`.
|
||||
///
|
||||
/// If `path` is absolute, it replaces the current path.
|
||||
///
|
||||
/// On Windows:
|
||||
///
|
||||
/// * if `path` has a root but no prefix (e.g., `\windows`), it
|
||||
/// replaces everything except for the prefix (if any) of `self`.
|
||||
/// * if `path` has a prefix but no root, it replaces `self`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Pushing a relative path extends the existing path:
|
||||
///
|
||||
/// ```
|
||||
/// use ruff_db::system::SystemPathBuf;
|
||||
///
|
||||
/// let mut path = SystemPathBuf::from("/tmp");
|
||||
/// path.push("file.bk");
|
||||
/// assert_eq!(path, SystemPathBuf::from("/tmp/file.bk"));
|
||||
/// ```
|
||||
///
|
||||
/// Pushing an absolute path replaces the existing path:
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// use ruff_db::system::SystemPathBuf;
|
||||
///
|
||||
/// let mut path = SystemPathBuf::from("/tmp");
|
||||
/// path.push("/etc");
|
||||
/// assert_eq!(path, SystemPathBuf::from("/etc"));
|
||||
/// ```
|
||||
pub fn push(&mut self, path: impl AsRef<SystemPath>) {
|
||||
self.0.push(&path.as_ref().0);
|
||||
}
|
||||
|
||||
pub fn into_utf8_path_buf(self) -> Utf8PathBuf {
|
||||
self.0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_path(&self) -> &SystemPath {
|
||||
SystemPath::new(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for SystemPathBuf {
|
||||
fn from(value: &str) -> Self {
|
||||
SystemPathBuf::from_utf8_path_buf(Utf8PathBuf::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SystemPathBuf {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<SystemPath> for SystemPathBuf {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &SystemPath {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<SystemPath> for SystemPath {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &SystemPath {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<SystemPath> for str {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &SystemPath {
|
||||
SystemPath::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<SystemPath> for String {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &SystemPath {
|
||||
SystemPath::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Path> for SystemPath {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Path {
|
||||
self.0.as_std_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SystemPathBuf {
|
||||
type Target = SystemPath;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SystemPath {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SystemPath {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for SystemPathBuf {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for SystemPathBuf {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use crate::files::File;
|
||||
use crate::system::{
|
||||
DirectoryEntry, MemoryFileSystem, Metadata, OsSystem, Result, System, SystemPath,
|
||||
};
|
||||
use crate::Db;
|
||||
use std::any::Any;
|
||||
|
||||
/// System implementation intended for testing.
|
||||
///
|
||||
/// It uses a memory-file system by default, but can be switched to the real file system for tests
|
||||
/// verifying more advanced file system features.
|
||||
///
|
||||
/// ## Warning
|
||||
/// Don't use this system for production code. It's intended for testing only.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct TestSystem {
|
||||
inner: TestFileSystem,
|
||||
}
|
||||
|
||||
impl TestSystem {
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.snapshot(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the memory file system.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If this test db isn't using a memory file system.
|
||||
pub fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||
if let TestFileSystem::Stub(fs) = &self.inner {
|
||||
fs
|
||||
} else {
|
||||
panic!("The test db is not using a memory file system");
|
||||
}
|
||||
}
|
||||
|
||||
fn use_os_system(&mut self, os: OsSystem) {
|
||||
self.inner = TestFileSystem::Os(os);
|
||||
}
|
||||
}
|
||||
|
||||
impl System for TestSystem {
|
||||
fn path_metadata(&self, path: &SystemPath) -> crate::system::Result<Metadata> {
|
||||
match &self.inner {
|
||||
TestFileSystem::Stub(fs) => fs.metadata(path),
|
||||
TestFileSystem::Os(fs) => fs.path_metadata(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_to_string(&self, path: &SystemPath) -> crate::system::Result<String> {
|
||||
match &self.inner {
|
||||
TestFileSystem::Stub(fs) => fs.read_to_string(path),
|
||||
TestFileSystem::Os(fs) => fs.read_to_string(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn path_exists(&self, path: &SystemPath) -> bool {
|
||||
match &self.inner {
|
||||
TestFileSystem::Stub(fs) => fs.exists(path),
|
||||
TestFileSystem::Os(fs) => fs.path_exists(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_directory(&self, path: &SystemPath) -> bool {
|
||||
match &self.inner {
|
||||
TestFileSystem::Stub(fs) => fs.is_directory(path),
|
||||
TestFileSystem::Os(fs) => fs.is_directory(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_file(&self, path: &SystemPath) -> bool {
|
||||
match &self.inner {
|
||||
TestFileSystem::Stub(fs) => fs.is_file(path),
|
||||
TestFileSystem::Os(fs) => fs.is_file(path),
|
||||
}
|
||||
}
|
||||
|
||||
fn current_directory(&self) -> &SystemPath {
|
||||
match &self.inner {
|
||||
TestFileSystem::Stub(fs) => fs.current_directory(),
|
||||
TestFileSystem::Os(fs) => fs.current_directory(),
|
||||
}
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
||||
fn read_directory<'a>(
|
||||
&'a self,
|
||||
path: &SystemPath,
|
||||
) -> Result<Box<dyn Iterator<Item = Result<DirectoryEntry>> + 'a>> {
|
||||
match &self.inner {
|
||||
TestFileSystem::Os(fs) => fs.read_directory(path),
|
||||
TestFileSystem::Stub(fs) => Ok(Box::new(fs.read_directory(path)?)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension trait for databases that use [`TestSystem`].
|
||||
///
|
||||
/// Provides various helper function that ease testing.
|
||||
pub trait DbWithTestSystem: Db + Sized {
|
||||
fn test_system(&self) -> &TestSystem;
|
||||
|
||||
fn test_system_mut(&mut self) -> &mut TestSystem;
|
||||
|
||||
/// Writes the content of the given file and notifies the Db about the change.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the system isn't using the memory file system.
|
||||
fn write_file(
|
||||
&mut self,
|
||||
path: impl AsRef<SystemPath>,
|
||||
content: impl ToString,
|
||||
) -> crate::system::Result<()> {
|
||||
let path = path.as_ref();
|
||||
let result = self
|
||||
.test_system()
|
||||
.memory_file_system()
|
||||
.write_file(path, content);
|
||||
|
||||
if result.is_ok() {
|
||||
File::touch_path(self, path);
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Writes the content of the given file and notifies the Db about the change.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the system isn't using the memory file system for testing.
|
||||
fn write_files<P, C, I>(&mut self, files: I) -> crate::system::Result<()>
|
||||
where
|
||||
I: IntoIterator<Item = (P, C)>,
|
||||
P: AsRef<SystemPath>,
|
||||
C: ToString,
|
||||
{
|
||||
for (path, content) in files {
|
||||
self.write_file(path, content)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Uses the real file system instead of the memory file system.
|
||||
///
|
||||
/// This useful for testing advanced file system features like permissions, symlinks, etc.
|
||||
///
|
||||
/// Note that any files written to the memory file system won't be copied over.
|
||||
fn use_os_system(&mut self, os: OsSystem) {
|
||||
self.test_system_mut().use_os_system(os);
|
||||
}
|
||||
|
||||
/// Returns the memory file system.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If this system isn't using a memory file system.
|
||||
fn memory_file_system(&self) -> &MemoryFileSystem {
|
||||
self.test_system().memory_file_system()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TestFileSystem {
|
||||
Stub(MemoryFileSystem),
|
||||
Os(OsSystem),
|
||||
}
|
||||
|
||||
impl TestFileSystem {
|
||||
fn snapshot(&self) -> Self {
|
||||
match self {
|
||||
Self::Stub(fs) => Self::Stub(fs.snapshot()),
|
||||
Self::Os(fs) => Self::Os(fs.snapshot()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestFileSystem {
|
||||
fn default() -> Self {
|
||||
Self::Stub(MemoryFileSystem::default())
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
//! Test helpers for working with Salsa databases
|
||||
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use salsa::id::AsId;
|
||||
use salsa::ingredient::Ingredient;
|
||||
use salsa::storage::HasIngredientsFor;
|
||||
|
||||
/// Assert that the Salsa query described by the generic parameter `C`
|
||||
/// was executed at least once with the input `input`
|
||||
/// in the history span represented by `events`.
|
||||
pub fn assert_function_query_was_run<'db, C, Db, Jar>(
|
||||
db: &'db Db,
|
||||
to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient<C>,
|
||||
input: &C::Input<'db>,
|
||||
events: &[salsa::Event],
|
||||
) where
|
||||
C: salsa::function::Configuration<Jar = Jar>
|
||||
+ salsa::storage::IngredientsFor<Jar = Jar, Ingredients = C>,
|
||||
Jar: HasIngredientsFor<C>,
|
||||
Db: salsa::DbWithJar<Jar>,
|
||||
C::Input<'db>: AsId,
|
||||
{
|
||||
function_query_was_run(db, to_function, input, events, true);
|
||||
}
|
||||
|
||||
/// Assert that there were no executions with the input `input`
|
||||
/// of the Salsa query described by the generic parameter `C`
|
||||
/// in the history span represented by `events`.
|
||||
pub fn assert_function_query_was_not_run<'db, C, Db, Jar>(
|
||||
db: &'db Db,
|
||||
to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient<C>,
|
||||
input: &C::Input<'db>,
|
||||
events: &[salsa::Event],
|
||||
) where
|
||||
C: salsa::function::Configuration<Jar = Jar>
|
||||
+ salsa::storage::IngredientsFor<Jar = Jar, Ingredients = C>,
|
||||
Jar: HasIngredientsFor<C>,
|
||||
Db: salsa::DbWithJar<Jar>,
|
||||
C::Input<'db>: AsId,
|
||||
{
|
||||
function_query_was_run(db, to_function, input, events, false);
|
||||
}
|
||||
|
||||
fn function_query_was_run<'db, C, Db, Jar>(
|
||||
db: &'db Db,
|
||||
to_function: impl FnOnce(&C) -> &salsa::function::FunctionIngredient<C>,
|
||||
input: &C::Input<'db>,
|
||||
events: &[salsa::Event],
|
||||
should_have_run: bool,
|
||||
) where
|
||||
C: salsa::function::Configuration<Jar = Jar>
|
||||
+ salsa::storage::IngredientsFor<Jar = Jar, Ingredients = C>,
|
||||
Jar: HasIngredientsFor<C>,
|
||||
Db: salsa::DbWithJar<Jar>,
|
||||
C::Input<'db>: AsId,
|
||||
{
|
||||
let (jar, _) =
|
||||
<_ as salsa::storage::HasJar<<C as salsa::storage::IngredientsFor>::Jar>>::jar(db);
|
||||
let ingredient = jar.ingredient();
|
||||
|
||||
let function_ingredient = to_function(ingredient);
|
||||
|
||||
let ingredient_index =
|
||||
<salsa::function::FunctionIngredient<C> as Ingredient<Db>>::ingredient_index(
|
||||
function_ingredient,
|
||||
);
|
||||
|
||||
let did_run = events.iter().any(|event| {
|
||||
if let salsa::EventKind::WillExecute { database_key } = event.kind {
|
||||
database_key.ingredient_index() == ingredient_index
|
||||
&& database_key.key_index() == input.as_id()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if should_have_run && !did_run {
|
||||
panic!(
|
||||
"Expected query {:?} to have run but it didn't",
|
||||
DebugIdx {
|
||||
db: PhantomData::<Db>,
|
||||
value_id: input.as_id(),
|
||||
ingredient: function_ingredient,
|
||||
}
|
||||
);
|
||||
} else if !should_have_run && did_run {
|
||||
panic!(
|
||||
"Expected query {:?} not to have run but it did",
|
||||
DebugIdx {
|
||||
db: PhantomData::<Db>,
|
||||
value_id: input.as_id(),
|
||||
ingredient: function_ingredient,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct DebugIdx<'a, I, Db>
|
||||
where
|
||||
I: Ingredient<Db>,
|
||||
{
|
||||
value_id: salsa::Id,
|
||||
ingredient: &'a I,
|
||||
db: PhantomData<Db>,
|
||||
}
|
||||
|
||||
impl<'a, I, Db> fmt::Debug for DebugIdx<'a, I, Db>
|
||||
where
|
||||
I: Ingredient<Db>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.ingredient.fmt_index(Some(self.value_id), f)
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,14 @@ use std::borrow::Cow;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::{self, Debug};
|
||||
use std::io::{self, Read};
|
||||
use std::sync::{Arc, Mutex, MutexGuard};
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
use zip::{read::ZipFile, ZipArchive, ZipWriter};
|
||||
use zip::{read::ZipFile, ZipArchive};
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
pub use path::{VendoredPath, VendoredPathBuf};
|
||||
|
||||
pub use self::path::{VendoredPath, VendoredPathBuf};
|
||||
|
||||
mod path;
|
||||
pub mod path;
|
||||
|
||||
type Result<T> = io::Result<T>;
|
||||
type LockedZipArchive<'a> = MutexGuard<'a, VendoredZipArchive>;
|
||||
@@ -21,75 +20,46 @@ type LockedZipArchive<'a> = MutexGuard<'a, VendoredZipArchive>;
|
||||
/// "Files" in the `VendoredFileSystem` are read-only and immutable.
|
||||
/// Directories are supported, but symlinks and hardlinks cannot exist.
|
||||
pub struct VendoredFileSystem {
|
||||
inner: Arc<Mutex<VendoredZipArchive>>,
|
||||
inner: Mutex<VendoredZipArchive>,
|
||||
}
|
||||
|
||||
impl VendoredFileSystem {
|
||||
pub fn new_static(raw_bytes: &'static [u8]) -> Result<Self> {
|
||||
Self::new_impl(Cow::Borrowed(raw_bytes))
|
||||
}
|
||||
|
||||
pub fn new(raw_bytes: Vec<u8>) -> Result<Self> {
|
||||
Self::new_impl(Cow::Owned(raw_bytes))
|
||||
}
|
||||
|
||||
fn new_impl(data: Cow<'static, [u8]>) -> Result<Self> {
|
||||
pub fn new(raw_bytes: &'static [u8]) -> Result<Self> {
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(VendoredZipArchive::new(data)?)),
|
||||
inner: Mutex::new(VendoredZipArchive::new(raw_bytes)?),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
inner: Arc::clone(&self.inner),
|
||||
}
|
||||
pub fn exists(&self, path: &VendoredPath) -> bool {
|
||||
let normalized = NormalizedVendoredPath::from(path);
|
||||
let mut archive = self.lock_archive();
|
||||
|
||||
// Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
|
||||
// different paths in a zip file, but we want to abstract over that difference here
|
||||
// so that paths relative to the `VendoredFileSystem`
|
||||
// work the same as other paths in Ruff.
|
||||
archive.lookup_path(&normalized).is_ok()
|
||||
|| archive
|
||||
.lookup_path(&normalized.with_trailing_slash())
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub fn exists(&self, path: impl AsRef<VendoredPath>) -> bool {
|
||||
fn exists(fs: &VendoredFileSystem, path: &VendoredPath) -> bool {
|
||||
let normalized = NormalizedVendoredPath::from(path);
|
||||
let mut archive = fs.lock_archive();
|
||||
pub fn metadata(&self, path: &VendoredPath) -> Option<Metadata> {
|
||||
let normalized = NormalizedVendoredPath::from(path);
|
||||
let mut archive = self.lock_archive();
|
||||
|
||||
// Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
|
||||
// different paths in a zip file, but we want to abstract over that difference here
|
||||
// so that paths relative to the `VendoredFileSystem`
|
||||
// work the same as other paths in Ruff.
|
||||
archive.lookup_path(&normalized).is_ok()
|
||||
|| archive
|
||||
.lookup_path(&normalized.with_trailing_slash())
|
||||
.is_ok()
|
||||
// Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
|
||||
// different paths in a zip file, but we want to abstract over that difference here
|
||||
// so that paths relative to the `VendoredFileSystem`
|
||||
// work the same as other paths in Ruff.
|
||||
if let Ok(zip_file) = archive.lookup_path(&normalized) {
|
||||
return Some(Metadata::from_zip_file(zip_file));
|
||||
}
|
||||
if let Ok(zip_file) = archive.lookup_path(&normalized.with_trailing_slash()) {
|
||||
return Some(Metadata::from_zip_file(zip_file));
|
||||
}
|
||||
|
||||
exists(self, path.as_ref())
|
||||
}
|
||||
|
||||
pub fn metadata(&self, path: impl AsRef<VendoredPath>) -> Result<Metadata> {
|
||||
fn metadata(fs: &VendoredFileSystem, path: &VendoredPath) -> Result<Metadata> {
|
||||
let normalized = NormalizedVendoredPath::from(path);
|
||||
let mut archive = fs.lock_archive();
|
||||
|
||||
// Must probe the zipfile twice, as "stdlib" and "stdlib/" are considered
|
||||
// different paths in a zip file, but we want to abstract over that difference here
|
||||
// so that paths relative to the `VendoredFileSystem`
|
||||
// work the same as other paths in Ruff.
|
||||
if let Ok(zip_file) = archive.lookup_path(&normalized) {
|
||||
return Ok(Metadata::from_zip_file(zip_file));
|
||||
}
|
||||
let zip_file = archive.lookup_path(&normalized.with_trailing_slash())?;
|
||||
Ok(Metadata::from_zip_file(zip_file))
|
||||
}
|
||||
|
||||
metadata(self, path.as_ref())
|
||||
}
|
||||
|
||||
pub fn is_directory(&self, path: impl AsRef<VendoredPath>) -> bool {
|
||||
self.metadata(path)
|
||||
.is_ok_and(|metadata| metadata.kind().is_directory())
|
||||
}
|
||||
|
||||
pub fn is_file(&self, path: impl AsRef<VendoredPath>) -> bool {
|
||||
self.metadata(path)
|
||||
.is_ok_and(|metadata| metadata.kind().is_file())
|
||||
None
|
||||
}
|
||||
|
||||
/// Read the entire contents of the zip file at `path` into a string
|
||||
@@ -98,16 +68,12 @@ impl VendoredFileSystem {
|
||||
/// - The path does not exist in the underlying zip archive
|
||||
/// - The path exists in the underlying zip archive, but represents a directory
|
||||
/// - The contents of the zip file at `path` contain invalid UTF-8
|
||||
pub fn read_to_string(&self, path: impl AsRef<VendoredPath>) -> Result<String> {
|
||||
fn read_to_string(fs: &VendoredFileSystem, path: &VendoredPath) -> Result<String> {
|
||||
let mut archive = fs.lock_archive();
|
||||
let mut zip_file = archive.lookup_path(&NormalizedVendoredPath::from(path))?;
|
||||
let mut buffer = String::new();
|
||||
zip_file.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
read_to_string(self, path.as_ref())
|
||||
pub fn read(&self, path: &VendoredPath) -> Result<String> {
|
||||
let mut archive = self.lock_archive();
|
||||
let mut zip_file = archive.lookup_path(&NormalizedVendoredPath::from(path))?;
|
||||
let mut buffer = String::new();
|
||||
zip_file.read_to_string(&mut buffer)?;
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// Acquire a lock on the underlying zip archive.
|
||||
@@ -146,20 +112,6 @@ impl fmt::Debug for VendoredFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VendoredFileSystem {
|
||||
fn default() -> Self {
|
||||
let mut bytes: Vec<u8> = Vec::new();
|
||||
let mut cursor = io::Cursor::new(&mut bytes);
|
||||
|
||||
{
|
||||
let mut writer = ZipWriter::new(&mut cursor);
|
||||
writer.finish().unwrap();
|
||||
}
|
||||
|
||||
VendoredFileSystem::new(bytes).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Private struct only used in `Debug` implementations
|
||||
///
|
||||
/// This could possibly be unified with the `Metadata` struct,
|
||||
@@ -243,10 +195,10 @@ impl Metadata {
|
||||
|
||||
/// Newtype wrapper around a ZipArchive.
|
||||
#[derive(Debug)]
|
||||
struct VendoredZipArchive(ZipArchive<io::Cursor<Cow<'static, [u8]>>>);
|
||||
struct VendoredZipArchive(ZipArchive<io::Cursor<&'static [u8]>>);
|
||||
|
||||
impl VendoredZipArchive {
|
||||
fn new(data: Cow<'static, [u8]>) -> Result<Self> {
|
||||
fn new(data: &'static [u8]) -> Result<Self> {
|
||||
Ok(Self(ZipArchive::new(io::Cursor::new(data))?))
|
||||
}
|
||||
|
||||
@@ -338,11 +290,11 @@ impl<'a> From<&'a VendoredPath> for NormalizedVendoredPath<'a> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use insta::assert_snapshot;
|
||||
use zip::result::ZipResult;
|
||||
use once_cell::sync::Lazy;
|
||||
use zip::write::FileOptions;
|
||||
use zip::{CompressionMethod, ZipWriter};
|
||||
|
||||
@@ -351,66 +303,37 @@ pub(crate) mod tests {
|
||||
const FUNCTOOLS_CONTENTS: &str = "def update_wrapper(): ...";
|
||||
const ASYNCIO_TASKS_CONTENTS: &str = "class Task: ...";
|
||||
|
||||
pub struct VendoredFileSystemBuilder {
|
||||
writer: ZipWriter<io::Cursor<Vec<u8>>>,
|
||||
}
|
||||
static MOCK_ZIP_ARCHIVE: Lazy<Box<[u8]>> = Lazy::new(|| {
|
||||
let mut typeshed_buffer = Vec::new();
|
||||
let typeshed = io::Cursor::new(&mut typeshed_buffer);
|
||||
|
||||
impl Default for VendoredFileSystemBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
let options = FileOptions::default()
|
||||
.compression_method(CompressionMethod::Zstd)
|
||||
.unix_permissions(0o644);
|
||||
|
||||
impl VendoredFileSystemBuilder {
|
||||
pub fn new() -> Self {
|
||||
let buffer = io::Cursor::new(Vec::new());
|
||||
{
|
||||
let mut archive = ZipWriter::new(typeshed);
|
||||
|
||||
Self {
|
||||
writer: ZipWriter::new(buffer),
|
||||
}
|
||||
archive.add_directory("stdlib/", options).unwrap();
|
||||
archive.start_file("stdlib/functools.pyi", options).unwrap();
|
||||
archive.write_all(FUNCTOOLS_CONTENTS.as_bytes()).unwrap();
|
||||
|
||||
archive.add_directory("stdlib/asyncio/", options).unwrap();
|
||||
archive
|
||||
.start_file("stdlib/asyncio/tasks.pyi", options)
|
||||
.unwrap();
|
||||
archive
|
||||
.write_all(ASYNCIO_TASKS_CONTENTS.as_bytes())
|
||||
.unwrap();
|
||||
|
||||
archive.finish().unwrap();
|
||||
}
|
||||
|
||||
pub fn add_file(
|
||||
&mut self,
|
||||
path: impl AsRef<VendoredPath>,
|
||||
content: &str,
|
||||
) -> std::io::Result<()> {
|
||||
self.writer
|
||||
.start_file(path.as_ref().as_str(), Self::options())?;
|
||||
self.writer.write_all(content.as_bytes())
|
||||
}
|
||||
|
||||
pub fn add_directory(&mut self, path: impl AsRef<VendoredPath>) -> ZipResult<()> {
|
||||
self.writer
|
||||
.add_directory(path.as_ref().as_str(), Self::options())
|
||||
}
|
||||
|
||||
pub fn finish(mut self) -> Result<VendoredFileSystem> {
|
||||
let buffer = self.writer.finish()?;
|
||||
|
||||
VendoredFileSystem::new(buffer.into_inner())
|
||||
}
|
||||
|
||||
fn options() -> FileOptions {
|
||||
FileOptions::default()
|
||||
.compression_method(CompressionMethod::Zstd)
|
||||
.unix_permissions(0o644)
|
||||
}
|
||||
}
|
||||
typeshed_buffer.into_boxed_slice()
|
||||
});
|
||||
|
||||
fn mock_typeshed() -> VendoredFileSystem {
|
||||
let mut builder = VendoredFileSystemBuilder::new();
|
||||
|
||||
builder.add_directory("stdlib/").unwrap();
|
||||
builder
|
||||
.add_file("stdlib/functools.pyi", FUNCTOOLS_CONTENTS)
|
||||
.unwrap();
|
||||
builder.add_directory("stdlib/asyncio/").unwrap();
|
||||
builder
|
||||
.add_file("stdlib/asyncio/tasks.pyi", ASYNCIO_TASKS_CONTENTS)
|
||||
.unwrap();
|
||||
|
||||
builder.finish().unwrap()
|
||||
VendoredFileSystem::new(&MOCK_ZIP_ARCHIVE).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -472,9 +395,9 @@ pub(crate) mod tests {
|
||||
let path = VendoredPath::new(dirname);
|
||||
|
||||
assert!(mock_typeshed.exists(path));
|
||||
assert!(mock_typeshed.read_to_string(path).is_err());
|
||||
assert!(mock_typeshed.read(path).is_err());
|
||||
let metadata = mock_typeshed.metadata(path).unwrap();
|
||||
assert!(metadata.kind().is_directory());
|
||||
assert!(metadata.kind.is_directory());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -511,9 +434,9 @@ pub(crate) mod tests {
|
||||
let mock_typeshed = mock_typeshed();
|
||||
let path = VendoredPath::new(path);
|
||||
assert!(!mock_typeshed.exists(path));
|
||||
assert!(mock_typeshed.metadata(path).is_err());
|
||||
assert!(mock_typeshed.metadata(path).is_none());
|
||||
assert!(mock_typeshed
|
||||
.read_to_string(path)
|
||||
.read(path)
|
||||
.is_err_and(|err| err.to_string().contains("file not found")));
|
||||
}
|
||||
|
||||
@@ -540,7 +463,7 @@ pub(crate) mod tests {
|
||||
fn test_file(mock_typeshed: &VendoredFileSystem, path: &VendoredPath) {
|
||||
assert!(mock_typeshed.exists(path));
|
||||
let metadata = mock_typeshed.metadata(path).unwrap();
|
||||
assert!(metadata.kind().is_file());
|
||||
assert!(metadata.kind.is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -548,11 +471,11 @@ pub(crate) mod tests {
|
||||
let mock_typeshed = mock_typeshed();
|
||||
let path = VendoredPath::new("stdlib/functools.pyi");
|
||||
test_file(&mock_typeshed, path);
|
||||
let functools_stub = mock_typeshed.read_to_string(path).unwrap();
|
||||
let functools_stub = mock_typeshed.read(path).unwrap();
|
||||
assert_eq!(functools_stub.as_str(), FUNCTOOLS_CONTENTS);
|
||||
// Test that using the RefCell doesn't mutate
|
||||
// the internal state of the underlying zip archive incorrectly:
|
||||
let functools_stub_again = mock_typeshed.read_to_string(path).unwrap();
|
||||
let functools_stub_again = mock_typeshed.read(path).unwrap();
|
||||
assert_eq!(functools_stub_again.as_str(), FUNCTOOLS_CONTENTS);
|
||||
}
|
||||
|
||||
@@ -569,7 +492,7 @@ pub(crate) mod tests {
|
||||
let mock_typeshed = mock_typeshed();
|
||||
let path = VendoredPath::new("stdlib/asyncio/tasks.pyi");
|
||||
test_file(&mock_typeshed, path);
|
||||
let asyncio_stub = mock_typeshed.read_to_string(path).unwrap();
|
||||
let asyncio_stub = mock_typeshed.read(path).unwrap();
|
||||
assert_eq!(asyncio_stub.as_str(), ASYNCIO_TASKS_CONTENTS);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,43 +30,6 @@ impl VendoredPath {
|
||||
pub fn components(&self) -> Utf8Components {
|
||||
self.0.components()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn extension(&self) -> Option<&str> {
|
||||
self.0.extension()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_pyi_extension(&self) -> VendoredPathBuf {
|
||||
VendoredPathBuf(self.0.with_extension("pyi"))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn join(&self, other: impl AsRef<VendoredPath>) -> VendoredPathBuf {
|
||||
VendoredPathBuf(self.0.join(other.as_ref()))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn ends_with(&self, suffix: impl AsRef<VendoredPath>) -> bool {
|
||||
self.0.ends_with(suffix.as_ref())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn parent(&self) -> Option<&Self> {
|
||||
self.0.parent().map(Self::new)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn file_stem(&self) -> Option<&str> {
|
||||
self.0.file_stem()
|
||||
}
|
||||
|
||||
pub fn strip_prefix(
|
||||
&self,
|
||||
prefix: impl AsRef<VendoredPath>,
|
||||
) -> Result<&Self, path::StripPrefixError> {
|
||||
self.0.strip_prefix(prefix.as_ref()).map(Self::new)
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
@@ -87,10 +50,6 @@ impl VendoredPathBuf {
|
||||
pub fn as_path(&self) -> &VendoredPath {
|
||||
VendoredPath::new(&self.0)
|
||||
}
|
||||
|
||||
pub fn push(&mut self, component: impl AsRef<VendoredPath>) {
|
||||
self.0.push(component.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<VendoredPath> for VendoredPathBuf {
|
||||
@@ -127,13 +86,6 @@ impl AsRef<path::Path> for VendoredPath {
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Utf8Path> for VendoredPath {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Utf8Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for VendoredPathBuf {
|
||||
type Target = VendoredPath;
|
||||
|
||||
@@ -142,12 +94,6 @@ impl Deref for VendoredPathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for VendoredPathBuf {
|
||||
fn from(value: &str) -> Self {
|
||||
Self(Utf8PathBuf::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a path::Path> for &'a VendoredPath {
|
||||
type Error = camino::FromPathError;
|
||||
|
||||
|
||||
409
crates/ruff_db/src/vfs.rs
Normal file
409
crates/ruff_db/src/vfs.rs
Normal file
@@ -0,0 +1,409 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use dashmap::mapref::entry::Entry;
|
||||
|
||||
pub use crate::vendored::{VendoredPath, VendoredPathBuf};
|
||||
pub use path::VfsPath;
|
||||
|
||||
use crate::file_revision::FileRevision;
|
||||
use crate::file_system::FileSystemPath;
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
use crate::vfs::private::FileStatus;
|
||||
use crate::{Db, FxDashMap};
|
||||
|
||||
mod path;
|
||||
|
||||
/// Interns a file system path and returns a salsa `File` ingredient.
|
||||
///
|
||||
/// Returns `None` if the path doesn't exist, isn't accessible, or if the path points to a directory.
|
||||
#[inline]
|
||||
pub fn system_path_to_file(db: &dyn Db, path: impl AsRef<FileSystemPath>) -> Option<VfsFile> {
|
||||
let file = db.vfs().file_system(db, path.as_ref());
|
||||
|
||||
// It's important that `vfs.file_system` creates a `VfsFile` even for files that don't exist or don't
|
||||
// exist anymore so that Salsa can track that the caller of this function depends on the existence of
|
||||
// that file. This function filters out files that don't exist, but Salsa will know that it must
|
||||
// re-run the calling query whenever the `file`'s status changes (because of the `.status` call here).
|
||||
match file.status(db) {
|
||||
FileStatus::Exists => Some(file),
|
||||
FileStatus::Deleted => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Interns a vendored file path. Returns `Some` if the vendored file for `path` exists and `None` otherwise.
|
||||
#[inline]
|
||||
pub fn vendored_path_to_file(db: &dyn Db, path: impl AsRef<VendoredPath>) -> Option<VfsFile> {
|
||||
db.vfs().vendored(db, path.as_ref())
|
||||
}
|
||||
|
||||
/// Interns a virtual file system path and returns a salsa [`VfsFile`] ingredient.
|
||||
///
|
||||
/// Returns `Some` if a file for `path` exists and is accessible by the user. Returns `None` otherwise.
|
||||
///
|
||||
/// See [`system_path_to_file`] and [`vendored_path_to_file`] if you always have either a file system or vendored path.
|
||||
#[inline]
|
||||
pub fn vfs_path_to_file(db: &dyn Db, path: &VfsPath) -> Option<VfsFile> {
|
||||
match path {
|
||||
VfsPath::FileSystem(path) => system_path_to_file(db, path),
|
||||
VfsPath::Vendored(path) => vendored_path_to_file(db, path),
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtual file system that supports files from different sources.
|
||||
///
|
||||
/// The [`Vfs`] supports accessing files from:
|
||||
///
|
||||
/// * The file system
|
||||
/// * Vendored files that are part of the distributed Ruff binary
|
||||
///
|
||||
/// ## Why do both the [`Vfs`] and [`FileSystem`](crate::FileSystem) trait exist?
|
||||
///
|
||||
/// It would have been an option to define [`FileSystem`](crate::FileSystem) in a way that all its operation accept
|
||||
/// a [`VfsPath`]. This would have allowed to unify most of [`Vfs`] and [`FileSystem`](crate::FileSystem). The reason why they are
|
||||
/// separate is that not all operations are supported for all [`VfsPath`]s:
|
||||
///
|
||||
/// * The only relevant operations for [`VendoredPath`]s are testing for existence and reading the content.
|
||||
/// * The vendored file system is immutable and doesn't support writing nor does it require watching for changes.
|
||||
/// * There's no requirement to walk the vendored typesystem.
|
||||
///
|
||||
/// The other reason is that most operations know if they are working with vendored or file system paths.
|
||||
/// Requiring them to convert the path to an `VfsPath` to test if the file exist is cumbersome.
|
||||
///
|
||||
/// The main downside of the approach is that vendored files needs their own stubbing mechanism.
|
||||
#[derive(Default)]
|
||||
pub struct Vfs {
|
||||
inner: Arc<VfsInner>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct VfsInner {
|
||||
/// Lookup table that maps [`VfsPath`]s to salsa interned [`VfsFile`] instances.
|
||||
///
|
||||
/// The map also stores entries for files that don't exist on the file system. This is necessary
|
||||
/// so that queries that depend on the existence of a file are re-executed when the file is created.
|
||||
///
|
||||
files_by_path: FxDashMap<VfsPath, VfsFile>,
|
||||
vendored: VendoredVfs,
|
||||
}
|
||||
|
||||
impl Vfs {
|
||||
/// Creates a new [`Vfs`] instance where the vendored files are stubbed out.
|
||||
pub fn with_stubbed_vendored() -> Self {
|
||||
Self {
|
||||
inner: Arc::new(VfsInner {
|
||||
vendored: VendoredVfs::Stubbed(FxDashMap::default()),
|
||||
..VfsInner::default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Looks up a file by its path.
|
||||
///
|
||||
/// For a non-existing file, creates a new salsa [`VfsFile`] ingredient and stores it for future lookups.
|
||||
///
|
||||
/// The operation always succeeds even if the path doesn't exist on disk, isn't accessible or if the path points to a directory.
|
||||
/// In these cases, a file with status [`FileStatus::Deleted`] is returned.
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
fn file_system(&self, db: &dyn Db, path: &FileSystemPath) -> VfsFile {
|
||||
*self
|
||||
.inner
|
||||
.files_by_path
|
||||
.entry(VfsPath::FileSystem(path.to_path_buf()))
|
||||
.or_insert_with(|| {
|
||||
let metadata = db.file_system().metadata(path);
|
||||
|
||||
match metadata {
|
||||
Ok(metadata) if metadata.file_type().is_file() => VfsFile::new(
|
||||
db,
|
||||
VfsPath::FileSystem(path.to_path_buf()),
|
||||
metadata.permissions(),
|
||||
metadata.revision(),
|
||||
FileStatus::Exists,
|
||||
Count::default(),
|
||||
),
|
||||
_ => VfsFile::new(
|
||||
db,
|
||||
VfsPath::FileSystem(path.to_path_buf()),
|
||||
None,
|
||||
FileRevision::zero(),
|
||||
FileStatus::Deleted,
|
||||
Count::default(),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Looks up a vendored file by its path. Returns `Some` if a vendored file for the given path
|
||||
/// exists and `None` otherwise.
|
||||
#[tracing::instrument(level = "debug", skip(self, db))]
|
||||
fn vendored(&self, db: &dyn Db, path: &VendoredPath) -> Option<VfsFile> {
|
||||
let file = match self
|
||||
.inner
|
||||
.files_by_path
|
||||
.entry(VfsPath::Vendored(path.to_path_buf()))
|
||||
{
|
||||
Entry::Occupied(entry) => *entry.get(),
|
||||
Entry::Vacant(entry) => {
|
||||
let revision = self.inner.vendored.revision(path)?;
|
||||
|
||||
let file = VfsFile::new(
|
||||
db,
|
||||
VfsPath::Vendored(path.to_path_buf()),
|
||||
Some(0o444),
|
||||
revision,
|
||||
FileStatus::Exists,
|
||||
Count::default(),
|
||||
);
|
||||
|
||||
entry.insert(file);
|
||||
|
||||
file
|
||||
}
|
||||
};
|
||||
|
||||
Some(file)
|
||||
}
|
||||
|
||||
/// Stubs out the vendored files with the given content.
|
||||
///
|
||||
/// ## Panics
|
||||
/// If there are pending snapshots referencing this `Vfs` instance.
|
||||
pub fn stub_vendored<P, S>(&mut self, vendored: impl IntoIterator<Item = (P, S)>)
|
||||
where
|
||||
P: AsRef<VendoredPath>,
|
||||
S: ToString,
|
||||
{
|
||||
let inner = Arc::get_mut(&mut self.inner).unwrap();
|
||||
|
||||
let stubbed = FxDashMap::default();
|
||||
|
||||
for (path, content) in vendored {
|
||||
stubbed.insert(path.as_ref().to_path_buf(), content.to_string());
|
||||
}
|
||||
|
||||
inner.vendored = VendoredVfs::Stubbed(stubbed);
|
||||
}
|
||||
|
||||
/// Creates a salsa like snapshot of the files. The instances share
|
||||
/// the same path-to-file mapping.
|
||||
pub fn snapshot(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self, db: &dyn Db, path: &VfsPath) -> String {
|
||||
match path {
|
||||
VfsPath::FileSystem(path) => db.file_system().read(path).unwrap_or_default(),
|
||||
|
||||
VfsPath::Vendored(vendored) => db
|
||||
.vfs()
|
||||
.inner
|
||||
.vendored
|
||||
.read(vendored)
|
||||
.expect("Vendored file to exist"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Vfs {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut map = f.debug_map();
|
||||
|
||||
for entry in self.inner.files_by_path.iter() {
|
||||
map.entry(entry.key(), entry.value());
|
||||
}
|
||||
map.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::input]
|
||||
pub struct VfsFile {
|
||||
/// The path of the file.
|
||||
#[id]
|
||||
#[return_ref]
|
||||
pub path: VfsPath,
|
||||
|
||||
/// The unix permissions of the file. Only supported on unix systems. Always `None` on Windows
|
||||
/// or when the file has been deleted.
|
||||
pub permissions: Option<u32>,
|
||||
|
||||
/// The file revision. A file has changed if the revisions don't compare equal.
|
||||
pub revision: FileRevision,
|
||||
|
||||
/// The status of the file.
|
||||
///
|
||||
/// Salsa doesn't support deleting inputs. The only way to signal to the depending queries that
|
||||
/// the file has been deleted is to change the status to `Deleted`.
|
||||
status: FileStatus,
|
||||
|
||||
/// Counter that counts the number of created file instances and active file instances.
|
||||
/// Only enabled in debug builds.
|
||||
#[allow(unused)]
|
||||
count: Count<VfsFile>,
|
||||
}
|
||||
|
||||
impl VfsFile {
|
||||
/// Reads the content of the file into a [`String`].
|
||||
///
|
||||
/// Reading the same file multiple times isn't guaranteed to return the same content. It's possible
|
||||
/// that the file has been modified in between the reads. It's even possible that a file that
|
||||
/// is considered to exist has been deleted in the meantime. If this happens, then the method returns
|
||||
/// an empty string, which is the closest to the content that the file contains now. Returning
|
||||
/// an empty string shouldn't be a problem because the query will be re-executed as soon as the
|
||||
/// changes are applied to the database.
|
||||
pub(crate) fn read(&self, db: &dyn Db) -> String {
|
||||
let path = self.path(db);
|
||||
|
||||
if path.is_file_system_path() {
|
||||
// Add a dependency on the revision to ensure the operation gets re-executed when the file changes.
|
||||
let _ = self.revision(db);
|
||||
}
|
||||
|
||||
db.vfs().read(db, path)
|
||||
}
|
||||
|
||||
/// Refreshes the file metadata by querying the file system if needed.
|
||||
/// TODO: The API should instead take all observed changes from the file system directly
|
||||
/// and then apply the VfsFile status accordingly. But for now, this is sufficient.
|
||||
pub fn touch_path(db: &mut dyn Db, path: &VfsPath) {
|
||||
Self::touch_impl(db, path, None);
|
||||
}
|
||||
|
||||
pub fn touch(self, db: &mut dyn Db) {
|
||||
let path = self.path(db).clone();
|
||||
Self::touch_impl(db, &path, Some(self));
|
||||
}
|
||||
|
||||
/// Private method providing the implementation for [`Self::touch_path`] and [`Self::touch`].
|
||||
fn touch_impl(db: &mut dyn Db, path: &VfsPath, file: Option<VfsFile>) {
|
||||
match path {
|
||||
VfsPath::FileSystem(path) => {
|
||||
let metadata = db.file_system().metadata(path);
|
||||
|
||||
let (status, revision) = match metadata {
|
||||
Ok(metadata) if metadata.file_type().is_file() => {
|
||||
(FileStatus::Exists, metadata.revision())
|
||||
}
|
||||
_ => (FileStatus::Deleted, FileRevision::zero()),
|
||||
};
|
||||
|
||||
let file = file.unwrap_or_else(|| db.vfs().file_system(db, path));
|
||||
file.set_status(db).to(status);
|
||||
file.set_revision(db).to(revision);
|
||||
}
|
||||
VfsPath::Vendored(_) => {
|
||||
// Readonly, can never be out of date.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum VendoredVfs {
|
||||
#[allow(unused)]
|
||||
Real(VendoredFileSystem),
|
||||
Stubbed(FxDashMap<VendoredPathBuf, String>),
|
||||
}
|
||||
|
||||
impl Default for VendoredVfs {
|
||||
fn default() -> Self {
|
||||
Self::Stubbed(FxDashMap::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl VendoredVfs {
|
||||
fn revision(&self, path: &VendoredPath) -> Option<FileRevision> {
|
||||
match self {
|
||||
VendoredVfs::Real(file_system) => file_system
|
||||
.metadata(path)
|
||||
.map(|metadata| metadata.revision()),
|
||||
VendoredVfs::Stubbed(stubbed) => stubbed
|
||||
.contains_key(&path.to_path_buf())
|
||||
.then_some(FileRevision::new(1)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read(&self, path: &VendoredPath) -> std::io::Result<String> {
|
||||
match self {
|
||||
VendoredVfs::Real(file_system) => file_system.read(path),
|
||||
VendoredVfs::Stubbed(stubbed) => {
|
||||
if let Some(contents) = stubbed.get(&path.to_path_buf()).as_deref().cloned() {
|
||||
Ok(contents)
|
||||
} else {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("Could not find file {path:?}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The types in here need to be public because they're salsa ingredients but we
|
||||
// don't want them to be publicly accessible. That's why we put them into a private module.
|
||||
mod private {
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum FileStatus {
|
||||
/// The file exists.
|
||||
Exists,
|
||||
|
||||
/// The file was deleted, didn't exist to begin with or the path isn't a file.
|
||||
Deleted,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::file_revision::FileRevision;
|
||||
use crate::tests::TestDb;
|
||||
use crate::vfs::{system_path_to_file, vendored_path_to_file};
|
||||
|
||||
#[test]
|
||||
fn file_system_existing_file() -> crate::file_system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
db.file_system_mut()
|
||||
.write_files([("test.py", "print('Hello world')")])?;
|
||||
|
||||
let test = system_path_to_file(&db, "test.py").expect("File to exist.");
|
||||
|
||||
assert_eq!(test.permissions(&db), Some(0o755));
|
||||
assert_ne!(test.revision(&db), FileRevision::zero());
|
||||
assert_eq!(&test.read(&db), "print('Hello world')");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_non_existing_file() {
|
||||
let db = TestDb::new();
|
||||
|
||||
let test = system_path_to_file(&db, "test.py");
|
||||
|
||||
assert_eq!(test, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stubbed_vendored_file() {
|
||||
let mut db = TestDb::new();
|
||||
|
||||
db.vfs_mut()
|
||||
.stub_vendored([("test.py", "def foo() -> str")]);
|
||||
|
||||
let test = vendored_path_to_file(&db, "test.py").expect("Vendored file to exist.");
|
||||
|
||||
assert_eq!(test.permissions(&db), Some(0o444));
|
||||
assert_ne!(test.revision(&db), FileRevision::zero());
|
||||
assert_eq!(&test.read(&db), "def foo() -> str");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stubbed_vendored_file_non_existing() {
|
||||
let db = TestDb::new();
|
||||
|
||||
assert_eq!(vendored_path_to_file(&db, "test.py"), None);
|
||||
}
|
||||
}
|
||||
161
crates/ruff_db/src/vfs/path.rs
Normal file
161
crates/ruff_db/src/vfs/path.rs
Normal file
@@ -0,0 +1,161 @@
|
||||
use crate::file_system::{FileSystemPath, FileSystemPathBuf};
|
||||
use crate::vendored::path::{VendoredPath, VendoredPathBuf};
|
||||
|
||||
/// Path to a file.
|
||||
///
|
||||
/// The path abstracts that files in Ruff can come from different sources:
|
||||
///
|
||||
/// * a file stored on disk
|
||||
/// * a vendored file that ships as part of the ruff binary
|
||||
/// * Future: A virtual file that references a slice of another file. For example, the CSS code in a python file.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub enum VfsPath {
|
||||
/// Path that points to a file on disk.
|
||||
FileSystem(FileSystemPathBuf),
|
||||
Vendored(VendoredPathBuf),
|
||||
}
|
||||
|
||||
impl VfsPath {
|
||||
/// Create a new path to a file on the file system.
|
||||
#[must_use]
|
||||
pub fn file_system(path: impl AsRef<FileSystemPath>) -> Self {
|
||||
VfsPath::FileSystem(path.as_ref().to_path_buf())
|
||||
}
|
||||
|
||||
/// Returns `Some` if the path is a file system path that points to a path on disk.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn into_file_system_path_buf(self) -> Option<FileSystemPathBuf> {
|
||||
match self {
|
||||
VfsPath::FileSystem(path) => Some(path),
|
||||
VfsPath::Vendored(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_file_system_path(&self) -> Option<&FileSystemPath> {
|
||||
match self {
|
||||
VfsPath::FileSystem(path) => Some(path.as_path()),
|
||||
VfsPath::Vendored(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a file system path that points to a path on disk.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn is_file_system_path(&self) -> bool {
|
||||
matches!(self, VfsPath::FileSystem(_))
|
||||
}
|
||||
|
||||
/// Returns `true` if the path is a vendored path.
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub const fn is_vendored_path(&self) -> bool {
|
||||
matches!(self, VfsPath::Vendored(_))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[inline]
|
||||
pub fn as_vendored_path(&self) -> Option<&VendoredPath> {
|
||||
match self {
|
||||
VfsPath::Vendored(path) => Some(path.as_path()),
|
||||
VfsPath::FileSystem(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Yields the underlying [`str`] slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
VfsPath::FileSystem(path) => path.as_str(),
|
||||
VfsPath::Vendored(path) => path.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for VfsPath {
|
||||
fn as_ref(&self) -> &str {
|
||||
self.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<FileSystemPathBuf> for VfsPath {
|
||||
fn from(value: FileSystemPathBuf) -> Self {
|
||||
Self::FileSystem(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FileSystemPath> for VfsPath {
|
||||
fn from(value: &FileSystemPath) -> Self {
|
||||
VfsPath::FileSystem(value.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<VendoredPathBuf> for VfsPath {
|
||||
fn from(value: VendoredPathBuf) -> Self {
|
||||
Self::Vendored(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&VendoredPath> for VfsPath {
|
||||
fn from(value: &VendoredPath) -> Self {
|
||||
Self::Vendored(value.to_path_buf())
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FileSystemPath> for VfsPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &FileSystemPath) -> bool {
|
||||
self.as_file_system_path()
|
||||
.is_some_and(|self_path| self_path == other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VfsPath> for FileSystemPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VfsPath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<FileSystemPathBuf> for VfsPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &FileSystemPathBuf) -> bool {
|
||||
self == other.as_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VfsPath> for FileSystemPathBuf {
|
||||
fn eq(&self, other: &VfsPath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VendoredPath> for VfsPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VendoredPath) -> bool {
|
||||
self.as_vendored_path()
|
||||
.is_some_and(|self_path| self_path == other)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VfsPath> for VendoredPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VfsPath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VendoredPathBuf> for VfsPath {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VendoredPathBuf) -> bool {
|
||||
other.as_path() == self
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<VfsPath> for VendoredPathBuf {
|
||||
#[inline]
|
||||
fn eq(&self, other: &VfsPath) -> bool {
|
||||
other == self
|
||||
}
|
||||
}
|
||||
@@ -727,11 +727,11 @@ impl<Context> std::fmt::Debug for Indent<'_, Context> {
|
||||
}
|
||||
}
|
||||
|
||||
/// It reduces the indentation for the given content depending on the closest [indent] or [align] parent element.
|
||||
/// It reduces the indention for the given content depending on the closest [indent] or [align] parent element.
|
||||
/// - [align] Undoes the spaces added by [align]
|
||||
/// - [indent] Reduces the indentation level by one
|
||||
/// - [indent] Reduces the indention level by one
|
||||
///
|
||||
/// This is a No-op if the indentation level is zero.
|
||||
/// This is a No-op if the indention level is zero.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -863,7 +863,7 @@ where
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ## Tab indentation
|
||||
/// ## Tab indention
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU8;
|
||||
@@ -904,11 +904,11 @@ where
|
||||
///
|
||||
/// - the printer indents the function's `}` by two spaces because it is inside of an `align`.
|
||||
/// - the block `console.log` gets indented by two tabs.
|
||||
/// This is because `align` increases the indentation level by one (same as `indent`)
|
||||
/// This is because `align` increases the indention level by one (same as `indent`)
|
||||
/// if you nest an `indent` inside an `align`.
|
||||
/// Meaning that, `align > ... > indent` results in the same indentation as `indent > ... > indent`.
|
||||
/// Meaning that, `align > ... > indent` results in the same indention as `indent > ... > indent`.
|
||||
///
|
||||
/// ## Spaces indentation
|
||||
/// ## Spaces indention
|
||||
///
|
||||
/// ```
|
||||
/// use std::num::NonZeroU8;
|
||||
@@ -952,11 +952,11 @@ where
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// The printing of `align` differs if using spaces as indentation sequence *and* it contains an `indent`.
|
||||
/// You can see the difference when comparing the indentation of the `console.log(...)` expression to the previous example:
|
||||
/// The printing of `align` differs if using spaces as indention sequence *and* it contains an `indent`.
|
||||
/// You can see the difference when comparing the indention of the `console.log(...)` expression to the previous example:
|
||||
///
|
||||
/// - tab indentation: Printer indents the expression with two tabs because the `align` increases the indentation level.
|
||||
/// - space indentation: Printer indents the expression by 4 spaces (one indentation level) **and** 2 spaces for the align.
|
||||
/// - tab indention: Printer indents the expression with two tabs because the `align` increases the indention level.
|
||||
/// - space indention: Printer indents the expression by 4 spaces (one indention level) **and** 2 spaces for the align.
|
||||
pub fn align<Content, Context>(count: u8, content: &Content) -> Align<Context>
|
||||
where
|
||||
Content: Format<Context>,
|
||||
@@ -992,12 +992,12 @@ impl<Context> std::fmt::Debug for Align<'_, Context> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a hard line break before and after the content and increases the indentation level for the content by one.
|
||||
/// Inserts a hard line break before and after the content and increases the indention level for the content by one.
|
||||
///
|
||||
/// Block indents indent a block of code, such as in a function body, and therefore insert a line
|
||||
/// break before and after the content.
|
||||
///
|
||||
/// Doesn't create an indentation if the passed in content is [`FormatElement.is_empty`].
|
||||
/// Doesn't create an indention if the passed in content is [`FormatElement.is_empty`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -1035,7 +1035,7 @@ pub fn block_indent<Context>(content: &impl Format<Context>) -> BlockIndent<Cont
|
||||
}
|
||||
|
||||
/// Indents the content by inserting a line break before and after the content and increasing
|
||||
/// the indentation level for the content by one if the enclosing group doesn't fit on a single line.
|
||||
/// the indention level for the content by one if the enclosing group doesn't fit on a single line.
|
||||
/// Doesn't change the formatting if the enclosing group fits on a single line.
|
||||
///
|
||||
/// # Examples
|
||||
@@ -2057,7 +2057,7 @@ impl<Context> std::fmt::Debug for IfGroupBreaks<'_, Context> {
|
||||
/// If you want to indent some content if the enclosing group breaks, use [`indent`].
|
||||
///
|
||||
/// Use [`if_group_breaks`] or [`if_group_fits_on_line`] if the fitting and breaking content differs more than just the
|
||||
/// indentation level.
|
||||
/// indention level.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
||||
@@ -20,7 +20,7 @@ pub enum Tag {
|
||||
StartAlign(Align),
|
||||
EndAlign,
|
||||
|
||||
/// Reduces the indentation of the specified content either by one level or to the root, depending on the mode.
|
||||
/// Reduces the indention of the specified content either by one level or to the root, depending on the mode.
|
||||
/// Reverse operation of `Indent` and can be used to *undo* an `Align` for nested content.
|
||||
StartDedent(DedentMode),
|
||||
EndDedent,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::format_element::tag::TagKind;
|
||||
use crate::format_element::PrintMode;
|
||||
use crate::printer::stack::{Stack, StackedStack};
|
||||
use crate::printer::{Indentation, MeasureMode};
|
||||
use crate::printer::{Indention, MeasureMode};
|
||||
use crate::{IndentStyle, InvalidDocumentError, PrintError, PrintResult};
|
||||
use std::fmt::Debug;
|
||||
use std::num::NonZeroU8;
|
||||
@@ -26,13 +26,13 @@ pub(super) struct StackFrame {
|
||||
/// data structures. Such structures should be stored on the [`PrinterState`] instead.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub(super) struct PrintElementArgs {
|
||||
indent: Indentation,
|
||||
indent: Indention,
|
||||
mode: PrintMode,
|
||||
measure_mode: MeasureMode,
|
||||
}
|
||||
|
||||
impl PrintElementArgs {
|
||||
pub(crate) fn new(indent: Indentation) -> Self {
|
||||
pub(crate) fn new(indent: Indention) -> Self {
|
||||
Self {
|
||||
indent,
|
||||
..Self::default()
|
||||
@@ -47,7 +47,7 @@ impl PrintElementArgs {
|
||||
self.measure_mode
|
||||
}
|
||||
|
||||
pub(super) fn indentation(self) -> Indentation {
|
||||
pub(super) fn indention(self) -> Indention {
|
||||
self.indent
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ impl PrintElementArgs {
|
||||
}
|
||||
|
||||
pub(crate) fn reset_indent(mut self) -> Self {
|
||||
self.indent = Indentation::default();
|
||||
self.indent = Indention::default();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ impl PrintElementArgs {
|
||||
impl Default for PrintElementArgs {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
indent: Indentation::Level(0),
|
||||
indent: Indention::Level(0),
|
||||
mode: PrintMode::Expanded,
|
||||
measure_mode: MeasureMode::FirstLine,
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ impl<'a> Printer<'a> {
|
||||
document: &'a Document,
|
||||
indent: u16,
|
||||
) -> PrintResult<Printed> {
|
||||
let indentation = Indentation::Level(indent);
|
||||
let indentation = Indention::Level(indent);
|
||||
self.state.pending_indent = indentation;
|
||||
|
||||
let mut stack = PrintCallStack::new(PrintElementArgs::new(indentation));
|
||||
@@ -135,7 +135,7 @@ impl<'a> Printer<'a> {
|
||||
self.print_char('\n');
|
||||
}
|
||||
|
||||
self.state.pending_indent = args.indentation();
|
||||
self.state.pending_indent = args.indention();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,7 +883,7 @@ struct PrinterState<'a> {
|
||||
pending_source_position: Option<TextSize>,
|
||||
|
||||
/// The current indentation that should be written before the next text.
|
||||
pending_indent: Indentation,
|
||||
pending_indent: Indention,
|
||||
|
||||
/// Caches if the code up to the next newline has been measured to fit on a single line.
|
||||
/// This is used to avoid re-measuring the same content multiple times.
|
||||
@@ -943,37 +943,37 @@ impl GroupModes {
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
enum Indentation {
|
||||
/// Indent the content by `count` levels by using the indentation sequence specified by the printer options.
|
||||
enum Indention {
|
||||
/// Indent the content by `count` levels by using the indention sequence specified by the printer options.
|
||||
Level(u16),
|
||||
|
||||
/// Indent the content by n-`level`s using the indentation sequence specified by the printer options and `align` spaces.
|
||||
/// Indent the content by n-`level`s using the indention sequence specified by the printer options and `align` spaces.
|
||||
Align { level: u16, align: NonZeroU8 },
|
||||
}
|
||||
|
||||
impl Indentation {
|
||||
impl Indention {
|
||||
const fn is_empty(self) -> bool {
|
||||
matches!(self, Indentation::Level(0))
|
||||
matches!(self, Indention::Level(0))
|
||||
}
|
||||
|
||||
/// Creates a new indentation level with a zero-indent.
|
||||
/// Creates a new indention level with a zero-indent.
|
||||
const fn new() -> Self {
|
||||
Indentation::Level(0)
|
||||
Indention::Level(0)
|
||||
}
|
||||
|
||||
/// Returns the indentation level
|
||||
/// Returns the indention level
|
||||
fn level(self) -> u16 {
|
||||
match self {
|
||||
Indentation::Level(count) => count,
|
||||
Indentation::Align { level: indent, .. } => indent,
|
||||
Indention::Level(count) => count,
|
||||
Indention::Align { level: indent, .. } => indent,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the number of trailing align spaces or 0 if none
|
||||
fn align(self) -> u8 {
|
||||
match self {
|
||||
Indentation::Level(_) => 0,
|
||||
Indentation::Align { align, .. } => align.into(),
|
||||
Indention::Level(_) => 0,
|
||||
Indention::Align { align, .. } => align.into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -985,15 +985,13 @@ impl Indentation {
|
||||
/// Keeps any the current value is [`Indent::Align`] and increments the level by one.
|
||||
fn increment_level(self, indent_style: IndentStyle) -> Self {
|
||||
match self {
|
||||
Indentation::Level(count) => Indentation::Level(count + 1),
|
||||
Indention::Level(count) => Indention::Level(count + 1),
|
||||
// Increase the indent AND convert the align to an indent
|
||||
Indentation::Align { level, .. } if indent_style.is_tab() => {
|
||||
Indentation::Level(level + 2)
|
||||
}
|
||||
Indentation::Align {
|
||||
Indention::Align { level, .. } if indent_style.is_tab() => Indention::Level(level + 2),
|
||||
Indention::Align {
|
||||
level: indent,
|
||||
align,
|
||||
} => Indentation::Align {
|
||||
} => Indention::Align {
|
||||
level: indent + 1,
|
||||
align,
|
||||
},
|
||||
@@ -1007,23 +1005,23 @@ impl Indentation {
|
||||
/// No-op if the level is already zero.
|
||||
fn decrement(self) -> Self {
|
||||
match self {
|
||||
Indentation::Level(level) => Indentation::Level(level.saturating_sub(1)),
|
||||
Indentation::Align { level, .. } => Indentation::Level(level),
|
||||
Indention::Level(level) => Indention::Level(level.saturating_sub(1)),
|
||||
Indention::Align { level, .. } => Indention::Level(level),
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds an `align` of `count` spaces to the current indentation.
|
||||
/// Adds an `align` of `count` spaces to the current indention.
|
||||
///
|
||||
/// It increments the `level` value if the current value is [`Indent::IndentAlign`].
|
||||
fn set_align(self, count: NonZeroU8) -> Self {
|
||||
match self {
|
||||
Indentation::Level(indent_count) => Indentation::Align {
|
||||
Indention::Level(indent_count) => Indention::Align {
|
||||
level: indent_count,
|
||||
align: count,
|
||||
},
|
||||
|
||||
// Convert the existing align to an indent
|
||||
Indentation::Align { level: indent, .. } => Indentation::Align {
|
||||
Indention::Align { level: indent, .. } => Indention::Align {
|
||||
level: indent + 1,
|
||||
align: count,
|
||||
},
|
||||
@@ -1031,9 +1029,9 @@ impl Indentation {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Indentation {
|
||||
impl Default for Indention {
|
||||
fn default() -> Self {
|
||||
Indentation::new()
|
||||
Indention::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1193,7 +1191,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
MeasureMode::AllLines | MeasureMode::AllLinesAllowTextOverflow => {
|
||||
// Continue measuring on the next line
|
||||
self.state.line_width = 0;
|
||||
self.state.pending_indent = args.indentation();
|
||||
self.state.pending_indent = args.indention();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1304,7 +1302,7 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {
|
||||
// to ensure any trailing comments (that, unfortunately, are attached to the statement and not the expression)
|
||||
// fit too.
|
||||
self.state.line_width = 0;
|
||||
self.state.pending_indent = unindented.indentation();
|
||||
self.state.pending_indent = unindented.indention();
|
||||
|
||||
return Ok(self.fits_text(Text::Token(")"), unindented));
|
||||
}
|
||||
@@ -1617,7 +1615,7 @@ impl From<bool> for Fits {
|
||||
/// State used when measuring if a group fits on a single line
|
||||
#[derive(Debug)]
|
||||
struct FitsState {
|
||||
pending_indent: Indentation,
|
||||
pending_indent: Indention,
|
||||
has_line_suffix: bool,
|
||||
line_width: u32,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.5.2"
|
||||
version = "0.5.1"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -17,7 +17,7 @@ ruff_cache = { workspace = true }
|
||||
ruff_diagnostics = { workspace = true, features = ["serde"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_macros = { workspace = true }
|
||||
ruff_python_ast = { workspace = true, features = ["serde", "cache"] }
|
||||
ruff_python_ast = { workspace = true, features = ["serde"] }
|
||||
ruff_python_codegen = { workspace = true }
|
||||
ruff_python_index = { workspace = true }
|
||||
ruff_python_literal = { workspace = true }
|
||||
@@ -79,7 +79,7 @@ colored = { workspace = true, features = ["no-color"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
schemars = ["dep:schemars", "ruff_python_ast/schemars"]
|
||||
schemars = ["dep:schemars"]
|
||||
# Enables rules for internal integration tests
|
||||
test-rules = []
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import anyio
|
||||
import asyncio
|
||||
import trio
|
||||
|
||||
|
||||
@@ -27,33 +25,3 @@ async def func():
|
||||
with trio.move_at():
|
||||
async with trio.open_nursery() as nursery:
|
||||
...
|
||||
|
||||
|
||||
async def func():
|
||||
with anyio.move_on_after():
|
||||
...
|
||||
|
||||
|
||||
async def func():
|
||||
with anyio.fail_after():
|
||||
...
|
||||
|
||||
|
||||
async def func():
|
||||
with anyio.CancelScope():
|
||||
...
|
||||
|
||||
|
||||
async def func():
|
||||
with anyio.CancelScope():
|
||||
...
|
||||
|
||||
|
||||
async def func():
|
||||
with asyncio.timeout():
|
||||
...
|
||||
|
||||
|
||||
async def func():
|
||||
with asyncio.timeout_at():
|
||||
...
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
async def func():
|
||||
...
|
||||
|
||||
|
||||
async def func(timeout):
|
||||
...
|
||||
|
||||
|
||||
async def func(timeout=10):
|
||||
...
|
||||
@@ -1,6 +1,4 @@
|
||||
import trio
|
||||
import anyio
|
||||
import asyncio
|
||||
|
||||
|
||||
async def func():
|
||||
@@ -16,33 +14,3 @@ async def func():
|
||||
async def func():
|
||||
while True:
|
||||
trio.sleep(10)
|
||||
|
||||
|
||||
async def func():
|
||||
while True:
|
||||
await anyio.sleep(10)
|
||||
|
||||
|
||||
async def func():
|
||||
while True:
|
||||
await anyio.sleep_until(10)
|
||||
|
||||
|
||||
async def func():
|
||||
while True:
|
||||
anyio.sleep(10)
|
||||
|
||||
|
||||
async def func():
|
||||
while True:
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def func():
|
||||
while True:
|
||||
await asyncio.sleep_until(10)
|
||||
|
||||
|
||||
async def func():
|
||||
while True:
|
||||
asyncio.sleep(10)
|
||||
|
||||
@@ -76,72 +76,3 @@ def func():
|
||||
sleep = 10
|
||||
|
||||
trio.run(main)
|
||||
|
||||
|
||||
async def func():
|
||||
import anyio
|
||||
from anyio import sleep
|
||||
|
||||
await anyio.sleep(0) # ASYNC115
|
||||
await anyio.sleep(1) # OK
|
||||
await anyio.sleep(0, 1) # OK
|
||||
await anyio.sleep(...) # OK
|
||||
await anyio.sleep() # OK
|
||||
|
||||
anyio.sleep(0) # ASYNC115
|
||||
foo = 0
|
||||
anyio.sleep(foo) # OK
|
||||
anyio.sleep(1) # OK
|
||||
time.sleep(0) # OK
|
||||
|
||||
sleep(0) # ASYNC115
|
||||
|
||||
bar = "bar"
|
||||
anyio.sleep(bar)
|
||||
|
||||
x, y = 0, 2000
|
||||
anyio.sleep(x) # OK
|
||||
anyio.sleep(y) # OK
|
||||
|
||||
(a, b, [c, (d, e)]) = (1, 2, (0, [4, 0]))
|
||||
anyio.sleep(c) # OK
|
||||
anyio.sleep(d) # OK
|
||||
anyio.sleep(e) # OK
|
||||
|
||||
m_x, m_y = 0
|
||||
anyio.sleep(m_y) # OK
|
||||
anyio.sleep(m_x) # OK
|
||||
|
||||
m_a = m_b = 0
|
||||
anyio.sleep(m_a) # OK
|
||||
anyio.sleep(m_b) # OK
|
||||
|
||||
m_c = (m_d, m_e) = (0, 0)
|
||||
anyio.sleep(m_c) # OK
|
||||
anyio.sleep(m_d) # OK
|
||||
anyio.sleep(m_e) # OK
|
||||
|
||||
|
||||
def func():
|
||||
import anyio
|
||||
|
||||
anyio.run(anyio.sleep(0)) # ASYNC115
|
||||
|
||||
|
||||
def func():
|
||||
import anyio
|
||||
|
||||
if (walrus := 0) == 0:
|
||||
anyio.sleep(walrus) # OK
|
||||
|
||||
|
||||
def func():
|
||||
import anyio
|
||||
|
||||
async def main() -> None:
|
||||
sleep = 0
|
||||
for _ in range(2):
|
||||
await anyio.sleep(sleep) # OK
|
||||
sleep = 10
|
||||
|
||||
anyio.run(main)
|
||||
|
||||
@@ -55,56 +55,3 @@ async def import_from_trio():
|
||||
|
||||
# catch from import
|
||||
await sleep(86401) # error: 116, "async"
|
||||
|
||||
|
||||
async def import_anyio():
|
||||
import anyio
|
||||
|
||||
# These examples are probably not meant to ever wake up:
|
||||
await anyio.sleep(100000) # error: 116, "async"
|
||||
|
||||
# 'inf literal' overflow trick
|
||||
await anyio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
await anyio.sleep(86399)
|
||||
await anyio.sleep(86400)
|
||||
await anyio.sleep(86400.01) # error: 116, "async"
|
||||
await anyio.sleep(86401) # error: 116, "async"
|
||||
|
||||
await anyio.sleep(-1) # will raise a runtime error
|
||||
await anyio.sleep(0) # handled by different check
|
||||
|
||||
# these ones _definitely_ never wake up (TODO)
|
||||
await anyio.sleep(float("inf"))
|
||||
await anyio.sleep(math.inf)
|
||||
await anyio.sleep(inf)
|
||||
|
||||
# don't require inf to be in math (TODO)
|
||||
await anyio.sleep(np.inf)
|
||||
|
||||
# don't evaluate expressions (TODO)
|
||||
one_day = 86401
|
||||
await anyio.sleep(86400 + 1)
|
||||
await anyio.sleep(60 * 60 * 24 + 1)
|
||||
await anyio.sleep(foo())
|
||||
await anyio.sleep(one_day)
|
||||
await anyio.sleep(86400 + foo())
|
||||
await anyio.sleep(86400 + ...)
|
||||
await anyio.sleep("hello")
|
||||
await anyio.sleep(...)
|
||||
|
||||
|
||||
def not_async_fun():
|
||||
import anyio
|
||||
|
||||
# does not require the call to be awaited, nor in an async fun
|
||||
anyio.sleep(86401) # error: 116, "async"
|
||||
# also checks that we don't break visit_Call
|
||||
anyio.run(anyio.sleep(86401)) # error: 116, "async"
|
||||
|
||||
|
||||
async def import_from_anyio():
|
||||
from anyio import sleep
|
||||
|
||||
# catch from import
|
||||
await sleep(86401) # error: 116, "async"
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
import urllib.request
|
||||
|
||||
urllib.request.urlopen(url='http://www.google.com')
|
||||
urllib.request.urlopen(url=f'http://www.google.com')
|
||||
urllib.request.urlopen(url='http://' + 'www' + '.google.com')
|
||||
urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
|
||||
urllib.request.urlopen('http://www.google.com')
|
||||
urllib.request.urlopen(f'http://www.google.com')
|
||||
urllib.request.urlopen('file:///foo/bar/baz')
|
||||
urllib.request.urlopen(url)
|
||||
|
||||
urllib.request.Request(url='http://www.google.com')
|
||||
urllib.request.Request(url=f'http://www.google.com')
|
||||
urllib.request.Request(url='http://' + 'www' + '.google.com')
|
||||
urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
urllib.request.Request(url=f'http://www.google.com', **kwargs)
|
||||
urllib.request.Request(url='http://www.google.com')
|
||||
urllib.request.Request('http://www.google.com')
|
||||
urllib.request.Request(f'http://www.google.com')
|
||||
urllib.request.Request('file:///foo/bar/baz')
|
||||
urllib.request.Request(url)
|
||||
|
||||
urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
urllib.request.URLopener().open('http://www.google.com')
|
||||
urllib.request.URLopener().open(f'http://www.google.com')
|
||||
urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
urllib.request.URLopener().open(url)
|
||||
|
||||
urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
||||
urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'))
|
||||
urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
|
||||
urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
|
||||
urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
||||
@@ -68,5 +68,3 @@ def func():
|
||||
np.longfloat(12+34j)
|
||||
|
||||
np.lookfor
|
||||
|
||||
np.NAN
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
"""Regression test for: https://github.com/astral-sh/ruff/issues/12309"""
|
||||
|
||||
import contextlib
|
||||
|
||||
foo = None
|
||||
with contextlib.suppress(ImportError):
|
||||
from some_module import foo
|
||||
|
||||
bar = None
|
||||
try:
|
||||
from some_module import bar
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
baz = None
|
||||
|
||||
from some_module import baz
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -13,10 +13,6 @@ if (k) in d and d[k]:
|
||||
if k in d and d[(k)]:
|
||||
pass
|
||||
|
||||
not ("key" in dct and dct["key"])
|
||||
|
||||
bool("key" in dct and dct["key"])
|
||||
|
||||
# OK
|
||||
v = "k" in d and d["k"]
|
||||
|
||||
|
||||
@@ -518,8 +518,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::BlockingSleepInAsyncFunction) {
|
||||
flake8_async::rules::blocking_sleep(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::LongSleepNotForever) {
|
||||
flake8_async::rules::long_sleep_not_forever(checker, call);
|
||||
if checker.enabled(Rule::SleepForeverCall) {
|
||||
flake8_async::rules::sleep_forever_call(checker, call);
|
||||
}
|
||||
if checker.any_enabled(&[Rule::Print, Rule::PPrint]) {
|
||||
flake8_print::rules::print_call(checker, call);
|
||||
@@ -975,8 +975,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::TrioSyncCall) {
|
||||
flake8_async::rules::sync_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::AsyncZeroSleep) {
|
||||
flake8_async::rules::async_zero_sleep(checker, call);
|
||||
if checker.enabled(Rule::TrioZeroSleepCall) {
|
||||
flake8_async::rules::zero_sleep_call(checker, call);
|
||||
}
|
||||
if checker.enabled(Rule::UnnecessaryDunderCall) {
|
||||
pylint::rules::unnecessary_dunder_call(checker, call);
|
||||
|
||||
@@ -361,7 +361,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::AsyncFunctionWithTimeout) {
|
||||
if checker.enabled(Rule::TrioAsyncFunctionWithTimeout) {
|
||||
flake8_async::rules::async_function_with_timeout(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
@@ -1313,8 +1313,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::UselessWithLock) {
|
||||
pylint::rules::useless_with_lock(checker, with_stmt);
|
||||
}
|
||||
if checker.enabled(Rule::CancelScopeNoCheckpoint) {
|
||||
flake8_async::rules::cancel_scope_no_checkpoint(checker, with_stmt, items);
|
||||
if checker.enabled(Rule::TrioTimeoutWithoutAwait) {
|
||||
flake8_async::rules::timeout_without_await(checker, with_stmt, items);
|
||||
}
|
||||
}
|
||||
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
|
||||
@@ -1330,8 +1330,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::TryExceptInLoop) {
|
||||
perflint::rules::try_except_in_loop(checker, body);
|
||||
}
|
||||
if checker.enabled(Rule::AsyncBusyWait) {
|
||||
flake8_async::rules::async_busy_wait(checker, while_stmt);
|
||||
if checker.enabled(Rule::TrioUnneededSleep) {
|
||||
flake8_async::rules::unneeded_sleep(checker, while_stmt);
|
||||
}
|
||||
}
|
||||
Stmt::For(
|
||||
|
||||
@@ -928,19 +928,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
self.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
Stmt::With(ast::StmtWith {
|
||||
items,
|
||||
body,
|
||||
is_async: _,
|
||||
range: _,
|
||||
}) => {
|
||||
for item in items {
|
||||
self.visit_with_item(item);
|
||||
}
|
||||
self.semantic.push_branch();
|
||||
self.visit_body(body);
|
||||
self.semantic.pop_branch();
|
||||
}
|
||||
Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
body,
|
||||
@@ -1153,13 +1140,6 @@ impl<'a> Visitor<'a> for Checker<'a> {
|
||||
self.visit_expr(body);
|
||||
self.visit_expr(orelse);
|
||||
}
|
||||
Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: UnaryOp::Not,
|
||||
operand,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_boolean_test(operand);
|
||||
}
|
||||
Expr::Call(ast::ExprCall {
|
||||
func,
|
||||
arguments,
|
||||
|
||||
@@ -293,12 +293,12 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pylint, "W3301") => (RuleGroup::Stable, rules::pylint::rules::NestedMinMax),
|
||||
|
||||
// flake8-async
|
||||
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::CancelScopeNoCheckpoint),
|
||||
(Flake8Async, "100") => (RuleGroup::Stable, rules::flake8_async::rules::TrioTimeoutWithoutAwait),
|
||||
(Flake8Async, "105") => (RuleGroup::Stable, rules::flake8_async::rules::TrioSyncCall),
|
||||
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncFunctionWithTimeout),
|
||||
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncBusyWait),
|
||||
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::AsyncZeroSleep),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::LongSleepNotForever),
|
||||
(Flake8Async, "109") => (RuleGroup::Stable, rules::flake8_async::rules::TrioAsyncFunctionWithTimeout),
|
||||
(Flake8Async, "110") => (RuleGroup::Stable, rules::flake8_async::rules::TrioUnneededSleep),
|
||||
(Flake8Async, "115") => (RuleGroup::Stable, rules::flake8_async::rules::TrioZeroSleepCall),
|
||||
(Flake8Async, "116") => (RuleGroup::Preview, rules::flake8_async::rules::SleepForeverCall),
|
||||
(Flake8Async, "210") => (RuleGroup::Stable, rules::flake8_async::rules::BlockingHttpCallInAsyncFunction),
|
||||
(Flake8Async, "220") => (RuleGroup::Stable, rules::flake8_async::rules::CreateSubprocessInAsyncFunction),
|
||||
(Flake8Async, "221") => (RuleGroup::Stable, rules::flake8_async::rules::RunProcessInAsyncFunction),
|
||||
|
||||
@@ -116,17 +116,14 @@ impl Emitter for TextEmitter {
|
||||
)?;
|
||||
|
||||
if self.flags.intersects(EmitterFlags::SHOW_SOURCE) {
|
||||
// The `0..0` range is used to highlight file-level diagnostics.
|
||||
if message.range() != TextRange::default() {
|
||||
writeln!(
|
||||
writer,
|
||||
"{}",
|
||||
MessageCodeFrame {
|
||||
message,
|
||||
notebook_index
|
||||
}
|
||||
)?;
|
||||
}
|
||||
writeln!(
|
||||
writer,
|
||||
"{}",
|
||||
MessageCodeFrame {
|
||||
message,
|
||||
notebook_index
|
||||
}
|
||||
)?;
|
||||
}
|
||||
|
||||
if self.flags.intersects(EmitterFlags::SHOW_FIX_DIFF) {
|
||||
|
||||
@@ -1,156 +1,70 @@
|
||||
use ruff_python_ast::name::QualifiedName;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) enum AsyncModule {
|
||||
/// `anyio`
|
||||
AnyIo,
|
||||
/// `asyncio`
|
||||
AsyncIo,
|
||||
/// `trio`
|
||||
Trio,
|
||||
}
|
||||
|
||||
impl AsyncModule {
|
||||
pub(super) fn try_from(qualified_name: &QualifiedName<'_>) -> Option<Self> {
|
||||
match qualified_name.segments() {
|
||||
["asyncio", ..] => Some(Self::AsyncIo),
|
||||
["anyio", ..] => Some(Self::AnyIo),
|
||||
["trio", ..] => Some(Self::Trio),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AsyncModule {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AsyncModule::AnyIo => write!(f, "asyncio"),
|
||||
AsyncModule::AsyncIo => write!(f, "anyio"),
|
||||
AsyncModule::Trio => write!(f, "trio"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub(super) enum MethodName {
|
||||
AsyncIoTimeout,
|
||||
AsyncIoTimeoutAt,
|
||||
AnyIoMoveOnAfter,
|
||||
AnyIoFailAfter,
|
||||
AnyIoCancelScope,
|
||||
TrioAcloseForcefully,
|
||||
TrioCancelScope,
|
||||
TrioCancelShieldedCheckpoint,
|
||||
TrioCheckpoint,
|
||||
TrioCheckpointIfCancelled,
|
||||
TrioFailAfter,
|
||||
TrioFailAt,
|
||||
TrioMoveOnAfter,
|
||||
TrioMoveOnAt,
|
||||
TrioOpenFile,
|
||||
TrioOpenProcess,
|
||||
TrioOpenSslOverTcpListeners,
|
||||
TrioOpenSslOverTcpStream,
|
||||
TrioOpenTcpListeners,
|
||||
TrioOpenTcpStream,
|
||||
TrioOpenUnixSocket,
|
||||
TrioPermanentlyDetachCoroutineObject,
|
||||
TrioReattachDetachedCoroutineObject,
|
||||
TrioRunProcess,
|
||||
TrioServeListeners,
|
||||
TrioServeSslOverTcp,
|
||||
TrioServeTcp,
|
||||
TrioSleep,
|
||||
TrioSleepForever,
|
||||
TrioTemporarilyDetachCoroutineObject,
|
||||
TrioWaitReadable,
|
||||
TrioWaitTaskRescheduled,
|
||||
TrioWaitWritable,
|
||||
AcloseForcefully,
|
||||
CancelScope,
|
||||
CancelShieldedCheckpoint,
|
||||
Checkpoint,
|
||||
CheckpointIfCancelled,
|
||||
FailAfter,
|
||||
FailAt,
|
||||
MoveOnAfter,
|
||||
MoveOnAt,
|
||||
OpenFile,
|
||||
OpenProcess,
|
||||
OpenSslOverTcpListeners,
|
||||
OpenSslOverTcpStream,
|
||||
OpenTcpListeners,
|
||||
OpenTcpStream,
|
||||
OpenUnixSocket,
|
||||
PermanentlyDetachCoroutineObject,
|
||||
ReattachDetachedCoroutineObject,
|
||||
RunProcess,
|
||||
ServeListeners,
|
||||
ServeSslOverTcp,
|
||||
ServeTcp,
|
||||
Sleep,
|
||||
SleepForever,
|
||||
TemporarilyDetachCoroutineObject,
|
||||
WaitReadable,
|
||||
WaitTaskRescheduled,
|
||||
WaitWritable,
|
||||
}
|
||||
|
||||
impl MethodName {
|
||||
/// Returns `true` if the method is async, `false` if it is sync.
|
||||
pub(super) fn is_async(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
MethodName::TrioAcloseForcefully
|
||||
| MethodName::TrioCancelShieldedCheckpoint
|
||||
| MethodName::TrioCheckpoint
|
||||
| MethodName::TrioCheckpointIfCancelled
|
||||
| MethodName::TrioOpenFile
|
||||
| MethodName::TrioOpenProcess
|
||||
| MethodName::TrioOpenSslOverTcpListeners
|
||||
| MethodName::TrioOpenSslOverTcpStream
|
||||
| MethodName::TrioOpenTcpListeners
|
||||
| MethodName::TrioOpenTcpStream
|
||||
| MethodName::TrioOpenUnixSocket
|
||||
| MethodName::TrioPermanentlyDetachCoroutineObject
|
||||
| MethodName::TrioReattachDetachedCoroutineObject
|
||||
| MethodName::TrioRunProcess
|
||||
| MethodName::TrioServeListeners
|
||||
| MethodName::TrioServeSslOverTcp
|
||||
| MethodName::TrioServeTcp
|
||||
| MethodName::TrioSleep
|
||||
| MethodName::TrioSleepForever
|
||||
| MethodName::TrioTemporarilyDetachCoroutineObject
|
||||
| MethodName::TrioWaitReadable
|
||||
| MethodName::TrioWaitTaskRescheduled
|
||||
| MethodName::TrioWaitWritable
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns `true` if the method a timeout context manager.
|
||||
pub(super) fn is_timeout_context(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
MethodName::AsyncIoTimeout
|
||||
| MethodName::AsyncIoTimeoutAt
|
||||
| MethodName::AnyIoMoveOnAfter
|
||||
| MethodName::AnyIoFailAfter
|
||||
| MethodName::AnyIoCancelScope
|
||||
| MethodName::TrioMoveOnAfter
|
||||
| MethodName::TrioMoveOnAt
|
||||
| MethodName::TrioFailAfter
|
||||
| MethodName::TrioFailAt
|
||||
| MethodName::TrioCancelScope
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns associated module
|
||||
pub(super) fn module(self) -> AsyncModule {
|
||||
match self {
|
||||
MethodName::AsyncIoTimeout | MethodName::AsyncIoTimeoutAt => AsyncModule::AsyncIo,
|
||||
MethodName::AnyIoMoveOnAfter
|
||||
| MethodName::AnyIoFailAfter
|
||||
| MethodName::AnyIoCancelScope => AsyncModule::AnyIo,
|
||||
MethodName::TrioAcloseForcefully
|
||||
| MethodName::TrioCancelScope
|
||||
| MethodName::TrioCancelShieldedCheckpoint
|
||||
| MethodName::TrioCheckpoint
|
||||
| MethodName::TrioCheckpointIfCancelled
|
||||
| MethodName::TrioFailAfter
|
||||
| MethodName::TrioFailAt
|
||||
| MethodName::TrioMoveOnAfter
|
||||
| MethodName::TrioMoveOnAt
|
||||
| MethodName::TrioOpenFile
|
||||
| MethodName::TrioOpenProcess
|
||||
| MethodName::TrioOpenSslOverTcpListeners
|
||||
| MethodName::TrioOpenSslOverTcpStream
|
||||
| MethodName::TrioOpenTcpListeners
|
||||
| MethodName::TrioOpenTcpStream
|
||||
| MethodName::TrioOpenUnixSocket
|
||||
| MethodName::TrioPermanentlyDetachCoroutineObject
|
||||
| MethodName::TrioReattachDetachedCoroutineObject
|
||||
| MethodName::TrioRunProcess
|
||||
| MethodName::TrioServeListeners
|
||||
| MethodName::TrioServeSslOverTcp
|
||||
| MethodName::TrioServeTcp
|
||||
| MethodName::TrioSleep
|
||||
| MethodName::TrioSleepForever
|
||||
| MethodName::TrioTemporarilyDetachCoroutineObject
|
||||
| MethodName::TrioWaitReadable
|
||||
| MethodName::TrioWaitTaskRescheduled
|
||||
| MethodName::TrioWaitWritable => AsyncModule::Trio,
|
||||
MethodName::AcloseForcefully
|
||||
| MethodName::CancelShieldedCheckpoint
|
||||
| MethodName::Checkpoint
|
||||
| MethodName::CheckpointIfCancelled
|
||||
| MethodName::OpenFile
|
||||
| MethodName::OpenProcess
|
||||
| MethodName::OpenSslOverTcpListeners
|
||||
| MethodName::OpenSslOverTcpStream
|
||||
| MethodName::OpenTcpListeners
|
||||
| MethodName::OpenTcpStream
|
||||
| MethodName::OpenUnixSocket
|
||||
| MethodName::PermanentlyDetachCoroutineObject
|
||||
| MethodName::ReattachDetachedCoroutineObject
|
||||
| MethodName::RunProcess
|
||||
| MethodName::ServeListeners
|
||||
| MethodName::ServeSslOverTcp
|
||||
| MethodName::ServeTcp
|
||||
| MethodName::Sleep
|
||||
| MethodName::SleepForever
|
||||
| MethodName::TemporarilyDetachCoroutineObject
|
||||
| MethodName::WaitReadable
|
||||
| MethodName::WaitTaskRescheduled
|
||||
| MethodName::WaitWritable => true,
|
||||
|
||||
MethodName::MoveOnAfter
|
||||
| MethodName::MoveOnAt
|
||||
| MethodName::FailAfter
|
||||
| MethodName::FailAt
|
||||
| MethodName::CancelScope => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,49 +72,42 @@ impl MethodName {
|
||||
impl MethodName {
|
||||
pub(super) fn try_from(qualified_name: &QualifiedName<'_>) -> Option<Self> {
|
||||
match qualified_name.segments() {
|
||||
["asyncio", "timeout"] => Some(Self::AsyncIoTimeout),
|
||||
["asyncio", "timeout_at"] => Some(Self::AsyncIoTimeoutAt),
|
||||
["anyio", "move_on_after"] => Some(Self::AnyIoMoveOnAfter),
|
||||
["anyio", "fail_after"] => Some(Self::AnyIoFailAfter),
|
||||
["anyio", "CancelScope"] => Some(Self::AnyIoCancelScope),
|
||||
["trio", "CancelScope"] => Some(Self::TrioCancelScope),
|
||||
["trio", "aclose_forcefully"] => Some(Self::TrioAcloseForcefully),
|
||||
["trio", "fail_after"] => Some(Self::TrioFailAfter),
|
||||
["trio", "fail_at"] => Some(Self::TrioFailAt),
|
||||
["trio", "CancelScope"] => Some(Self::CancelScope),
|
||||
["trio", "aclose_forcefully"] => Some(Self::AcloseForcefully),
|
||||
["trio", "fail_after"] => Some(Self::FailAfter),
|
||||
["trio", "fail_at"] => Some(Self::FailAt),
|
||||
["trio", "lowlevel", "cancel_shielded_checkpoint"] => {
|
||||
Some(Self::TrioCancelShieldedCheckpoint)
|
||||
Some(Self::CancelShieldedCheckpoint)
|
||||
}
|
||||
["trio", "lowlevel", "checkpoint"] => Some(Self::TrioCheckpoint),
|
||||
["trio", "lowlevel", "checkpoint_if_cancelled"] => {
|
||||
Some(Self::TrioCheckpointIfCancelled)
|
||||
}
|
||||
["trio", "lowlevel", "open_process"] => Some(Self::TrioOpenProcess),
|
||||
["trio", "lowlevel", "checkpoint"] => Some(Self::Checkpoint),
|
||||
["trio", "lowlevel", "checkpoint_if_cancelled"] => Some(Self::CheckpointIfCancelled),
|
||||
["trio", "lowlevel", "open_process"] => Some(Self::OpenProcess),
|
||||
["trio", "lowlevel", "permanently_detach_coroutine_object"] => {
|
||||
Some(Self::TrioPermanentlyDetachCoroutineObject)
|
||||
Some(Self::PermanentlyDetachCoroutineObject)
|
||||
}
|
||||
["trio", "lowlevel", "reattach_detached_coroutine_object"] => {
|
||||
Some(Self::TrioReattachDetachedCoroutineObject)
|
||||
Some(Self::ReattachDetachedCoroutineObject)
|
||||
}
|
||||
["trio", "lowlevel", "temporarily_detach_coroutine_object"] => {
|
||||
Some(Self::TrioTemporarilyDetachCoroutineObject)
|
||||
Some(Self::TemporarilyDetachCoroutineObject)
|
||||
}
|
||||
["trio", "lowlevel", "wait_readable"] => Some(Self::TrioWaitReadable),
|
||||
["trio", "lowlevel", "wait_task_rescheduled"] => Some(Self::TrioWaitTaskRescheduled),
|
||||
["trio", "lowlevel", "wait_writable"] => Some(Self::TrioWaitWritable),
|
||||
["trio", "move_on_after"] => Some(Self::TrioMoveOnAfter),
|
||||
["trio", "move_on_at"] => Some(Self::TrioMoveOnAt),
|
||||
["trio", "open_file"] => Some(Self::TrioOpenFile),
|
||||
["trio", "open_ssl_over_tcp_listeners"] => Some(Self::TrioOpenSslOverTcpListeners),
|
||||
["trio", "open_ssl_over_tcp_stream"] => Some(Self::TrioOpenSslOverTcpStream),
|
||||
["trio", "open_tcp_listeners"] => Some(Self::TrioOpenTcpListeners),
|
||||
["trio", "open_tcp_stream"] => Some(Self::TrioOpenTcpStream),
|
||||
["trio", "open_unix_socket"] => Some(Self::TrioOpenUnixSocket),
|
||||
["trio", "run_process"] => Some(Self::TrioRunProcess),
|
||||
["trio", "serve_listeners"] => Some(Self::TrioServeListeners),
|
||||
["trio", "serve_ssl_over_tcp"] => Some(Self::TrioServeSslOverTcp),
|
||||
["trio", "serve_tcp"] => Some(Self::TrioServeTcp),
|
||||
["trio", "sleep"] => Some(Self::TrioSleep),
|
||||
["trio", "sleep_forever"] => Some(Self::TrioSleepForever),
|
||||
["trio", "lowlevel", "wait_readable"] => Some(Self::WaitReadable),
|
||||
["trio", "lowlevel", "wait_task_rescheduled"] => Some(Self::WaitTaskRescheduled),
|
||||
["trio", "lowlevel", "wait_writable"] => Some(Self::WaitWritable),
|
||||
["trio", "move_on_after"] => Some(Self::MoveOnAfter),
|
||||
["trio", "move_on_at"] => Some(Self::MoveOnAt),
|
||||
["trio", "open_file"] => Some(Self::OpenFile),
|
||||
["trio", "open_ssl_over_tcp_listeners"] => Some(Self::OpenSslOverTcpListeners),
|
||||
["trio", "open_ssl_over_tcp_stream"] => Some(Self::OpenSslOverTcpStream),
|
||||
["trio", "open_tcp_listeners"] => Some(Self::OpenTcpListeners),
|
||||
["trio", "open_tcp_stream"] => Some(Self::OpenTcpStream),
|
||||
["trio", "open_unix_socket"] => Some(Self::OpenUnixSocket),
|
||||
["trio", "run_process"] => Some(Self::RunProcess),
|
||||
["trio", "serve_listeners"] => Some(Self::ServeListeners),
|
||||
["trio", "serve_ssl_over_tcp"] => Some(Self::ServeSslOverTcp),
|
||||
["trio", "serve_tcp"] => Some(Self::ServeTcp),
|
||||
["trio", "sleep"] => Some(Self::Sleep),
|
||||
["trio", "sleep_forever"] => Some(Self::SleepForever),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -209,51 +116,42 @@ impl MethodName {
|
||||
impl std::fmt::Display for MethodName {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
MethodName::AsyncIoTimeout => write!(f, "asyncio.timeout"),
|
||||
MethodName::AsyncIoTimeoutAt => write!(f, "asyncio.timeout_at"),
|
||||
MethodName::AnyIoMoveOnAfter => write!(f, "anyio.move_on_after"),
|
||||
MethodName::AnyIoFailAfter => write!(f, "anyio.fail_after"),
|
||||
MethodName::AnyIoCancelScope => write!(f, "anyio.CancelScope"),
|
||||
MethodName::TrioAcloseForcefully => write!(f, "trio.aclose_forcefully"),
|
||||
MethodName::TrioCancelScope => write!(f, "trio.CancelScope"),
|
||||
MethodName::TrioCancelShieldedCheckpoint => {
|
||||
MethodName::AcloseForcefully => write!(f, "trio.aclose_forcefully"),
|
||||
MethodName::CancelScope => write!(f, "trio.CancelScope"),
|
||||
MethodName::CancelShieldedCheckpoint => {
|
||||
write!(f, "trio.lowlevel.cancel_shielded_checkpoint")
|
||||
}
|
||||
MethodName::TrioCheckpoint => write!(f, "trio.lowlevel.checkpoint"),
|
||||
MethodName::TrioCheckpointIfCancelled => {
|
||||
write!(f, "trio.lowlevel.checkpoint_if_cancelled")
|
||||
}
|
||||
MethodName::TrioFailAfter => write!(f, "trio.fail_after"),
|
||||
MethodName::TrioFailAt => write!(f, "trio.fail_at"),
|
||||
MethodName::TrioMoveOnAfter => write!(f, "trio.move_on_after"),
|
||||
MethodName::TrioMoveOnAt => write!(f, "trio.move_on_at"),
|
||||
MethodName::TrioOpenFile => write!(f, "trio.open_file"),
|
||||
MethodName::TrioOpenProcess => write!(f, "trio.lowlevel.open_process"),
|
||||
MethodName::TrioOpenSslOverTcpListeners => {
|
||||
write!(f, "trio.open_ssl_over_tcp_listeners")
|
||||
}
|
||||
MethodName::TrioOpenSslOverTcpStream => write!(f, "trio.open_ssl_over_tcp_stream"),
|
||||
MethodName::TrioOpenTcpListeners => write!(f, "trio.open_tcp_listeners"),
|
||||
MethodName::TrioOpenTcpStream => write!(f, "trio.open_tcp_stream"),
|
||||
MethodName::TrioOpenUnixSocket => write!(f, "trio.open_unix_socket"),
|
||||
MethodName::TrioPermanentlyDetachCoroutineObject => {
|
||||
MethodName::Checkpoint => write!(f, "trio.lowlevel.checkpoint"),
|
||||
MethodName::CheckpointIfCancelled => write!(f, "trio.lowlevel.checkpoint_if_cancelled"),
|
||||
MethodName::FailAfter => write!(f, "trio.fail_after"),
|
||||
MethodName::FailAt => write!(f, "trio.fail_at"),
|
||||
MethodName::MoveOnAfter => write!(f, "trio.move_on_after"),
|
||||
MethodName::MoveOnAt => write!(f, "trio.move_on_at"),
|
||||
MethodName::OpenFile => write!(f, "trio.open_file"),
|
||||
MethodName::OpenProcess => write!(f, "trio.lowlevel.open_process"),
|
||||
MethodName::OpenSslOverTcpListeners => write!(f, "trio.open_ssl_over_tcp_listeners"),
|
||||
MethodName::OpenSslOverTcpStream => write!(f, "trio.open_ssl_over_tcp_stream"),
|
||||
MethodName::OpenTcpListeners => write!(f, "trio.open_tcp_listeners"),
|
||||
MethodName::OpenTcpStream => write!(f, "trio.open_tcp_stream"),
|
||||
MethodName::OpenUnixSocket => write!(f, "trio.open_unix_socket"),
|
||||
MethodName::PermanentlyDetachCoroutineObject => {
|
||||
write!(f, "trio.lowlevel.permanently_detach_coroutine_object")
|
||||
}
|
||||
MethodName::TrioReattachDetachedCoroutineObject => {
|
||||
MethodName::ReattachDetachedCoroutineObject => {
|
||||
write!(f, "trio.lowlevel.reattach_detached_coroutine_object")
|
||||
}
|
||||
MethodName::TrioRunProcess => write!(f, "trio.run_process"),
|
||||
MethodName::TrioServeListeners => write!(f, "trio.serve_listeners"),
|
||||
MethodName::TrioServeSslOverTcp => write!(f, "trio.serve_ssl_over_tcp"),
|
||||
MethodName::TrioServeTcp => write!(f, "trio.serve_tcp"),
|
||||
MethodName::TrioSleep => write!(f, "trio.sleep"),
|
||||
MethodName::TrioSleepForever => write!(f, "trio.sleep_forever"),
|
||||
MethodName::TrioTemporarilyDetachCoroutineObject => {
|
||||
MethodName::RunProcess => write!(f, "trio.run_process"),
|
||||
MethodName::ServeListeners => write!(f, "trio.serve_listeners"),
|
||||
MethodName::ServeSslOverTcp => write!(f, "trio.serve_ssl_over_tcp"),
|
||||
MethodName::ServeTcp => write!(f, "trio.serve_tcp"),
|
||||
MethodName::Sleep => write!(f, "trio.sleep"),
|
||||
MethodName::SleepForever => write!(f, "trio.sleep_forever"),
|
||||
MethodName::TemporarilyDetachCoroutineObject => {
|
||||
write!(f, "trio.lowlevel.temporarily_detach_coroutine_object")
|
||||
}
|
||||
MethodName::TrioWaitReadable => write!(f, "trio.lowlevel.wait_readable"),
|
||||
MethodName::TrioWaitTaskRescheduled => write!(f, "trio.lowlevel.wait_task_rescheduled"),
|
||||
MethodName::TrioWaitWritable => write!(f, "trio.lowlevel.wait_writable"),
|
||||
MethodName::WaitReadable => write!(f, "trio.lowlevel.wait_readable"),
|
||||
MethodName::WaitTaskRescheduled => write!(f, "trio.lowlevel.wait_task_rescheduled"),
|
||||
MethodName::WaitWritable => write!(f, "trio.lowlevel.wait_writable"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,19 +9,17 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::assert_messages;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::settings::LinterSettings;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
#[test_case(Rule::CancelScopeNoCheckpoint, Path::new("ASYNC100.py"))]
|
||||
#[test_case(Rule::TrioTimeoutWithoutAwait, Path::new("ASYNC100.py"))]
|
||||
#[test_case(Rule::TrioSyncCall, Path::new("ASYNC105.py"))]
|
||||
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_0.py"))]
|
||||
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_1.py"))]
|
||||
#[test_case(Rule::AsyncBusyWait, Path::new("ASYNC110.py"))]
|
||||
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
|
||||
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
|
||||
#[test_case(Rule::TrioAsyncFunctionWithTimeout, Path::new("ASYNC109.py"))]
|
||||
#[test_case(Rule::TrioUnneededSleep, Path::new("ASYNC110.py"))]
|
||||
#[test_case(Rule::TrioZeroSleepCall, Path::new("ASYNC115.py"))]
|
||||
#[test_case(Rule::SleepForeverCall, Path::new("ASYNC116.py"))]
|
||||
#[test_case(Rule::BlockingHttpCallInAsyncFunction, Path::new("ASYNC210.py"))]
|
||||
#[test_case(Rule::CreateSubprocessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||
#[test_case(Rule::RunProcessInAsyncFunction, Path::new("ASYNC22x.py"))]
|
||||
@@ -37,27 +35,4 @@ mod tests {
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::CancelScopeNoCheckpoint, Path::new("ASYNC100.py"))]
|
||||
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_0.py"))]
|
||||
#[test_case(Rule::AsyncFunctionWithTimeout, Path::new("ASYNC109_1.py"))]
|
||||
#[test_case(Rule::AsyncBusyWait, Path::new("ASYNC110.py"))]
|
||||
#[test_case(Rule::AsyncZeroSleep, Path::new("ASYNC115.py"))]
|
||||
#[test_case(Rule::LongSleepNotForever, Path::new("ASYNC116.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_async").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of an async sleep function in a `while` loop.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Instead of sleeping in a `while` loop, and waiting for a condition
|
||||
/// to become true, it's preferable to `await` on an `Event` object such
|
||||
/// as: `asyncio.Event`, `trio.Event`, or `anyio.Event`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// DONE = False
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// while not DONE:
|
||||
/// await asyncio.sleep(1)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// DONE = asyncio.Event()
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await DONE.wait()
|
||||
/// ```
|
||||
///
|
||||
/// [`asyncio` events]: https://docs.python.org/3/library/asyncio-sync.html#asyncio.Event
|
||||
/// [`anyio` events]: https://trio.readthedocs.io/en/latest/reference-core.html#trio.Event
|
||||
/// [`trio` events]: https://anyio.readthedocs.io/en/latest/api.html#anyio.Event
|
||||
#[violation]
|
||||
pub struct AsyncBusyWait {
|
||||
module: AsyncModule,
|
||||
}
|
||||
|
||||
impl Violation for AsyncBusyWait {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { module } = self;
|
||||
format!("Use `{module}.Event` instead of awaiting `{module}.sleep` in a `while` loop")
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC110
|
||||
pub(crate) fn async_busy_wait(checker: &mut Checker, while_stmt: &ast::StmtWhile) {
|
||||
// The body should be a single `await` call.
|
||||
let [stmt] = while_stmt.body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
|
||||
return;
|
||||
};
|
||||
let Expr::Await(ast::ExprAwait { value, .. }) = value.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(qualified_name) = checker.semantic().resolve_qualified_name(func.as_ref()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if matches!(checker.settings.preview, PreviewMode::Disabled) {
|
||||
if matches!(qualified_name.segments(), ["trio", "sleep" | "sleep_until"]) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
AsyncBusyWait {
|
||||
module: AsyncModule::Trio,
|
||||
},
|
||||
while_stmt.range(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if matches!(
|
||||
qualified_name.segments(),
|
||||
["trio" | "anyio", "sleep" | "sleep_until"] | ["asyncio", "sleep"]
|
||||
) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
AsyncBusyWait {
|
||||
module: AsyncModule::try_from(&qualified_name).unwrap(),
|
||||
},
|
||||
while_stmt.range(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,60 +5,38 @@ use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
use crate::settings::types::PreviewMode;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `async` functions with a `timeout` argument.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Rather than implementing asynchronous timeout behavior manually, prefer
|
||||
/// built-in timeout functionality, such as `asyncio.timeout`, `trio.fail_after`,
|
||||
/// or `anyio.move_on_after`, among others.
|
||||
/// trio's built-in timeout functionality, available as `trio.fail_after`,
|
||||
/// `trio.move_on_after`, `trio.fail_at`, and `trio.move_on_at`.
|
||||
///
|
||||
/// ## Known problems
|
||||
/// To avoid false positives, this rule is only enabled if `trio` is imported
|
||||
/// in the module.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def long_running_task(timeout):
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// async def main():
|
||||
/// async def func():
|
||||
/// await long_running_task(timeout=2)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def long_running_task():
|
||||
/// ...
|
||||
///
|
||||
///
|
||||
/// async def main():
|
||||
/// with asyncio.timeout(2):
|
||||
/// async def func():
|
||||
/// with trio.fail_after(2):
|
||||
/// await long_running_task()
|
||||
/// ```
|
||||
///
|
||||
/// [`asyncio` timeouts]: https://docs.python.org/3/library/asyncio-task.html#timeouts
|
||||
/// [`anyio` timeouts]: https://anyio.readthedocs.io/en/stable/cancellation.html
|
||||
/// [`trio` timeouts]: https://trio.readthedocs.io/en/stable/reference-core.html#cancellation-and-timeouts
|
||||
#[violation]
|
||||
pub struct AsyncFunctionWithTimeout {
|
||||
module: AsyncModule,
|
||||
}
|
||||
pub struct TrioAsyncFunctionWithTimeout;
|
||||
|
||||
impl Violation for AsyncFunctionWithTimeout {
|
||||
impl Violation for TrioAsyncFunctionWithTimeout {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Async function definition with a `timeout` parameter")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { module } = self;
|
||||
let recommendation = match module {
|
||||
AsyncModule::AnyIo => "anyio.fail_after",
|
||||
AsyncModule::Trio => "trio.fail_after",
|
||||
AsyncModule::AsyncIo => "asyncio.timeout",
|
||||
};
|
||||
Some(format!("Use `{recommendation}` instead"))
|
||||
format!("Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,31 +50,18 @@ pub(crate) fn async_function_with_timeout(
|
||||
return;
|
||||
}
|
||||
|
||||
// If `trio` isn't in scope, avoid raising the diagnostic.
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the function doesn't have a `timeout` parameter, avoid raising the diagnostic.
|
||||
let Some(timeout) = function_def.parameters.find("timeout") else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Get preferred module.
|
||||
let module = if checker.semantic().seen_module(Modules::ANYIO) {
|
||||
AsyncModule::AnyIo
|
||||
} else if checker.semantic().seen_module(Modules::TRIO) {
|
||||
AsyncModule::Trio
|
||||
} else {
|
||||
AsyncModule::AsyncIo
|
||||
};
|
||||
|
||||
if matches!(checker.settings.preview, PreviewMode::Disabled) {
|
||||
if matches!(module, AsyncModule::Trio) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
AsyncFunctionWithTimeout { module },
|
||||
timeout.range(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
AsyncFunctionWithTimeout { module },
|
||||
timeout.range(),
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TrioAsyncFunctionWithTimeout,
|
||||
timeout.range(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `trio.sleep(0)` or `anyio.sleep(0)`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `trio.sleep(0)` is equivalent to calling `trio.lowlevel.checkpoint()`.
|
||||
/// However, the latter better conveys the intent of the code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.sleep(0)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.lowlevel.checkpoint()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct AsyncZeroSleep {
|
||||
module: AsyncModule,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for AsyncZeroSleep {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { module } = self;
|
||||
format!("Use `{module}.lowlevel.checkpoint()` instead of `{module}.sleep(0)`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let Self { module } = self;
|
||||
format!("Replace with `{module}.lowlevel.checkpoint()`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC115
|
||||
pub(crate) fn async_zero_sleep(checker: &mut Checker, call: &ExprCall) {
|
||||
if !(checker.semantic().seen_module(Modules::TRIO)
|
||||
|| checker.semantic().seen_module(Modules::ANYIO))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if call.arguments.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg) = call.arguments.find_argument("seconds", 0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
match arg {
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||
if !matches!(value, Number::Int(Int::ZERO)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let Some(qualified_name) = checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(module) = AsyncModule::try_from(&qualified_name) {
|
||||
let is_relevant_module = if checker.settings.preview.is_enabled() {
|
||||
matches!(module, AsyncModule::Trio | AsyncModule::AnyIo)
|
||||
} else {
|
||||
matches!(module, AsyncModule::Trio)
|
||||
};
|
||||
|
||||
let is_sleep = is_relevant_module && matches!(qualified_name.segments(), [_, "sleep"]);
|
||||
|
||||
if !is_sleep {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(AsyncZeroSleep { module }, call.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(&module.to_string(), "lowlevel"),
|
||||
call.func.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let reference_edit =
|
||||
Edit::range_replacement(format!("{binding}.checkpoint"), call.func.range());
|
||||
let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range());
|
||||
Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit]))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,21 @@
|
||||
pub(crate) use async_busy_wait::*;
|
||||
pub(crate) use async_function_with_timeout::*;
|
||||
pub(crate) use async_zero_sleep::*;
|
||||
pub(crate) use blocking_http_call::*;
|
||||
pub(crate) use blocking_open_call::*;
|
||||
pub(crate) use blocking_process_invocation::*;
|
||||
pub(crate) use blocking_sleep::*;
|
||||
pub(crate) use cancel_scope_no_checkpoint::*;
|
||||
pub(crate) use long_sleep_not_forever::*;
|
||||
pub(crate) use sleep_forever_call::*;
|
||||
pub(crate) use sync_call::*;
|
||||
pub(crate) use timeout_without_await::*;
|
||||
pub(crate) use unneeded_sleep::*;
|
||||
pub(crate) use zero_sleep_call::*;
|
||||
|
||||
mod async_busy_wait;
|
||||
mod async_function_with_timeout;
|
||||
mod async_zero_sleep;
|
||||
mod blocking_http_call;
|
||||
mod blocking_open_call;
|
||||
mod blocking_process_invocation;
|
||||
mod blocking_sleep;
|
||||
mod cancel_scope_no_checkpoint;
|
||||
mod long_sleep_not_forever;
|
||||
mod sleep_forever_call;
|
||||
mod sync_call;
|
||||
mod timeout_without_await;
|
||||
mod unneeded_sleep;
|
||||
mod zero_sleep_call;
|
||||
|
||||
@@ -4,17 +4,15 @@ use ruff_python_ast::{Expr, ExprCall, ExprNumberLiteral, Number};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
use crate::{checkers::ast::Checker, importer::ImportRequest};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `trio.sleep()` or `anyio.sleep()` with a delay greater than 24 hours.
|
||||
/// Checks for uses of `trio.sleep()` with an interval greater than 24 hours.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Calling `sleep()` with a delay greater than 24 hours is usually intended
|
||||
/// to sleep indefinitely. Instead of using a large delay,
|
||||
/// `trio.sleep_forever()` or `anyio.sleep_forever()` better conveys the intent.
|
||||
/// `trio.sleep()` with an interval greater than 24 hours is usually intended
|
||||
/// to sleep indefinitely. Instead of using a large interval,
|
||||
/// `trio.sleep_forever()` better conveys the intent.
|
||||
///
|
||||
///
|
||||
/// ## Example
|
||||
@@ -35,31 +33,23 @@ use crate::rules::flake8_async::helpers::AsyncModule;
|
||||
/// await trio.sleep_forever()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct LongSleepNotForever {
|
||||
module: AsyncModule,
|
||||
}
|
||||
pub struct SleepForeverCall;
|
||||
|
||||
impl Violation for LongSleepNotForever {
|
||||
impl Violation for SleepForeverCall {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { module } = self;
|
||||
format!(
|
||||
"`{module}.sleep()` with >24 hour interval should usually be `{module}.sleep_forever()`"
|
||||
)
|
||||
format!("`trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let Self { module } = self;
|
||||
Some(format!("Replace with `{module}.sleep_forever()`"))
|
||||
Some(format!("Replace with `trio.sleep_forever()`"))
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC116
|
||||
pub(crate) fn long_sleep_not_forever(checker: &mut Checker, call: &ExprCall) {
|
||||
if !(checker.semantic().seen_module(Modules::TRIO)
|
||||
|| checker.semantic().seen_module(Modules::ANYIO))
|
||||
{
|
||||
pub(crate) fn sleep_forever_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,6 +61,14 @@ pub(crate) fn long_sleep_not_forever(checker: &mut Checker, call: &ExprCall) {
|
||||
return;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["trio", "sleep"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let Expr::NumberLiteral(ExprNumberLiteral { value, .. }) = arg else {
|
||||
return;
|
||||
};
|
||||
@@ -96,34 +94,11 @@ pub(crate) fn long_sleep_not_forever(checker: &mut Checker, call: &ExprCall) {
|
||||
Number::Complex { .. } => return,
|
||||
}
|
||||
|
||||
let Some(qualified_name) = checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(module) = AsyncModule::try_from(&qualified_name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let is_relevant_module = if checker.settings.preview.is_enabled() {
|
||||
matches!(module, AsyncModule::AnyIo | AsyncModule::Trio)
|
||||
} else {
|
||||
matches!(module, AsyncModule::Trio)
|
||||
};
|
||||
|
||||
let is_sleep = is_relevant_module && matches!(qualified_name.segments(), [_, "sleep"]);
|
||||
|
||||
if !is_sleep {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(LongSleepNotForever { module }, call.range());
|
||||
let mut diagnostic = Diagnostic::new(SleepForeverCall, call.range());
|
||||
let replacement_function = "sleep_forever";
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from(&module.to_string(), replacement_function),
|
||||
&ImportRequest::import_from("trio", replacement_function),
|
||||
call.func.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
@@ -3,44 +3,40 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::helpers::AwaitVisitor;
|
||||
use ruff_python_ast::visitor::Visitor;
|
||||
use ruff_python_ast::{StmtWith, WithItem};
|
||||
use ruff_python_semantic::Modules;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_async::helpers::{AsyncModule, MethodName};
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::rules::flake8_async::helpers::MethodName;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for timeout context managers which do not contain a checkpoint.
|
||||
/// Checks for trio functions that should contain await but don't.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Some asynchronous context managers, such as `asyncio.timeout` and
|
||||
/// Some trio context managers, such as `trio.fail_after` and
|
||||
/// `trio.move_on_after`, have no effect unless they contain an `await`
|
||||
/// statement. The use of such context managers without an `await` statement is
|
||||
/// statement. The use of such functions without an `await` statement is
|
||||
/// likely a mistake.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// async def func():
|
||||
/// with asyncio.timeout(2):
|
||||
/// with trio.move_on_after(2):
|
||||
/// do_something()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// async def func():
|
||||
/// with asyncio.timeout(2):
|
||||
/// with trio.move_on_after(2):
|
||||
/// do_something()
|
||||
/// await awaitable()
|
||||
/// ```
|
||||
///
|
||||
/// [`asyncio` timeouts]: https://docs.python.org/3/library/asyncio-task.html#timeouts
|
||||
/// [`anyio` timeouts]: https://anyio.readthedocs.io/en/stable/cancellation.html
|
||||
/// [`trio` timeouts]: https://trio.readthedocs.io/en/stable/reference-core.html#cancellation-and-timeouts
|
||||
#[violation]
|
||||
pub struct CancelScopeNoCheckpoint {
|
||||
pub struct TrioTimeoutWithoutAwait {
|
||||
method_name: MethodName,
|
||||
}
|
||||
|
||||
impl Violation for CancelScopeNoCheckpoint {
|
||||
impl Violation for TrioTimeoutWithoutAwait {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let Self { method_name } = self;
|
||||
@@ -49,11 +45,15 @@ impl Violation for CancelScopeNoCheckpoint {
|
||||
}
|
||||
|
||||
/// ASYNC100
|
||||
pub(crate) fn cancel_scope_no_checkpoint(
|
||||
pub(crate) fn timeout_without_await(
|
||||
checker: &mut Checker,
|
||||
with_stmt: &StmtWith,
|
||||
with_items: &[WithItem],
|
||||
) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(method_name) = with_items.iter().find_map(|item| {
|
||||
let call = item.context_expr.as_call_expr()?;
|
||||
let qualified_name = checker
|
||||
@@ -64,7 +64,14 @@ pub(crate) fn cancel_scope_no_checkpoint(
|
||||
return;
|
||||
};
|
||||
|
||||
if !method_name.is_timeout_context() {
|
||||
if !matches!(
|
||||
method_name,
|
||||
MethodName::MoveOnAfter
|
||||
| MethodName::MoveOnAt
|
||||
| MethodName::FailAfter
|
||||
| MethodName::FailAt
|
||||
| MethodName::CancelScope
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -74,17 +81,8 @@ pub(crate) fn cancel_scope_no_checkpoint(
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(checker.settings.preview, PreviewMode::Disabled) {
|
||||
if matches!(method_name.module(), AsyncModule::Trio) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
CancelScopeNoCheckpoint { method_name },
|
||||
with_stmt.range,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
CancelScopeNoCheckpoint { method_name },
|
||||
with_stmt.range,
|
||||
));
|
||||
}
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
TrioTimeoutWithoutAwait { method_name },
|
||||
with_stmt.range,
|
||||
));
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, Stmt};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of `trio.sleep` in a `while` loop.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Instead of sleeping in a `while` loop, and waiting for a condition
|
||||
/// to become true, it's preferable to `wait()` on a `trio.Event`.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// DONE = False
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// while not DONE:
|
||||
/// await trio.sleep(1)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// DONE = trio.Event()
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await DONE.wait()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct TrioUnneededSleep;
|
||||
|
||||
impl Violation for TrioUnneededSleep {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop")
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC110
|
||||
pub(crate) fn unneeded_sleep(checker: &mut Checker, while_stmt: &ast::StmtWhile) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The body should be a single `await` call.
|
||||
let [stmt] = while_stmt.body.as_slice() else {
|
||||
return;
|
||||
};
|
||||
let Stmt::Expr(ast::StmtExpr { value, .. }) = stmt else {
|
||||
return;
|
||||
};
|
||||
let Expr::Await(ast::ExprAwait { value, .. }) = value.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(func.as_ref())
|
||||
.is_some_and(|path| matches!(path.segments(), ["trio", "sleep" | "sleep_until"]))
|
||||
{
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(TrioUnneededSleep, while_stmt.range()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Expr, ExprCall, Int, Number};
|
||||
use ruff_python_semantic::Modules;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::importer::ImportRequest;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `trio.sleep(0)`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// `trio.sleep(0)` is equivalent to calling `trio.lowlevel.checkpoint()`.
|
||||
/// However, the latter better conveys the intent of the code.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.sleep(0)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// import trio
|
||||
///
|
||||
///
|
||||
/// async def func():
|
||||
/// await trio.lowlevel.checkpoint()
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct TrioZeroSleepCall;
|
||||
|
||||
impl AlwaysFixableViolation for TrioZeroSleepCall {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`")
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Replace with `trio.lowlevel.checkpoint()`")
|
||||
}
|
||||
}
|
||||
|
||||
/// ASYNC115
|
||||
pub(crate) fn zero_sleep_call(checker: &mut Checker, call: &ExprCall) {
|
||||
if !checker.semantic().seen_module(Modules::TRIO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if call.arguments.len() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(arg) = call.arguments.find_argument("seconds", 0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["trio", "sleep"]))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
match arg {
|
||||
Expr::NumberLiteral(ast::ExprNumberLiteral { value, .. }) => {
|
||||
if !matches!(value, Number::Int(Int::ZERO)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(TrioZeroSleepCall, call.range());
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (import_edit, binding) = checker.importer().get_or_import_symbol(
|
||||
&ImportRequest::import_from("trio", "lowlevel"),
|
||||
call.func.start(),
|
||||
checker.semantic(),
|
||||
)?;
|
||||
let reference_edit =
|
||||
Edit::range_replacement(format!("{binding}.checkpoint"), call.func.range());
|
||||
let arg_edit = Edit::range_replacement("()".to_string(), call.arguments.range());
|
||||
Ok(Fix::safe_edits(import_edit, [reference_edit, arg_edit]))
|
||||
});
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC100.py:7:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:5:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
6 | async def func():
|
||||
7 | with trio.fail_after():
|
||||
4 | async def func():
|
||||
5 | with trio.fail_after():
|
||||
| _____^
|
||||
8 | | ...
|
||||
6 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:17:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
ASYNC100.py:15:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
16 | async def func():
|
||||
17 | with trio.move_on_after():
|
||||
14 | async def func():
|
||||
15 | with trio.move_on_after():
|
||||
| _____^
|
||||
18 | | ...
|
||||
16 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC109_0.py:8:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
ASYNC109.py:8:16: ASYNC109 Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior
|
||||
|
|
||||
8 | async def func(timeout):
|
||||
| ^^^^^^^ ASYNC109
|
||||
9 | ...
|
||||
|
|
||||
= help: Use `trio.fail_after` instead
|
||||
|
||||
ASYNC109_0.py:12:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
ASYNC109.py:12:16: ASYNC109 Prefer `trio.fail_after` and `trio.move_on_after` over manual `async` timeout behavior
|
||||
|
|
||||
12 | async def func(timeout=10):
|
||||
| ^^^^^^^^^^ ASYNC109
|
||||
13 | ...
|
||||
|
|
||||
= help: Use `trio.fail_after` instead
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC110.py:7:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
||||
ASYNC110.py:5:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
||||
|
|
||||
6 | async def func():
|
||||
7 | while True:
|
||||
4 | async def func():
|
||||
5 | while True:
|
||||
| _____^
|
||||
8 | | await trio.sleep(10)
|
||||
6 | | await trio.sleep(10)
|
||||
| |____________________________^ ASYNC110
|
||||
|
|
||||
|
||||
ASYNC110.py:12:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
||||
ASYNC110.py:10:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
||||
|
|
||||
11 | async def func():
|
||||
12 | while True:
|
||||
9 | async def func():
|
||||
10 | while True:
|
||||
| _____^
|
||||
13 | | await trio.sleep_until(10)
|
||||
11 | | await trio.sleep_until(10)
|
||||
| |__________________________________^ ASYNC110
|
||||
|
|
||||
|
||||
@@ -143,6 +143,3 @@ ASYNC116.py:57:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usu
|
||||
56 57 | # catch from import
|
||||
57 |- await sleep(86401) # error: 116, "async"
|
||||
58 |+ await sleep_forever() # error: 116, "async"
|
||||
58 59 |
|
||||
59 60 |
|
||||
60 61 | async def import_anyio():
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC100.py:7:5: ASYNC100 A `with trio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
6 | async def func():
|
||||
7 | with trio.fail_after():
|
||||
| _____^
|
||||
8 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:17:5: ASYNC100 A `with trio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
16 | async def func():
|
||||
17 | with trio.move_on_after():
|
||||
| _____^
|
||||
18 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:33:5: ASYNC100 A `with anyio.move_on_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
32 | async def func():
|
||||
33 | with anyio.move_on_after():
|
||||
| _____^
|
||||
34 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:38:5: ASYNC100 A `with anyio.fail_after(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
37 | async def func():
|
||||
38 | with anyio.fail_after():
|
||||
| _____^
|
||||
39 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:43:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
42 | async def func():
|
||||
43 | with anyio.CancelScope():
|
||||
| _____^
|
||||
44 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:48:5: ASYNC100 A `with anyio.CancelScope(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
47 | async def func():
|
||||
48 | with anyio.CancelScope():
|
||||
| _____^
|
||||
49 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:53:5: ASYNC100 A `with asyncio.timeout(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
52 | async def func():
|
||||
53 | with asyncio.timeout():
|
||||
| _____^
|
||||
54 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
|
||||
ASYNC100.py:58:5: ASYNC100 A `with asyncio.timeout_at(...):` context does not contain any `await` statements. This makes it pointless, as the timeout can only be triggered by a checkpoint.
|
||||
|
|
||||
57 | async def func():
|
||||
58 | with asyncio.timeout_at():
|
||||
| _____^
|
||||
59 | | ...
|
||||
| |___________^ ASYNC100
|
||||
|
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC109_0.py:8:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
|
|
||||
8 | async def func(timeout):
|
||||
| ^^^^^^^ ASYNC109
|
||||
9 | ...
|
||||
|
|
||||
= help: Use `trio.fail_after` instead
|
||||
|
||||
ASYNC109_0.py:12:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
|
|
||||
12 | async def func(timeout=10):
|
||||
| ^^^^^^^^^^ ASYNC109
|
||||
13 | ...
|
||||
|
|
||||
= help: Use `trio.fail_after` instead
|
||||
@@ -1,18 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC109_1.py:5:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
|
|
||||
5 | async def func(timeout):
|
||||
| ^^^^^^^ ASYNC109
|
||||
6 | ...
|
||||
|
|
||||
= help: Use `asyncio.timeout` instead
|
||||
|
||||
ASYNC109_1.py:9:16: ASYNC109 Async function definition with a `timeout` parameter
|
||||
|
|
||||
9 | async def func(timeout=10):
|
||||
| ^^^^^^^^^^ ASYNC109
|
||||
10 | ...
|
||||
|
|
||||
= help: Use `asyncio.timeout` instead
|
||||
@@ -1,47 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC110.py:7:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
||||
|
|
||||
6 | async def func():
|
||||
7 | while True:
|
||||
| _____^
|
||||
8 | | await trio.sleep(10)
|
||||
| |____________________________^ ASYNC110
|
||||
|
|
||||
|
||||
ASYNC110.py:12:5: ASYNC110 Use `trio.Event` instead of awaiting `trio.sleep` in a `while` loop
|
||||
|
|
||||
11 | async def func():
|
||||
12 | while True:
|
||||
| _____^
|
||||
13 | | await trio.sleep_until(10)
|
||||
| |__________________________________^ ASYNC110
|
||||
|
|
||||
|
||||
ASYNC110.py:22:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop
|
||||
|
|
||||
21 | async def func():
|
||||
22 | while True:
|
||||
| _____^
|
||||
23 | | await anyio.sleep(10)
|
||||
| |_____________________________^ ASYNC110
|
||||
|
|
||||
|
||||
ASYNC110.py:27:5: ASYNC110 Use `asyncio.Event` instead of awaiting `asyncio.sleep` in a `while` loop
|
||||
|
|
||||
26 | async def func():
|
||||
27 | while True:
|
||||
| _____^
|
||||
28 | | await anyio.sleep_until(10)
|
||||
| |___________________________________^ ASYNC110
|
||||
|
|
||||
|
||||
ASYNC110.py:37:5: ASYNC110 Use `anyio.Event` instead of awaiting `anyio.sleep` in a `while` loop
|
||||
|
|
||||
36 | async def func():
|
||||
37 | while True:
|
||||
| _____^
|
||||
38 | | await asyncio.sleep(10)
|
||||
| |_______________________________^ ASYNC110
|
||||
|
|
||||
@@ -1,248 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC115.py:5:11: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
||||
|
|
||||
3 | from trio import sleep
|
||||
4 |
|
||||
5 | await trio.sleep(0) # ASYNC115
|
||||
| ^^^^^^^^^^^^^ ASYNC115
|
||||
6 | await trio.sleep(1) # OK
|
||||
7 | await trio.sleep(0, 1) # OK
|
||||
|
|
||||
= help: Replace with `trio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | import trio
|
||||
3 3 | from trio import sleep
|
||||
4 4 |
|
||||
5 |- await trio.sleep(0) # ASYNC115
|
||||
5 |+ await trio.lowlevel.checkpoint() # ASYNC115
|
||||
6 6 | await trio.sleep(1) # OK
|
||||
7 7 | await trio.sleep(0, 1) # OK
|
||||
8 8 | await trio.sleep(...) # OK
|
||||
|
||||
ASYNC115.py:11:5: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
||||
|
|
||||
9 | await trio.sleep() # OK
|
||||
10 |
|
||||
11 | trio.sleep(0) # ASYNC115
|
||||
| ^^^^^^^^^^^^^ ASYNC115
|
||||
12 | foo = 0
|
||||
13 | trio.sleep(foo) # OK
|
||||
|
|
||||
= help: Replace with `trio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | await trio.sleep(...) # OK
|
||||
9 9 | await trio.sleep() # OK
|
||||
10 10 |
|
||||
11 |- trio.sleep(0) # ASYNC115
|
||||
11 |+ trio.lowlevel.checkpoint() # ASYNC115
|
||||
12 12 | foo = 0
|
||||
13 13 | trio.sleep(foo) # OK
|
||||
14 14 | trio.sleep(1) # OK
|
||||
|
||||
ASYNC115.py:17:5: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
||||
|
|
||||
15 | time.sleep(0) # OK
|
||||
16 |
|
||||
17 | sleep(0) # ASYNC115
|
||||
| ^^^^^^^^ ASYNC115
|
||||
18 |
|
||||
19 | bar = "bar"
|
||||
|
|
||||
= help: Replace with `trio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | trio.sleep(1) # OK
|
||||
15 15 | time.sleep(0) # OK
|
||||
16 16 |
|
||||
17 |- sleep(0) # ASYNC115
|
||||
17 |+ trio.lowlevel.checkpoint() # ASYNC115
|
||||
18 18 |
|
||||
19 19 | bar = "bar"
|
||||
20 20 | trio.sleep(bar)
|
||||
|
||||
ASYNC115.py:48:14: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
||||
|
|
||||
46 | import trio
|
||||
47 |
|
||||
48 | trio.run(trio.sleep(0)) # ASYNC115
|
||||
| ^^^^^^^^^^^^^ ASYNC115
|
||||
|
|
||||
= help: Replace with `trio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
45 45 | def func():
|
||||
46 46 | import trio
|
||||
47 47 |
|
||||
48 |- trio.run(trio.sleep(0)) # ASYNC115
|
||||
48 |+ trio.run(trio.lowlevel.checkpoint()) # ASYNC115
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 51 | from trio import Event, sleep
|
||||
|
||||
ASYNC115.py:55:5: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
||||
|
|
||||
54 | def func():
|
||||
55 | sleep(0) # ASYNC115
|
||||
| ^^^^^^^^ ASYNC115
|
||||
|
|
||||
= help: Replace with `trio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
48 48 | trio.run(trio.sleep(0)) # ASYNC115
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 |-from trio import Event, sleep
|
||||
51 |+from trio import Event, sleep, lowlevel
|
||||
52 52 |
|
||||
53 53 |
|
||||
54 54 | def func():
|
||||
55 |- sleep(0) # ASYNC115
|
||||
55 |+ lowlevel.checkpoint() # ASYNC115
|
||||
56 56 |
|
||||
57 57 |
|
||||
58 58 | async def func():
|
||||
|
||||
ASYNC115.py:59:11: ASYNC115 [*] Use `trio.lowlevel.checkpoint()` instead of `trio.sleep(0)`
|
||||
|
|
||||
58 | async def func():
|
||||
59 | await sleep(seconds=0) # ASYNC115
|
||||
| ^^^^^^^^^^^^^^^^ ASYNC115
|
||||
|
|
||||
= help: Replace with `trio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
48 48 | trio.run(trio.sleep(0)) # ASYNC115
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 |-from trio import Event, sleep
|
||||
51 |+from trio import Event, sleep, lowlevel
|
||||
52 52 |
|
||||
53 53 |
|
||||
54 54 | def func():
|
||||
--------------------------------------------------------------------------------
|
||||
56 56 |
|
||||
57 57 |
|
||||
58 58 | async def func():
|
||||
59 |- await sleep(seconds=0) # ASYNC115
|
||||
59 |+ await lowlevel.checkpoint() # ASYNC115
|
||||
60 60 |
|
||||
61 61 |
|
||||
62 62 | def func():
|
||||
|
||||
ASYNC115.py:85:11: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)`
|
||||
|
|
||||
83 | from anyio import sleep
|
||||
84 |
|
||||
85 | await anyio.sleep(0) # ASYNC115
|
||||
| ^^^^^^^^^^^^^^ ASYNC115
|
||||
86 | await anyio.sleep(1) # OK
|
||||
87 | await anyio.sleep(0, 1) # OK
|
||||
|
|
||||
= help: Replace with `asyncio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 51 | from trio import Event, sleep
|
||||
52 |+from asyncio import lowlevel
|
||||
52 53 |
|
||||
53 54 |
|
||||
54 55 | def func():
|
||||
--------------------------------------------------------------------------------
|
||||
82 83 | import anyio
|
||||
83 84 | from anyio import sleep
|
||||
84 85 |
|
||||
85 |- await anyio.sleep(0) # ASYNC115
|
||||
86 |+ await lowlevel.checkpoint() # ASYNC115
|
||||
86 87 | await anyio.sleep(1) # OK
|
||||
87 88 | await anyio.sleep(0, 1) # OK
|
||||
88 89 | await anyio.sleep(...) # OK
|
||||
|
||||
ASYNC115.py:91:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)`
|
||||
|
|
||||
89 | await anyio.sleep() # OK
|
||||
90 |
|
||||
91 | anyio.sleep(0) # ASYNC115
|
||||
| ^^^^^^^^^^^^^^ ASYNC115
|
||||
92 | foo = 0
|
||||
93 | anyio.sleep(foo) # OK
|
||||
|
|
||||
= help: Replace with `asyncio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 51 | from trio import Event, sleep
|
||||
52 |+from asyncio import lowlevel
|
||||
52 53 |
|
||||
53 54 |
|
||||
54 55 | def func():
|
||||
--------------------------------------------------------------------------------
|
||||
88 89 | await anyio.sleep(...) # OK
|
||||
89 90 | await anyio.sleep() # OK
|
||||
90 91 |
|
||||
91 |- anyio.sleep(0) # ASYNC115
|
||||
92 |+ lowlevel.checkpoint() # ASYNC115
|
||||
92 93 | foo = 0
|
||||
93 94 | anyio.sleep(foo) # OK
|
||||
94 95 | anyio.sleep(1) # OK
|
||||
|
||||
ASYNC115.py:97:5: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)`
|
||||
|
|
||||
95 | time.sleep(0) # OK
|
||||
96 |
|
||||
97 | sleep(0) # ASYNC115
|
||||
| ^^^^^^^^ ASYNC115
|
||||
98 |
|
||||
99 | bar = "bar"
|
||||
|
|
||||
= help: Replace with `asyncio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 51 | from trio import Event, sleep
|
||||
52 |+from asyncio import lowlevel
|
||||
52 53 |
|
||||
53 54 |
|
||||
54 55 | def func():
|
||||
--------------------------------------------------------------------------------
|
||||
94 95 | anyio.sleep(1) # OK
|
||||
95 96 | time.sleep(0) # OK
|
||||
96 97 |
|
||||
97 |- sleep(0) # ASYNC115
|
||||
98 |+ lowlevel.checkpoint() # ASYNC115
|
||||
98 99 |
|
||||
99 100 | bar = "bar"
|
||||
100 101 | anyio.sleep(bar)
|
||||
|
||||
ASYNC115.py:128:15: ASYNC115 [*] Use `asyncio.lowlevel.checkpoint()` instead of `asyncio.sleep(0)`
|
||||
|
|
||||
126 | import anyio
|
||||
127 |
|
||||
128 | anyio.run(anyio.sleep(0)) # ASYNC115
|
||||
| ^^^^^^^^^^^^^^ ASYNC115
|
||||
|
|
||||
= help: Replace with `asyncio.lowlevel.checkpoint()`
|
||||
|
||||
ℹ Safe fix
|
||||
49 49 |
|
||||
50 50 |
|
||||
51 51 | from trio import Event, sleep
|
||||
52 |+from asyncio import lowlevel
|
||||
52 53 |
|
||||
53 54 |
|
||||
54 55 | def func():
|
||||
--------------------------------------------------------------------------------
|
||||
125 126 | def func():
|
||||
126 127 | import anyio
|
||||
127 128 |
|
||||
128 |- anyio.run(anyio.sleep(0)) # ASYNC115
|
||||
129 |+ anyio.run(lowlevel.checkpoint()) # ASYNC115
|
||||
129 130 |
|
||||
130 131 |
|
||||
131 132 | def func():
|
||||
@@ -1,339 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_async/mod.rs
|
||||
---
|
||||
ASYNC116.py:11:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
10 | # These examples are probably not meant to ever wake up:
|
||||
11 | await trio.sleep(100000) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
12 |
|
||||
13 | # 'inf literal' overflow trick
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
8 8 | import trio
|
||||
9 9 |
|
||||
10 10 | # These examples are probably not meant to ever wake up:
|
||||
11 |- await trio.sleep(100000) # error: 116, "async"
|
||||
11 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
12 12 |
|
||||
13 13 | # 'inf literal' overflow trick
|
||||
14 14 | await trio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
ASYNC116.py:14:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
13 | # 'inf literal' overflow trick
|
||||
14 | await trio.sleep(1e999) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
15 |
|
||||
16 | await trio.sleep(86399)
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
11 11 | await trio.sleep(100000) # error: 116, "async"
|
||||
12 12 |
|
||||
13 13 | # 'inf literal' overflow trick
|
||||
14 |- await trio.sleep(1e999) # error: 116, "async"
|
||||
14 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
15 15 |
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
|
||||
ASYNC116.py:18:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
16 | await trio.sleep(86399)
|
||||
17 | await trio.sleep(86400)
|
||||
18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
19 | await trio.sleep(86401) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
15 15 |
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
18 |- await trio.sleep(86400.01) # error: 116, "async"
|
||||
18 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
19 19 | await trio.sleep(86401) # error: 116, "async"
|
||||
20 20 |
|
||||
21 21 | await trio.sleep(-1) # will raise a runtime error
|
||||
|
||||
ASYNC116.py:19:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
17 | await trio.sleep(86400)
|
||||
18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
19 | await trio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
20 |
|
||||
21 | await trio.sleep(-1) # will raise a runtime error
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
16 16 | await trio.sleep(86399)
|
||||
17 17 | await trio.sleep(86400)
|
||||
18 18 | await trio.sleep(86400.01) # error: 116, "async"
|
||||
19 |- await trio.sleep(86401) # error: 116, "async"
|
||||
19 |+ await trio.sleep_forever() # error: 116, "async"
|
||||
20 20 |
|
||||
21 21 | await trio.sleep(-1) # will raise a runtime error
|
||||
22 22 | await trio.sleep(0) # handled by different check
|
||||
|
||||
ASYNC116.py:48:5: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 | trio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
49 | # also checks that we don't break visit_Call
|
||||
50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
45 45 | import trio
|
||||
46 46 |
|
||||
47 47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 |- trio.sleep(86401) # error: 116, "async"
|
||||
48 |+ trio.sleep_forever() # error: 116, "async"
|
||||
49 49 | # also checks that we don't break visit_Call
|
||||
50 50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
51 51 |
|
||||
|
||||
ASYNC116.py:50:14: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
48 | trio.sleep(86401) # error: 116, "async"
|
||||
49 | # also checks that we don't break visit_Call
|
||||
50 | trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
47 47 | # does not require the call to be awaited, nor in an async fun
|
||||
48 48 | trio.sleep(86401) # error: 116, "async"
|
||||
49 49 | # also checks that we don't break visit_Call
|
||||
50 |- trio.run(trio.sleep(86401)) # error: 116, "async"
|
||||
50 |+ trio.run(trio.sleep_forever()) # error: 116, "async"
|
||||
51 51 |
|
||||
52 52 |
|
||||
53 53 | async def import_from_trio():
|
||||
|
||||
ASYNC116.py:57:11: ASYNC116 [*] `trio.sleep()` with >24 hour interval should usually be `trio.sleep_forever()`
|
||||
|
|
||||
56 | # catch from import
|
||||
57 | await sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `trio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from trio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
54 55 | from trio import sleep
|
||||
55 56 |
|
||||
56 57 | # catch from import
|
||||
57 |- await sleep(86401) # error: 116, "async"
|
||||
58 |+ await sleep_forever() # error: 116, "async"
|
||||
58 59 |
|
||||
59 60 |
|
||||
60 61 | async def import_anyio():
|
||||
|
||||
ASYNC116.py:64:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
63 | # These examples are probably not meant to ever wake up:
|
||||
64 | await anyio.sleep(100000) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
65 |
|
||||
66 | # 'inf literal' overflow trick
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
61 62 | import anyio
|
||||
62 63 |
|
||||
63 64 | # These examples are probably not meant to ever wake up:
|
||||
64 |- await anyio.sleep(100000) # error: 116, "async"
|
||||
65 |+ await sleep_forever() # error: 116, "async"
|
||||
65 66 |
|
||||
66 67 | # 'inf literal' overflow trick
|
||||
67 68 | await anyio.sleep(1e999) # error: 116, "async"
|
||||
|
||||
ASYNC116.py:67:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
66 | # 'inf literal' overflow trick
|
||||
67 | await anyio.sleep(1e999) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
68 |
|
||||
69 | await anyio.sleep(86399)
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
64 65 | await anyio.sleep(100000) # error: 116, "async"
|
||||
65 66 |
|
||||
66 67 | # 'inf literal' overflow trick
|
||||
67 |- await anyio.sleep(1e999) # error: 116, "async"
|
||||
68 |+ await sleep_forever() # error: 116, "async"
|
||||
68 69 |
|
||||
69 70 | await anyio.sleep(86399)
|
||||
70 71 | await anyio.sleep(86400)
|
||||
|
||||
ASYNC116.py:71:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
69 | await anyio.sleep(86399)
|
||||
70 | await anyio.sleep(86400)
|
||||
71 | await anyio.sleep(86400.01) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
72 | await anyio.sleep(86401) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
68 69 |
|
||||
69 70 | await anyio.sleep(86399)
|
||||
70 71 | await anyio.sleep(86400)
|
||||
71 |- await anyio.sleep(86400.01) # error: 116, "async"
|
||||
72 |+ await sleep_forever() # error: 116, "async"
|
||||
72 73 | await anyio.sleep(86401) # error: 116, "async"
|
||||
73 74 |
|
||||
74 75 | await anyio.sleep(-1) # will raise a runtime error
|
||||
|
||||
ASYNC116.py:72:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
70 | await anyio.sleep(86400)
|
||||
71 | await anyio.sleep(86400.01) # error: 116, "async"
|
||||
72 | await anyio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
73 |
|
||||
74 | await anyio.sleep(-1) # will raise a runtime error
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
69 70 | await anyio.sleep(86399)
|
||||
70 71 | await anyio.sleep(86400)
|
||||
71 72 | await anyio.sleep(86400.01) # error: 116, "async"
|
||||
72 |- await anyio.sleep(86401) # error: 116, "async"
|
||||
73 |+ await sleep_forever() # error: 116, "async"
|
||||
73 74 |
|
||||
74 75 | await anyio.sleep(-1) # will raise a runtime error
|
||||
75 76 | await anyio.sleep(0) # handled by different check
|
||||
|
||||
ASYNC116.py:101:5: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
100 | # does not require the call to be awaited, nor in an async fun
|
||||
101 | anyio.sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
102 | # also checks that we don't break visit_Call
|
||||
103 | anyio.run(anyio.sleep(86401)) # error: 116, "async"
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
98 99 | import anyio
|
||||
99 100 |
|
||||
100 101 | # does not require the call to be awaited, nor in an async fun
|
||||
101 |- anyio.sleep(86401) # error: 116, "async"
|
||||
102 |+ sleep_forever() # error: 116, "async"
|
||||
102 103 | # also checks that we don't break visit_Call
|
||||
103 104 | anyio.run(anyio.sleep(86401)) # error: 116, "async"
|
||||
104 105 |
|
||||
|
||||
ASYNC116.py:103:15: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
101 | anyio.sleep(86401) # error: 116, "async"
|
||||
102 | # also checks that we don't break visit_Call
|
||||
103 | anyio.run(anyio.sleep(86401)) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
100 101 | # does not require the call to be awaited, nor in an async fun
|
||||
101 102 | anyio.sleep(86401) # error: 116, "async"
|
||||
102 103 | # also checks that we don't break visit_Call
|
||||
103 |- anyio.run(anyio.sleep(86401)) # error: 116, "async"
|
||||
104 |+ anyio.run(sleep_forever()) # error: 116, "async"
|
||||
104 105 |
|
||||
105 106 |
|
||||
106 107 | async def import_from_anyio():
|
||||
|
||||
ASYNC116.py:110:11: ASYNC116 [*] `asyncio.sleep()` with >24 hour interval should usually be `asyncio.sleep_forever()`
|
||||
|
|
||||
109 | # catch from import
|
||||
110 | await sleep(86401) # error: 116, "async"
|
||||
| ^^^^^^^^^^^^ ASYNC116
|
||||
|
|
||||
= help: Replace with `asyncio.sleep_forever()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
2 2 | # ASYNCIO_NO_ERROR - no asyncio.sleep_forever, so check intentionally doesn't trigger.
|
||||
3 3 | import math
|
||||
4 4 | from math import inf
|
||||
5 |+from asyncio import sleep_forever
|
||||
5 6 |
|
||||
6 7 |
|
||||
7 8 | async def import_trio():
|
||||
--------------------------------------------------------------------------------
|
||||
107 108 | from anyio import sleep
|
||||
108 109 |
|
||||
109 110 | # catch from import
|
||||
110 |- await sleep(86401) # error: 116, "async"
|
||||
111 |+ await sleep_forever() # error: 116, "async"
|
||||
@@ -1,10 +1,9 @@
|
||||
//! Check for calls to suspicious functions, or calls into suspicious modules.
|
||||
//!
|
||||
//! See: <https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html>
|
||||
use itertools::Either;
|
||||
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall, Operator};
|
||||
use ruff_python_ast::{self as ast, Decorator, Expr, ExprCall};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -826,56 +825,6 @@ impl Violation for SuspiciousFTPLibUsage {
|
||||
|
||||
/// S301, S302, S303, S304, S305, S306, S307, S308, S310, S311, S312, S313, S314, S315, S316, S317, S318, S319, S320, S321, S323
|
||||
pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
/// Returns `true` if the iterator starts with the given prefix.
|
||||
fn has_prefix(mut chars: impl Iterator<Item = char>, prefix: &str) -> bool {
|
||||
for expected in prefix.chars() {
|
||||
let Some(actual) = chars.next() else {
|
||||
return false;
|
||||
};
|
||||
if actual != expected {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Returns `true` if the iterator starts with an HTTP or HTTPS prefix.
|
||||
fn has_http_prefix(chars: impl Iterator<Item = char> + Clone) -> bool {
|
||||
has_prefix(chars.clone().skip_while(|c| c.is_whitespace()), "http://")
|
||||
|| has_prefix(chars.skip_while(|c| c.is_whitespace()), "https://")
|
||||
}
|
||||
|
||||
/// Return the leading characters for an expression, if it's a string literal, f-string, or
|
||||
/// string concatenation.
|
||||
fn leading_chars(expr: &Expr) -> Option<impl Iterator<Item = char> + Clone + '_> {
|
||||
match expr {
|
||||
// Ex) `"foo"`
|
||||
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
|
||||
Some(Either::Left(value.chars()))
|
||||
}
|
||||
// Ex) f"foo"
|
||||
Expr::FString(ast::ExprFString { value, .. }) => {
|
||||
value.elements().next().and_then(|element| {
|
||||
if let ast::FStringElement::Literal(ast::FStringLiteralElement {
|
||||
value, ..
|
||||
}) = element
|
||||
{
|
||||
Some(Either::Right(value.chars()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
// Ex) "foo" + "bar"
|
||||
Expr::BinOp(ast::ExprBinOp {
|
||||
op: Operator::Add,
|
||||
left,
|
||||
..
|
||||
}) => leading_chars(left),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
let Some(diagnostic_kind) = checker.semantic().resolve_qualified_name(call.func.as_ref()).and_then(|qualified_name| {
|
||||
match qualified_name.segments() {
|
||||
// Pickle
|
||||
@@ -901,12 +850,16 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
// MarkSafe
|
||||
["django", "utils", "safestring" | "html", "mark_safe"] => Some(SuspiciousMarkSafeUsage.into()),
|
||||
// URLOpen (`Request`)
|
||||
["urllib", "request", "Request"] |
|
||||
["six", "moves", "urllib", "request", "Request"] => {
|
||||
// If the `url` argument is a string literal or an f-string, allow `http` and `https` schemes.
|
||||
["urllib", "request","Request"] |
|
||||
["six", "moves", "urllib", "request","Request"] => {
|
||||
// If the `url` argument is a string literal, allow `http` and `https` schemes.
|
||||
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
|
||||
if call.arguments.find_argument("url", 0).and_then(leading_chars).is_some_and(has_http_prefix) {
|
||||
return None;
|
||||
if let Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) = &call.arguments.find_argument("url", 0) {
|
||||
let url = value.to_str().trim_start();
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
return None;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Some(SuspiciousURLOpenUsage.into())
|
||||
@@ -915,24 +868,27 @@ pub(crate) fn suspicious_function_call(checker: &mut Checker, call: &ExprCall) {
|
||||
["urllib", "request", "urlopen" | "urlretrieve" ] |
|
||||
["six", "moves", "urllib", "request", "urlopen" | "urlretrieve" ] => {
|
||||
if call.arguments.args.iter().all(|arg| !arg.is_starred_expr()) && call.arguments.keywords.iter().all(|keyword| keyword.arg.is_some()) {
|
||||
match call.arguments.find_argument("url", 0) {
|
||||
// If the `url` argument is a `urllib.request.Request` object, allow `http` and `https` schemes.
|
||||
Some(Expr::Call(ExprCall { func, arguments, .. })) => {
|
||||
if checker.semantic().resolve_qualified_name(func.as_ref()).is_some_and(|name| name.segments() == ["urllib", "request", "Request"]) {
|
||||
if arguments.find_argument("url", 0).and_then(leading_chars).is_some_and(has_http_prefix) {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
if let Some(arg) = &call.arguments.find_argument("url", 0) {
|
||||
// If the `url` argument is a string literal, allow `http` and `https` schemes.
|
||||
Some(expr) => {
|
||||
if leading_chars(expr).is_some_and(has_http_prefix) {
|
||||
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = arg {
|
||||
let url = value.to_str().trim_start();
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
_ => {}
|
||||
// If the `url` argument is a `urllib.request.Request` object, allow `http` and `https` schemes.
|
||||
if let Expr::Call(ExprCall { func, arguments, .. }) = arg {
|
||||
if checker.semantic().resolve_qualified_name(func.as_ref()).is_some_and(|name| name.segments() == ["urllib", "request", "Request"]) {
|
||||
if let Some( Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) = arguments.find_argument("url", 0) {
|
||||
let url = value.to_str().trim_start();
|
||||
if url.starts_with("http://") || url.starts_with("https://") {
|
||||
return None;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(SuspiciousURLOpenUsage.into())
|
||||
|
||||
@@ -1,232 +1,150 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_bandit/mod.rs
|
||||
---
|
||||
S310.py:4:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
3 | urllib.request.urlopen(url='http://www.google.com')
|
||||
4 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
5 | urllib.request.urlopen('http://www.google.com')
|
||||
6 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
|
|
||||
|
||||
S310.py:6:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
4 | urllib.request.urlopen(url=f'http://www.google.com')
|
||||
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
|
||||
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
|
||||
8 | urllib.request.urlopen('http://www.google.com')
|
||||
4 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
5 | urllib.request.urlopen('http://www.google.com')
|
||||
6 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
7 | urllib.request.urlopen(url)
|
||||
|
|
||||
|
||||
S310.py:7:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
5 | urllib.request.urlopen(url='http://' + 'www' + '.google.com')
|
||||
6 | urllib.request.urlopen(url='http://www.google.com', **kwargs)
|
||||
7 | urllib.request.urlopen(url=f'http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
8 | urllib.request.urlopen('http://www.google.com')
|
||||
9 | urllib.request.urlopen(f'http://www.google.com')
|
||||
5 | urllib.request.urlopen('http://www.google.com')
|
||||
6 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
7 | urllib.request.urlopen(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
8 |
|
||||
9 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
|
|
||||
|
||||
S310.py:10:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
S310.py:9:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
8 | urllib.request.urlopen('http://www.google.com')
|
||||
9 | urllib.request.urlopen(f'http://www.google.com')
|
||||
10 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
11 | urllib.request.urlopen(url)
|
||||
7 | urllib.request.urlopen(url)
|
||||
8 |
|
||||
9 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
10 | urllib.request.Request(url='http://www.google.com')
|
||||
11 | urllib.request.Request('http://www.google.com')
|
||||
|
|
||||
|
||||
S310.py:11:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
S310.py:12:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
9 | urllib.request.urlopen(f'http://www.google.com')
|
||||
10 | urllib.request.urlopen('file:///foo/bar/baz')
|
||||
11 | urllib.request.urlopen(url)
|
||||
10 | urllib.request.Request(url='http://www.google.com')
|
||||
11 | urllib.request.Request('http://www.google.com')
|
||||
12 | urllib.request.Request('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
13 | urllib.request.Request(url)
|
||||
|
|
||||
|
||||
S310.py:13:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
11 | urllib.request.Request('http://www.google.com')
|
||||
12 | urllib.request.Request('file:///foo/bar/baz')
|
||||
13 | urllib.request.Request(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
12 |
|
||||
13 | urllib.request.Request(url='http://www.google.com')
|
||||
14 |
|
||||
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
|
|
||||
|
||||
S310.py:15:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
13 | urllib.request.Request(url)
|
||||
14 |
|
||||
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
17 | urllib.request.URLopener().open('http://www.google.com')
|
||||
|
|
||||
|
||||
S310.py:16:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
14 | urllib.request.Request(url=f'http://www.google.com')
|
||||
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
|
||||
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
|
||||
18 | urllib.request.Request('http://www.google.com')
|
||||
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
17 | urllib.request.URLopener().open('http://www.google.com')
|
||||
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
|
|
||||
|
||||
S310.py:17:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
15 | urllib.request.Request(url='http://' + 'www' + '.google.com')
|
||||
16 | urllib.request.Request(url='http://www.google.com', **kwargs)
|
||||
17 | urllib.request.Request(url=f'http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
18 | urllib.request.Request('http://www.google.com')
|
||||
19 | urllib.request.Request(f'http://www.google.com')
|
||||
|
|
||||
|
||||
S310.py:20:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
18 | urllib.request.Request('http://www.google.com')
|
||||
19 | urllib.request.Request(f'http://www.google.com')
|
||||
20 | urllib.request.Request('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
21 | urllib.request.Request(url)
|
||||
|
|
||||
|
||||
S310.py:21:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
19 | urllib.request.Request(f'http://www.google.com')
|
||||
20 | urllib.request.Request('file:///foo/bar/baz')
|
||||
21 | urllib.request.Request(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
22 |
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
|
|
||||
|
||||
S310.py:23:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
21 | urllib.request.Request(url)
|
||||
22 |
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
15 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
17 | urllib.request.URLopener().open('http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
19 | urllib.request.URLopener().open(url)
|
||||
|
|
||||
|
||||
S310.py:18:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
16 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
17 | urllib.request.URLopener().open('http://www.google.com')
|
||||
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
19 | urllib.request.URLopener().open(url)
|
||||
|
|
||||
|
||||
S310.py:19:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
17 | urllib.request.URLopener().open('http://www.google.com')
|
||||
18 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
19 | urllib.request.URLopener().open(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
20 |
|
||||
21 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
||||
|
|
||||
|
||||
S310.py:22:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
21 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
||||
22 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
23 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
24 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
|
|
||||
|
||||
S310.py:24:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
22 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
23 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
24 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
25 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
|
||||
|
||||
S310.py:24:24: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
22 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
23 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
24 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
25 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
|
||||
|
||||
S310.py:25:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
23 | urllib.request.URLopener().open(fullurl='http://www.google.com')
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
|
|
||||
|
||||
S310.py:26:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
24 | urllib.request.URLopener().open(fullurl=f'http://www.google.com')
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
|
|
||||
|
||||
S310.py:27:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
25 | urllib.request.URLopener().open(fullurl='http://' + 'www' + '.google.com')
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
|
|
||||
|
||||
S310.py:28:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
26 | urllib.request.URLopener().open(fullurl='http://www.google.com', **kwargs)
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
|
|
||||
|
||||
S310.py:29:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
27 | urllib.request.URLopener().open(fullurl=f'http://www.google.com', **kwargs)
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
|
|
||||
|
||||
S310.py:30:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
28 | urllib.request.URLopener().open('http://www.google.com')
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
32 | urllib.request.URLopener().open(url)
|
||||
|
|
||||
|
||||
S310.py:31:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
29 | urllib.request.URLopener().open(f'http://www.google.com')
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
32 | urllib.request.URLopener().open(url)
|
||||
|
|
||||
|
||||
S310.py:32:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
30 | urllib.request.URLopener().open('http://' + 'www' + '.google.com')
|
||||
31 | urllib.request.URLopener().open('file:///foo/bar/baz')
|
||||
32 | urllib.request.URLopener().open(url)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
33 |
|
||||
34 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'))
|
||||
|
|
||||
|
||||
S310.py:37:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
35 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'))
|
||||
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
|
||||
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
|
|
||||
|
||||
S310.py:38:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
36 | urllib.request.urlopen(url=urllib.request.Request('http://' + 'www' + '.google.com'))
|
||||
37 | urllib.request.urlopen(url=urllib.request.Request('http://www.google.com'), **kwargs)
|
||||
38 | urllib.request.urlopen(url=urllib.request.Request(f'http://www.google.com'), **kwargs)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
|
|
||||
|
||||
S310.py:41:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
|
||||
|
||||
S310.py:41:24: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
39 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
|
|
||||
|
||||
S310.py:42:1: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
23 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
24 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
25 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
|
|
||||
|
||||
S310.py:42:24: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
S310.py:25:24: S310 Audit URL open for permitted schemes. Allowing use of `file:` or custom schemes is often unexpected.
|
||||
|
|
||||
40 | urllib.request.urlopen(urllib.request.Request(f'http://www.google.com'))
|
||||
41 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
42 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
23 | urllib.request.urlopen(urllib.request.Request('http://www.google.com'))
|
||||
24 | urllib.request.urlopen(urllib.request.Request('file:///foo/bar/baz'))
|
||||
25 | urllib.request.urlopen(urllib.request.Request(url))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ S310
|
||||
|
|
||||
|
||||
@@ -2,3 +2,9 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_copyright/mod.rs
|
||||
---
|
||||
<filename>:1:1: CPY001 Missing copyright notice at top of file
|
||||
|
|
||||
1 | কককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককককক
|
||||
| CPY001
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -2,3 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_copyright/mod.rs
|
||||
---
|
||||
<filename>:1:1: CPY001 Missing copyright notice at top of file
|
||||
|
|
||||
1 | # Copyright (C) 2023 Some Author
|
||||
| CPY001
|
||||
2 |
|
||||
3 | import os
|
||||
|
|
||||
|
||||
|
||||
|
||||
@@ -2,3 +2,11 @@
|
||||
source: crates/ruff_linter/src/rules/flake8_copyright/mod.rs
|
||||
---
|
||||
<filename>:1:1: CPY001 Missing copyright notice at top of file
|
||||
|
|
||||
1 | # Content Content Content Content Content Content Content Content Content Content
|
||||
| CPY001
|
||||
2 | # Content Content Content Content Content Content Content Content Content Content
|
||||
3 | # Content Content Content Content Content Content Content Content Content Content
|
||||
|
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user