Compare commits
1 Commits
0.6.5
...
dhruv/rest
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dc0a523de |
@@ -45,7 +45,7 @@ repos:
|
||||
)$
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.24.5
|
||||
rev: v1.24.3
|
||||
hooks:
|
||||
- id: typos
|
||||
|
||||
@@ -59,7 +59,7 @@ repos:
|
||||
pass_filenames: false # This makes it a lot faster
|
||||
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.4
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
- id: ruff
|
||||
|
||||
59
CHANGELOG.md
59
CHANGELOG.md
@@ -1,64 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## 0.6.5
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`pydoclint`\] Ignore `DOC201` when function name is "**new**" ([#13300](https://github.com/astral-sh/ruff/pull/13300))
|
||||
- \[`refurb`\] Implement `slice-to-remove-prefix-or-suffix` (`FURB188`) ([#13256](https://github.com/astral-sh/ruff/pull/13256))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`eradicate`\] Ignore script-comments with multiple end-tags (`ERA001`) ([#13283](https://github.com/astral-sh/ruff/pull/13283))
|
||||
- \[`pyflakes`\] Improve error message for `UndefinedName` when a builtin was added in a newer version than specified in Ruff config (`F821`) ([#13293](https://github.com/astral-sh/ruff/pull/13293))
|
||||
|
||||
### Server
|
||||
|
||||
- Add support for extensionless Python files for server ([#13326](https://github.com/astral-sh/ruff/pull/13326))
|
||||
- Fix configuration inheritance for configurations specified in the LSP settings ([#13285](https://github.com/astral-sh/ruff/pull/13285))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`ruff`\] Handle unary operators in `decimal-from-float-literal` (`RUF032`) ([#13275](https://github.com/astral-sh/ruff/pull/13275))
|
||||
|
||||
### CLI
|
||||
|
||||
- Only include rules with diagnostics in SARIF metadata ([#13268](https://github.com/astral-sh/ruff/pull/13268))
|
||||
|
||||
### Playground
|
||||
|
||||
- Add "Copy as pyproject.toml/ruff.toml" and "Paste from TOML" ([#13328](https://github.com/astral-sh/ruff/pull/13328))
|
||||
- Fix errors not shown for restored snippet on page load ([#13262](https://github.com/astral-sh/ruff/pull/13262))
|
||||
|
||||
## 0.6.4
|
||||
|
||||
### Preview features
|
||||
|
||||
- \[`flake8-builtins`\] Use dynamic builtins list based on Python version ([#13172](https://github.com/astral-sh/ruff/pull/13172))
|
||||
- \[`pydoclint`\] Permit yielding `None` in `DOC402` and `DOC403` ([#13148](https://github.com/astral-sh/ruff/pull/13148))
|
||||
- \[`pylint`\] Update diagnostic message for `PLW3201` ([#13194](https://github.com/astral-sh/ruff/pull/13194))
|
||||
- \[`ruff`\] Implement `post-init-default` (`RUF033`) ([#13192](https://github.com/astral-sh/ruff/pull/13192))
|
||||
- \[`ruff`\] Implement useless if-else (`RUF034`) ([#13218](https://github.com/astral-sh/ruff/pull/13218))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- \[`flake8-pyi`\] Respect `pep8_naming.classmethod-decorators` settings when determining if a method is a classmethod in `custom-type-var-return-type` (`PYI019`) ([#13162](https://github.com/astral-sh/ruff/pull/13162))
|
||||
- \[`flake8-pyi`\] Teach various rules that annotations might be stringized ([#12951](https://github.com/astral-sh/ruff/pull/12951))
|
||||
- \[`pylint`\] Avoid `no-self-use` for `attrs`-style validators ([#13166](https://github.com/astral-sh/ruff/pull/13166))
|
||||
- \[`pylint`\] Recurse into subscript subexpressions when searching for list/dict lookups (`PLR1733`, `PLR1736`) ([#13186](https://github.com/astral-sh/ruff/pull/13186))
|
||||
- \[`pyupgrade`\] Detect `aiofiles.open` calls in `UP015` ([#13173](https://github.com/astral-sh/ruff/pull/13173))
|
||||
- \[`pyupgrade`\] Mark `sys.version_info[0] < 3` and similar comparisons as outdated (`UP036`) ([#13175](https://github.com/astral-sh/ruff/pull/13175))
|
||||
|
||||
### CLI
|
||||
|
||||
- Enrich messages of SARIF results ([#13180](https://github.com/astral-sh/ruff/pull/13180))
|
||||
- Handle singular case for incompatible rules warning in `ruff format` output ([#13212](https://github.com/astral-sh/ruff/pull/13212))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- \[`pydocstyle`\] Improve heuristics for detecting Google-style docstrings ([#13142](https://github.com/astral-sh/ruff/pull/13142))
|
||||
- \[`refurb`\] Treat `sep` arguments with effects as unsafe removals (`FURB105`) ([#13165](https://github.com/astral-sh/ruff/pull/13165))
|
||||
|
||||
## 0.6.3
|
||||
|
||||
### Preview features
|
||||
|
||||
125
Cargo.lock
generated
125
Cargo.lock
generated
@@ -194,15 +194,6 @@ version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.10.0"
|
||||
@@ -520,15 +511,6 @@ dependencies = [
|
||||
"rustc-hash 1.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.4.0"
|
||||
@@ -634,16 +616,6 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctrlc"
|
||||
version = "3.4.5"
|
||||
@@ -722,16 +694,6 @@ version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "4.0.0"
|
||||
@@ -917,16 +879,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getopts"
|
||||
version = "0.2.21"
|
||||
@@ -1160,8 +1112,6 @@ dependencies = [
|
||||
"globset",
|
||||
"lazy_static",
|
||||
"linked-hash-map",
|
||||
"pest",
|
||||
"pest_derive",
|
||||
"regex",
|
||||
"serde",
|
||||
"similar",
|
||||
@@ -1757,51 +1707,6 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "pest"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd53dff83f26735fdc1ca837098ccf133605d794cdae66acfc2bfac3ec809d95"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"thiserror",
|
||||
"ucd-trie",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_derive"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a548d2beca6773b1c244554d36fcf8548a8a58e74156968211567250e48e49a"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_generator"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c93a82e8d145725dcbaf44e5ea887c8a869efdcc28706df2d08c69e17077183"
|
||||
dependencies = [
|
||||
"pest",
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pest_meta"
|
||||
version = "2.7.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a941429fea7e08bedec25e4f6785b6ffaacc6b755da98df5ef3e7dcf4a124c4f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"pest",
|
||||
"sha2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
@@ -2031,7 +1936,6 @@ dependencies = [
|
||||
"smallvec",
|
||||
"static_assertions",
|
||||
"tempfile",
|
||||
"test-case",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"walkdir",
|
||||
@@ -2187,7 +2091,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2380,7 +2284,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2700,7 +2604,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
@@ -3031,17 +2935,6 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
@@ -3442,18 +3335,6 @@ version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
|
||||
|
||||
[[package]]
|
||||
name = "ucd-trie"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9"
|
||||
|
||||
[[package]]
|
||||
name = "unic-char-property"
|
||||
version = "0.9.0"
|
||||
|
||||
@@ -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.6.5/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.5/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.6.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.6.3/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.6.5
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -33,7 +33,6 @@ rustc-hash = { workspace = true }
|
||||
hashbrown = { workspace = true }
|
||||
smallvec = { workspace = true }
|
||||
static_assertions = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
path-slash = { workspace = true }
|
||||
|
||||
16
crates/red_knot_python_semantic/src/builtins.rs
Normal file
16
crates/red_knot_python_semantic/src/builtins.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::Db;
|
||||
|
||||
/// Salsa query to get the builtins scope.
|
||||
///
|
||||
/// Can return None if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
#[salsa::tracked]
|
||||
pub(crate) fn builtins_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
let builtins_name =
|
||||
ModuleName::new_static("builtins").expect("Expected 'builtins' to be a valid module name");
|
||||
let builtins_file = resolve_module(db, builtins_name)?.file();
|
||||
Some(global_scope(db, builtins_file))
|
||||
}
|
||||
@@ -10,6 +10,7 @@ pub use python_version::PythonVersion;
|
||||
pub use semantic_model::{HasTy, SemanticModel};
|
||||
|
||||
pub mod ast_node_ref;
|
||||
mod builtins;
|
||||
mod db;
|
||||
mod module_name;
|
||||
mod module_resolver;
|
||||
@@ -19,7 +20,6 @@ mod python_version;
|
||||
pub mod semantic_index;
|
||||
mod semantic_model;
|
||||
pub(crate) mod site_packages;
|
||||
mod stdlib;
|
||||
pub mod types;
|
||||
|
||||
type FxOrderSet<V> = ordermap::set::OrderSet<V, BuildHasherDefault<FxHasher>>;
|
||||
|
||||
@@ -59,10 +59,6 @@ impl ModulePath {
|
||||
self.relative_path.push(component);
|
||||
}
|
||||
|
||||
pub(crate) fn pop(&mut self) -> bool {
|
||||
self.relative_path.pop()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(super) fn is_directory(&self, resolver: &ResolverContext) -> bool {
|
||||
let ModulePath {
|
||||
|
||||
@@ -569,16 +569,24 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
|
||||
|
||||
package_path.push(module_name);
|
||||
|
||||
// Check for a regular package first (highest priority)
|
||||
package_path.push("__init__");
|
||||
if let Some(regular_package) = resolve_file_module(&package_path, &resolver_state) {
|
||||
return Some((search_path.clone(), regular_package, ModuleKind::Package));
|
||||
// Must be a `__init__.pyi` or `__init__.py` or it isn't a package.
|
||||
let kind = if package_path.is_directory(&resolver_state) {
|
||||
package_path.push("__init__");
|
||||
ModuleKind::Package
|
||||
} else {
|
||||
ModuleKind::Module
|
||||
};
|
||||
|
||||
// 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(&resolver_state) {
|
||||
return Some((search_path.clone(), stub, kind));
|
||||
}
|
||||
|
||||
// Check for a file module next
|
||||
package_path.pop();
|
||||
if let Some(file_module) = resolve_file_module(&package_path, &resolver_state) {
|
||||
return Some((search_path.clone(), file_module, ModuleKind::Module));
|
||||
if let Some(module) = package_path
|
||||
.with_py_extension()
|
||||
.and_then(|path| path.to_file(&resolver_state))
|
||||
{
|
||||
return Some((search_path.clone(), module, kind));
|
||||
}
|
||||
|
||||
// For regular packages, don't search the next search path. All files of that
|
||||
@@ -599,23 +607,6 @@ fn resolve_name(db: &dyn Db, name: &ModuleName) -> Option<(SearchPath, File, Mod
|
||||
None
|
||||
}
|
||||
|
||||
/// If `module` exists on disk with either a `.pyi` or `.py` extension,
|
||||
/// return the [`File`] corresponding to that path.
|
||||
///
|
||||
/// `.pyi` files take priority, as they always have priority when
|
||||
/// resolving modules.
|
||||
fn resolve_file_module(module: &ModulePath, resolver_state: &ResolverContext) -> Option<File> {
|
||||
// Stubs have precedence over source files
|
||||
module
|
||||
.with_pyi_extension()
|
||||
.to_file(resolver_state)
|
||||
.or_else(|| {
|
||||
module
|
||||
.with_py_extension()
|
||||
.and_then(|path| path.to_file(resolver_state))
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_package<'a, 'db, I>(
|
||||
module_search_path: &SearchPath,
|
||||
components: I,
|
||||
@@ -642,10 +633,7 @@ where
|
||||
|
||||
if is_regular_package {
|
||||
in_namespace_package = false;
|
||||
} else if package_path.is_directory(resolver_state)
|
||||
// Pure modules hide namespace packages with the same name
|
||||
&& resolve_file_module(&package_path, resolver_state).is_none()
|
||||
{
|
||||
} else if package_path.is_directory(resolver_state) {
|
||||
// A directory without an `__init__.py` is a namespace package, continue with the next folder.
|
||||
in_namespace_package = true;
|
||||
} else if in_namespace_package {
|
||||
@@ -1103,25 +1091,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_file_takes_priority_over_namespace_package() {
|
||||
//const SRC: &[FileSpec] = &[("foo.py", "x = 1")];
|
||||
const SRC: &[FileSpec] = &[("foo.py", "x = 1"), ("foo/bar.py", "x = 2")];
|
||||
|
||||
let TestCase { db, src, .. } = TestCaseBuilder::new().with_src_files(SRC).build();
|
||||
|
||||
let foo_module_name = ModuleName::new_static("foo").unwrap();
|
||||
let foo_bar_module_name = ModuleName::new_static("foo.bar").unwrap();
|
||||
|
||||
// `foo.py` takes priority over the `foo` namespace package
|
||||
let foo_module = resolve_module(&db, foo_module_name.clone()).unwrap();
|
||||
assert_eq!(foo_module.file().path(&db), &src.join("foo.py"));
|
||||
|
||||
// `foo.bar` isn't recognised as a module
|
||||
let foo_bar_module = resolve_module(&db, foo_bar_module_name.clone());
|
||||
assert_eq!(foo_bar_module, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typing_stub_over_module() {
|
||||
const SRC: &[FileSpec] = &[("foo.py", "print('Hello, world!')"), ("foo.pyi", "x: int")];
|
||||
|
||||
@@ -21,7 +21,6 @@ use crate::Db;
|
||||
|
||||
pub mod ast_ids;
|
||||
mod builder;
|
||||
pub(crate) mod constraint;
|
||||
pub mod definition;
|
||||
pub mod expression;
|
||||
pub mod symbol;
|
||||
|
||||
@@ -26,10 +26,7 @@ use crate::semantic_index::use_def::{FlowSnapshot, UseDefMapBuilder};
|
||||
use crate::semantic_index::SemanticIndex;
|
||||
use crate::Db;
|
||||
|
||||
use super::constraint::{Constraint, PatternConstraint};
|
||||
use super::definition::{
|
||||
ExceptHandlerDefinitionNodeRef, MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef,
|
||||
};
|
||||
use super::definition::{MatchPatternDefinitionNodeRef, WithItemDefinitionNodeRef};
|
||||
|
||||
pub(super) struct SemanticIndexBuilder<'db> {
|
||||
// Builder state
|
||||
@@ -196,50 +193,21 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
countme::Count::default(),
|
||||
);
|
||||
|
||||
let existing_definition = self
|
||||
.definitions_by_node
|
||||
self.definitions_by_node
|
||||
.insert(definition_node.key(), definition);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
|
||||
self.current_use_def_map_mut()
|
||||
.record_definition(symbol, definition);
|
||||
|
||||
definition
|
||||
}
|
||||
|
||||
fn add_expression_constraint(&mut self, constraint_node: &ast::Expr) -> Expression<'db> {
|
||||
fn add_constraint(&mut self, constraint_node: &ast::Expr) -> Expression<'db> {
|
||||
let expression = self.add_standalone_expression(constraint_node);
|
||||
self.current_use_def_map_mut()
|
||||
.record_constraint(Constraint::Expression(expression));
|
||||
self.current_use_def_map_mut().record_constraint(expression);
|
||||
|
||||
expression
|
||||
}
|
||||
|
||||
fn add_pattern_constraint(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
pattern: &ast::Pattern,
|
||||
) -> PatternConstraint<'db> {
|
||||
#[allow(unsafe_code)]
|
||||
let (subject, pattern) = unsafe {
|
||||
(
|
||||
AstNodeRef::new(self.module.clone(), subject),
|
||||
AstNodeRef::new(self.module.clone(), pattern),
|
||||
)
|
||||
};
|
||||
let pattern_constraint = PatternConstraint::new(
|
||||
self.db,
|
||||
self.file,
|
||||
self.current_scope(),
|
||||
subject,
|
||||
pattern,
|
||||
countme::Count::default(),
|
||||
);
|
||||
self.current_use_def_map_mut()
|
||||
.record_constraint(Constraint::Pattern(pattern_constraint));
|
||||
pattern_constraint
|
||||
}
|
||||
|
||||
/// Record an expression that needs to be a Salsa ingredient, because we need to infer its type
|
||||
/// standalone (type narrowing tests, RHS of an assignment.)
|
||||
fn add_standalone_expression(&mut self, expression_node: &ast::Expr) -> Expression<'db> {
|
||||
@@ -359,11 +327,10 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
// Insert a mapping from the parameter to the same definition.
|
||||
// This ensures that calling `HasTy::ty` on the inner parameter returns
|
||||
// a valid type (and doesn't panic)
|
||||
let existing_definition = self.definitions_by_node.insert(
|
||||
self.definitions_by_node.insert(
|
||||
DefinitionNodeRef::from(AnyParameterRef::Variadic(&with_default.parameter)).key(),
|
||||
definition,
|
||||
);
|
||||
debug_assert_eq!(existing_definition, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,6 +494,7 @@ where
|
||||
}
|
||||
ast::Stmt::AnnAssign(node) => {
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
// TODO deferred annotation visiting
|
||||
self.visit_expr(&node.annotation);
|
||||
if let Some(value) = &node.value {
|
||||
self.visit_expr(value);
|
||||
@@ -552,7 +520,7 @@ where
|
||||
ast::Stmt::If(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
let pre_if = self.flow_snapshot();
|
||||
self.add_expression_constraint(&node.test);
|
||||
self.add_constraint(&node.test);
|
||||
self.visit_body(&node.body);
|
||||
let mut post_clauses: Vec<FlowSnapshot> = vec![];
|
||||
for clause in &node.elif_else_clauses {
|
||||
@@ -577,23 +545,14 @@ where
|
||||
self.flow_merge(pre_if);
|
||||
}
|
||||
}
|
||||
ast::Stmt::While(ast::StmtWhile {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_expr(test);
|
||||
ast::Stmt::While(node) => {
|
||||
self.visit_expr(&node.test);
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
|
||||
// Save aside any break states from an outer loop
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
|
||||
// TODO: definitions created inside the body should be fully visible
|
||||
// to other statements/expressions inside the body --Alex/Carl
|
||||
self.visit_body(body);
|
||||
|
||||
self.visit_body(&node.body);
|
||||
// Get the break states from the body of this loop, and restore the saved outer
|
||||
// ones.
|
||||
let break_states =
|
||||
@@ -602,7 +561,7 @@ where
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(pre_loop);
|
||||
self.visit_body(orelse);
|
||||
self.visit_body(&node.orelse);
|
||||
|
||||
// Breaking out of a while loop bypasses the `else` clause, so merge in the break
|
||||
// states after visiting `else`.
|
||||
@@ -636,35 +595,15 @@ where
|
||||
orelse,
|
||||
},
|
||||
) => {
|
||||
// TODO add control flow similar to `ast::Stmt::While` above
|
||||
self.add_standalone_expression(iter);
|
||||
self.visit_expr(iter);
|
||||
|
||||
let pre_loop = self.flow_snapshot();
|
||||
let saved_break_states = std::mem::take(&mut self.loop_break_states);
|
||||
|
||||
debug_assert!(self.current_assignment.is_none());
|
||||
self.current_assignment = Some(for_stmt.into());
|
||||
self.visit_expr(target);
|
||||
self.current_assignment = None;
|
||||
|
||||
// TODO: Definitions created by loop variables
|
||||
// (and definitions created inside the body)
|
||||
// are fully visible to other statements/expressions inside the body --Alex/Carl
|
||||
self.visit_body(body);
|
||||
|
||||
let break_states =
|
||||
std::mem::replace(&mut self.loop_break_states, saved_break_states);
|
||||
|
||||
// We may execute the `else` clause without ever executing the body, so merge in
|
||||
// the pre-loop state before visiting `else`.
|
||||
self.flow_merge(pre_loop);
|
||||
self.visit_body(orelse);
|
||||
|
||||
// Breaking out of a `for` loop bypasses the `else` clause, so merge in the break
|
||||
// states after visiting `else`.
|
||||
for break_state in break_states {
|
||||
self.flow_merge(break_state);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Match(ast::StmtMatch {
|
||||
subject,
|
||||
@@ -673,75 +612,9 @@ where
|
||||
}) => {
|
||||
self.add_standalone_expression(subject);
|
||||
self.visit_expr(subject);
|
||||
|
||||
let after_subject = self.flow_snapshot();
|
||||
let Some((first, remaining)) = cases.split_first() else {
|
||||
return;
|
||||
};
|
||||
self.add_pattern_constraint(subject, &first.pattern);
|
||||
self.visit_match_case(first);
|
||||
|
||||
let mut post_case_snapshots = vec![];
|
||||
for case in remaining {
|
||||
post_case_snapshots.push(self.flow_snapshot());
|
||||
self.flow_restore(after_subject.clone());
|
||||
self.add_pattern_constraint(subject, &case.pattern);
|
||||
for case in cases {
|
||||
self.visit_match_case(case);
|
||||
}
|
||||
for post_clause_state in post_case_snapshots {
|
||||
self.flow_merge(post_clause_state);
|
||||
}
|
||||
if !cases
|
||||
.last()
|
||||
.is_some_and(|case| case.guard.is_none() && case.pattern.is_wildcard())
|
||||
{
|
||||
self.flow_merge(after_subject);
|
||||
}
|
||||
}
|
||||
ast::Stmt::Try(ast::StmtTry {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
is_star,
|
||||
range: _,
|
||||
}) => {
|
||||
self.visit_body(body);
|
||||
|
||||
for except_handler in handlers {
|
||||
let ast::ExceptHandler::ExceptHandler(except_handler) = except_handler;
|
||||
let ast::ExceptHandlerExceptHandler {
|
||||
name: symbol_name,
|
||||
type_: handled_exceptions,
|
||||
body: handler_body,
|
||||
range: _,
|
||||
} = except_handler;
|
||||
|
||||
if let Some(handled_exceptions) = handled_exceptions {
|
||||
self.visit_expr(handled_exceptions);
|
||||
}
|
||||
|
||||
// If `handled_exceptions` above was `None`, it's something like `except as e:`,
|
||||
// which is invalid syntax. However, it's still pretty obvious here that the user
|
||||
// *wanted* `e` to be bound, so we should still create a definition here nonetheless.
|
||||
if let Some(symbol_name) = symbol_name {
|
||||
let symbol = self
|
||||
.add_or_update_symbol(symbol_name.id.clone(), SymbolFlags::IS_DEFINED);
|
||||
|
||||
self.add_definition(
|
||||
symbol,
|
||||
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
|
||||
handler: except_handler,
|
||||
is_star: *is_star,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
self.visit_body(handler_body);
|
||||
}
|
||||
|
||||
self.visit_body(orelse);
|
||||
self.visit_body(finalbody);
|
||||
}
|
||||
_ => {
|
||||
walk_stmt(self, stmt);
|
||||
@@ -756,22 +629,21 @@ where
|
||||
|
||||
match expr {
|
||||
ast::Expr::Name(name_node @ ast::ExprName { id, ctx, .. }) => {
|
||||
let flags = match (ctx, self.current_assignment) {
|
||||
(ast::ExprContext::Store, Some(CurrentAssignment::AugAssign(_))) => {
|
||||
// For augmented assignment, the target expression is also used.
|
||||
SymbolFlags::IS_DEFINED | SymbolFlags::IS_USED
|
||||
}
|
||||
(ast::ExprContext::Store, Some(CurrentAssignment::AnnAssign(ann_assign)))
|
||||
if ann_assign.value.is_none() =>
|
||||
{
|
||||
// An annotated assignment that doesn't assign a value is not a Definition
|
||||
SymbolFlags::empty()
|
||||
}
|
||||
(ast::ExprContext::Load, _) => SymbolFlags::IS_USED,
|
||||
(ast::ExprContext::Store, _) => SymbolFlags::IS_DEFINED,
|
||||
(ast::ExprContext::Del, _) => SymbolFlags::IS_DEFINED,
|
||||
(ast::ExprContext::Invalid, _) => SymbolFlags::empty(),
|
||||
let mut flags = match ctx {
|
||||
ast::ExprContext::Load => SymbolFlags::IS_USED,
|
||||
ast::ExprContext::Store => SymbolFlags::IS_DEFINED,
|
||||
ast::ExprContext::Del => SymbolFlags::IS_DEFINED,
|
||||
ast::ExprContext::Invalid => SymbolFlags::empty(),
|
||||
};
|
||||
if matches!(
|
||||
self.current_assignment,
|
||||
Some(CurrentAssignment::AugAssign(_))
|
||||
) && !ctx.is_invalid()
|
||||
{
|
||||
// For augmented assignment, the target expression is also used, so we should
|
||||
// record that as a use.
|
||||
flags |= SymbolFlags::IS_USED;
|
||||
}
|
||||
let symbol = self.add_or_update_symbol(id.clone(), flags);
|
||||
if flags.contains(SymbolFlags::IS_DEFINED) {
|
||||
match self.current_assignment {
|
||||
@@ -796,7 +668,6 @@ where
|
||||
ForStmtDefinitionNodeRef {
|
||||
iterable: &node.iter,
|
||||
target: name_node,
|
||||
is_async: node.is_async,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -813,7 +684,6 @@ where
|
||||
iterable: &node.iter,
|
||||
target: name_node,
|
||||
first,
|
||||
is_async: node.is_async,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::ast_node_ref::AstNodeRef;
|
||||
use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Constraint<'db> {
|
||||
Expression(Expression<'db>),
|
||||
Pattern(PatternConstraint<'db>),
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(crate) struct PatternConstraint<'db> {
|
||||
#[id]
|
||||
pub(crate) file: File,
|
||||
|
||||
#[id]
|
||||
pub(crate) file_scope: FileScopeId,
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) subject: AstNodeRef<ast::Expr>,
|
||||
|
||||
#[no_eq]
|
||||
#[return_ref]
|
||||
pub(crate) pattern: AstNodeRef<ast::Pattern>,
|
||||
|
||||
#[no_eq]
|
||||
count: countme::Count<PatternConstraint<'static>>,
|
||||
}
|
||||
|
||||
impl<'db> PatternConstraint<'db> {
|
||||
pub(crate) fn scope(self, db: &'db dyn Db) -> ScopeId<'db> {
|
||||
self.file_scope(db).to_scope_id(db, self.file(db))
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,6 @@ pub(crate) enum DefinitionNodeRef<'a> {
|
||||
Parameter(ast::AnyParameterRef<'a>),
|
||||
WithItem(WithItemDefinitionNodeRef<'a>),
|
||||
MatchPattern(MatchPatternDefinitionNodeRef<'a>),
|
||||
ExceptHandler(ExceptHandlerDefinitionNodeRef<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::StmtFunctionDef> for DefinitionNodeRef<'a> {
|
||||
@@ -153,13 +152,6 @@ pub(crate) struct WithItemDefinitionNodeRef<'a> {
|
||||
pub(crate) struct ForStmtDefinitionNodeRef<'a> {
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub(crate) struct ExceptHandlerDefinitionNodeRef<'a> {
|
||||
pub(crate) handler: &'a ast::ExceptHandlerExceptHandler,
|
||||
pub(crate) is_star: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -167,7 +159,6 @@ pub(crate) struct ComprehensionDefinitionNodeRef<'a> {
|
||||
pub(crate) iterable: &'a ast::Expr,
|
||||
pub(crate) target: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
pub(crate) is_async: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -215,25 +206,20 @@ impl DefinitionNodeRef<'_> {
|
||||
DefinitionNodeRef::AugmentedAssignment(augmented_assignment) => {
|
||||
DefinitionKind::AugmentedAssignment(AstNodeRef::new(parsed, augmented_assignment))
|
||||
}
|
||||
DefinitionNodeRef::For(ForStmtDefinitionNodeRef {
|
||||
iterable,
|
||||
target,
|
||||
is_async,
|
||||
}) => DefinitionKind::For(ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::For(ForStmtDefinitionNodeRef { iterable, target }) => {
|
||||
DefinitionKind::For(ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
})
|
||||
}
|
||||
DefinitionNodeRef::Comprehension(ComprehensionDefinitionNodeRef {
|
||||
iterable,
|
||||
target,
|
||||
first,
|
||||
is_async,
|
||||
}) => DefinitionKind::Comprehension(ComprehensionDefinitionKind {
|
||||
iterable: AstNodeRef::new(parsed.clone(), iterable),
|
||||
target: AstNodeRef::new(parsed, target),
|
||||
first,
|
||||
is_async,
|
||||
}),
|
||||
DefinitionNodeRef::Parameter(parameter) => match parameter {
|
||||
ast::AnyParameterRef::Variadic(parameter) => {
|
||||
@@ -258,13 +244,6 @@ impl DefinitionNodeRef<'_> {
|
||||
identifier: AstNodeRef::new(parsed, identifier),
|
||||
index,
|
||||
}),
|
||||
DefinitionNodeRef::ExceptHandler(ExceptHandlerDefinitionNodeRef {
|
||||
handler,
|
||||
is_star,
|
||||
}) => DefinitionKind::ExceptHandler(ExceptHandlerDefinitionKind {
|
||||
handler: AstNodeRef::new(parsed.clone(), handler),
|
||||
is_star,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,7 +265,6 @@ impl DefinitionNodeRef<'_> {
|
||||
Self::For(ForStmtDefinitionNodeRef {
|
||||
iterable: _,
|
||||
target,
|
||||
is_async: _,
|
||||
}) => target.into(),
|
||||
Self::Comprehension(ComprehensionDefinitionNodeRef { target, .. }) => target.into(),
|
||||
Self::Parameter(node) => match node {
|
||||
@@ -297,7 +275,6 @@ impl DefinitionNodeRef<'_> {
|
||||
Self::MatchPattern(MatchPatternDefinitionNodeRef { identifier, .. }) => {
|
||||
identifier.into()
|
||||
}
|
||||
Self::ExceptHandler(ExceptHandlerDefinitionNodeRef { handler, .. }) => handler.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,7 +295,6 @@ pub enum DefinitionKind {
|
||||
ParameterWithDefault(AstNodeRef<ast::ParameterWithDefault>),
|
||||
WithItem(WithItemDefinitionKind),
|
||||
MatchPattern(MatchPatternDefinitionKind),
|
||||
ExceptHandler(ExceptHandlerDefinitionKind),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -344,7 +320,6 @@ pub struct ComprehensionDefinitionKind {
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl ComprehensionDefinitionKind {
|
||||
@@ -359,10 +334,6 @@ impl ComprehensionDefinitionKind {
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
|
||||
pub(crate) fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -417,7 +388,6 @@ impl WithItemDefinitionKind {
|
||||
pub struct ForStmtDefinitionKind {
|
||||
iterable: AstNodeRef<ast::Expr>,
|
||||
target: AstNodeRef<ast::ExprName>,
|
||||
is_async: bool,
|
||||
}
|
||||
|
||||
impl ForStmtDefinitionKind {
|
||||
@@ -428,26 +398,6 @@ impl ForStmtDefinitionKind {
|
||||
pub(crate) fn target(&self) -> &ast::ExprName {
|
||||
self.target.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_async(&self) -> bool {
|
||||
self.is_async
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExceptHandlerDefinitionKind {
|
||||
handler: AstNodeRef<ast::ExceptHandlerExceptHandler>,
|
||||
is_star: bool,
|
||||
}
|
||||
|
||||
impl ExceptHandlerDefinitionKind {
|
||||
pub(crate) fn handled_exceptions(&self) -> Option<&ast::Expr> {
|
||||
self.handler.node().type_.as_deref()
|
||||
}
|
||||
|
||||
pub(crate) fn is_star(&self) -> bool {
|
||||
self.is_star
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
|
||||
@@ -518,9 +468,3 @@ impl From<&ast::Identifier> for DefinitionNodeKey {
|
||||
Self(NodeKey::from_node(identifier))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ast::ExceptHandlerExceptHandler> for DefinitionNodeKey {
|
||||
fn from(handler: &ast::ExceptHandlerExceptHandler) -> Self {
|
||||
Self(NodeKey::from_node(handler))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,11 +146,10 @@ use self::symbol_state::{
|
||||
};
|
||||
use crate::semantic_index::ast_ids::ScopedUseId;
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::ScopedSymbolId;
|
||||
use ruff_index::IndexVec;
|
||||
|
||||
use super::constraint::Constraint;
|
||||
|
||||
mod bitset;
|
||||
mod symbol_state;
|
||||
|
||||
@@ -160,8 +159,8 @@ pub(crate) struct UseDefMap<'db> {
|
||||
/// Array of [`Definition`] in this scope.
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
|
||||
/// Array of [`Constraint`] in this scope.
|
||||
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
/// Array of constraints (as [`Expression`]) in this scope.
|
||||
all_constraints: IndexVec<ScopedConstraintId, Expression<'db>>,
|
||||
|
||||
/// [`SymbolState`] visible at a [`ScopedUseId`].
|
||||
definitions_by_use: IndexVec<ScopedUseId, SymbolState>,
|
||||
@@ -205,7 +204,7 @@ impl<'db> UseDefMap<'db> {
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DefinitionWithConstraintsIterator<'map, 'db> {
|
||||
all_definitions: &'map IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Expression<'db>>,
|
||||
inner: DefinitionIdWithConstraintsIterator<'map>,
|
||||
}
|
||||
|
||||
@@ -233,12 +232,12 @@ pub(crate) struct DefinitionWithConstraints<'map, 'db> {
|
||||
}
|
||||
|
||||
pub(crate) struct ConstraintsIterator<'map, 'db> {
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
all_constraints: &'map IndexVec<ScopedConstraintId, Expression<'db>>,
|
||||
constraint_ids: ConstraintIdIterator<'map>,
|
||||
}
|
||||
|
||||
impl<'map, 'db> Iterator for ConstraintsIterator<'map, 'db> {
|
||||
type Item = Constraint<'db>;
|
||||
type Item = Expression<'db>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.constraint_ids
|
||||
@@ -260,8 +259,8 @@ pub(super) struct UseDefMapBuilder<'db> {
|
||||
/// Append-only array of [`Definition`]; None is unbound.
|
||||
all_definitions: IndexVec<ScopedDefinitionId, Definition<'db>>,
|
||||
|
||||
/// Append-only array of [`Constraint`].
|
||||
all_constraints: IndexVec<ScopedConstraintId, Constraint<'db>>,
|
||||
/// Append-only array of constraints (as [`Expression`]).
|
||||
all_constraints: IndexVec<ScopedConstraintId, Expression<'db>>,
|
||||
|
||||
/// Visible definitions at each so-far-recorded use.
|
||||
definitions_by_use: IndexVec<ScopedUseId, SymbolState>,
|
||||
@@ -291,7 +290,7 @@ impl<'db> UseDefMapBuilder<'db> {
|
||||
self.definitions_by_symbol[symbol] = SymbolState::with(def_id);
|
||||
}
|
||||
|
||||
pub(super) fn record_constraint(&mut self, constraint: Constraint<'db>) {
|
||||
pub(super) fn record_constraint(&mut self, constraint: Expression<'db>) {
|
||||
let constraint_id = self.all_constraints.push(constraint);
|
||||
for definitions in &mut self.definitions_by_symbol {
|
||||
definitions.add_constraint(constraint_id);
|
||||
|
||||
@@ -32,26 +32,17 @@ impl<const B: usize> BitSet<B> {
|
||||
bitset
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(super) fn is_empty(&self) -> bool {
|
||||
self.blocks().iter().all(|&b| b == 0)
|
||||
}
|
||||
|
||||
/// Convert from Inline to Heap, if needed, and resize the Heap vector, if needed.
|
||||
fn resize(&mut self, value: u32) {
|
||||
let num_blocks_needed = (value / 64) + 1;
|
||||
self.resize_blocks(num_blocks_needed as usize);
|
||||
}
|
||||
|
||||
fn resize_blocks(&mut self, num_blocks_needed: usize) {
|
||||
match self {
|
||||
Self::Inline(blocks) => {
|
||||
let mut vec = blocks.to_vec();
|
||||
vec.resize(num_blocks_needed, 0);
|
||||
vec.resize(num_blocks_needed as usize, 0);
|
||||
*self = Self::Heap(vec);
|
||||
}
|
||||
Self::Heap(vec) => {
|
||||
vec.resize(num_blocks_needed, 0);
|
||||
vec.resize(num_blocks_needed as usize, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,20 +89,6 @@ impl<const B: usize> BitSet<B> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Union in-place with another [`BitSet`].
|
||||
#[allow(unused)]
|
||||
pub(super) fn union(&mut self, other: &BitSet<B>) {
|
||||
let mut max_len = self.blocks().len();
|
||||
let other_len = other.blocks().len();
|
||||
if other_len > max_len {
|
||||
max_len = other_len;
|
||||
self.resize_blocks(max_len);
|
||||
}
|
||||
for (my_block, other_block) in self.blocks_mut().iter_mut().zip(other.blocks()) {
|
||||
*my_block |= other_block;
|
||||
}
|
||||
}
|
||||
|
||||
/// Return an iterator over the values (in ascending order) in this [`BitSet`].
|
||||
pub(super) fn iter(&self) -> BitSetIterator<'_, B> {
|
||||
let blocks = self.blocks();
|
||||
@@ -241,59 +218,6 @@ mod tests {
|
||||
assert_bitset(&b1, &[89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union() {
|
||||
let mut b1 = BitSet::<1>::with(2);
|
||||
let b2 = BitSet::<1>::with(4);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[2, 4]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_1() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(5);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 5, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_mixed_2() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(23);
|
||||
b2.insert(89);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 23, 89]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap() {
|
||||
let mut b1 = BitSet::<1>::with(4);
|
||||
let mut b2 = BitSet::<1>::with(4);
|
||||
b1.insert(89);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[4, 89, 90]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn union_heap_2() {
|
||||
let mut b1 = BitSet::<1>::with(89);
|
||||
let mut b2 = BitSet::<1>::with(89);
|
||||
b1.insert(91);
|
||||
b2.insert(90);
|
||||
|
||||
b1.union(&b2);
|
||||
assert_bitset(&b1, &[89, 90, 91]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_blocks() {
|
||||
let mut b = BitSet::<2>::with(120);
|
||||
@@ -301,11 +225,4 @@ mod tests {
|
||||
assert!(matches!(b, BitSet::Inline(_)));
|
||||
assert_bitset(&b, &[45, 120]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let b = BitSet::<1>::default();
|
||||
|
||||
assert!(b.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{resolve_module, Module};
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::semantic_index;
|
||||
use crate::types::{definition_ty, global_symbol_ty, infer_scope_types, Type};
|
||||
use crate::types::{definition_ty, global_symbol_ty_by_name, infer_scope_types, Type};
|
||||
use crate::Db;
|
||||
|
||||
pub struct SemanticModel<'db> {
|
||||
@@ -40,7 +40,7 @@ impl<'db> SemanticModel<'db> {
|
||||
}
|
||||
|
||||
pub fn global_symbol_ty(&self, module: &Module, symbol_name: &str) -> Type<'db> {
|
||||
global_symbol_ty(self.db, module.file(), symbol_name)
|
||||
global_symbol_ty_by_name(self.db, module.file(), symbol_name)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::resolve_module;
|
||||
use crate::semantic_index::global_scope;
|
||||
use crate::semantic_index::symbol::ScopeId;
|
||||
use crate::types::{global_symbol_ty, Type};
|
||||
use crate::Db;
|
||||
|
||||
/// Enumeration of various core stdlib modules, for which we have dedicated Salsa queries.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum CoreStdlibModule {
|
||||
Builtins,
|
||||
Types,
|
||||
Typeshed,
|
||||
}
|
||||
|
||||
impl CoreStdlibModule {
|
||||
fn name(self) -> ModuleName {
|
||||
let module_name = match self {
|
||||
Self::Builtins => "builtins",
|
||||
Self::Types => "types",
|
||||
Self::Typeshed => "_typeshed",
|
||||
};
|
||||
ModuleName::new_static(module_name)
|
||||
.unwrap_or_else(|| panic!("{module_name} should be a valid module name!"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in a given core module
|
||||
///
|
||||
/// Returns `Unbound` if the given core module cannot be resolved for some reason
|
||||
fn core_module_symbol_ty<'db>(
|
||||
db: &'db dyn Db,
|
||||
core_module: CoreStdlibModule,
|
||||
symbol: &str,
|
||||
) -> Type<'db> {
|
||||
resolve_module(db, core_module.name())
|
||||
.map(|module| global_symbol_ty(db, module.file(), symbol))
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the builtins namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `builtins` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn builtins_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Builtins, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `types` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `types` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn types_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Types, symbol)
|
||||
}
|
||||
|
||||
/// Lookup the type of `symbol` in the `_typeshed` module namespace.
|
||||
///
|
||||
/// Returns `Unbound` if the `_typeshed` module isn't available for some reason.
|
||||
#[inline]
|
||||
pub(crate) fn typeshed_symbol_ty<'db>(db: &'db dyn Db, symbol: &str) -> Type<'db> {
|
||||
core_module_symbol_ty(db, CoreStdlibModule::Typeshed, symbol)
|
||||
}
|
||||
|
||||
/// Get the scope of a core stdlib module.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing the core module in question.
|
||||
fn core_module_scope(db: &dyn Db, core_module: CoreStdlibModule) -> Option<ScopeId<'_>> {
|
||||
resolve_module(db, core_module.name()).map(|module| global_scope(db, module.file()))
|
||||
}
|
||||
|
||||
/// Get the `builtins` module scope.
|
||||
///
|
||||
/// Can return `None` if a custom typeshed is used that is missing `builtins.pyi`.
|
||||
pub(crate) fn builtins_module_scope(db: &dyn Db) -> Option<ScopeId<'_>> {
|
||||
core_module_scope(db, CoreStdlibModule::Builtins)
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
use infer::TypeInferenceBuilder;
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast as ast;
|
||||
|
||||
use crate::module_resolver::file_to_module;
|
||||
use crate::builtins::builtins_scope;
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::definition::{Definition, DefinitionKind};
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId};
|
||||
@@ -10,7 +9,6 @@ use crate::semantic_index::{
|
||||
global_scope, semantic_index, symbol_table, use_def_map, DefinitionWithConstraints,
|
||||
DefinitionWithConstraintsIterator,
|
||||
};
|
||||
use crate::stdlib::{builtins_symbol_ty, types_symbol_ty, typeshed_symbol_ty};
|
||||
use crate::types::narrow::narrowing_constraint;
|
||||
use crate::{Db, FxOrderSet};
|
||||
|
||||
@@ -18,6 +16,7 @@ pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder};
|
||||
pub(crate) use self::diagnostic::TypeCheckDiagnostics;
|
||||
pub(crate) use self::infer::{
|
||||
infer_deferred_types, infer_definition_types, infer_expression_types, infer_scope_types,
|
||||
TypeInference,
|
||||
};
|
||||
|
||||
mod builder;
|
||||
@@ -41,7 +40,7 @@ pub fn check_types(db: &dyn Db, file: File) -> TypeCheckDiagnostics {
|
||||
}
|
||||
|
||||
/// Infer the public type of a symbol (its type as seen from outside its scope).
|
||||
pub(crate) fn symbol_ty_by_id<'db>(
|
||||
pub(crate) fn symbol_ty<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
symbol: ScopedSymbolId,
|
||||
@@ -59,17 +58,30 @@ pub(crate) fn symbol_ty_by_id<'db>(
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty` that takes a symbol name instead of an ID.
|
||||
pub(crate) fn symbol_ty<'db>(db: &'db dyn Db, scope: ScopeId<'db>, name: &str) -> Type<'db> {
|
||||
pub(crate) fn symbol_ty_by_name<'db>(
|
||||
db: &'db dyn Db,
|
||||
scope: ScopeId<'db>,
|
||||
name: &str,
|
||||
) -> Type<'db> {
|
||||
let table = symbol_table(db, scope);
|
||||
table
|
||||
.symbol_id_by_name(name)
|
||||
.map(|symbol| symbol_ty_by_id(db, scope, symbol))
|
||||
.map(|symbol| symbol_ty(db, scope, symbol))
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty` that looks up a module-global symbol by name in a file.
|
||||
pub(crate) fn global_symbol_ty<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
|
||||
symbol_ty(db, global_scope(db, file), name)
|
||||
pub(crate) fn global_symbol_ty_by_name<'db>(db: &'db dyn Db, file: File, name: &str) -> Type<'db> {
|
||||
symbol_ty_by_name(db, global_scope(db, file), name)
|
||||
}
|
||||
|
||||
/// Shorthand for `symbol_ty` that looks up a symbol in the builtins.
|
||||
///
|
||||
/// Returns `Unbound` if the builtins module isn't available for some reason.
|
||||
pub(crate) fn builtins_symbol_ty_by_name<'db>(db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
builtins_scope(db)
|
||||
.map(|builtins| symbol_ty_by_name(db, builtins, name))
|
||||
.unwrap_or(Type::Unbound)
|
||||
}
|
||||
|
||||
/// Infer the type of a [`Definition`].
|
||||
@@ -121,8 +133,8 @@ pub(crate) fn definitions_ty<'db>(
|
||||
definition,
|
||||
constraints,
|
||||
}| {
|
||||
let mut constraint_tys = constraints
|
||||
.filter_map(|constraint| narrowing_constraint(db, constraint, definition));
|
||||
let mut constraint_tys =
|
||||
constraints.filter_map(|test| narrowing_constraint(db, test, definition));
|
||||
let definition_ty = definition_ty(db, definition);
|
||||
if let Some(first_constraint_ty) = constraint_tys.next() {
|
||||
let mut builder = IntersectionBuilder::new(db);
|
||||
@@ -145,7 +157,14 @@ pub(crate) fn definitions_ty<'db>(
|
||||
.expect("definitions_ty should never be called with zero definitions and no unbound_ty.");
|
||||
|
||||
if let Some(second) = all_types.next() {
|
||||
UnionType::from_elements(db, [first, second].into_iter().chain(all_types))
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
builder = builder.add(first).add(second);
|
||||
|
||||
for variant in all_types {
|
||||
builder = builder.add(variant);
|
||||
}
|
||||
|
||||
builder.build()
|
||||
} else {
|
||||
first
|
||||
}
|
||||
@@ -189,9 +208,6 @@ pub enum Type<'db> {
|
||||
LiteralString,
|
||||
/// A bytes literal
|
||||
BytesLiteral(BytesLiteralType<'db>),
|
||||
/// A heterogeneous tuple type, with elements of the given types in source order.
|
||||
// TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`.
|
||||
Tuple(TupleType<'db>),
|
||||
// TODO protocols, callable types, overloads, generics, type vars
|
||||
}
|
||||
|
||||
@@ -290,53 +306,17 @@ impl<'db> Type<'db> {
|
||||
pub fn replace_unbound_with(&self, db: &'db dyn Db, replacement: Type<'db>) -> Type<'db> {
|
||||
match self {
|
||||
Type::Unbound => replacement,
|
||||
Type::Union(union) => {
|
||||
union.map(db, |element| element.replace_unbound_with(db, replacement))
|
||||
}
|
||||
Type::Union(union) => union
|
||||
.elements(db)
|
||||
.into_iter()
|
||||
.fold(UnionBuilder::new(db), |builder, ty| {
|
||||
builder.add(ty.replace_unbound_with(db, replacement))
|
||||
})
|
||||
.build(),
|
||||
ty => *ty,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is [assignable to] type `target`.
|
||||
///
|
||||
/// [assignable to]: https://typing.readthedocs.io/en/latest/spec/concepts.html#the-assignable-to-or-consistent-subtyping-relation
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool {
|
||||
if self.is_equivalent_to(db, target) {
|
||||
return true;
|
||||
}
|
||||
match (self, target) {
|
||||
(Type::Unknown | Type::Any | Type::Never, _) => true,
|
||||
(_, Type::Unknown | Type::Any) => true,
|
||||
(Type::IntLiteral(_), Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "int") =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::StringLiteral(_), Type::LiteralString) => true,
|
||||
(Type::StringLiteral(_) | Type::LiteralString, Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "str") =>
|
||||
{
|
||||
true
|
||||
}
|
||||
(Type::BytesLiteral(_), Type::Instance(class))
|
||||
if class.is_stdlib_symbol(db, "builtins", "bytes") =>
|
||||
{
|
||||
true
|
||||
}
|
||||
// TODO
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return true if this type is equivalent to type `other`.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_equivalent_to(self, _db: &'db dyn Db, other: Type<'db>) -> bool {
|
||||
// TODO equivalent but not identical structural types, differently-ordered unions and
|
||||
// intersections, other cases?
|
||||
self == other
|
||||
}
|
||||
|
||||
/// Resolve a member access of a type.
|
||||
///
|
||||
/// For example, if `foo` is `Type::Instance(<Bar>)`,
|
||||
@@ -351,7 +331,7 @@ impl<'db> Type<'db> {
|
||||
/// us to explicitly consider whether to handle an error or propagate
|
||||
/// it up the call stack.
|
||||
#[must_use]
|
||||
pub fn member(&self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub fn member(&self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
match self {
|
||||
Type::Any => Type::Any,
|
||||
Type::Never => {
|
||||
@@ -368,13 +348,19 @@ impl<'db> Type<'db> {
|
||||
// TODO: attribute lookup on function type
|
||||
Type::Unknown
|
||||
}
|
||||
Type::Module(file) => global_symbol_ty(db, *file, name),
|
||||
Type::Module(file) => global_symbol_ty_by_name(db, *file, name),
|
||||
Type::Class(class) => class.class_member(db, name),
|
||||
Type::Instance(_) => {
|
||||
// TODO MRO? get_own_instance_member, get_instance_member
|
||||
Type::Unknown
|
||||
}
|
||||
Type::Union(union) => union.map(db, |element| element.member(db, name)),
|
||||
Type::Union(union) => union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.fold(UnionBuilder::new(db), |builder, element_ty| {
|
||||
builder.add(element_ty.member(db, name))
|
||||
})
|
||||
.build(),
|
||||
Type::Intersection(_) => {
|
||||
// TODO perform the get_member on each type in the intersection
|
||||
// TODO return the intersection of those results
|
||||
@@ -399,10 +385,6 @@ impl<'db> Type<'db> {
|
||||
// TODO defer to Type::Instance(<bytes from typeshed>).member
|
||||
Type::Unknown
|
||||
}
|
||||
Type::Tuple(_) => {
|
||||
// TODO: implement tuple methods
|
||||
Type::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,138 +415,13 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the type of an object that is iterated over in some way,
|
||||
/// return the type of objects that are yielded by that iteration.
|
||||
///
|
||||
/// E.g., for the following loop, given the type of `x`, infer the type of `y`:
|
||||
/// ```python
|
||||
/// for y in x:
|
||||
/// pass
|
||||
/// ```
|
||||
fn iterate(&self, db: &'db dyn Db) -> IterationOutcome<'db> {
|
||||
if let Type::Tuple(tuple_type) = self {
|
||||
return IterationOutcome::Iterable {
|
||||
element_ty: UnionType::from_elements(db, &**tuple_type.elements(db)),
|
||||
};
|
||||
}
|
||||
|
||||
// `self` represents the type of the iterable;
|
||||
// `__iter__` and `__next__` are both looked up on the class of the iterable:
|
||||
let iterable_meta_type = self.to_meta_type(db);
|
||||
|
||||
let dunder_iter_method = iterable_meta_type.member(db, "__iter__");
|
||||
if !dunder_iter_method.is_unbound() {
|
||||
let Some(iterator_ty) = dunder_iter_method.call(db) else {
|
||||
return IterationOutcome::NotIterable {
|
||||
not_iterable_ty: *self,
|
||||
};
|
||||
};
|
||||
|
||||
let dunder_next_method = iterator_ty.to_meta_type(db).member(db, "__next__");
|
||||
return dunder_next_method
|
||||
.call(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
not_iterable_ty: *self,
|
||||
});
|
||||
}
|
||||
|
||||
// Although it's not considered great practice,
|
||||
// classes that define `__getitem__` are also iterable,
|
||||
// even if they do not define `__iter__`.
|
||||
//
|
||||
// TODO(Alex) this is only valid if the `__getitem__` method is annotated as
|
||||
// accepting `int` or `SupportsIndex`
|
||||
let dunder_get_item_method = iterable_meta_type.member(db, "__getitem__");
|
||||
|
||||
dunder_get_item_method
|
||||
.call(db)
|
||||
.map(|element_ty| IterationOutcome::Iterable { element_ty })
|
||||
.unwrap_or(IterationOutcome::NotIterable {
|
||||
not_iterable_ty: *self,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn to_instance(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
pub fn to_instance(&self) -> Type<'db> {
|
||||
match self {
|
||||
Type::Any => Type::Any,
|
||||
Type::Unknown => Type::Unknown,
|
||||
Type::Unbound => Type::Unknown,
|
||||
Type::Never => Type::Never,
|
||||
Type::Class(class) => Type::Instance(*class),
|
||||
Type::Union(union) => union.map(db, |element| element.to_instance(db)),
|
||||
// TODO: we can probably do better here: --Alex
|
||||
Type::Intersection(_) => Type::Unknown,
|
||||
// TODO: calling `.to_instance()` on any of these should result in a diagnostic,
|
||||
// since they already indicate that the object is an instance of some kind:
|
||||
Type::BooleanLiteral(_)
|
||||
| Type::BytesLiteral(_)
|
||||
| Type::Function(_)
|
||||
| Type::Instance(_)
|
||||
| Type::Module(_)
|
||||
| Type::IntLiteral(_)
|
||||
| Type::StringLiteral(_)
|
||||
| Type::Tuple(_)
|
||||
| Type::LiteralString
|
||||
| Type::None => Type::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a type that is assumed to represent an instance of a class,
|
||||
/// return a type that represents that class itself.
|
||||
#[must_use]
|
||||
pub fn to_meta_type(&self, db: &'db dyn Db) -> Type<'db> {
|
||||
match self {
|
||||
Type::Unbound => Type::Unbound,
|
||||
Type::Never => Type::Never,
|
||||
Type::Instance(class) => Type::Class(*class),
|
||||
Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)),
|
||||
Type::BooleanLiteral(_) => builtins_symbol_ty(db, "bool"),
|
||||
Type::BytesLiteral(_) => builtins_symbol_ty(db, "bytes"),
|
||||
Type::IntLiteral(_) => builtins_symbol_ty(db, "int"),
|
||||
Type::Function(_) => types_symbol_ty(db, "FunctionType"),
|
||||
Type::Module(_) => types_symbol_ty(db, "ModuleType"),
|
||||
Type::None => typeshed_symbol_ty(db, "NoneType"),
|
||||
// TODO not accurate if there's a custom metaclass...
|
||||
Type::Class(_) => builtins_symbol_ty(db, "type"),
|
||||
// TODO can we do better here? `type[LiteralString]`?
|
||||
Type::StringLiteral(_) | Type::LiteralString => builtins_symbol_ty(db, "str"),
|
||||
// TODO: `type[Any]`?
|
||||
Type::Any => Type::Any,
|
||||
// TODO: `type[Unknown]`?
|
||||
Type::Unknown => Type::Unknown,
|
||||
// TODO intersections
|
||||
Type::Intersection(_) => Type::Unknown,
|
||||
Type::Tuple(_) => builtins_symbol_ty(db, "tuple"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> From<&Type<'db>> for Type<'db> {
|
||||
fn from(value: &Type<'db>) -> Self {
|
||||
*value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum IterationOutcome<'db> {
|
||||
Iterable { element_ty: Type<'db> },
|
||||
NotIterable { not_iterable_ty: Type<'db> },
|
||||
}
|
||||
|
||||
impl<'db> IterationOutcome<'db> {
|
||||
fn unwrap_with_diagnostic(
|
||||
self,
|
||||
iterable_node: ast::AnyNodeRef,
|
||||
inference_builder: &mut TypeInferenceBuilder<'db>,
|
||||
) -> Type<'db> {
|
||||
match self {
|
||||
Self::Iterable { element_ty } => element_ty,
|
||||
Self::NotIterable { not_iterable_ty } => {
|
||||
inference_builder.not_iterable_diagnostic(iterable_node, not_iterable_ty);
|
||||
Type::Unknown
|
||||
}
|
||||
_ => Type::Unknown, // TODO type errors
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -629,15 +486,6 @@ pub struct ClassType<'db> {
|
||||
}
|
||||
|
||||
impl<'db> ClassType<'db> {
|
||||
/// Return true if this class is a standard library type with given module name and name.
|
||||
#[allow(unused)]
|
||||
pub(crate) fn is_stdlib_symbol(self, db: &'db dyn Db, module_name: &str, name: &str) -> bool {
|
||||
name == self.name(db)
|
||||
&& file_to_module(db, self.body_scope(db).file(db)).is_some_and(|module| {
|
||||
module.search_path().is_standard_library() && module.name() == module_name
|
||||
})
|
||||
}
|
||||
|
||||
/// Return an iterator over the types of this class's bases.
|
||||
///
|
||||
/// # Panics:
|
||||
@@ -656,7 +504,7 @@ impl<'db> ClassType<'db> {
|
||||
/// Returns the class member of this class named `name`.
|
||||
///
|
||||
/// The member resolves to a member of the class itself or any of its bases.
|
||||
pub fn class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub fn class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
let member = self.own_class_member(db, name);
|
||||
if !member.is_unbound() {
|
||||
return member;
|
||||
@@ -666,12 +514,12 @@ impl<'db> ClassType<'db> {
|
||||
}
|
||||
|
||||
/// Returns the inferred type of the class member named `name`.
|
||||
pub fn own_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub fn own_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
let scope = self.body_scope(db);
|
||||
symbol_ty(db, scope, name)
|
||||
symbol_ty_by_name(db, scope, name)
|
||||
}
|
||||
|
||||
pub fn inherited_class_member(self, db: &'db dyn Db, name: &str) -> Type<'db> {
|
||||
pub fn inherited_class_member(self, db: &'db dyn Db, name: &ast::name::Name) -> Type<'db> {
|
||||
for base in self.bases(db) {
|
||||
let member = base.member(db, name);
|
||||
if !member.is_unbound() {
|
||||
@@ -694,30 +542,6 @@ impl<'db> UnionType<'db> {
|
||||
pub fn contains(&self, db: &'db dyn Db, ty: Type<'db>) -> bool {
|
||||
self.elements(db).contains(&ty)
|
||||
}
|
||||
|
||||
/// Create a union from a list of elements
|
||||
/// (which may be eagerly simplified into a different variant of [`Type`] altogether)
|
||||
pub fn from_elements<T: Into<Type<'db>>>(
|
||||
db: &'db dyn Db,
|
||||
elements: impl IntoIterator<Item = T>,
|
||||
) -> Type<'db> {
|
||||
elements
|
||||
.into_iter()
|
||||
.fold(UnionBuilder::new(db), |builder, element| {
|
||||
builder.add(element.into())
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
/// Apply a transformation function to all elements of the union,
|
||||
/// and create a new union from the resulting set of types
|
||||
pub fn map(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
transform_fn: impl Fn(&Type<'db>) -> Type<'db>,
|
||||
) -> Type<'db> {
|
||||
Self::from_elements(db, self.elements(db).into_iter().map(transform_fn))
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
@@ -747,35 +571,29 @@ pub struct BytesLiteralType<'db> {
|
||||
value: Box<[u8]>,
|
||||
}
|
||||
|
||||
#[salsa::interned]
|
||||
pub struct TupleType<'db> {
|
||||
#[return_ref]
|
||||
elements: Box<[Type<'db>]>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{builtins_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::ProgramSettings;
|
||||
use anyhow::Context;
|
||||
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
|
||||
use super::TypeCheckDiagnostics;
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
let db = TestDb::new();
|
||||
|
||||
let src_root = SystemPathBuf::from("/src");
|
||||
db.memory_file_system()
|
||||
.create_directory_all(&src_root)
|
||||
.create_directory_all("/src")
|
||||
.unwrap();
|
||||
|
||||
Program::from_settings(
|
||||
&db,
|
||||
&ProgramSettings {
|
||||
target_version: PythonVersion::default(),
|
||||
search_paths: SearchPathSettings::new(src_root),
|
||||
search_paths: SearchPathSettings::new(SystemPathBuf::from("/src")),
|
||||
},
|
||||
)
|
||||
.expect("Valid search path settings");
|
||||
@@ -783,73 +601,91 @@ mod tests {
|
||||
db
|
||||
}
|
||||
|
||||
/// A test representation of a type that can be transformed unambiguously into a real Type,
|
||||
/// given a db.
|
||||
#[derive(Debug)]
|
||||
enum Ty {
|
||||
Never,
|
||||
Unknown,
|
||||
Any,
|
||||
IntLiteral(i64),
|
||||
StringLiteral(&'static str),
|
||||
LiteralString,
|
||||
BytesLiteral(&'static str),
|
||||
BuiltinInstance(&'static str),
|
||||
Union(Vec<Ty>),
|
||||
fn assert_diagnostic_messages(diagnostics: &TypeCheckDiagnostics, expected: &[&str]) {
|
||||
let messages: Vec<&str> = diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| diagnostic.message())
|
||||
.collect();
|
||||
assert_eq!(&messages, expected);
|
||||
}
|
||||
|
||||
impl Ty {
|
||||
fn into_type(self, db: &TestDb) -> Type<'_> {
|
||||
match self {
|
||||
Ty::Never => Type::Never,
|
||||
Ty::Unknown => Type::Unknown,
|
||||
Ty::Any => Type::Any,
|
||||
Ty::IntLiteral(n) => Type::IntLiteral(n),
|
||||
Ty::StringLiteral(s) => {
|
||||
Type::StringLiteral(StringLiteralType::new(db, (*s).into()))
|
||||
}
|
||||
Ty::LiteralString => Type::LiteralString,
|
||||
Ty::BytesLiteral(s) => {
|
||||
Type::BytesLiteral(BytesLiteralType::new(db, s.as_bytes().into()))
|
||||
}
|
||||
Ty::BuiltinInstance(s) => builtins_symbol_ty(db, s).to_instance(db),
|
||||
Ty::Union(tys) => {
|
||||
UnionType::from_elements(db, tys.into_iter().map(|ty| ty.into_type(db)))
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn unresolved_import_statement() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_file("src/foo.py", "import bar\n")
|
||||
.context("Failed to write foo.py")?;
|
||||
|
||||
let foo = system_path_to_file(&db, "src/foo.py").context("Failed to resolve foo.py")?;
|
||||
|
||||
let diagnostics = super::check_types(&db, foo);
|
||||
assert_diagnostic_messages(&diagnostics, &["Cannot resolve import 'bar'."]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Ty::Unknown, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::Any, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::Never, Ty::IntLiteral(1))]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Unknown)]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::Any)]
|
||||
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("int"))]
|
||||
#[test_case(Ty::StringLiteral("foo"), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::StringLiteral("foo"), Ty::LiteralString)]
|
||||
#[test_case(Ty::LiteralString, Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::BytesLiteral("foo"), Ty::BuiltinInstance("bytes"))]
|
||||
fn is_assignable_to(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
assert!(from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
|
||||
#[test]
|
||||
fn unresolved_import_from_statement() {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_file("src/foo.py", "from bar import baz\n")
|
||||
.unwrap();
|
||||
let foo = system_path_to_file(&db, "src/foo.py").unwrap();
|
||||
let diagnostics = super::check_types(&db, foo);
|
||||
assert_diagnostic_messages(&diagnostics, &["Cannot resolve import 'bar'."]);
|
||||
}
|
||||
|
||||
#[test_case(Ty::IntLiteral(1), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::BuiltinInstance("str"))]
|
||||
#[test_case(Ty::BuiltinInstance("int"), Ty::IntLiteral(1))]
|
||||
fn is_not_assignable_to(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
assert!(!from.into_type(&db).is_assignable_to(&db, to.into_type(&db)));
|
||||
#[test]
|
||||
fn unresolved_import_from_resolved_module() {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_files([("/src/a.py", ""), ("/src/b.py", "from a import thing")])
|
||||
.unwrap();
|
||||
|
||||
let b_file = system_path_to_file(&db, "/src/b.py").unwrap();
|
||||
let b_file_diagnostics = super::check_types(&db, b_file);
|
||||
assert_diagnostic_messages(&b_file_diagnostics, &["Module 'a' has no member 'thing'"]);
|
||||
}
|
||||
|
||||
#[test_case(
|
||||
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)]),
|
||||
Ty::Union(vec![Ty::IntLiteral(1), Ty::IntLiteral(2)])
|
||||
)]
|
||||
fn is_equivalent_to(from: Ty, to: Ty) {
|
||||
let db = setup_db();
|
||||
#[test]
|
||||
fn resolved_import_of_symbol_from_unresolved_import() {
|
||||
let mut db = setup_db();
|
||||
|
||||
assert!(from.into_type(&db).is_equivalent_to(&db, to.into_type(&db)));
|
||||
db.write_files([
|
||||
("/src/a.py", "import foo as foo"),
|
||||
("/src/b.py", "from a import foo"),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let a_file_diagnostics = super::check_types(&db, a_file);
|
||||
assert_diagnostic_messages(&a_file_diagnostics, &["Cannot resolve import 'foo'."]);
|
||||
|
||||
// Importing the unresolved import into a second first-party file should not trigger
|
||||
// an additional "unresolved import" violation
|
||||
let b_file = system_path_to_file(&db, "/src/b.py").unwrap();
|
||||
let b_file_diagnostics = super::check_types(&db, b_file);
|
||||
assert_eq!(&*b_file_diagnostics, &[]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_callable() {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
nonsense = 123
|
||||
x = nonsense()
|
||||
",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let a_file = system_path_to_file(&db, "/src/a.py").unwrap();
|
||||
let a_file_diagnostics = super::check_types(&db, a_file);
|
||||
assert_diagnostic_messages(
|
||||
&a_file_diagnostics,
|
||||
&["Object of type 'Literal[123]' is not callable"],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,10 +25,13 @@
|
||||
//! * No type in an intersection can be a supertype of any other type in the intersection (just
|
||||
//! eliminate the supertype from the intersection).
|
||||
//! * An intersection containing two non-overlapping types should simplify to [`Type::Never`].
|
||||
use crate::types::{builtins_symbol_ty, IntersectionType, Type, UnionType};
|
||||
|
||||
use crate::types::{IntersectionType, Type, UnionType};
|
||||
use crate::{Db, FxOrderSet};
|
||||
use ordermap::set::MutableValues;
|
||||
|
||||
use super::builtins_symbol_ty_by_name;
|
||||
|
||||
pub(crate) struct UnionBuilder<'db> {
|
||||
elements: FxOrderSet<Type<'db>>,
|
||||
db: &'db dyn Db,
|
||||
@@ -65,7 +68,7 @@ impl<'db> UnionBuilder<'db> {
|
||||
if let Some(true_index) = self.elements.get_index_of(&Type::BooleanLiteral(true)) {
|
||||
if self.elements.contains(&Type::BooleanLiteral(false)) {
|
||||
*self.elements.get_index_mut2(true_index).unwrap() =
|
||||
builtins_symbol_ty(self.db, "bool");
|
||||
builtins_symbol_ty_by_name(self.db, "bool");
|
||||
self.elements.remove(&Type::BooleanLiteral(false));
|
||||
}
|
||||
}
|
||||
@@ -169,12 +172,11 @@ impl<'db> IntersectionBuilder<'db> {
|
||||
if self.intersections.len() == 1 {
|
||||
self.intersections.pop().unwrap().build(self.db)
|
||||
} else {
|
||||
UnionType::from_elements(
|
||||
self.db,
|
||||
self.intersections
|
||||
.into_iter()
|
||||
.map(|inner| inner.build(self.db)),
|
||||
)
|
||||
let mut builder = UnionBuilder::new(self.db);
|
||||
for inner in self.intersections {
|
||||
builder = builder.add(inner.build(self.db));
|
||||
}
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -272,11 +274,11 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionType};
|
||||
use super::{IntersectionBuilder, IntersectionType, Type, UnionBuilder, UnionType};
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::program::{Program, SearchPathSettings};
|
||||
use crate::python_version::PythonVersion;
|
||||
use crate::types::{builtins_symbol_ty, UnionBuilder};
|
||||
use crate::types::builtins_symbol_ty_by_name;
|
||||
use crate::ProgramSettings;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
@@ -311,7 +313,11 @@ mod tests {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let union = UnionType::from_elements(&db, [t0, t1]).expect_union();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(t0)
|
||||
.add(t1)
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
assert_eq!(union.elements_vec(&db), &[t0, t1]);
|
||||
}
|
||||
@@ -320,7 +326,8 @@ mod tests {
|
||||
fn build_union_single() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0]);
|
||||
let ty = UnionBuilder::new(&db).add(t0).build();
|
||||
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
@@ -328,6 +335,7 @@ mod tests {
|
||||
fn build_union_empty() {
|
||||
let db = setup_db();
|
||||
let ty = UnionBuilder::new(&db).build();
|
||||
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
@@ -335,24 +343,36 @@ mod tests {
|
||||
fn build_union_never() {
|
||||
let db = setup_db();
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let ty = UnionType::from_elements(&db, [t0, Type::Never]);
|
||||
let ty = UnionBuilder::new(&db).add(t0).add(Type::Never).build();
|
||||
|
||||
assert_eq!(ty, t0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_union_bool() {
|
||||
let db = setup_db();
|
||||
let bool_ty = builtins_symbol_ty(&db, "bool");
|
||||
let bool_ty = builtins_symbol_ty_by_name(&db, "bool");
|
||||
|
||||
let t0 = Type::BooleanLiteral(true);
|
||||
let t1 = Type::BooleanLiteral(true);
|
||||
let t2 = Type::BooleanLiteral(false);
|
||||
let t3 = Type::IntLiteral(17);
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t3]).expect_union();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(t0)
|
||||
.add(t1)
|
||||
.add(t3)
|
||||
.build()
|
||||
.expect_union();
|
||||
assert_eq!(union.elements_vec(&db), &[t0, t3]);
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(t0)
|
||||
.add(t1)
|
||||
.add(t2)
|
||||
.add(t3)
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
let union = UnionType::from_elements(&db, [t0, t1, t2, t3]).expect_union();
|
||||
assert_eq!(union.elements_vec(&db), &[bool_ty, t3]);
|
||||
}
|
||||
|
||||
@@ -362,8 +382,12 @@ mod tests {
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let t2 = Type::IntLiteral(2);
|
||||
let u1 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let union = UnionType::from_elements(&db, [u1, t2]).expect_union();
|
||||
let u1 = UnionBuilder::new(&db).add(t0).add(t1).build();
|
||||
let union = UnionBuilder::new(&db)
|
||||
.add(u1)
|
||||
.add(t2)
|
||||
.build()
|
||||
.expect_union();
|
||||
|
||||
assert_eq!(union.elements_vec(&db), &[t0, t1, t2]);
|
||||
}
|
||||
@@ -439,7 +463,7 @@ mod tests {
|
||||
let t0 = Type::IntLiteral(0);
|
||||
let t1 = Type::IntLiteral(1);
|
||||
let ta = Type::Any;
|
||||
let u0 = UnionType::from_elements(&db, [t0, t1]);
|
||||
let u0 = UnionBuilder::new(&db).add(t0).add(t1).build();
|
||||
|
||||
let union = IntersectionBuilder::new(&db)
|
||||
.add_positive(ta)
|
||||
|
||||
@@ -86,23 +86,6 @@ impl std::fmt::Display for DisplayRepresentation<'_> {
|
||||
|
||||
escape.bytes_repr().write(f)
|
||||
}
|
||||
Type::Tuple(tuple) => {
|
||||
f.write_str("tuple[")?;
|
||||
let elements = tuple.elements(self.db);
|
||||
if elements.is_empty() {
|
||||
f.write_str("()")?;
|
||||
} else {
|
||||
let mut first = true;
|
||||
for element in &**elements {
|
||||
if !first {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
first = false;
|
||||
element.display(self.db).fmt(f)?;
|
||||
}
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,7 +236,9 @@ mod tests {
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::types::{global_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType};
|
||||
use crate::types::{
|
||||
global_symbol_ty_by_name, BytesLiteralType, StringLiteralType, Type, UnionBuilder,
|
||||
};
|
||||
use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings};
|
||||
|
||||
fn setup_db() -> TestDb {
|
||||
@@ -295,23 +280,26 @@ mod tests {
|
||||
)?;
|
||||
let mod_file = system_path_to_file(&db, "src/main.py").expect("Expected file to exist.");
|
||||
|
||||
let union_elements = &[
|
||||
let vec: Vec<Type<'_>> = vec![
|
||||
Type::Unknown,
|
||||
Type::IntLiteral(-1),
|
||||
global_symbol_ty(&db, mod_file, "A"),
|
||||
global_symbol_ty_by_name(&db, mod_file, "A"),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, Box::from("A"))),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([0]))),
|
||||
Type::BytesLiteral(BytesLiteralType::new(&db, Box::from([7]))),
|
||||
Type::IntLiteral(0),
|
||||
Type::IntLiteral(1),
|
||||
Type::StringLiteral(StringLiteralType::new(&db, Box::from("B"))),
|
||||
global_symbol_ty(&db, mod_file, "foo"),
|
||||
global_symbol_ty(&db, mod_file, "bar"),
|
||||
global_symbol_ty(&db, mod_file, "B"),
|
||||
global_symbol_ty_by_name(&db, mod_file, "foo"),
|
||||
global_symbol_ty_by_name(&db, mod_file, "bar"),
|
||||
global_symbol_ty_by_name(&db, mod_file, "B"),
|
||||
Type::BooleanLiteral(true),
|
||||
Type::None,
|
||||
];
|
||||
let union = UnionType::from_elements(&db, union_elements).expect_union();
|
||||
let builder = vec.iter().fold(UnionBuilder::new(&db), |builder, literal| {
|
||||
builder.add(*literal)
|
||||
});
|
||||
let union = builder.build().expect_union();
|
||||
let display = format!("{}", union.display(&db));
|
||||
assert_eq!(
|
||||
display,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
||||
use crate::semantic_index::ast_ids::HasScopedAstId;
|
||||
use crate::semantic_index::constraint::{Constraint, PatternConstraint};
|
||||
use crate::semantic_index::definition::Definition;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{ScopeId, ScopedSymbolId, SymbolTable};
|
||||
use crate::semantic_index::symbol_table;
|
||||
use crate::types::{infer_expression_types, IntersectionBuilder, Type};
|
||||
use crate::types::{infer_expression_types, IntersectionBuilder, Type, TypeInference};
|
||||
use crate::Db;
|
||||
use ruff_python_ast as ast;
|
||||
use rustc_hash::FxHashMap;
|
||||
@@ -28,114 +27,62 @@ use std::sync::Arc;
|
||||
/// constraint is applied to that definition, so we'd just return `None`.
|
||||
pub(crate) fn narrowing_constraint<'db>(
|
||||
db: &'db dyn Db,
|
||||
constraint: Constraint<'db>,
|
||||
test: Expression<'db>,
|
||||
definition: Definition<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
match constraint {
|
||||
Constraint::Expression(expression) => {
|
||||
all_narrowing_constraints_for_expression(db, expression)
|
||||
.get(&definition.symbol(db))
|
||||
.copied()
|
||||
}
|
||||
Constraint::Pattern(pattern) => all_narrowing_constraints_for_pattern(db, pattern)
|
||||
.get(&definition.symbol(db))
|
||||
.copied(),
|
||||
}
|
||||
all_narrowing_constraints(db, test)
|
||||
.get(&definition.symbol(db))
|
||||
.copied()
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn all_narrowing_constraints_for_pattern<'db>(
|
||||
fn all_narrowing_constraints<'db>(
|
||||
db: &'db dyn Db,
|
||||
pattern: PatternConstraint<'db>,
|
||||
test: Expression<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
NarrowingConstraintsBuilder::new(db, Constraint::Pattern(pattern)).finish()
|
||||
}
|
||||
|
||||
#[salsa::tracked(return_ref)]
|
||||
fn all_narrowing_constraints_for_expression<'db>(
|
||||
db: &'db dyn Db,
|
||||
expression: Expression<'db>,
|
||||
) -> NarrowingConstraints<'db> {
|
||||
NarrowingConstraintsBuilder::new(db, Constraint::Expression(expression)).finish()
|
||||
NarrowingConstraintsBuilder::new(db, test).finish()
|
||||
}
|
||||
|
||||
type NarrowingConstraints<'db> = FxHashMap<ScopedSymbolId, Type<'db>>;
|
||||
|
||||
struct NarrowingConstraintsBuilder<'db> {
|
||||
db: &'db dyn Db,
|
||||
constraint: Constraint<'db>,
|
||||
expression: Expression<'db>,
|
||||
constraints: NarrowingConstraints<'db>,
|
||||
}
|
||||
|
||||
impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
fn new(db: &'db dyn Db, constraint: Constraint<'db>) -> Self {
|
||||
fn new(db: &'db dyn Db, expression: Expression<'db>) -> Self {
|
||||
Self {
|
||||
db,
|
||||
constraint,
|
||||
expression,
|
||||
constraints: NarrowingConstraints::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn finish(mut self) -> NarrowingConstraints<'db> {
|
||||
match self.constraint {
|
||||
Constraint::Expression(expression) => self.evaluate_expression_constraint(expression),
|
||||
Constraint::Pattern(pattern) => self.evaluate_pattern_constraint(pattern),
|
||||
if let ast::Expr::Compare(expr_compare) = self.expression.node_ref(self.db).node() {
|
||||
self.add_expr_compare(expr_compare);
|
||||
}
|
||||
// TODO other test expression kinds
|
||||
|
||||
self.constraints.shrink_to_fit();
|
||||
self.constraints
|
||||
}
|
||||
|
||||
fn evaluate_expression_constraint(&mut self, expression: Expression<'db>) {
|
||||
if let ast::Expr::Compare(expr_compare) = expression.node_ref(self.db).node() {
|
||||
self.add_expr_compare(expr_compare, expression);
|
||||
}
|
||||
// TODO other test expression kinds
|
||||
}
|
||||
|
||||
fn evaluate_pattern_constraint(&mut self, pattern: PatternConstraint<'db>) {
|
||||
let subject = pattern.subject(self.db);
|
||||
|
||||
match pattern.pattern(self.db).node() {
|
||||
ast::Pattern::MatchValue(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchSingleton(singleton_pattern) => {
|
||||
self.add_match_pattern_singleton(subject, singleton_pattern);
|
||||
}
|
||||
ast::Pattern::MatchSequence(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchMapping(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchClass(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchStar(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchAs(_) => {
|
||||
// TODO
|
||||
}
|
||||
ast::Pattern::MatchOr(_) => {
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn symbols(&self) -> Arc<SymbolTable> {
|
||||
symbol_table(self.db, self.scope())
|
||||
}
|
||||
|
||||
fn scope(&self) -> ScopeId<'db> {
|
||||
match self.constraint {
|
||||
Constraint::Expression(expression) => expression.scope(self.db),
|
||||
Constraint::Pattern(pattern) => pattern.scope(self.db),
|
||||
}
|
||||
self.expression.scope(self.db)
|
||||
}
|
||||
|
||||
fn add_expr_compare(&mut self, expr_compare: &ast::ExprCompare, expression: Expression<'db>) {
|
||||
fn inference(&self) -> &'db TypeInference<'db> {
|
||||
infer_expression_types(self.db, self.expression)
|
||||
}
|
||||
|
||||
fn add_expr_compare(&mut self, expr_compare: &ast::ExprCompare) {
|
||||
let ast::ExprCompare {
|
||||
range: _,
|
||||
left,
|
||||
@@ -152,7 +99,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
let inference = self.inference();
|
||||
for (op, comparator) in std::iter::zip(&**ops, &**comparators) {
|
||||
let comp_ty = inference.expression_ty(comparator.scoped_ast_id(self.db, scope));
|
||||
if matches!(op, ast::CmpOp::IsNot) {
|
||||
@@ -165,22 +112,4 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_match_pattern_singleton(
|
||||
&mut self,
|
||||
subject: &ast::Expr,
|
||||
pattern: &ast::PatternMatchSingleton,
|
||||
) {
|
||||
if let Some(ast::ExprName { id, .. }) = subject.as_name_expr() {
|
||||
// SAFETY: we should always have a symbol for every Name node.
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let ty = match pattern.value {
|
||||
ast::Singleton::None => Type::None,
|
||||
ast::Singleton::True => Type::BooleanLiteral(true),
|
||||
ast::Singleton::False => Type::BooleanLiteral(false),
|
||||
};
|
||||
self.constraints.insert(symbol, ty);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -66,4 +66,4 @@ codspeed = ["codspeed-criterion-compat"]
|
||||
mimalloc = { workspace = true }
|
||||
|
||||
[target.'cfg(all(not(target_os = "windows"), not(target_os = "openbsd"), any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "powerpc64")))'.dev-dependencies]
|
||||
tikv-jemallocator = { workspace = true, features = ["unprefixed_malloc_on_supported_platforms"] }
|
||||
tikv-jemallocator = { workspace = true }
|
||||
|
||||
@@ -28,24 +28,6 @@ static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||
#[global_allocator]
|
||||
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;
|
||||
|
||||
// Disable decay after 10s because it can show up as *random* slow allocations
|
||||
// in benchmarks. We don't need purging in benchmarks because it isn't important
|
||||
// to give unallocated pages back to the OS.
|
||||
// https://jemalloc.net/jemalloc.3.html#opt.dirty_decay_ms
|
||||
#[cfg(all(
|
||||
not(target_os = "windows"),
|
||||
not(target_os = "openbsd"),
|
||||
any(
|
||||
target_arch = "x86_64",
|
||||
target_arch = "aarch64",
|
||||
target_arch = "powerpc64"
|
||||
)
|
||||
))]
|
||||
#[allow(non_upper_case_globals)]
|
||||
#[export_name = "malloc_conf"]
|
||||
#[allow(unsafe_code)]
|
||||
pub static malloc_conf: &[u8] = b"dirty_decay_ms:-1,muzzy_decay_ms:-1\0";
|
||||
|
||||
fn create_test_cases() -> Result<Vec<TestCase>, TestFileDownloadError> {
|
||||
Ok(vec![
|
||||
TestCase::fast(TestFile::try_download("numpy/globals.py", "https://raw.githubusercontent.com/numpy/numpy/89d64415e349ca75a25250f22b874aa16e5c0973/numpy/_globals.py")?),
|
||||
|
||||
@@ -23,7 +23,6 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
|
||||
|
||||
// The failed import from 'collections.abc' is due to lack of support for 'import *'.
|
||||
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
"/src/tomllib/_parser.py:5:24: Module '__future__' has no member 'annotations'",
|
||||
"/src/tomllib/_parser.py:7:29: Module 'collections.abc' has no member 'Iterable'",
|
||||
"Line 69 is too long (89 characters)",
|
||||
"Use double quotes for strings",
|
||||
@@ -33,6 +32,7 @@ static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"Use double quotes for strings",
|
||||
"/src/tomllib/_parser.py:628:75: Name 'e' used when not defined.",
|
||||
];
|
||||
|
||||
fn get_test_file(name: &str) -> TestFile {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
@@ -73,7 +73,7 @@ unicode-normalization = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { workspace = true, features = ["filters", "json", "redactions"] }
|
||||
insta = { workspace = true }
|
||||
test-case = { workspace = true }
|
||||
# Disable colored output in tests
|
||||
colored = { workspace = true, features = ["no-color"] }
|
||||
|
||||
@@ -57,18 +57,7 @@ dictionary = {
|
||||
# ]
|
||||
# ///
|
||||
|
||||
# Script tag with multiple closing tags (OK)
|
||||
# /// script
|
||||
# [tool.uv]
|
||||
# extra-index-url = ["https://pypi.org/simple", """\
|
||||
# https://example.com/
|
||||
# ///
|
||||
# """
|
||||
# ]
|
||||
# ///
|
||||
print(1)
|
||||
|
||||
# Script tag without a closing tag (Error)
|
||||
# Script tag without a closing tag (OK)
|
||||
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
|
||||
@@ -207,10 +207,3 @@ def foo(s: str) -> str | None:
|
||||
s (str): A string.
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
class Spam:
|
||||
# OK
|
||||
def __new__(cls) -> 'Spam':
|
||||
"""New!!"""
|
||||
return cls()
|
||||
|
||||
@@ -94,9 +94,6 @@ class Apples:
|
||||
def __mro_entries__(self, bases):
|
||||
pass
|
||||
|
||||
# Removed with Python 3
|
||||
def __unicode__(self):
|
||||
pass
|
||||
|
||||
def __foo_bar__(): # this is not checked by the [bad-dunder-name] rule
|
||||
...
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
# Test suite from Refurb
|
||||
# See https://github.com/dosisod/refurb/blob/db02242b142285e615a664a8d3324470bb711306/test/data/err_188.py
|
||||
|
||||
# these should match
|
||||
|
||||
def remove_extension_via_slice(filename: str) -> str:
|
||||
if filename.endswith(".txt"):
|
||||
filename = filename[:-4]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_via_slice_len(filename: str, extension: str) -> str:
|
||||
if filename.endswith(extension):
|
||||
filename = filename[:-len(extension)]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_via_ternary(filename: str) -> str:
|
||||
return filename[:-4] if filename.endswith(".txt") else filename
|
||||
|
||||
|
||||
def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
|
||||
return filename[:-len(extension)] if filename.endswith(extension) else filename
|
||||
|
||||
|
||||
def remove_prefix(filename: str) -> str:
|
||||
return filename[4:] if filename.startswith("abc-") else filename
|
||||
|
||||
|
||||
def remove_prefix_via_len(filename: str, prefix: str) -> str:
|
||||
return filename[len(prefix):] if filename.startswith(prefix) else filename
|
||||
|
||||
|
||||
# these should not
|
||||
|
||||
def remove_extension_with_mismatched_len(filename: str) -> str:
|
||||
if filename.endswith(".txt"):
|
||||
filename = filename[:3]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_assign_to_different_var(filename: str) -> str:
|
||||
if filename.endswith(".txt"):
|
||||
other_var = filename[:-4]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_with_multiple_stmts(filename: str) -> str:
|
||||
if filename.endswith(".txt"):
|
||||
print("do some work")
|
||||
|
||||
filename = filename[:-4]
|
||||
|
||||
if filename.endswith(".txt"):
|
||||
filename = filename[:-4]
|
||||
|
||||
print("do some work")
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_from_unrelated_var(filename: str) -> str:
|
||||
xyz = "abc.txt"
|
||||
|
||||
if filename.endswith(".txt"):
|
||||
filename = xyz[:-4]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_in_elif(filename: str) -> str:
|
||||
if filename:
|
||||
pass
|
||||
|
||||
elif filename.endswith(".txt"):
|
||||
filename = filename[:-4]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_in_multiple_elif(filename: str) -> str:
|
||||
if filename:
|
||||
pass
|
||||
|
||||
elif filename:
|
||||
pass
|
||||
|
||||
elif filename.endswith(".txt"):
|
||||
filename = filename[:-4]
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_in_if_with_else(filename: str) -> str:
|
||||
if filename.endswith(".txt"):
|
||||
filename = filename[:-4]
|
||||
|
||||
else:
|
||||
pass
|
||||
|
||||
return filename
|
||||
|
||||
|
||||
def remove_extension_ternary_name_mismatch(filename: str):
|
||||
xyz = ""
|
||||
|
||||
_ = xyz[:-4] if filename.endswith(".txt") else filename
|
||||
_ = filename[:-4] if xyz.endswith(".txt") else filename
|
||||
_ = filename[:-4] if filename.endswith(".txt") else xyz
|
||||
|
||||
|
||||
def remove_extension_slice_amount_mismatch(filename: str) -> None:
|
||||
extension = ".txt"
|
||||
|
||||
_ = filename[:-1] if filename.endswith(".txt") else filename
|
||||
_ = filename[:-1] if filename.endswith(extension) else filename
|
||||
_ = filename[:-len("")] if filename.endswith(extension) else filename
|
||||
|
||||
|
||||
def remove_prefix_size_mismatch(filename: str) -> str:
|
||||
return filename[3:] if filename.startswith("abc-") else filename
|
||||
|
||||
|
||||
def remove_prefix_name_mismatch(filename: str) -> None:
|
||||
xyz = ""
|
||||
|
||||
_ = xyz[4:] if filename.startswith("abc-") else filename
|
||||
_ = filename[4:] if xyz.startswith("abc-") else filename
|
||||
_ = filename[4:] if filename.startswith("abc-") else xyz
|
||||
|
||||
# ---- End of refurb test suite ---- #
|
||||
|
||||
# ---- Begin ruff specific test suite --- #
|
||||
|
||||
# these should be linted
|
||||
|
||||
def remove_suffix_multiple_attribute_expr() -> None:
|
||||
import foo.bar
|
||||
|
||||
SUFFIX = "suffix"
|
||||
|
||||
x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
|
||||
|
||||
def remove_prefix_comparable_literal_expr() -> None:
|
||||
return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
|
||||
|
||||
def shadow_builtins(filename: str, extension: str) -> None:
|
||||
from builtins import len as builtins_len
|
||||
|
||||
return filename[:-builtins_len(extension)] if filename.endswith(extension) else filename
|
||||
@@ -50,13 +50,6 @@ a = 10.0
|
||||
|
||||
val = Decimal(a)
|
||||
|
||||
# See https://github.com/astral-sh/ruff/issues/13258
|
||||
val = Decimal(~4.0) # Skip
|
||||
|
||||
val = Decimal(++4.0) # Suggest `Decimal("4.0")`
|
||||
|
||||
val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
|
||||
|
||||
|
||||
# Tests with shadowed name
|
||||
class Decimal():
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Valid
|
||||
x = 1 if True else 2
|
||||
|
||||
# Invalid
|
||||
x = 1 if True else 1
|
||||
|
||||
# Invalid
|
||||
x = "a" if True else "a"
|
||||
|
||||
# Invalid
|
||||
x = 0.1 if False else 0.1
|
||||
@@ -1404,12 +1404,6 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::IfExpInsteadOfOrOperator) {
|
||||
refurb::rules::if_exp_instead_of_or_operator(checker, if_exp);
|
||||
}
|
||||
if checker.enabled(Rule::UselessIfElse) {
|
||||
ruff::rules::useless_if_else(checker, if_exp);
|
||||
}
|
||||
if checker.enabled(Rule::SliceToRemovePrefixOrSuffix) {
|
||||
refurb::rules::slice_to_remove_affix_expr(checker, if_exp);
|
||||
}
|
||||
}
|
||||
Expr::ListComp(
|
||||
comp @ ast::ExprListComp {
|
||||
|
||||
@@ -1178,9 +1178,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::CheckAndRemoveFromSet) {
|
||||
refurb::rules::check_and_remove_from_set(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::SliceToRemovePrefixOrSuffix) {
|
||||
refurb::rules::slice_to_remove_affix_stmt(checker, if_);
|
||||
}
|
||||
if checker.enabled(Rule::TooManyBooleanExpressions) {
|
||||
pylint::rules::too_many_boolean_expressions(checker, if_);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_semantic::Exceptions;
|
||||
use ruff_python_stdlib::builtins::version_builtin_was_added;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
@@ -36,12 +35,9 @@ pub(crate) fn unresolved_references(checker: &mut Checker) {
|
||||
}
|
||||
}
|
||||
|
||||
let symbol_name = reference.name(checker.locator);
|
||||
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName {
|
||||
name: symbol_name.to_string(),
|
||||
minor_version_builtin_added: version_builtin_was_added(symbol_name),
|
||||
name: reference.name(checker.locator).to_string(),
|
||||
},
|
||||
reference.range(),
|
||||
));
|
||||
|
||||
@@ -1951,25 +1951,20 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
|
||||
fn bind_builtins(&mut self) {
|
||||
let mut bind_builtin = |builtin| {
|
||||
// Add the builtin to the scope.
|
||||
let binding_id = self.semantic.push_builtin();
|
||||
let scope = self.semantic.global_scope_mut();
|
||||
scope.add(builtin, binding_id);
|
||||
};
|
||||
|
||||
let standard_builtins = python_builtins(
|
||||
self.settings.target_version.minor(),
|
||||
self.source_type.is_ipynb(),
|
||||
);
|
||||
for builtin in standard_builtins {
|
||||
bind_builtin(builtin);
|
||||
}
|
||||
for builtin in MAGIC_GLOBALS {
|
||||
bind_builtin(builtin);
|
||||
}
|
||||
for builtin in &self.settings.builtins {
|
||||
bind_builtin(builtin);
|
||||
for builtin in standard_builtins
|
||||
.iter()
|
||||
.chain(MAGIC_GLOBALS.iter())
|
||||
.copied()
|
||||
.chain(self.settings.builtins.iter().map(String::as_str))
|
||||
{
|
||||
// Add the builtin to the scope.
|
||||
let binding_id = self.semantic.push_builtin();
|
||||
let scope = self.semantic.global_scope_mut();
|
||||
scope.add(builtin, binding_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(crate) fn check_tokens(
|
||||
ruff::rules::ambiguous_unicode_character_comment(
|
||||
&mut diagnostics,
|
||||
locator,
|
||||
range,
|
||||
*range,
|
||||
settings,
|
||||
);
|
||||
}
|
||||
@@ -154,13 +154,7 @@ pub(crate) fn check_tokens(
|
||||
Rule::ShebangNotFirstLine,
|
||||
Rule::ShebangMissingPython,
|
||||
]) {
|
||||
flake8_executable::rules::from_tokens(
|
||||
&mut diagnostics,
|
||||
path,
|
||||
locator,
|
||||
comment_ranges,
|
||||
settings,
|
||||
);
|
||||
flake8_executable::rules::from_tokens(&mut diagnostics, path, locator, comment_ranges);
|
||||
}
|
||||
|
||||
if settings.rules.any_enabled(&[
|
||||
|
||||
@@ -961,7 +961,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Ruff, "031") => (RuleGroup::Preview, rules::ruff::rules::IncorrectlyParenthesizedTupleInSubscript),
|
||||
(Ruff, "032") => (RuleGroup::Preview, rules::ruff::rules::DecimalFromFloatLiteral),
|
||||
(Ruff, "033") => (RuleGroup::Preview, rules::ruff::rules::PostInitDefault),
|
||||
(Ruff, "034") => (RuleGroup::Preview, rules::ruff::rules::UselessIfElse),
|
||||
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
|
||||
(Ruff, "101") => (RuleGroup::Stable, rules::ruff::rules::RedirectedNOQA),
|
||||
|
||||
@@ -1067,7 +1066,6 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Refurb, "180") => (RuleGroup::Preview, rules::refurb::rules::MetaClassABCMeta),
|
||||
(Refurb, "181") => (RuleGroup::Stable, rules::refurb::rules::HashlibDigestHex),
|
||||
(Refurb, "187") => (RuleGroup::Stable, rules::refurb::rules::ListReverseCopy),
|
||||
(Refurb, "188") => (RuleGroup::Preview, rules::refurb::rules::SliceToRemovePrefixOrSuffix),
|
||||
(Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax),
|
||||
|
||||
// flake8-logging
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Serialize, Serializer};
|
||||
use serde_json::json;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use ruff_source_file::OneIndexed;
|
||||
|
||||
@@ -27,10 +27,6 @@ impl Emitter for SarifEmitter {
|
||||
.map(SarifResult::from_message)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let unique_rules: HashSet<_> = results.iter().filter_map(|result| result.rule).collect();
|
||||
let mut rules: Vec<SarifRule> = unique_rules.into_iter().map(SarifRule::from).collect();
|
||||
rules.sort_by(|a, b| a.code.cmp(&b.code));
|
||||
|
||||
let output = json!({
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
"version": "2.1.0",
|
||||
@@ -39,7 +35,7 @@ impl Emitter for SarifEmitter {
|
||||
"driver": {
|
||||
"name": "ruff",
|
||||
"informationUri": "https://github.com/astral-sh/ruff",
|
||||
"rules": rules,
|
||||
"rules": Rule::iter().map(SarifRule::from).collect::<Vec<_>>(),
|
||||
"version": VERSION.to_string(),
|
||||
}
|
||||
},
|
||||
@@ -186,6 +182,7 @@ impl Serialize for SarifResult {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use crate::message::tests::{
|
||||
capture_emitter_output, create_messages, create_syntax_error_messages,
|
||||
};
|
||||
@@ -212,11 +209,16 @@ mod tests {
|
||||
#[test]
|
||||
fn test_results() {
|
||||
let content = get_output();
|
||||
let value = serde_json::from_str::<serde_json::Value>(&content).unwrap();
|
||||
|
||||
insta::assert_json_snapshot!(value, {
|
||||
".runs[0].tool.driver.version" => "[VERSION]",
|
||||
".runs[0].results[].locations[].physicalLocation.artifactLocation.uri" => "[URI]",
|
||||
});
|
||||
let sarif = serde_json::from_str::<serde_json::Value>(content.as_str()).unwrap();
|
||||
let rules = sarif["runs"][0]["tool"]["driver"]["rules"]
|
||||
.as_array()
|
||||
.unwrap();
|
||||
let results = sarif["runs"][0]["results"].as_array().unwrap();
|
||||
assert_eq!(results.len(), 3);
|
||||
assert_eq!(
|
||||
results[0]["message"]["text"].as_str().unwrap(),
|
||||
"`os` imported but unused"
|
||||
);
|
||||
assert!(rules.len() > 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/message/sarif.rs
|
||||
expression: value
|
||||
---
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
||||
"runs": [
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "[URI]"
|
||||
},
|
||||
"region": {
|
||||
"endColumn": 10,
|
||||
"endLine": 1,
|
||||
"startColumn": 8,
|
||||
"startLine": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"text": "`os` imported but unused"
|
||||
},
|
||||
"ruleId": "F401"
|
||||
},
|
||||
{
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "[URI]"
|
||||
},
|
||||
"region": {
|
||||
"endColumn": 6,
|
||||
"endLine": 6,
|
||||
"startColumn": 5,
|
||||
"startLine": 6
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"text": "Local variable `x` is assigned to but never used"
|
||||
},
|
||||
"ruleId": "F841"
|
||||
},
|
||||
{
|
||||
"level": "error",
|
||||
"locations": [
|
||||
{
|
||||
"physicalLocation": {
|
||||
"artifactLocation": {
|
||||
"uri": "[URI]"
|
||||
},
|
||||
"region": {
|
||||
"endColumn": 5,
|
||||
"endLine": 1,
|
||||
"startColumn": 4,
|
||||
"startLine": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"message": {
|
||||
"text": "Undefined name `a`"
|
||||
},
|
||||
"ruleId": "F821"
|
||||
}
|
||||
],
|
||||
"tool": {
|
||||
"driver": {
|
||||
"informationUri": "https://github.com/astral-sh/ruff",
|
||||
"name": "ruff",
|
||||
"rules": [
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for unused imports.\n\n## Why is this bad?\nUnused imports add a performance overhead at runtime, and risk creating\nimport cycles. They also increase the cognitive load of reading the code.\n\nIf an import statement is used to check for the availability or existence\nof a module, consider using `importlib.util.find_spec` instead.\n\nIf an import statement is used to re-export a symbol as part of a module's\npublic interface, consider using a \"redundant\" import alias, which\ninstructs Ruff (and other tools) to respect the re-export, and avoid\nmarking it as unused, as in:\n\n```python\nfrom module import member as member\n```\n\nAlternatively, you can use `__all__` to declare a symbol as part of the module's\ninterface, as in:\n\n```python\n# __init__.py\nimport some_module\n\n__all__ = [\"some_module\"]\n```\n\n## Fix safety\n\nFixes to remove unused imports are safe, except in `__init__.py` files.\n\nApplying fixes to `__init__.py` files is currently in preview. The fix offered depends on the\ntype of the unused import. Ruff will suggest a safe fix to export first-party imports with\neither a redundant alias or, if already present in the file, an `__all__` entry. If multiple\n`__all__` declarations are present, Ruff will not offer a fix. Ruff will suggest an unsafe fix\nto remove third-party and standard library imports -- the fix is unsafe because the module's\ninterface changes.\n\n## Example\n\n```python\nimport numpy as np # unused import\n\n\ndef area(radius):\n return 3.14 * radius**2\n```\n\nUse instead:\n\n```python\ndef area(radius):\n return 3.14 * radius**2\n```\n\nTo check the availability of a module, use `importlib.util.find_spec`:\n\n```python\nfrom importlib.util import find_spec\n\nif find_spec(\"numpy\") is not None:\n print(\"numpy is installed\")\nelse:\n print(\"numpy is not installed\")\n```\n\n## Options\n- `lint.ignore-init-module-imports`\n\n## References\n- [Python documentation: `import`](https://docs.python.org/3/reference/simple_stmts.html#the-import-statement)\n- [Python documentation: `importlib.util.find_spec`](https://docs.python.org/3/library/importlib.html#importlib.util.find_spec)\n- [Typing documentation: interface conventions](https://typing.readthedocs.io/en/latest/source/libraries.html#library-interface-public-and-private-symbols)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
},
|
||||
"helpUri": "https://docs.astral.sh/ruff/rules/unused-import",
|
||||
"id": "F401",
|
||||
"properties": {
|
||||
"id": "F401",
|
||||
"kind": "Pyflakes",
|
||||
"name": "unused-import",
|
||||
"problem.severity": "error"
|
||||
},
|
||||
"shortDescription": {
|
||||
"text": "`{name}` imported but unused; consider using `importlib.util.find_spec` to test for availability"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for uses of undefined names.\n\n## Why is this bad?\nAn undefined name is likely to raise `NameError` at runtime.\n\n## Example\n```python\ndef double():\n return n * 2 # raises `NameError` if `n` is undefined when `double` is called\n```\n\nUse instead:\n```python\ndef double(n):\n return n * 2\n```\n\n## Options\n- [`target-version`]: Can be used to configure which symbols Ruff will understand\n as being available in the `builtins` namespace.\n\n## References\n- [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "Undefined name `{name}`. {tip}"
|
||||
},
|
||||
"helpUri": "https://docs.astral.sh/ruff/rules/undefined-name",
|
||||
"id": "F821",
|
||||
"properties": {
|
||||
"id": "F821",
|
||||
"kind": "Pyflakes",
|
||||
"name": "undefined-name",
|
||||
"problem.severity": "error"
|
||||
},
|
||||
"shortDescription": {
|
||||
"text": "Undefined name `{name}`. {tip}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fullDescription": {
|
||||
"text": "## What it does\nChecks for the presence of unused variables in function scopes.\n\n## Why is this bad?\nA variable that is defined but not used is likely a mistake, and should\nbe removed to avoid confusion.\n\nIf a variable is intentionally defined-but-not-used, it should be\nprefixed with an underscore, or some other value that adheres to the\n[`lint.dummy-variable-rgx`] pattern.\n\nUnder [preview mode](https://docs.astral.sh/ruff/preview), this rule also\ntriggers on unused unpacked assignments (for example, `x, y = foo()`).\n\n## Example\n```python\ndef foo():\n x = 1\n y = 2\n return x\n```\n\nUse instead:\n```python\ndef foo():\n x = 1\n return x\n```\n\n## Options\n- `lint.dummy-variable-rgx`\n"
|
||||
},
|
||||
"help": {
|
||||
"text": "Local variable `{name}` is assigned to but never used"
|
||||
},
|
||||
"helpUri": "https://docs.astral.sh/ruff/rules/unused-variable",
|
||||
"id": "F841",
|
||||
"properties": {
|
||||
"id": "F841",
|
||||
"kind": "Pyflakes",
|
||||
"name": "unused-variable",
|
||||
"problem.severity": "error"
|
||||
},
|
||||
"shortDescription": {
|
||||
"text": "Local variable `{name}` is assigned to but never used"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "[VERSION]"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"version": "2.1.0"
|
||||
}
|
||||
@@ -361,7 +361,7 @@ impl<'a> FileNoqaDirectives<'a> {
|
||||
let mut lines = vec![];
|
||||
|
||||
for range in comment_ranges {
|
||||
match ParsedFileExemption::try_extract(&contents[range]) {
|
||||
match ParsedFileExemption::try_extract(&contents[*range]) {
|
||||
Err(err) => {
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
@@ -403,7 +403,7 @@ impl<'a> FileNoqaDirectives<'a> {
|
||||
};
|
||||
|
||||
lines.push(FileNoqaDirectiveLine {
|
||||
range,
|
||||
range: *range,
|
||||
parsed_file_exemption: exemption,
|
||||
matches,
|
||||
});
|
||||
@@ -922,7 +922,7 @@ impl<'a> NoqaDirectives<'a> {
|
||||
let mut directives = Vec::new();
|
||||
|
||||
for range in comment_ranges {
|
||||
match Directive::try_extract(locator.slice(range), range.start()) {
|
||||
match Directive::try_extract(locator.slice(*range), range.start()) {
|
||||
Err(err) => {
|
||||
#[allow(deprecated)]
|
||||
let line = locator.compute_line_index(range.start());
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::settings::LinterSettings;
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::{Locator, UniversalNewlineIterator};
|
||||
use ruff_text_size::TextRange;
|
||||
use ruff_source_file::Locator;
|
||||
|
||||
use crate::settings::LinterSettings;
|
||||
|
||||
use super::super::detection::comment_contains_code;
|
||||
|
||||
@@ -50,110 +50,40 @@ pub(crate) fn commented_out_code(
|
||||
comment_ranges: &CommentRanges,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let mut comments = comment_ranges.into_iter().peekable();
|
||||
// Iterate over all comments in the document.
|
||||
while let Some(range) = comments.next() {
|
||||
let line = locator.line(range.start());
|
||||
// Skip comments within `/// script` tags.
|
||||
let mut in_script_tag = false;
|
||||
|
||||
if is_script_tag_start(line) {
|
||||
if skip_script_comments(range, &mut comments, locator) {
|
||||
continue;
|
||||
// Iterate over all comments in the document.
|
||||
for range in comment_ranges {
|
||||
let line = locator.lines(*range);
|
||||
|
||||
// Detect `/// script` tags.
|
||||
if in_script_tag {
|
||||
if is_script_tag_end(line) {
|
||||
in_script_tag = false;
|
||||
}
|
||||
} else {
|
||||
if is_script_tag_start(line) {
|
||||
in_script_tag = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Skip comments within `/// script` tags.
|
||||
if in_script_tag {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify that the comment is on its own line, and that it contains code.
|
||||
if is_own_line_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
|
||||
let mut diagnostic = Diagnostic::new(CommentedOutCode, range);
|
||||
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
|
||||
diagnostic.set_fix(Fix::display_only_edit(Edit::range_deletion(
|
||||
locator.full_lines_range(range),
|
||||
locator.full_lines_range(*range),
|
||||
)));
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the rest of a [PEP 723](https://peps.python.org/pep-0723/)
|
||||
/// script comment and moves `comments` past the script comment's end unless
|
||||
/// the script comment is invalid.
|
||||
///
|
||||
/// Returns `true` if it is a valid script comment.
|
||||
fn skip_script_comments<I>(
|
||||
script_start: TextRange,
|
||||
comments: &mut std::iter::Peekable<I>,
|
||||
locator: &Locator,
|
||||
) -> bool
|
||||
where
|
||||
I: Iterator<Item = TextRange>,
|
||||
{
|
||||
let line_end = locator.full_line_end(script_start.end());
|
||||
let rest = locator.after(line_end);
|
||||
let mut end_offset = None;
|
||||
let mut lines = UniversalNewlineIterator::with_offset(rest, line_end).peekable();
|
||||
|
||||
while let Some(line) = lines.next() {
|
||||
let Some(content) = script_line_content(&line) else {
|
||||
break;
|
||||
};
|
||||
|
||||
if content == "///" {
|
||||
// > Precedence for an ending line # /// is given when the next line is not a valid
|
||||
// > embedded content line as described above.
|
||||
// > For example, the following is a single fully valid block:
|
||||
// > ```python
|
||||
// > # /// some-toml
|
||||
// > # embedded-csharp = """
|
||||
// > # /// <summary>
|
||||
// > # /// text
|
||||
// > # ///
|
||||
// > # /// </summary>
|
||||
// > # public class MyClass { }
|
||||
// > # """
|
||||
// > # ///
|
||||
// ````
|
||||
if lines.next().is_some_and(|line| is_valid_script_line(&line)) {
|
||||
continue;
|
||||
}
|
||||
end_offset = Some(line.full_end());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// > Unclosed blocks MUST be ignored.
|
||||
let Some(end_offset) = end_offset else {
|
||||
return false;
|
||||
};
|
||||
|
||||
// Skip over all script-comments.
|
||||
while let Some(comment) = comments.peek() {
|
||||
if comment.start() >= end_offset {
|
||||
break;
|
||||
}
|
||||
|
||||
comments.next();
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn script_line_content(line: &str) -> Option<&str> {
|
||||
let Some(rest) = line.strip_prefix('#') else {
|
||||
// Not a comment
|
||||
return None;
|
||||
};
|
||||
|
||||
// An empty line
|
||||
if rest.is_empty() {
|
||||
return Some("");
|
||||
}
|
||||
|
||||
// > If there are characters after the # then the first character MUST be a space.
|
||||
rest.strip_prefix(' ')
|
||||
}
|
||||
|
||||
fn is_valid_script_line(line: &str) -> bool {
|
||||
script_line_content(line).is_some()
|
||||
}
|
||||
|
||||
/// Returns `true` if line contains an own-line comment.
|
||||
fn is_own_line_comment(line: &str) -> bool {
|
||||
for char in line.chars() {
|
||||
@@ -174,77 +104,9 @@ fn is_script_tag_start(line: &str) -> bool {
|
||||
line == "# /// script"
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::rules::eradicate::rules::commented_out_code::skip_script_comments;
|
||||
use ruff_python_parser::parse_module;
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::TextSize;
|
||||
#[test]
|
||||
fn script_comment() {
|
||||
let code = r#"
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = [
|
||||
# "requests<3",
|
||||
# "rich",
|
||||
# ]
|
||||
# ///
|
||||
|
||||
a = 10 # abc
|
||||
"#;
|
||||
|
||||
let parsed = parse_module(code).unwrap();
|
||||
let locator = Locator::new(code);
|
||||
|
||||
let comments = CommentRanges::from(parsed.tokens());
|
||||
let mut comments = comments.into_iter().peekable();
|
||||
|
||||
let script_start = code.find("# /// script").unwrap();
|
||||
let script_start_range = locator.full_line_range(TextSize::try_from(script_start).unwrap());
|
||||
|
||||
let valid = skip_script_comments(script_start_range, &mut comments, &Locator::new(code));
|
||||
|
||||
assert!(valid);
|
||||
|
||||
let next_comment = comments.next();
|
||||
|
||||
assert!(next_comment.is_some());
|
||||
assert_eq!(&code[next_comment.unwrap()], "# abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn script_comment_end_precedence() {
|
||||
let code = r#"
|
||||
# /// script
|
||||
# [tool.uv]
|
||||
# extra-index-url = ["https://pypi.org/simple", """\
|
||||
# https://example.com/
|
||||
# ///
|
||||
# """
|
||||
# ]
|
||||
# ///
|
||||
|
||||
a = 10 # abc
|
||||
"#;
|
||||
|
||||
let parsed = parse_module(code).unwrap();
|
||||
let locator = Locator::new(code);
|
||||
|
||||
let comments = CommentRanges::from(parsed.tokens());
|
||||
let mut comments = comments.into_iter().peekable();
|
||||
|
||||
let script_start = code.find("# /// script").unwrap();
|
||||
let script_start_range = locator.full_line_range(TextSize::try_from(script_start).unwrap());
|
||||
|
||||
let valid = skip_script_comments(script_start_range, &mut comments, &Locator::new(code));
|
||||
|
||||
assert!(valid);
|
||||
|
||||
let next_comment = comments.next();
|
||||
|
||||
assert!(next_comment.is_some());
|
||||
assert_eq!(&code[next_comment.unwrap()], "# abc");
|
||||
}
|
||||
/// Returns `true` if the line appears to start a script tag.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
fn is_script_tag_end(line: &str) -> bool {
|
||||
line == "# ///"
|
||||
}
|
||||
|
||||
@@ -321,38 +321,3 @@ ERA001.py:47:1: ERA001 Found commented-out code
|
||||
48 47 | # ///
|
||||
49 48 |
|
||||
50 49 | # Script tag (OK)
|
||||
|
||||
ERA001.py:75:1: ERA001 Found commented-out code
|
||||
|
|
||||
73 | # /// script
|
||||
74 | # requires-python = ">=3.11"
|
||||
75 | # dependencies = [
|
||||
| ^^^^^^^^^^^^^^^^^^ ERA001
|
||||
76 | # "requests<3",
|
||||
77 | # "rich",
|
||||
|
|
||||
= help: Remove commented-out code
|
||||
|
||||
ℹ Display-only fix
|
||||
72 72 |
|
||||
73 73 | # /// script
|
||||
74 74 | # requires-python = ">=3.11"
|
||||
75 |-# dependencies = [
|
||||
76 75 | # "requests<3",
|
||||
77 76 | # "rich",
|
||||
78 77 | # ]
|
||||
|
||||
ERA001.py:78:1: ERA001 Found commented-out code
|
||||
|
|
||||
76 | # "requests<3",
|
||||
77 | # "rich",
|
||||
78 | # ]
|
||||
| ^^^ ERA001
|
||||
|
|
||||
= help: Remove commented-out code
|
||||
|
||||
ℹ Display-only fix
|
||||
75 75 | # dependencies = [
|
||||
76 76 | # "requests<3",
|
||||
77 77 | # "rich",
|
||||
78 |-# ]
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
use std::path::Path;
|
||||
|
||||
use crate::codes::Rule;
|
||||
use crate::comments::shebang::ShebangDirective;
|
||||
use crate::settings::LinterSettings;
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_python_trivia::CommentRanges;
|
||||
use ruff_source_file::Locator;
|
||||
@@ -12,6 +9,8 @@ pub(crate) use shebang_missing_python::*;
|
||||
pub(crate) use shebang_not_executable::*;
|
||||
pub(crate) use shebang_not_first_line::*;
|
||||
|
||||
use crate::comments::shebang::ShebangDirective;
|
||||
|
||||
mod shebang_leading_whitespace;
|
||||
mod shebang_missing_executable_file;
|
||||
mod shebang_missing_python;
|
||||
@@ -23,39 +22,34 @@ pub(crate) fn from_tokens(
|
||||
path: &Path,
|
||||
locator: &Locator,
|
||||
comment_ranges: &CommentRanges,
|
||||
settings: &LinterSettings,
|
||||
) {
|
||||
let mut has_any_shebang = false;
|
||||
for range in comment_ranges {
|
||||
let comment = locator.slice(range);
|
||||
let comment = locator.slice(*range);
|
||||
if let Some(shebang) = ShebangDirective::try_extract(comment) {
|
||||
has_any_shebang = true;
|
||||
|
||||
if let Some(diagnostic) = shebang_missing_python(range, &shebang) {
|
||||
if let Some(diagnostic) = shebang_missing_python(*range, &shebang) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if settings.rules.enabled(Rule::ShebangNotExecutable) {
|
||||
if let Some(diagnostic) = shebang_not_executable(path, range) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(diagnostic) = shebang_leading_whitespace(range, locator) {
|
||||
if let Some(diagnostic) = shebang_not_executable(path, *range) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if let Some(diagnostic) = shebang_not_first_line(range, locator) {
|
||||
if let Some(diagnostic) = shebang_leading_whitespace(*range, locator) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
if let Some(diagnostic) = shebang_not_first_line(*range, locator) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !has_any_shebang {
|
||||
if settings.rules.enabled(Rule::ShebangMissingExecutableFile) {
|
||||
if let Some(diagnostic) = shebang_missing_executable_file(path) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
if let Some(diagnostic) = shebang_missing_executable_file(path) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ pub(crate) fn type_comment_in_stub(
|
||||
comment_ranges: &CommentRanges,
|
||||
) {
|
||||
for range in comment_ranges {
|
||||
let comment = locator.slice(range);
|
||||
let comment = locator.slice(*range);
|
||||
|
||||
if TYPE_COMMENT_REGEX.is_match(comment) && !TYPE_IGNORE_REGEX.is_match(comment) {
|
||||
diagnostics.push(Diagnostic::new(TypeCommentInStub, range));
|
||||
diagnostics.push(Diagnostic::new(TypeCommentInStub, *range));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +35,6 @@ use crate::settings::LinterSettings;
|
||||
///
|
||||
/// import typing
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.isort.required-imports`
|
||||
#[violation]
|
||||
pub struct MissingRequiredImport(pub String);
|
||||
|
||||
|
||||
@@ -741,10 +741,6 @@ fn returns_documented(
|
||||
|| (matches!(convention, Some(Convention::Google)) && starts_with_returns(docstring))
|
||||
}
|
||||
|
||||
fn should_document_returns(function_def: &ast::StmtFunctionDef) -> bool {
|
||||
!matches!(function_def.name.as_str(), "__new__")
|
||||
}
|
||||
|
||||
fn starts_with_yields(docstring: &Docstring) -> bool {
|
||||
if let Some(first_word) = docstring.body().as_str().split(' ').next() {
|
||||
return matches!(first_word, "Yield" | "Yields");
|
||||
@@ -872,9 +868,7 @@ pub(crate) fn check_docstring(
|
||||
|
||||
// DOC201
|
||||
if checker.enabled(Rule::DocstringMissingReturns) {
|
||||
if should_document_returns(function_def)
|
||||
&& !returns_documented(docstring, &docstring_sections, convention)
|
||||
{
|
||||
if !returns_documented(docstring, &docstring_sections, convention) {
|
||||
let extra_property_decorators = checker.settings.pydocstyle.property_decorators();
|
||||
if !definition.is_property(extra_property_decorators, semantic) {
|
||||
if let Some(body_return) = body_entries.returns.first() {
|
||||
|
||||
@@ -67,7 +67,9 @@ impl Violation for TripleSingleQuotes {
|
||||
pub(crate) fn triple_quotes(checker: &mut Checker, docstring: &Docstring) {
|
||||
let leading_quote = docstring.leading_quote();
|
||||
|
||||
let prefixes = leading_quote.trim_end_matches(['\'', '"']).to_owned();
|
||||
let prefixes = leading_quote
|
||||
.trim_end_matches(|c| c == '\'' || c == '"')
|
||||
.to_owned();
|
||||
|
||||
let expected_quote = if docstring.body().contains("\"\"\"") {
|
||||
if docstring.body().contains("\'\'\'") {
|
||||
|
||||
@@ -208,18 +208,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f821_with_builtin_added_on_new_py_version_but_old_target_version_specified() {
|
||||
let diagnostics = test_snippet(
|
||||
"PythonFinalizationError",
|
||||
&LinterSettings {
|
||||
target_version: crate::settings::types::PythonVersion::Py312,
|
||||
..LinterSettings::for_rule(Rule::UndefinedName)
|
||||
},
|
||||
);
|
||||
assert_messages!(diagnostics);
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnusedVariable, Path::new("F841_4.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("__init__.py"))]
|
||||
#[test_case(Rule::UnusedImport, Path::new("F401_24/__init__.py"))]
|
||||
|
||||
@@ -19,35 +19,17 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// return n * 2
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - [`target-version`]: Can be used to configure which symbols Ruff will understand
|
||||
/// as being available in the `builtins` namespace.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: Naming and binding](https://docs.python.org/3/reference/executionmodel.html#naming-and-binding)
|
||||
#[violation]
|
||||
pub struct UndefinedName {
|
||||
pub(crate) name: String,
|
||||
pub(crate) minor_version_builtin_added: Option<u8>,
|
||||
}
|
||||
|
||||
impl Violation for UndefinedName {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UndefinedName {
|
||||
name,
|
||||
minor_version_builtin_added,
|
||||
} = self;
|
||||
let tip = minor_version_builtin_added.map(|version_added| {
|
||||
format!(
|
||||
r#"Consider specifying `requires-python = ">= 3.{version_added}"` or `tool.ruff.target-version = "py3{version_added}"` in your `pyproject.toml` file."#
|
||||
)
|
||||
});
|
||||
|
||||
if let Some(tip) = tip {
|
||||
format!("Undefined name `{name}`. {tip}")
|
||||
} else {
|
||||
format!("Undefined name `{name}`")
|
||||
}
|
||||
let UndefinedName { name } = self;
|
||||
format!("Undefined name `{name}`")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
|
||||
---
|
||||
<filename>:1:1: F821 Undefined name `PythonFinalizationError`. Consider specifying `requires-python = ">= 3.13"` or `tool.ruff.target-version = "py313"` in your `pyproject.toml` file.
|
||||
|
|
||||
1 | PythonFinalizationError
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ F821
|
||||
|
|
||||
@@ -55,7 +55,7 @@ pub(crate) fn blanket_type_ignore(
|
||||
locator: &Locator,
|
||||
) {
|
||||
for range in comment_ranges {
|
||||
let line = locator.slice(range);
|
||||
let line = locator.slice(*range);
|
||||
|
||||
// Match, e.g., `# type: ignore` or `# type: ignore[attr-defined]`.
|
||||
// See: https://github.com/python/mypy/blob/b43e0d34247a6d1b3b9d9094d184bbfcb9808bb9/mypy/fastparse.py#L248
|
||||
|
||||
@@ -8,10 +8,10 @@ use crate::checkers::ast::Checker;
|
||||
use crate::rules::pylint::helpers::is_known_dunder_method;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for dunder methods that have no special meaning in Python 3.
|
||||
/// Checks for misspelled and unknown dunder names in method definitions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Misspelled or no longer supported dunder name methods may cause your code to not function
|
||||
/// Misspelled dunder name methods may cause your code to not function
|
||||
/// as expected.
|
||||
///
|
||||
/// Since dunder methods are associated with customizing the behavior
|
||||
@@ -51,7 +51,7 @@ impl Violation for BadDunderMethodName {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BadDunderMethodName { name } = self;
|
||||
format!("Dunder method `{name}` has no special meaning in Python 3")
|
||||
format!("Bad or misspelled dunder method name `{name}`")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ pub(crate) fn empty_comments(
|
||||
}
|
||||
|
||||
// If the line contains an empty comment, add a diagnostic.
|
||||
if let Some(diagnostic) = empty_comment(range, locator) {
|
||||
if let Some(diagnostic) = empty_comment(*range, locator) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pylint/mod.rs
|
||||
---
|
||||
bad_dunder_method_name.py:5:9: PLW3201 Dunder method `_init_` has no special meaning in Python 3
|
||||
bad_dunder_method_name.py:5:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
||||
|
|
||||
4 | class Apples:
|
||||
5 | def _init_(self): # [bad-dunder-name]
|
||||
@@ -9,7 +9,7 @@ bad_dunder_method_name.py:5:9: PLW3201 Dunder method `_init_` has no special mea
|
||||
6 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:8:9: PLW3201 Dunder method `__hello__` has no special meaning in Python 3
|
||||
bad_dunder_method_name.py:8:9: PLW3201 Bad or misspelled dunder method name `__hello__`
|
||||
|
|
||||
6 | pass
|
||||
7 |
|
||||
@@ -18,7 +18,7 @@ bad_dunder_method_name.py:8:9: PLW3201 Dunder method `__hello__` has no special
|
||||
9 | print("hello")
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:11:9: PLW3201 Dunder method `__init_` has no special meaning in Python 3
|
||||
bad_dunder_method_name.py:11:9: PLW3201 Bad or misspelled dunder method name `__init_`
|
||||
|
|
||||
9 | print("hello")
|
||||
10 |
|
||||
@@ -28,7 +28,7 @@ bad_dunder_method_name.py:11:9: PLW3201 Dunder method `__init_` has no special m
|
||||
13 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:15:9: PLW3201 Dunder method `_init_` has no special meaning in Python 3
|
||||
bad_dunder_method_name.py:15:9: PLW3201 Bad or misspelled dunder method name `_init_`
|
||||
|
|
||||
13 | pass
|
||||
14 |
|
||||
@@ -38,7 +38,7 @@ bad_dunder_method_name.py:15:9: PLW3201 Dunder method `_init_` has no special me
|
||||
17 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:19:9: PLW3201 Dunder method `___neg__` has no special meaning in Python 3
|
||||
bad_dunder_method_name.py:19:9: PLW3201 Bad or misspelled dunder method name `___neg__`
|
||||
|
|
||||
17 | pass
|
||||
18 |
|
||||
@@ -48,7 +48,7 @@ bad_dunder_method_name.py:19:9: PLW3201 Dunder method `___neg__` has no special
|
||||
21 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:23:9: PLW3201 Dunder method `__inv__` has no special meaning in Python 3
|
||||
bad_dunder_method_name.py:23:9: PLW3201 Bad or misspelled dunder method name `__inv__`
|
||||
|
|
||||
21 | pass
|
||||
22 |
|
||||
@@ -58,10 +58,4 @@ bad_dunder_method_name.py:23:9: PLW3201 Dunder method `__inv__` has no special m
|
||||
25 | pass
|
||||
|
|
||||
|
||||
bad_dunder_method_name.py:98:9: PLW3201 Dunder method `__unicode__` has no special meaning in Python 3
|
||||
|
|
||||
97 | # Removed with Python 3
|
||||
98 | def __unicode__(self):
|
||||
| ^^^^^^^^^^^ PLW3201
|
||||
99 | pass
|
||||
|
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The `unittest` module has deprecated aliases for some of its methods.
|
||||
/// The deprecated aliases were removed in Python 3.12. Instead of aliases,
|
||||
/// The aliases may be removed in future versions of Python. Instead,
|
||||
/// use their non-deprecated counterparts.
|
||||
///
|
||||
/// ## Example
|
||||
@@ -37,7 +37,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python 3.11 documentation: Deprecated aliases](https://docs.python.org/3.11/library/unittest.html#deprecated-aliases)
|
||||
/// - [Python documentation: Deprecated aliases](https://docs.python.org/3/library/unittest.html#deprecated-aliases)
|
||||
#[violation]
|
||||
pub struct DeprecatedUnittestAlias {
|
||||
alias: String,
|
||||
|
||||
@@ -46,7 +46,6 @@ mod tests {
|
||||
#[test_case(Rule::WriteWholeFile, Path::new("FURB103.py"))]
|
||||
#[test_case(Rule::FStringNumberFormat, Path::new("FURB116.py"))]
|
||||
#[test_case(Rule::SortedMinMax, Path::new("FURB192.py"))]
|
||||
#[test_case(Rule::SliceToRemovePrefixOrSuffix, Path::new("FURB188.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -23,7 +23,6 @@ pub(crate) use repeated_append::*;
|
||||
pub(crate) use repeated_global::*;
|
||||
pub(crate) use single_item_membership_test::*;
|
||||
pub(crate) use slice_copy::*;
|
||||
pub(crate) use slice_to_remove_prefix_or_suffix::*;
|
||||
pub(crate) use sorted_min_max::*;
|
||||
pub(crate) use type_none_comparison::*;
|
||||
pub(crate) use unnecessary_enumerate::*;
|
||||
@@ -56,7 +55,6 @@ mod repeated_append;
|
||||
mod repeated_global;
|
||||
mod single_item_membership_test;
|
||||
mod slice_copy;
|
||||
mod slice_to_remove_prefix_or_suffix;
|
||||
mod sorted_min_max;
|
||||
mod type_none_comparison;
|
||||
mod unnecessary_enumerate;
|
||||
|
||||
@@ -1,474 +0,0 @@
|
||||
use crate::{checkers::ast::Checker, settings::types::PythonVersion};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextLen};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the removal of a prefix or suffix from a string by assigning
|
||||
/// the string to a slice after checking `.startswith()` or `.endswith()`, respectively.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// The methods [`str.removeprefix`] and [`str.removesuffix`],
|
||||
/// introduced in Python 3.9, have the same behavior
|
||||
/// and are more readable and efficient.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// filename[:-4] if filename.endswith(".txt") else filename
|
||||
/// ```
|
||||
///
|
||||
/// ```python
|
||||
/// if text.startswith("pre"):
|
||||
/// text = text[3:]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// filename = filename.removesuffix(".txt")
|
||||
/// ```
|
||||
///
|
||||
/// ```python
|
||||
/// text = text.removeprefix("pre")
|
||||
/// ```
|
||||
///
|
||||
/// [`str.removeprefix`]: https://docs.python.org/3/library/stdtypes.html#str.removeprefix
|
||||
/// [`str.removesuffix`]: https://docs.python.org/3/library/stdtypes.html#str.removesuffix
|
||||
#[violation]
|
||||
pub struct SliceToRemovePrefixOrSuffix {
|
||||
string: String,
|
||||
affix_kind: AffixKind,
|
||||
stmt_or_expression: StmtOrExpr,
|
||||
}
|
||||
|
||||
impl AlwaysFixableViolation for SliceToRemovePrefixOrSuffix {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
match self.affix_kind {
|
||||
AffixKind::StartsWith => {
|
||||
format!("Prefer `removeprefix` over conditionally replacing with slice.")
|
||||
}
|
||||
AffixKind::EndsWith => {
|
||||
format!("Prefer `removesuffix` over conditionally replacing with slice.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let method_name = self.affix_kind.as_str();
|
||||
let replacement = self.affix_kind.replacement();
|
||||
let context = match self.stmt_or_expression {
|
||||
StmtOrExpr::Statement => "assignment",
|
||||
StmtOrExpr::Expression => "ternary expression",
|
||||
};
|
||||
format!("Use {replacement} instead of {context} conditional upon {method_name}.")
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB188
|
||||
pub(crate) fn slice_to_remove_affix_expr(checker: &mut Checker, if_expr: &ast::ExprIf) {
|
||||
if checker.settings.target_version < PythonVersion::Py39 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(removal_data) = affix_removal_data_expr(if_expr) {
|
||||
if affix_matches_slice_bound(&removal_data, checker.semantic()) {
|
||||
let kind = removal_data.affix_query.kind;
|
||||
let text = removal_data.text;
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SliceToRemovePrefixOrSuffix {
|
||||
affix_kind: kind,
|
||||
string: checker.locator().slice(text).to_string(),
|
||||
stmt_or_expression: StmtOrExpr::Expression,
|
||||
},
|
||||
if_expr.range,
|
||||
);
|
||||
let replacement =
|
||||
generate_removeaffix_expr(text, &removal_data.affix_query, checker.locator());
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||
replacement,
|
||||
if_expr.start(),
|
||||
if_expr.end(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB188
|
||||
pub(crate) fn slice_to_remove_affix_stmt(checker: &mut Checker, if_stmt: &ast::StmtIf) {
|
||||
if checker.settings.target_version < PythonVersion::Py39 {
|
||||
return;
|
||||
}
|
||||
if let Some(removal_data) = affix_removal_data_stmt(if_stmt) {
|
||||
if affix_matches_slice_bound(&removal_data, checker.semantic()) {
|
||||
let kind = removal_data.affix_query.kind;
|
||||
let text = removal_data.text;
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
SliceToRemovePrefixOrSuffix {
|
||||
affix_kind: kind,
|
||||
string: checker.locator().slice(text).to_string(),
|
||||
stmt_or_expression: StmtOrExpr::Statement,
|
||||
},
|
||||
if_stmt.range,
|
||||
);
|
||||
|
||||
let replacement = generate_assignment_with_removeaffix(
|
||||
text,
|
||||
&removal_data.affix_query,
|
||||
checker.locator(),
|
||||
);
|
||||
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::replacement(
|
||||
replacement,
|
||||
if_stmt.start(),
|
||||
if_stmt.end(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an expression of the form:
|
||||
///
|
||||
/// ```python
|
||||
/// text[slice] if text.func(affix) else text
|
||||
/// ```
|
||||
///
|
||||
/// where `func` is either `startswith` or `endswith`,
|
||||
/// this function collects `text`,`func`, `affix`, and the non-null
|
||||
/// bound of the slice. Otherwise, returns `None`.
|
||||
fn affix_removal_data_expr(if_expr: &ast::ExprIf) -> Option<RemoveAffixData> {
|
||||
let ast::ExprIf {
|
||||
test,
|
||||
body,
|
||||
orelse,
|
||||
range: _,
|
||||
} = if_expr;
|
||||
|
||||
let ast::ExprSubscript { value, slice, .. } = body.as_subscript_expr()?;
|
||||
// Variable names correspond to:
|
||||
// ```python
|
||||
// value[slice] if test else orelse
|
||||
// ```
|
||||
affix_removal_data(value, test, orelse, slice)
|
||||
}
|
||||
|
||||
/// Given a statement of the form:
|
||||
///
|
||||
/// ```python
|
||||
/// if text.func(affix):
|
||||
/// text = text[slice]
|
||||
/// ```
|
||||
///
|
||||
/// where `func` is either `startswith` or `endswith`,
|
||||
/// this function collects `text`,`func`, `affix`, and the non-null
|
||||
/// bound of the slice. Otherwise, returns `None`.
|
||||
fn affix_removal_data_stmt(if_stmt: &ast::StmtIf) -> Option<RemoveAffixData> {
|
||||
let ast::StmtIf {
|
||||
test,
|
||||
body,
|
||||
elif_else_clauses,
|
||||
range: _,
|
||||
} = if_stmt;
|
||||
|
||||
// Cannot safely transform, e.g.,
|
||||
// ```python
|
||||
// if text.startswith(prefix):
|
||||
// text = text[len(prefix):]
|
||||
// else:
|
||||
// text = "something completely different"
|
||||
// ```
|
||||
if !elif_else_clauses.is_empty() {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Cannot safely transform, e.g.,
|
||||
// ```python
|
||||
// if text.startswith(prefix):
|
||||
// text = f"{prefix} something completely different"
|
||||
// text = text[len(prefix):]
|
||||
// ```
|
||||
let [statement] = body.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// Variable names correspond to:
|
||||
// ```python
|
||||
// if test:
|
||||
// else_or_target_name = value[slice]
|
||||
// ```
|
||||
let ast::StmtAssign {
|
||||
value,
|
||||
targets,
|
||||
range: _,
|
||||
} = statement.as_assign_stmt()?;
|
||||
let [target] = targets.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let ast::ExprSubscript { value, slice, .. } = value.as_subscript_expr()?;
|
||||
|
||||
affix_removal_data(value, test, target, slice)
|
||||
}
|
||||
|
||||
/// Suppose given a statement of the form:
|
||||
/// ```python
|
||||
/// if test:
|
||||
/// else_or_target_name = value[slice]
|
||||
/// ```
|
||||
/// or an expression of the form:
|
||||
/// ```python
|
||||
/// value[slice] if test else else_or_target_name
|
||||
/// ```
|
||||
/// This function verifies that
|
||||
/// - `value` and `else_or_target_name`
|
||||
/// are equal to a common name `text`
|
||||
/// - `test` is of the form `text.startswith(prefix)`
|
||||
/// or `text.endswith(suffix)`
|
||||
/// - `slice` has no upper bound in the case of a prefix,
|
||||
/// and no lower bound in the case of a suffix
|
||||
///
|
||||
/// If these conditions are satisfied, the function
|
||||
/// returns the corresponding `RemoveAffixData` object;
|
||||
/// otherwise it returns `None`.
|
||||
fn affix_removal_data<'a>(
|
||||
value: &'a ast::Expr,
|
||||
test: &'a ast::Expr,
|
||||
else_or_target: &'a ast::Expr,
|
||||
slice: &'a ast::Expr,
|
||||
) -> Option<RemoveAffixData<'a>> {
|
||||
let compr_value = ast::comparable::ComparableExpr::from(value);
|
||||
let compr_else_or_target = ast::comparable::ComparableExpr::from(else_or_target);
|
||||
if compr_value != compr_else_or_target {
|
||||
return None;
|
||||
}
|
||||
let slice = slice.as_slice_expr()?;
|
||||
let compr_test_expr = ast::comparable::ComparableExpr::from(
|
||||
&test.as_call_expr()?.func.as_attribute_expr()?.value,
|
||||
);
|
||||
let func_name = test
|
||||
.as_call_expr()?
|
||||
.func
|
||||
.as_attribute_expr()?
|
||||
.attr
|
||||
.id
|
||||
.as_str();
|
||||
|
||||
let func_args = &test.as_call_expr()?.arguments.args;
|
||||
|
||||
let [affix] = func_args.as_ref() else {
|
||||
return None;
|
||||
};
|
||||
if compr_value != compr_test_expr || compr_test_expr != compr_else_or_target {
|
||||
return None;
|
||||
}
|
||||
let (affix_kind, bound) = match func_name {
|
||||
"startswith" if slice.upper.is_none() => (AffixKind::StartsWith, slice.lower.as_ref()?),
|
||||
"endswith" if slice.lower.is_none() => (AffixKind::EndsWith, slice.upper.as_ref()?),
|
||||
_ => return None,
|
||||
};
|
||||
Some(RemoveAffixData {
|
||||
text: value,
|
||||
bound,
|
||||
affix_query: AffixQuery {
|
||||
kind: affix_kind,
|
||||
affix,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Tests whether the slice of the given string actually removes the
|
||||
/// detected affix.
|
||||
///
|
||||
/// For example, in the situation
|
||||
///
|
||||
/// ```python
|
||||
/// text[:bound] if text.endswith(suffix) else text
|
||||
/// ```
|
||||
///
|
||||
/// This function verifies that `bound == -len(suffix)` in two cases:
|
||||
/// - `suffix` is a string literal and `bound` is a number literal
|
||||
/// - `suffix` is an expression and `bound` is
|
||||
/// exactly `-len(suffix)` (as AST nodes, prior to evaluation.)
|
||||
fn affix_matches_slice_bound(data: &RemoveAffixData, semantic: &SemanticModel) -> bool {
|
||||
let RemoveAffixData {
|
||||
text: _,
|
||||
bound,
|
||||
affix_query: AffixQuery { kind, affix },
|
||||
} = *data;
|
||||
|
||||
match (kind, bound, affix) {
|
||||
(
|
||||
AffixKind::StartsWith,
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: num,
|
||||
range: _,
|
||||
}),
|
||||
ast::Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
range: _,
|
||||
value: string_val,
|
||||
}),
|
||||
) => num
|
||||
.as_int()
|
||||
.and_then(ast::Int::as_u32) // Only support prefix removal for size at most `u32::MAX`
|
||||
.is_some_and(|x| x == string_val.to_str().text_len().to_u32()),
|
||||
(
|
||||
AffixKind::StartsWith,
|
||||
ast::Expr::Call(ast::ExprCall {
|
||||
range: _,
|
||||
func,
|
||||
arguments,
|
||||
}),
|
||||
_,
|
||||
) => {
|
||||
arguments.len() == 1
|
||||
&& arguments.find_positional(0).is_some_and(|arg| {
|
||||
let compr_affix = ast::comparable::ComparableExpr::from(affix);
|
||||
let compr_arg = ast::comparable::ComparableExpr::from(arg);
|
||||
compr_affix == compr_arg
|
||||
})
|
||||
&& semantic.match_builtin_expr(func, "len")
|
||||
}
|
||||
(
|
||||
AffixKind::EndsWith,
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: ast::UnaryOp::USub,
|
||||
operand,
|
||||
range: _,
|
||||
}),
|
||||
ast::Expr::StringLiteral(ast::ExprStringLiteral {
|
||||
range: _,
|
||||
value: string_val,
|
||||
}),
|
||||
) => operand.as_number_literal_expr().is_some_and(
|
||||
|ast::ExprNumberLiteral { value, .. }| {
|
||||
// Only support prefix removal for size at most `u32::MAX`
|
||||
value
|
||||
.as_int()
|
||||
.and_then(ast::Int::as_u32)
|
||||
.is_some_and(|x| x == string_val.to_str().text_len().to_u32())
|
||||
},
|
||||
),
|
||||
(
|
||||
AffixKind::EndsWith,
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
op: ast::UnaryOp::USub,
|
||||
operand,
|
||||
range: _,
|
||||
}),
|
||||
_,
|
||||
) => operand.as_call_expr().is_some_and(
|
||||
|ast::ExprCall {
|
||||
range: _,
|
||||
func,
|
||||
arguments,
|
||||
}| {
|
||||
arguments.len() == 1
|
||||
&& arguments.find_positional(0).is_some_and(|arg| {
|
||||
let compr_affix = ast::comparable::ComparableExpr::from(affix);
|
||||
let compr_arg = ast::comparable::ComparableExpr::from(arg);
|
||||
compr_affix == compr_arg
|
||||
})
|
||||
&& semantic.match_builtin_expr(func, "len")
|
||||
},
|
||||
),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates the source code string
|
||||
/// ```python
|
||||
/// text = text.removeprefix(prefix)
|
||||
/// ```
|
||||
/// or
|
||||
/// ```python
|
||||
/// text = text.removesuffix(prefix)
|
||||
/// ```
|
||||
/// as appropriate.
|
||||
fn generate_assignment_with_removeaffix(
|
||||
text: &ast::Expr,
|
||||
affix_query: &AffixQuery,
|
||||
locator: &Locator,
|
||||
) -> String {
|
||||
let text_str = locator.slice(text);
|
||||
let affix_str = locator.slice(affix_query.affix);
|
||||
let replacement = affix_query.kind.replacement();
|
||||
format!("{text_str} = {text_str}.{replacement}({affix_str})")
|
||||
}
|
||||
|
||||
/// Generates the source code string
|
||||
/// ```python
|
||||
/// text.removeprefix(prefix)
|
||||
/// ```
|
||||
/// or
|
||||
///
|
||||
/// ```python
|
||||
/// text.removesuffix(suffix)
|
||||
/// ```
|
||||
/// as appropriate.
|
||||
fn generate_removeaffix_expr(
|
||||
text: &ast::Expr,
|
||||
affix_query: &AffixQuery,
|
||||
locator: &Locator,
|
||||
) -> String {
|
||||
let text_str = locator.slice(text);
|
||||
let affix_str = locator.slice(affix_query.affix);
|
||||
let replacement = affix_query.kind.replacement();
|
||||
format!("{text_str}.{replacement}({affix_str})")
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum StmtOrExpr {
|
||||
Statement,
|
||||
Expression,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum AffixKind {
|
||||
StartsWith,
|
||||
EndsWith,
|
||||
}
|
||||
|
||||
impl AffixKind {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::StartsWith => "startswith",
|
||||
Self::EndsWith => "endswith",
|
||||
}
|
||||
}
|
||||
|
||||
const fn replacement(self) -> &'static str {
|
||||
match self {
|
||||
Self::StartsWith => "removeprefix",
|
||||
Self::EndsWith => "removesuffix",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Components of `startswith(prefix)` or `endswith(suffix)`.
|
||||
#[derive(Debug)]
|
||||
struct AffixQuery<'a> {
|
||||
/// Whether the method called is `startswith` or `endswith`.
|
||||
kind: AffixKind,
|
||||
/// Node representing the prefix or suffix being passed to the string method.
|
||||
affix: &'a ast::Expr,
|
||||
}
|
||||
|
||||
/// Ingredients for a statement or expression
|
||||
/// which potentially removes a prefix or suffix from a string.
|
||||
///
|
||||
/// Specifically
|
||||
#[derive(Debug)]
|
||||
struct RemoveAffixData<'a> {
|
||||
/// Node representing the string whose prefix or suffix we want to remove
|
||||
text: &'a ast::Expr,
|
||||
/// Node representing the bound used to slice the string
|
||||
bound: &'a ast::Expr,
|
||||
/// Contains the prefix or suffix used in `text.startswith(prefix)` or `text.endswith(suffix)`
|
||||
affix_query: AffixQuery<'a>,
|
||||
}
|
||||
@@ -1,177 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB188.py:7:5: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
||||
|
|
||||
6 | def remove_extension_via_slice(filename: str) -> str:
|
||||
7 | if filename.endswith(".txt"):
|
||||
| _____^
|
||||
8 | | filename = filename[:-4]
|
||||
| |________________________________^ FURB188
|
||||
9 |
|
||||
10 | return filename
|
||||
|
|
||||
= help: Use removesuffix instead of assignment conditional upon endswith.
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | # these should match
|
||||
5 5 |
|
||||
6 6 | def remove_extension_via_slice(filename: str) -> str:
|
||||
7 |- if filename.endswith(".txt"):
|
||||
8 |- filename = filename[:-4]
|
||||
7 |+ filename = filename.removesuffix(".txt")
|
||||
9 8 |
|
||||
10 9 | return filename
|
||||
11 10 |
|
||||
|
||||
FURB188.py:14:5: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
||||
|
|
||||
13 | def remove_extension_via_slice_len(filename: str, extension: str) -> str:
|
||||
14 | if filename.endswith(extension):
|
||||
| _____^
|
||||
15 | | filename = filename[:-len(extension)]
|
||||
| |_____________________________________________^ FURB188
|
||||
16 |
|
||||
17 | return filename
|
||||
|
|
||||
= help: Use removesuffix instead of assignment conditional upon endswith.
|
||||
|
||||
ℹ Safe fix
|
||||
11 11 |
|
||||
12 12 |
|
||||
13 13 | def remove_extension_via_slice_len(filename: str, extension: str) -> str:
|
||||
14 |- if filename.endswith(extension):
|
||||
15 |- filename = filename[:-len(extension)]
|
||||
14 |+ filename = filename.removesuffix(extension)
|
||||
16 15 |
|
||||
17 16 | return filename
|
||||
18 17 |
|
||||
|
||||
FURB188.py:21:12: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
||||
|
|
||||
20 | def remove_extension_via_ternary(filename: str) -> str:
|
||||
21 | return filename[:-4] if filename.endswith(".txt") else filename
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
|
|
||||
= help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 |
|
||||
19 19 |
|
||||
20 20 | def remove_extension_via_ternary(filename: str) -> str:
|
||||
21 |- return filename[:-4] if filename.endswith(".txt") else filename
|
||||
21 |+ return filename.removesuffix(".txt")
|
||||
22 22 |
|
||||
23 23 |
|
||||
24 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
|
||||
|
||||
FURB188.py:25:12: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
||||
|
|
||||
24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
|
||||
25 | return filename[:-len(extension)] if filename.endswith(extension) else filename
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
|
|
||||
= help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 |
|
||||
23 23 |
|
||||
24 24 | def remove_extension_via_ternary_with_len(filename: str, extension: str) -> str:
|
||||
25 |- return filename[:-len(extension)] if filename.endswith(extension) else filename
|
||||
25 |+ return filename.removesuffix(extension)
|
||||
26 26 |
|
||||
27 27 |
|
||||
28 28 | def remove_prefix(filename: str) -> str:
|
||||
|
||||
FURB188.py:29:12: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
||||
|
|
||||
28 | def remove_prefix(filename: str) -> str:
|
||||
29 | return filename[4:] if filename.startswith("abc-") else filename
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
|
|
||||
= help: Use removeprefix instead of ternary expression conditional upon startswith.
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 |
|
||||
27 27 |
|
||||
28 28 | def remove_prefix(filename: str) -> str:
|
||||
29 |- return filename[4:] if filename.startswith("abc-") else filename
|
||||
29 |+ return filename.removeprefix("abc-")
|
||||
30 30 |
|
||||
31 31 |
|
||||
32 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
|
||||
|
||||
FURB188.py:33:12: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
||||
|
|
||||
32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
|
||||
33 | return filename[len(prefix):] if filename.startswith(prefix) else filename
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
|
|
||||
= help: Use removeprefix instead of ternary expression conditional upon startswith.
|
||||
|
||||
ℹ Safe fix
|
||||
30 30 |
|
||||
31 31 |
|
||||
32 32 | def remove_prefix_via_len(filename: str, prefix: str) -> str:
|
||||
33 |- return filename[len(prefix):] if filename.startswith(prefix) else filename
|
||||
33 |+ return filename.removeprefix(prefix)
|
||||
34 34 |
|
||||
35 35 |
|
||||
36 36 | # these should not
|
||||
|
||||
FURB188.py:146:9: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
||||
|
|
||||
144 | SUFFIX = "suffix"
|
||||
145 |
|
||||
146 | x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
147 |
|
||||
148 | def remove_prefix_comparable_literal_expr() -> None:
|
||||
|
|
||||
= help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||
|
||||
ℹ Safe fix
|
||||
143 143 |
|
||||
144 144 | SUFFIX = "suffix"
|
||||
145 145 |
|
||||
146 |- x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
|
||||
146 |+ x = foo.bar.baz.removesuffix(SUFFIX)
|
||||
147 147 |
|
||||
148 148 | def remove_prefix_comparable_literal_expr() -> None:
|
||||
149 149 | return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
|
||||
|
||||
FURB188.py:149:12: FURB188 [*] Prefer `removeprefix` over conditionally replacing with slice.
|
||||
|
|
||||
148 | def remove_prefix_comparable_literal_expr() -> None:
|
||||
149 | return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
150 |
|
||||
151 | def shadow_builtins(filename: str, extension: str) -> None:
|
||||
|
|
||||
= help: Use removeprefix instead of ternary expression conditional upon startswith.
|
||||
|
||||
ℹ Safe fix
|
||||
146 146 | x = foo.bar.baz[:-len(SUFFIX)] if foo.bar.baz.endswith(SUFFIX) else foo.bar.baz
|
||||
147 147 |
|
||||
148 148 | def remove_prefix_comparable_literal_expr() -> None:
|
||||
149 |- return ("abc" "def")[3:] if ("abc" "def").startswith("abc") else "abc" "def"
|
||||
149 |+ return "abc" "def".removeprefix("abc")
|
||||
150 150 |
|
||||
151 151 | def shadow_builtins(filename: str, extension: str) -> None:
|
||||
152 152 | from builtins import len as builtins_len
|
||||
|
||||
FURB188.py:154:12: FURB188 [*] Prefer `removesuffix` over conditionally replacing with slice.
|
||||
|
|
||||
152 | from builtins import len as builtins_len
|
||||
153 |
|
||||
154 | return filename[:-builtins_len(extension)] if filename.endswith(extension) else filename
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB188
|
||||
|
|
||||
= help: Use removesuffix instead of ternary expression conditional upon endswith.
|
||||
|
||||
ℹ Safe fix
|
||||
151 151 | def shadow_builtins(filename: str, extension: str) -> None:
|
||||
152 152 | from builtins import len as builtins_len
|
||||
153 153 |
|
||||
154 |- return filename[:-builtins_len(extension)] if filename.endswith(extension) else filename
|
||||
154 |+ return filename.removesuffix(extension)
|
||||
@@ -58,7 +58,6 @@ mod tests {
|
||||
#[test_case(Rule::AssertWithPrintMessage, Path::new("RUF030.py"))]
|
||||
#[test_case(Rule::IncorrectlyParenthesizedTupleInSubscript, Path::new("RUF031.py"))]
|
||||
#[test_case(Rule::DecimalFromFloatLiteral, Path::new("RUF032.py"))]
|
||||
#[test_case(Rule::UselessIfElse, Path::new("RUF034.py"))]
|
||||
#[test_case(Rule::RedirectedNOQA, Path::new("RUF101.py"))]
|
||||
#[test_case(Rule::PostInitDefault, Path::new("RUF033.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
use std::fmt;
|
||||
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::{self as ast};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_source_file::Locator;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
@@ -52,90 +49,37 @@ pub(crate) fn decimal_from_float_literal_syntax(checker: &mut Checker, call: &as
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(float) = extract_float_literal(arg, Sign::Positive) {
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
.is_some_and(|qualified_name| {
|
||||
matches!(qualified_name.segments(), ["decimal", "Decimal"])
|
||||
})
|
||||
{
|
||||
let diagnostic = Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix(
|
||||
fix_float_literal(arg.range(), float, checker.locator(), checker.stylist()),
|
||||
);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
if !is_arg_float_literal(arg) {
|
||||
return;
|
||||
}
|
||||
|
||||
if checker
|
||||
.semantic()
|
||||
.resolve_qualified_name(call.func.as_ref())
|
||||
.is_some_and(|qualified_name| matches!(qualified_name.segments(), ["decimal", "Decimal"]))
|
||||
{
|
||||
let diagnostic =
|
||||
Diagnostic::new(DecimalFromFloatLiteral, arg.range()).with_fix(fix_float_literal(
|
||||
arg.range(),
|
||||
&checker.generator().expr(arg),
|
||||
checker.stylist(),
|
||||
));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Sign {
|
||||
Positive,
|
||||
Negative,
|
||||
}
|
||||
|
||||
impl Sign {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Positive => "",
|
||||
Self::Negative => "-",
|
||||
}
|
||||
}
|
||||
|
||||
const fn flip(self) -> Self {
|
||||
match self {
|
||||
Self::Negative => Self::Positive,
|
||||
Self::Positive => Self::Negative,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Sign {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
struct Float {
|
||||
/// The range of the float excluding the sign.
|
||||
/// E.g. for `+--+-+-4.3`, this will be the range of `4.3`
|
||||
value_range: TextRange,
|
||||
/// The resolved sign of the float (either `-` or `+`)
|
||||
sign: Sign,
|
||||
}
|
||||
|
||||
fn extract_float_literal(arg: &ast::Expr, sign: Sign) -> Option<Float> {
|
||||
fn is_arg_float_literal(arg: &ast::Expr) -> bool {
|
||||
match arg {
|
||||
ast::Expr::NumberLiteral(number_literal_expr) if number_literal_expr.value.is_float() => {
|
||||
Some(Float {
|
||||
value_range: arg.range(),
|
||||
sign,
|
||||
})
|
||||
}
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
operand,
|
||||
op: ast::UnaryOp::UAdd,
|
||||
ast::Expr::NumberLiteral(ast::ExprNumberLiteral {
|
||||
value: ast::Number::Float(_),
|
||||
..
|
||||
}) => extract_float_literal(operand, sign),
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp {
|
||||
operand,
|
||||
op: ast::UnaryOp::USub,
|
||||
..
|
||||
}) => extract_float_literal(operand, sign.flip()),
|
||||
_ => None,
|
||||
}) => true,
|
||||
ast::Expr::UnaryOp(ast::ExprUnaryOp { operand, .. }) => is_arg_float_literal(operand),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_float_literal(
|
||||
original_range: TextRange,
|
||||
float: Float,
|
||||
locator: &Locator,
|
||||
stylist: &Stylist,
|
||||
) -> Fix {
|
||||
let quote = stylist.quote();
|
||||
let Float { value_range, sign } = float;
|
||||
let float_value = locator.slice(value_range);
|
||||
let content = format!("{quote}{sign}{float_value}{quote}");
|
||||
Fix::unsafe_edit(Edit::range_replacement(content, original_range))
|
||||
fn fix_float_literal(range: TextRange, float_literal: &str, stylist: &Stylist) -> Fix {
|
||||
let content = format!("{quote}{float_literal}{quote}", quote = stylist.quote());
|
||||
Fix::unsafe_edit(Edit::range_replacement(content, range))
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ pub(crate) fn ignored_formatter_suppression_comment(checker: &mut Checker, suite
|
||||
.into_iter()
|
||||
.filter_map(|range| {
|
||||
Some(SuppressionComment {
|
||||
range,
|
||||
range: *range,
|
||||
kind: SuppressionKind::from_comment(locator.slice(range))?,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -29,7 +29,6 @@ pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
|
||||
pub(crate) use unnecessary_key_check::*;
|
||||
pub(crate) use unused_async::*;
|
||||
pub(crate) use unused_noqa::*;
|
||||
pub(crate) use useless_if_else::*;
|
||||
pub(crate) use zip_instead_of_pairwise::*;
|
||||
|
||||
mod ambiguous_unicode_character;
|
||||
@@ -67,7 +66,6 @@ mod unnecessary_iterable_allocation_for_first_element;
|
||||
mod unnecessary_key_check;
|
||||
mod unused_async;
|
||||
mod unused_noqa;
|
||||
mod useless_if_else;
|
||||
mod zip_instead_of_pairwise;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for useless if-else conditions with identical arms.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Useless if-else conditions add unnecessary complexity to the code without
|
||||
/// providing any logical benefit.
|
||||
///
|
||||
/// Assigning the value directly is clearer and more explicit, and
|
||||
/// should be preferred.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// # Bad
|
||||
/// foo = x if y else x
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// # Good
|
||||
/// foo = x
|
||||
/// ```
|
||||
#[violation]
|
||||
pub struct UselessIfElse;
|
||||
|
||||
impl Violation for UselessIfElse {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Useless if-else condition")
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF031
|
||||
pub(crate) fn useless_if_else(checker: &mut Checker, if_expr: &ast::ExprIf) {
|
||||
let ast::ExprIf {
|
||||
body,
|
||||
orelse,
|
||||
range,
|
||||
..
|
||||
} = if_expr;
|
||||
|
||||
// Skip if the body and orelse are not the same
|
||||
if ComparableExpr::from(body) != ComparableExpr::from(orelse) {
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(UselessIfElse, *range));
|
||||
}
|
||||
@@ -127,105 +127,65 @@ RUF032.py:45:15: RUF032 [*] `Decimal()` called with float literal argument
|
||||
47 47 | val = Decimal("-10.0")
|
||||
48 48 |
|
||||
|
||||
RUF032.py:56:15: RUF032 [*] `Decimal()` called with float literal argument
|
||||
RUF032.py:81:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
54 | val = Decimal(~4.0) # Skip
|
||||
55 |
|
||||
56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")`
|
||||
| ^^^^^ RUF032
|
||||
57 |
|
||||
58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
|
||||
|
|
||||
= help: Use a string literal instead
|
||||
|
||||
ℹ Unsafe fix
|
||||
53 53 | # See https://github.com/astral-sh/ruff/issues/13258
|
||||
54 54 | val = Decimal(~4.0) # Skip
|
||||
55 55 |
|
||||
56 |-val = Decimal(++4.0) # Suggest `Decimal("4.0")`
|
||||
56 |+val = Decimal("4.0") # Suggest `Decimal("4.0")`
|
||||
57 57 |
|
||||
58 58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
|
||||
59 59 |
|
||||
|
||||
RUF032.py:58:15: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")`
|
||||
57 |
|
||||
58 | val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
|
||||
| ^^^^^^^^^^^ RUF032
|
||||
|
|
||||
= help: Use a string literal instead
|
||||
|
||||
ℹ Unsafe fix
|
||||
55 55 |
|
||||
56 56 | val = Decimal(++4.0) # Suggest `Decimal("4.0")`
|
||||
57 57 |
|
||||
58 |-val = Decimal(-+--++--4.0) # Suggest `Decimal("-4.0")`
|
||||
58 |+val = Decimal("-4.0") # Suggest `Decimal("-4.0")`
|
||||
59 59 |
|
||||
60 60 |
|
||||
61 61 | # Tests with shadowed name
|
||||
|
||||
RUF032.py:88:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
86 | # Retest with fully qualified import
|
||||
87 |
|
||||
88 | val = decimal.Decimal(0.0) # Should error
|
||||
79 | # Retest with fully qualified import
|
||||
80 |
|
||||
81 | val = decimal.Decimal(0.0) # Should error
|
||||
| ^^^ RUF032
|
||||
89 |
|
||||
90 | val = decimal.Decimal("0.0")
|
||||
82 |
|
||||
83 | val = decimal.Decimal("0.0")
|
||||
|
|
||||
= help: Use a string literal instead
|
||||
|
||||
ℹ Unsafe fix
|
||||
85 85 |
|
||||
86 86 | # Retest with fully qualified import
|
||||
87 87 |
|
||||
88 |-val = decimal.Decimal(0.0) # Should error
|
||||
88 |+val = decimal.Decimal("0.0") # Should error
|
||||
89 89 |
|
||||
90 90 | val = decimal.Decimal("0.0")
|
||||
91 91 |
|
||||
78 78 |
|
||||
79 79 | # Retest with fully qualified import
|
||||
80 80 |
|
||||
81 |-val = decimal.Decimal(0.0) # Should error
|
||||
81 |+val = decimal.Decimal("0.0") # Should error
|
||||
82 82 |
|
||||
83 83 | val = decimal.Decimal("0.0")
|
||||
84 84 |
|
||||
|
||||
RUF032.py:92:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
RUF032.py:85:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
90 | val = decimal.Decimal("0.0")
|
||||
91 |
|
||||
92 | val = decimal.Decimal(10.0) # Should error
|
||||
83 | val = decimal.Decimal("0.0")
|
||||
84 |
|
||||
85 | val = decimal.Decimal(10.0) # Should error
|
||||
| ^^^^ RUF032
|
||||
93 |
|
||||
94 | val = decimal.Decimal("10.0")
|
||||
86 |
|
||||
87 | val = decimal.Decimal("10.0")
|
||||
|
|
||||
= help: Use a string literal instead
|
||||
|
||||
ℹ Unsafe fix
|
||||
89 89 |
|
||||
90 90 | val = decimal.Decimal("0.0")
|
||||
91 91 |
|
||||
92 |-val = decimal.Decimal(10.0) # Should error
|
||||
92 |+val = decimal.Decimal("10.0") # Should error
|
||||
93 93 |
|
||||
94 94 | val = decimal.Decimal("10.0")
|
||||
95 95 |
|
||||
82 82 |
|
||||
83 83 | val = decimal.Decimal("0.0")
|
||||
84 84 |
|
||||
85 |-val = decimal.Decimal(10.0) # Should error
|
||||
85 |+val = decimal.Decimal("10.0") # Should error
|
||||
86 86 |
|
||||
87 87 | val = decimal.Decimal("10.0")
|
||||
88 88 |
|
||||
|
||||
RUF032.py:96:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
RUF032.py:89:23: RUF032 [*] `Decimal()` called with float literal argument
|
||||
|
|
||||
94 | val = decimal.Decimal("10.0")
|
||||
95 |
|
||||
96 | val = decimal.Decimal(-10.0) # Should error
|
||||
87 | val = decimal.Decimal("10.0")
|
||||
88 |
|
||||
89 | val = decimal.Decimal(-10.0) # Should error
|
||||
| ^^^^^ RUF032
|
||||
97 |
|
||||
98 | val = decimal.Decimal("-10.0")
|
||||
90 |
|
||||
91 | val = decimal.Decimal("-10.0")
|
||||
|
|
||||
= help: Use a string literal instead
|
||||
|
||||
ℹ Unsafe fix
|
||||
93 93 |
|
||||
94 94 | val = decimal.Decimal("10.0")
|
||||
95 95 |
|
||||
96 |-val = decimal.Decimal(-10.0) # Should error
|
||||
96 |+val = decimal.Decimal("-10.0") # Should error
|
||||
97 97 |
|
||||
98 98 | val = decimal.Decimal("-10.0")
|
||||
99 99 |
|
||||
86 86 |
|
||||
87 87 | val = decimal.Decimal("10.0")
|
||||
88 88 |
|
||||
89 |-val = decimal.Decimal(-10.0) # Should error
|
||||
89 |+val = decimal.Decimal("-10.0") # Should error
|
||||
90 90 |
|
||||
91 91 | val = decimal.Decimal("-10.0")
|
||||
92 92 |
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/ruff/mod.rs
|
||||
---
|
||||
RUF034.py:5:5: RUF034 Useless if-else condition
|
||||
|
|
||||
4 | # Invalid
|
||||
5 | x = 1 if True else 1
|
||||
| ^^^^^^^^^^^^^^^^ RUF034
|
||||
6 |
|
||||
7 | # Invalid
|
||||
|
|
||||
|
||||
RUF034.py:8:5: RUF034 Useless if-else condition
|
||||
|
|
||||
7 | # Invalid
|
||||
8 | x = "a" if True else "a"
|
||||
| ^^^^^^^^^^^^^^^^^^^^ RUF034
|
||||
9 |
|
||||
10 | # Invalid
|
||||
|
|
||||
|
||||
RUF034.py:11:5: RUF034 Useless if-else condition
|
||||
|
|
||||
10 | # Invalid
|
||||
11 | x = 0.1 if False else 0.1
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ RUF034
|
||||
|
|
||||
@@ -3124,29 +3124,6 @@ impl Pattern {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if the [`Pattern`] is a [wildcard pattern].
|
||||
///
|
||||
/// The following are wildcard patterns:
|
||||
/// ```python
|
||||
/// match subject:
|
||||
/// case _ as x: ...
|
||||
/// case _ | _: ...
|
||||
/// case _: ...
|
||||
/// ```
|
||||
///
|
||||
/// [wildcard pattern]: https://docs.python.org/3/reference/compound_stmts.html#wildcard-patterns
|
||||
pub fn is_wildcard(&self) -> bool {
|
||||
match self {
|
||||
Pattern::MatchAs(PatternMatchAs { pattern, .. }) => {
|
||||
pattern.as_deref().map_or(true, Pattern::is_wildcard)
|
||||
}
|
||||
Pattern::MatchOr(PatternMatchOr { patterns, .. }) => {
|
||||
patterns.iter().all(Pattern::is_wildcard)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [MatchValue](https://docs.python.org/3/library/ast.html#ast.MatchValue)
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
use ruff_python_parser::parse_module;
|
||||
|
||||
#[test]
|
||||
fn pattern_is_wildcard() {
|
||||
let source_code = r"
|
||||
match subject:
|
||||
case _ as x: ...
|
||||
case _ | _: ...
|
||||
case _: ...
|
||||
";
|
||||
let parsed = parse_module(source_code).unwrap();
|
||||
let cases = &parsed.syntax().body[0].as_match_stmt().unwrap().cases;
|
||||
for case in cases {
|
||||
assert!(case.pattern.is_wildcard());
|
||||
}
|
||||
}
|
||||
@@ -97,8 +97,9 @@ impl StringParser {
|
||||
|
||||
#[inline]
|
||||
fn next_char(&mut self) -> Option<char> {
|
||||
self.source[self.cursor..].chars().next().inspect(|c| {
|
||||
self.source[self.cursor..].chars().next().map(|c| {
|
||||
self.cursor += c.len_utf8();
|
||||
c
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -23,197 +23,182 @@ pub const MAGIC_GLOBALS: &[&str] = &[
|
||||
"__file__",
|
||||
];
|
||||
|
||||
static ALWAYS_AVAILABLE_BUILTINS: &[&str] = &[
|
||||
"ArithmeticError",
|
||||
"AssertionError",
|
||||
"AttributeError",
|
||||
"BaseException",
|
||||
"BlockingIOError",
|
||||
"BrokenPipeError",
|
||||
"BufferError",
|
||||
"BytesWarning",
|
||||
"ChildProcessError",
|
||||
"ConnectionAbortedError",
|
||||
"ConnectionError",
|
||||
"ConnectionRefusedError",
|
||||
"ConnectionResetError",
|
||||
"DeprecationWarning",
|
||||
"EOFError",
|
||||
"Ellipsis",
|
||||
"EnvironmentError",
|
||||
"Exception",
|
||||
"False",
|
||||
"FileExistsError",
|
||||
"FileNotFoundError",
|
||||
"FloatingPointError",
|
||||
"FutureWarning",
|
||||
"GeneratorExit",
|
||||
"IOError",
|
||||
"ImportError",
|
||||
"ImportWarning",
|
||||
"IndentationError",
|
||||
"IndexError",
|
||||
"InterruptedError",
|
||||
"IsADirectoryError",
|
||||
"KeyError",
|
||||
"KeyboardInterrupt",
|
||||
"LookupError",
|
||||
"MemoryError",
|
||||
"ModuleNotFoundError",
|
||||
"NameError",
|
||||
"None",
|
||||
"NotADirectoryError",
|
||||
"NotImplemented",
|
||||
"NotImplementedError",
|
||||
"OSError",
|
||||
"OverflowError",
|
||||
"PendingDeprecationWarning",
|
||||
"PermissionError",
|
||||
"ProcessLookupError",
|
||||
"RecursionError",
|
||||
"ReferenceError",
|
||||
"ResourceWarning",
|
||||
"RuntimeError",
|
||||
"RuntimeWarning",
|
||||
"StopAsyncIteration",
|
||||
"StopIteration",
|
||||
"SyntaxError",
|
||||
"SyntaxWarning",
|
||||
"SystemError",
|
||||
"SystemExit",
|
||||
"TabError",
|
||||
"TimeoutError",
|
||||
"True",
|
||||
"TypeError",
|
||||
"UnboundLocalError",
|
||||
"UnicodeDecodeError",
|
||||
"UnicodeEncodeError",
|
||||
"UnicodeError",
|
||||
"UnicodeTranslateError",
|
||||
"UnicodeWarning",
|
||||
"UserWarning",
|
||||
"ValueError",
|
||||
"Warning",
|
||||
"ZeroDivisionError",
|
||||
"__build_class__",
|
||||
"__debug__",
|
||||
"__doc__",
|
||||
"__import__",
|
||||
"__loader__",
|
||||
"__name__",
|
||||
"__package__",
|
||||
"__spec__",
|
||||
"abs",
|
||||
"all",
|
||||
"any",
|
||||
"ascii",
|
||||
"bin",
|
||||
"bool",
|
||||
"breakpoint",
|
||||
"bytearray",
|
||||
"bytes",
|
||||
"callable",
|
||||
"chr",
|
||||
"classmethod",
|
||||
"compile",
|
||||
"complex",
|
||||
"copyright",
|
||||
"credits",
|
||||
"delattr",
|
||||
"dict",
|
||||
"dir",
|
||||
"divmod",
|
||||
"enumerate",
|
||||
"eval",
|
||||
"exec",
|
||||
"exit",
|
||||
"filter",
|
||||
"float",
|
||||
"format",
|
||||
"frozenset",
|
||||
"getattr",
|
||||
"globals",
|
||||
"hasattr",
|
||||
"hash",
|
||||
"help",
|
||||
"hex",
|
||||
"id",
|
||||
"input",
|
||||
"int",
|
||||
"isinstance",
|
||||
"issubclass",
|
||||
"iter",
|
||||
"len",
|
||||
"license",
|
||||
"list",
|
||||
"locals",
|
||||
"map",
|
||||
"max",
|
||||
"memoryview",
|
||||
"min",
|
||||
"next",
|
||||
"object",
|
||||
"oct",
|
||||
"open",
|
||||
"ord",
|
||||
"pow",
|
||||
"print",
|
||||
"property",
|
||||
"quit",
|
||||
"range",
|
||||
"repr",
|
||||
"reversed",
|
||||
"round",
|
||||
"set",
|
||||
"setattr",
|
||||
"slice",
|
||||
"sorted",
|
||||
"staticmethod",
|
||||
"str",
|
||||
"sum",
|
||||
"super",
|
||||
"tuple",
|
||||
"type",
|
||||
"vars",
|
||||
"zip",
|
||||
];
|
||||
static PY310_PLUS_BUILTINS: &[&str] = &["EncodingWarning", "aiter", "anext"];
|
||||
static PY311_PLUS_BUILTINS: &[&str] = &["BaseExceptionGroup", "ExceptionGroup"];
|
||||
static PY313_PLUS_BUILTINS: &[&str] = &["PythonFinalizationError"];
|
||||
|
||||
/// Return the list of builtins for the given Python minor version.
|
||||
///
|
||||
/// Intended to be kept in sync with [`is_python_builtin`].
|
||||
pub fn python_builtins(minor_version: u8, is_notebook: bool) -> impl Iterator<Item = &'static str> {
|
||||
let py310_builtins = if minor_version >= 10 {
|
||||
Some(PY310_PLUS_BUILTINS)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let py311_builtins = if minor_version >= 11 {
|
||||
Some(PY311_PLUS_BUILTINS)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let py313_builtins = if minor_version >= 13 {
|
||||
Some(PY313_PLUS_BUILTINS)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let ipython_builtins = if is_notebook {
|
||||
Some(IPYTHON_BUILTINS)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
pub fn python_builtins(minor_version: u8, is_notebook: bool) -> Vec<&'static str> {
|
||||
let mut builtins = vec![
|
||||
"ArithmeticError",
|
||||
"AssertionError",
|
||||
"AttributeError",
|
||||
"BaseException",
|
||||
"BlockingIOError",
|
||||
"BrokenPipeError",
|
||||
"BufferError",
|
||||
"BytesWarning",
|
||||
"ChildProcessError",
|
||||
"ConnectionAbortedError",
|
||||
"ConnectionError",
|
||||
"ConnectionRefusedError",
|
||||
"ConnectionResetError",
|
||||
"DeprecationWarning",
|
||||
"EOFError",
|
||||
"Ellipsis",
|
||||
"EnvironmentError",
|
||||
"Exception",
|
||||
"False",
|
||||
"FileExistsError",
|
||||
"FileNotFoundError",
|
||||
"FloatingPointError",
|
||||
"FutureWarning",
|
||||
"GeneratorExit",
|
||||
"IOError",
|
||||
"ImportError",
|
||||
"ImportWarning",
|
||||
"IndentationError",
|
||||
"IndexError",
|
||||
"InterruptedError",
|
||||
"IsADirectoryError",
|
||||
"KeyError",
|
||||
"KeyboardInterrupt",
|
||||
"LookupError",
|
||||
"MemoryError",
|
||||
"ModuleNotFoundError",
|
||||
"NameError",
|
||||
"None",
|
||||
"NotADirectoryError",
|
||||
"NotImplemented",
|
||||
"NotImplementedError",
|
||||
"OSError",
|
||||
"OverflowError",
|
||||
"PendingDeprecationWarning",
|
||||
"PermissionError",
|
||||
"ProcessLookupError",
|
||||
"RecursionError",
|
||||
"ReferenceError",
|
||||
"ResourceWarning",
|
||||
"RuntimeError",
|
||||
"RuntimeWarning",
|
||||
"StopAsyncIteration",
|
||||
"StopIteration",
|
||||
"SyntaxError",
|
||||
"SyntaxWarning",
|
||||
"SystemError",
|
||||
"SystemExit",
|
||||
"TabError",
|
||||
"TimeoutError",
|
||||
"True",
|
||||
"TypeError",
|
||||
"UnboundLocalError",
|
||||
"UnicodeDecodeError",
|
||||
"UnicodeEncodeError",
|
||||
"UnicodeError",
|
||||
"UnicodeTranslateError",
|
||||
"UnicodeWarning",
|
||||
"UserWarning",
|
||||
"ValueError",
|
||||
"Warning",
|
||||
"ZeroDivisionError",
|
||||
"__build_class__",
|
||||
"__debug__",
|
||||
"__doc__",
|
||||
"__import__",
|
||||
"__loader__",
|
||||
"__name__",
|
||||
"__package__",
|
||||
"__spec__",
|
||||
"abs",
|
||||
"all",
|
||||
"any",
|
||||
"ascii",
|
||||
"bin",
|
||||
"bool",
|
||||
"breakpoint",
|
||||
"bytearray",
|
||||
"bytes",
|
||||
"callable",
|
||||
"chr",
|
||||
"classmethod",
|
||||
"compile",
|
||||
"complex",
|
||||
"copyright",
|
||||
"credits",
|
||||
"delattr",
|
||||
"dict",
|
||||
"dir",
|
||||
"divmod",
|
||||
"enumerate",
|
||||
"eval",
|
||||
"exec",
|
||||
"exit",
|
||||
"filter",
|
||||
"float",
|
||||
"format",
|
||||
"frozenset",
|
||||
"getattr",
|
||||
"globals",
|
||||
"hasattr",
|
||||
"hash",
|
||||
"help",
|
||||
"hex",
|
||||
"id",
|
||||
"input",
|
||||
"int",
|
||||
"isinstance",
|
||||
"issubclass",
|
||||
"iter",
|
||||
"len",
|
||||
"license",
|
||||
"list",
|
||||
"locals",
|
||||
"map",
|
||||
"max",
|
||||
"memoryview",
|
||||
"min",
|
||||
"next",
|
||||
"object",
|
||||
"oct",
|
||||
"open",
|
||||
"ord",
|
||||
"pow",
|
||||
"print",
|
||||
"property",
|
||||
"quit",
|
||||
"range",
|
||||
"repr",
|
||||
"reversed",
|
||||
"round",
|
||||
"set",
|
||||
"setattr",
|
||||
"slice",
|
||||
"sorted",
|
||||
"staticmethod",
|
||||
"str",
|
||||
"sum",
|
||||
"super",
|
||||
"tuple",
|
||||
"type",
|
||||
"vars",
|
||||
"zip",
|
||||
];
|
||||
|
||||
py310_builtins
|
||||
.into_iter()
|
||||
.chain(py311_builtins)
|
||||
.chain(py313_builtins)
|
||||
.chain(ipython_builtins)
|
||||
.flatten()
|
||||
.chain(ALWAYS_AVAILABLE_BUILTINS)
|
||||
.copied()
|
||||
if minor_version >= 10 {
|
||||
builtins.extend(&["EncodingWarning", "aiter", "anext"]);
|
||||
}
|
||||
|
||||
if minor_version >= 11 {
|
||||
builtins.extend(&["BaseExceptionGroup", "ExceptionGroup"]);
|
||||
}
|
||||
|
||||
if minor_version >= 13 {
|
||||
builtins.push("PythonFinalizationError");
|
||||
}
|
||||
|
||||
if is_notebook {
|
||||
builtins.extend(IPYTHON_BUILTINS);
|
||||
}
|
||||
|
||||
builtins
|
||||
}
|
||||
|
||||
/// Returns `true` if the given name is that of a Python builtin.
|
||||
@@ -385,22 +370,6 @@ pub fn is_python_builtin(name: &str, minor_version: u8, is_notebook: bool) -> bo
|
||||
)
|
||||
}
|
||||
|
||||
/// Return `Some(version)`, where `version` corresponds to the Python minor version
|
||||
/// in which the builtin was added
|
||||
pub fn version_builtin_was_added(name: &str) -> Option<u8> {
|
||||
if PY310_PLUS_BUILTINS.contains(&name) {
|
||||
Some(10)
|
||||
} else if PY311_PLUS_BUILTINS.contains(&name) {
|
||||
Some(11)
|
||||
} else if PY313_PLUS_BUILTINS.contains(&name) {
|
||||
Some(13)
|
||||
} else if ALWAYS_AVAILABLE_BUILTINS.contains(&name) {
|
||||
Some(0)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the given name is that of a Python builtin iterator.
|
||||
pub fn is_iterator(name: &str) -> bool {
|
||||
matches!(
|
||||
|
||||
@@ -215,10 +215,10 @@ impl Debug for CommentRanges {
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a CommentRanges {
|
||||
type Item = TextRange;
|
||||
type IntoIter = std::iter::Copied<std::slice::Iter<'a, TextRange>>;
|
||||
type Item = &'a TextRange;
|
||||
type IntoIter = std::slice::Iter<'a, TextRange>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.raw.iter().copied()
|
||||
self.raw.iter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ use lsp_types::{PositionEncodingKind, Url};
|
||||
pub use notebook::NotebookDocument;
|
||||
pub(crate) use range::{NotebookRange, RangeExt, ToRangeExt};
|
||||
pub(crate) use replacement::Replacement;
|
||||
pub(crate) use text_document::DocumentVersion;
|
||||
pub use text_document::TextDocument;
|
||||
pub(crate) use text_document::{DocumentVersion, LanguageId};
|
||||
|
||||
use crate::{fix::Fixes, session::ResolvedClientCapabilities};
|
||||
|
||||
|
||||
@@ -20,23 +20,6 @@ pub struct TextDocument {
|
||||
/// The latest version of the document, set by the LSP client. The server will panic in
|
||||
/// debug mode if we attempt to update the document with an 'older' version.
|
||||
version: DocumentVersion,
|
||||
/// The language ID of the document as provided by the client.
|
||||
language_id: Option<LanguageId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum LanguageId {
|
||||
Python,
|
||||
Other,
|
||||
}
|
||||
|
||||
impl From<&str> for LanguageId {
|
||||
fn from(language_id: &str) -> Self {
|
||||
match language_id {
|
||||
"python" => Self::Python,
|
||||
_ => Self::Other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextDocument {
|
||||
@@ -46,16 +29,9 @@ impl TextDocument {
|
||||
contents,
|
||||
index,
|
||||
version,
|
||||
language_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_language_id(mut self, language_id: &str) -> Self {
|
||||
self.language_id = Some(LanguageId::from(language_id));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_contents(self) -> String {
|
||||
self.contents
|
||||
}
|
||||
@@ -72,10 +48,6 @@ impl TextDocument {
|
||||
self.version
|
||||
}
|
||||
|
||||
pub fn language_id(&self) -> Option<LanguageId> {
|
||||
self.language_id
|
||||
}
|
||||
|
||||
pub fn apply_changes(
|
||||
&mut self,
|
||||
changes: Vec<lsp_types::TextDocumentContentChangeEvent>,
|
||||
|
||||
@@ -38,7 +38,6 @@ pub(crate) fn fix_all(
|
||||
file_resolver_settings,
|
||||
Some(linter_settings),
|
||||
None,
|
||||
query.text_document_language_id(),
|
||||
) {
|
||||
return Ok(Fixes::default());
|
||||
}
|
||||
|
||||
@@ -77,7 +77,6 @@ pub(crate) fn check(
|
||||
file_resolver_settings,
|
||||
Some(linter_settings),
|
||||
None,
|
||||
query.text_document_language_id(),
|
||||
) {
|
||||
return DiagnosticsMap::default();
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ use ruff_linter::settings::LinterSettings;
|
||||
use ruff_workspace::resolver::{match_any_exclusion, match_any_inclusion};
|
||||
use ruff_workspace::{FileResolverSettings, FormatterSettings};
|
||||
|
||||
use crate::edit::LanguageId;
|
||||
|
||||
/// Return `true` if the document at the given [`Path`] should be excluded.
|
||||
///
|
||||
/// The tool-specific settings should be provided if the request for the document is specific to
|
||||
@@ -21,7 +19,6 @@ pub(crate) fn is_document_excluded(
|
||||
resolver_settings: &FileResolverSettings,
|
||||
linter_settings: Option<&LinterSettings>,
|
||||
formatter_settings: Option<&FormatterSettings>,
|
||||
language_id: Option<LanguageId>,
|
||||
) -> bool {
|
||||
if let Some(exclusion) = match_any_exclusion(
|
||||
path,
|
||||
@@ -41,14 +38,8 @@ pub(crate) fn is_document_excluded(
|
||||
) {
|
||||
tracing::debug!("Included path via `{}`: {}", inclusion, path.display());
|
||||
false
|
||||
} else if let Some(LanguageId::Python) = language_id {
|
||||
tracing::debug!("Included path via Python language ID: {}", path.display());
|
||||
false
|
||||
} else {
|
||||
tracing::debug!(
|
||||
"Ignored path as it's not in the inclusion set: {}",
|
||||
path.display()
|
||||
);
|
||||
// Path is excluded by not being in the inclusion set.
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,11 @@ impl super::SyncNotificationHandler for DidOpen {
|
||||
types::DidOpenTextDocumentParams {
|
||||
text_document:
|
||||
types::TextDocumentItem {
|
||||
uri,
|
||||
text,
|
||||
version,
|
||||
language_id,
|
||||
uri, text, version, ..
|
||||
},
|
||||
}: types::DidOpenTextDocumentParams,
|
||||
) -> Result<()> {
|
||||
let document = TextDocument::new(text, version).with_language_id(&language_id);
|
||||
let document = TextDocument::new(text, version);
|
||||
|
||||
session.open_text_document(uri.clone(), document);
|
||||
|
||||
|
||||
@@ -90,7 +90,6 @@ fn format_text_document(
|
||||
file_resolver_settings,
|
||||
None,
|
||||
Some(formatter_settings),
|
||||
text_document.language_id(),
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ fn format_text_document_range(
|
||||
file_resolver_settings,
|
||||
None,
|
||||
Some(formatter_settings),
|
||||
text_document.language_id(),
|
||||
) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use rustc_hash::FxHashMap;
|
||||
|
||||
pub(crate) use ruff_settings::RuffSettings;
|
||||
|
||||
use crate::edit::LanguageId;
|
||||
use crate::{
|
||||
edit::{DocumentKey, DocumentVersion, NotebookDocument},
|
||||
PositionEncoding, TextDocument,
|
||||
@@ -604,12 +603,4 @@ impl DocumentQuery {
|
||||
.and_then(|cell_uri| notebook.cell_document_by_uri(cell_uri)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn text_document_language_id(&self) -> Option<LanguageId> {
|
||||
if let DocumentQuery::Text { document, .. } = self {
|
||||
document.language_id()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +340,7 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
|
||||
|
||||
// Merge in the editor-specified configuration file, if it exists.
|
||||
let editor_configuration = if let Some(config_file_path) = configuration {
|
||||
match open_configuration_file(&config_file_path) {
|
||||
match open_configuration_file(&config_file_path, project_root) {
|
||||
Ok(config_from_file) => editor_configuration.combine(config_from_file),
|
||||
Err(err) => {
|
||||
tracing::error!("Unable to find editor-specified configuration file: {err}");
|
||||
@@ -363,18 +363,11 @@ impl<'a> ConfigurationTransformer for EditorConfigurationTransformer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn open_configuration_file(config_path: &Path) -> crate::Result<Configuration> {
|
||||
ruff_workspace::resolver::resolve_configuration(
|
||||
config_path,
|
||||
Relativity::Parent,
|
||||
&IdentityTransformer,
|
||||
)
|
||||
}
|
||||
fn open_configuration_file(
|
||||
config_path: &Path,
|
||||
project_root: &Path,
|
||||
) -> crate::Result<Configuration> {
|
||||
let options = ruff_workspace::pyproject::load_options(config_path)?;
|
||||
|
||||
struct IdentityTransformer;
|
||||
|
||||
impl ConfigurationTransformer for IdentityTransformer {
|
||||
fn transform(&self, config: Configuration) -> Configuration {
|
||||
config
|
||||
}
|
||||
Configuration::from_options(options, Some(config_path), project_root)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_wasm"
|
||||
version = "0.6.5"
|
||||
version = "0.6.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -140,7 +140,7 @@ pub fn find_user_settings_toml() -> Option<PathBuf> {
|
||||
}
|
||||
|
||||
/// Load `Options` from a `pyproject.toml` or `ruff.toml` file.
|
||||
pub(super) fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
pub fn load_options<P: AsRef<Path>>(path: P) -> Result<Options> {
|
||||
if path.as_ref().ends_with("pyproject.toml") {
|
||||
let pyproject = parse_pyproject_toml(&path)?;
|
||||
let mut ruff = pyproject
|
||||
|
||||
@@ -263,7 +263,7 @@ pub trait ConfigurationTransformer {
|
||||
// configuration file extends another in the same path, we'll re-parse the same
|
||||
// file at least twice (possibly more than twice, since we'll also parse it when
|
||||
// resolving the "default" configuration).
|
||||
pub fn resolve_configuration(
|
||||
fn resolve_configuration(
|
||||
pyproject: &Path,
|
||||
relativity: Relativity,
|
||||
transformer: &dyn ConfigurationTransformer,
|
||||
|
||||
@@ -458,21 +458,19 @@ parentheses:
|
||||
|
||||
```python
|
||||
# Input
|
||||
for a, [b, d,] in c:
|
||||
for a, f(b,) in c:
|
||||
pass
|
||||
|
||||
# Black
|
||||
for a, [
|
||||
for a, f(
|
||||
b,
|
||||
d,
|
||||
] in c:
|
||||
) in c:
|
||||
pass
|
||||
|
||||
# Ruff
|
||||
for a, [
|
||||
for a, f(
|
||||
b,
|
||||
d,
|
||||
] in c:
|
||||
) in c:
|
||||
pass
|
||||
```
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ Ruff can be used as a [pre-commit](https://pre-commit.com) hook via [`ruff-pre-c
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.5
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -91,7 +91,7 @@ To enable lint fixes, add the `--fix` argument to the lint hook:
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.5
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -105,7 +105,7 @@ To run the hooks over Jupyter Notebooks too, add `jupyter` to the list of allowe
|
||||
```yaml
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.6.5
|
||||
rev: v0.6.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PyYAML==6.0.2
|
||||
ruff==0.6.4
|
||||
ruff==0.6.3
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material @ git+ssh://git@github.com/astral-sh/mkdocs-material-insiders.git@38c0b8187325c3bab386b666daf3518ac036f2f4
|
||||
mkdocs-redirects==1.2.1
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
PyYAML==6.0.2
|
||||
ruff==0.6.4
|
||||
ruff==0.6.3
|
||||
mkdocs==1.6.1
|
||||
mkdocs-material==9.1.18
|
||||
mkdocs-redirects==1.2.1
|
||||
|
||||
@@ -109,3 +109,16 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Omits the nav title "Ruff" entirely unless on a small screen, in which case
|
||||
the nav title is needed for backwards navigation in the collapsible
|
||||
nav variant.
|
||||
|
||||
See https://github.com/astral-sh/uv/issues/5130 */
|
||||
.md-nav__title {
|
||||
display: none;
|
||||
}
|
||||
@media screen and (max-width: 1219px) {
|
||||
.md-nav__title {
|
||||
display: flex ;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,16 +4,16 @@ theme:
|
||||
logo: assets/bolt.svg
|
||||
favicon: assets/favicon.ico
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.instant.prefetch
|
||||
- navigation.tracking
|
||||
- content.code.annotate
|
||||
- toc.integrate
|
||||
- toc.follow
|
||||
- navigation.path
|
||||
- navigation.top
|
||||
- content.code.copy
|
||||
- content.tabs.link
|
||||
- navigation.footer
|
||||
- navigation.instant
|
||||
- navigation.instant.prefetch
|
||||
- navigation.path
|
||||
- navigation.top
|
||||
- navigation.tracking
|
||||
- toc.follow
|
||||
palette:
|
||||
# Note: Using the system theme works with the insiders version
|
||||
# https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/#automatic-light-dark-mode
|
||||
@@ -37,7 +37,6 @@ repo_name: ruff
|
||||
site_author: charliermarsh
|
||||
site_url: https://docs.astral.sh/ruff/
|
||||
site_dir: site/ruff
|
||||
site_description: An extremely fast Python linter and code formatter, written in Rust.
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- pymdownx.details
|
||||
@@ -72,6 +71,15 @@ not_in_nav: |
|
||||
extra:
|
||||
analytics:
|
||||
provider: fathom
|
||||
social:
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/astral-sh/ruff
|
||||
- icon: fontawesome/brands/discord
|
||||
link: https://discord.com/invite/astral-sh
|
||||
- icon: fontawesome/brands/python
|
||||
link: https://pypi.org/project/ruff/
|
||||
- icon: fontawesome/brands/x-twitter
|
||||
link: https://x.com/astral_sh
|
||||
validation:
|
||||
omitted_files: warn
|
||||
absolute_links: warn
|
||||
|
||||
22
playground/api/package-lock.json
generated
22
playground/api/package-lock.json
generated
@@ -16,7 +16,7 @@
|
||||
"@cloudflare/workers-types": "^4.20230801.0",
|
||||
"miniflare": "^3.20230801.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "3.75.0"
|
||||
"wrangler": "3.73.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/kv-asset-handler": {
|
||||
@@ -128,9 +128,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@cloudflare/workers-types": {
|
||||
"version": "4.20240903.0",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240903.0.tgz",
|
||||
"integrity": "sha512-a4mqgtVsPWg3JNNlQdLRE0Z6/mHr/uXa1ANDw6Zd7in438UCbeb+j7Z954Sf93G24jExpAn9VZ8kUUml0RwZbQ==",
|
||||
"version": "4.20240821.1",
|
||||
"resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20240821.1.tgz",
|
||||
"integrity": "sha512-icAkbnAqgVl6ef9lgLTom8na+kj2RBw2ViPAQ586hbdj0xZcnrjK7P46Eu08OU9D/lNDgN2sKU/sxhe2iK/gIg==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0"
|
||||
},
|
||||
@@ -1105,9 +1105,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/miniflare": {
|
||||
"version": "3.20240821.1",
|
||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240821.1.tgz",
|
||||
"integrity": "sha512-81qdiryDG7VXzZuoa0EwhkaIYYrn7+StRIrd/2i7SPqPUNICUBjbhFFKqTnvE1+fqIPPB6l8ShKFaFvmnZOASg==",
|
||||
"version": "3.20240821.0",
|
||||
"resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20240821.0.tgz",
|
||||
"integrity": "sha512-4BhLGpssQxM/O6TZmJ10GkT3wBJK6emFkZ3V87/HyvQmVt8zMxEBvyw5uv6kdtp+7F54Nw6IKFJjPUL8rFVQrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1591,9 +1591,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wrangler": {
|
||||
"version": "3.75.0",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.75.0.tgz",
|
||||
"integrity": "sha512-CitNuNj0O1z6qbonUXmpUbxeWpU3nx28Kc4ZT33tMdeooQssb063Ie7+ZCdfS3kPhRHSwGdtOV22xFYytHON8w==",
|
||||
"version": "3.73.0",
|
||||
"resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.73.0.tgz",
|
||||
"integrity": "sha512-VrdDR2OpvsCQp+r5Of3rDP1W64cNN/LHLVx1roULOlPS8PZiv7rUYgkwhdCQ61+HICAaeSxWYIzkL5+B9+8W3g==",
|
||||
"dev": true,
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -1605,7 +1605,7 @@
|
||||
"chokidar": "^3.5.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"esbuild": "0.17.19",
|
||||
"miniflare": "3.20240821.1",
|
||||
"miniflare": "3.20240821.0",
|
||||
"nanoid": "^3.3.3",
|
||||
"path-to-regexp": "^6.2.0",
|
||||
"resolve": "^1.22.8",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"@cloudflare/workers-types": "^4.20230801.0",
|
||||
"miniflare": "^3.20230801.1",
|
||||
"typescript": "^5.1.6",
|
||||
"wrangler": "3.75.0"
|
||||
"wrangler": "3.73.0"
|
||||
},
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
172
playground/package-lock.json
generated
172
playground/package-lock.json
generated
@@ -14,8 +14,7 @@
|
||||
"monaco-editor": "^0.51.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-panels": "^2.0.0",
|
||||
"smol-toml": "^1.3.0"
|
||||
"react-resizable-panels": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
@@ -874,13 +873,6 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rtsao/scc": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
|
||||
"integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/core": {
|
||||
"version": "1.5.24",
|
||||
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.5.24.tgz",
|
||||
@@ -1146,17 +1138,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz",
|
||||
"integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
|
||||
"integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.4.0",
|
||||
"@typescript-eslint/type-utils": "8.4.0",
|
||||
"@typescript-eslint/utils": "8.4.0",
|
||||
"@typescript-eslint/visitor-keys": "8.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.3.0",
|
||||
"@typescript-eslint/type-utils": "8.3.0",
|
||||
"@typescript-eslint/utils": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1180,16 +1172,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz",
|
||||
"integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
|
||||
"integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.4.0",
|
||||
"@typescript-eslint/types": "8.4.0",
|
||||
"@typescript-eslint/typescript-estree": "8.4.0",
|
||||
"@typescript-eslint/visitor-keys": "8.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.3.0",
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/typescript-estree": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1209,14 +1201,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz",
|
||||
"integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
|
||||
"integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.4.0",
|
||||
"@typescript-eslint/visitor-keys": "8.4.0"
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1227,14 +1219,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz",
|
||||
"integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
|
||||
"integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.4.0",
|
||||
"@typescript-eslint/utils": "8.4.0",
|
||||
"@typescript-eslint/typescript-estree": "8.3.0",
|
||||
"@typescript-eslint/utils": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1252,9 +1244,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz",
|
||||
"integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
|
||||
"integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1266,14 +1258,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz",
|
||||
"integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
|
||||
"integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.4.0",
|
||||
"@typescript-eslint/visitor-keys": "8.4.0",
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/visitor-keys": "8.3.0",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1321,16 +1313,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz",
|
||||
"integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
|
||||
"integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.4.0",
|
||||
"@typescript-eslint/types": "8.4.0",
|
||||
"@typescript-eslint/typescript-estree": "8.4.0"
|
||||
"@typescript-eslint/scope-manager": "8.3.0",
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"@typescript-eslint/typescript-estree": "8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1344,13 +1336,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz",
|
||||
"integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==",
|
||||
"version": "8.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
|
||||
"integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.4.0",
|
||||
"@typescript-eslint/types": "8.3.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2394,11 +2386,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-module-utils": {
|
||||
"version": "2.11.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.11.0.tgz",
|
||||
"integrity": "sha512-gbBE5Hitek/oG6MUVj6sFuzEjA/ClzNflVrLovHi/JgLdC7fiN5gLAY1WIPW1a0V5I999MnsrvVrCOGmmVqDBQ==",
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz",
|
||||
"integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^3.2.7"
|
||||
},
|
||||
@@ -2421,28 +2412,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.30.0.tgz",
|
||||
"integrity": "sha512-/mHNE9jINJfiD2EKkg1BKyPyUk4zdnT54YgbOgfjSakWT5oyX/qQLVNTkehyfpcMxZXMy1zyonZ2v7hZTX43Yw==",
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
|
||||
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
"array.prototype.findlastindex": "^1.2.5",
|
||||
"array-includes": "^3.1.7",
|
||||
"array.prototype.findlastindex": "^1.2.3",
|
||||
"array.prototype.flat": "^1.3.2",
|
||||
"array.prototype.flatmap": "^1.3.2",
|
||||
"debug": "^3.2.7",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.9",
|
||||
"eslint-module-utils": "^2.9.0",
|
||||
"hasown": "^2.0.2",
|
||||
"is-core-module": "^2.15.1",
|
||||
"eslint-module-utils": "^2.8.0",
|
||||
"hasown": "^2.0.0",
|
||||
"is-core-module": "^2.13.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^3.1.2",
|
||||
"object.fromentries": "^2.0.8",
|
||||
"object.groupby": "^1.0.3",
|
||||
"object.values": "^1.2.0",
|
||||
"object.fromentries": "^2.0.7",
|
||||
"object.groupby": "^1.0.1",
|
||||
"object.values": "^1.1.7",
|
||||
"semver": "^6.3.1",
|
||||
"tsconfig-paths": "^3.15.0"
|
||||
},
|
||||
@@ -2458,7 +2447,6 @@
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
@@ -2468,7 +2456,6 @@
|
||||
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
|
||||
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"esutils": "^2.0.2"
|
||||
},
|
||||
@@ -2481,15 +2468,14 @@
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-react": {
|
||||
"version": "7.35.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.2.tgz",
|
||||
"integrity": "sha512-Rbj2R9zwP2GYNcIak4xoAMV57hrBh3hTaR0k7hVjwCQgryE/pw5px4b13EYjduOI0hfXyZhwBxaGpOTbWSGzKQ==",
|
||||
"version": "7.35.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.35.0.tgz",
|
||||
"integrity": "sha512-v501SSMOWv8gerHkk+IIQBkcGRGrO2nfybfj5pLxuJNFTPxxA3PSryhXTK+9pNbtkggheDdsC0E9Q8CuPk6JKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -3198,16 +3184,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
|
||||
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
|
||||
"version": "2.13.1",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
|
||||
"integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
"hasown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
@@ -4029,9 +4011,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.45",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.45.tgz",
|
||||
"integrity": "sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==",
|
||||
"version": "8.4.43",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.43.tgz",
|
||||
"integrity": "sha512-gJAQVYbh5R3gYm33FijzCZj7CHyQ3hWMgJMprLUlIYqCwTeZhBQ19wp0e9mA25BUbEvY5+EXuuaAjqQsrBxQBQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -4561,18 +4543,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.0.tgz",
|
||||
"integrity": "sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/cyyynthia"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
@@ -5052,14 +5022,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.3.tgz",
|
||||
"integrity": "sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==",
|
||||
"version": "5.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
||||
"integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"postcss": "^8.4.41",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
|
||||
@@ -21,8 +21,7 @@
|
||||
"monaco-editor": "^0.51.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-resizable-panels": "^2.0.0",
|
||||
"smol-toml": "^1.3.0"
|
||||
"react-resizable-panels": "^2.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.0.26",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import MonacoEditor from "@monaco-editor/react";
|
||||
import Editor from "@monaco-editor/react";
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export enum SecondaryTool {
|
||||
@@ -72,7 +72,7 @@ function Content({
|
||||
}
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
<Editor
|
||||
options={{
|
||||
readOnly: true,
|
||||
minimap: { enabled: false },
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
* Editor for the settings JSON.
|
||||
*/
|
||||
|
||||
import { useCallback } from "react";
|
||||
import Editor, { useMonaco } from "@monaco-editor/react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import schema from "../../../ruff.schema.json";
|
||||
import { Theme } from "./theme";
|
||||
import MonacoEditor from "@monaco-editor/react";
|
||||
import { editor } from "monaco-editor";
|
||||
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
|
||||
|
||||
export default function SettingsEditor({
|
||||
visible,
|
||||
@@ -19,147 +18,41 @@ export default function SettingsEditor({
|
||||
theme: Theme;
|
||||
onChange: (source: string) => void;
|
||||
}) {
|
||||
const monaco = useMonaco();
|
||||
|
||||
useEffect(() => {
|
||||
monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
schemas: [
|
||||
{
|
||||
uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json",
|
||||
fileMatch: ["*"],
|
||||
schema,
|
||||
},
|
||||
],
|
||||
});
|
||||
}, [monaco]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
onChange(value ?? "");
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleMount = useCallback((editor: IStandaloneCodeEditor) => {
|
||||
editor.addAction({
|
||||
id: "copyAsRuffToml",
|
||||
label: "Copy as ruff.toml",
|
||||
contextMenuGroupId: "9_cutcopypaste",
|
||||
contextMenuOrder: 3,
|
||||
|
||||
async run(editor): Promise<undefined> {
|
||||
const model = editor.getModel();
|
||||
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toml = await import("smol-toml");
|
||||
const settings = model.getValue();
|
||||
const tomlSettings = toml.stringify(JSON.parse(settings));
|
||||
|
||||
await navigator.clipboard.writeText(tomlSettings);
|
||||
},
|
||||
});
|
||||
|
||||
editor.addAction({
|
||||
id: "copyAsPyproject.toml",
|
||||
label: "Copy as pyproject.toml",
|
||||
contextMenuGroupId: "9_cutcopypaste",
|
||||
contextMenuOrder: 4,
|
||||
|
||||
async run(editor): Promise<undefined> {
|
||||
const model = editor.getModel();
|
||||
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settings = model.getValue();
|
||||
const toml = await import("smol-toml");
|
||||
const tomlSettings = toml.stringify(
|
||||
prefixWithRuffToml(JSON.parse(settings)),
|
||||
);
|
||||
|
||||
await navigator.clipboard.writeText(tomlSettings);
|
||||
},
|
||||
});
|
||||
editor.onDidPaste((event) => {
|
||||
const model = editor.getModel();
|
||||
|
||||
if (model == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow pasting a TOML settings configuration if it replaces the entire settings.
|
||||
if (model.getFullModelRange().equalsRange(event.range)) {
|
||||
const pasted = model.getValueInRange(event.range);
|
||||
|
||||
// Text starting with a `{` must be JSON. Don't even try to parse as TOML.
|
||||
if (!pasted.trimStart().startsWith("{")) {
|
||||
import("smol-toml").then((toml) => {
|
||||
try {
|
||||
const parsed = toml.parse(pasted);
|
||||
const cleansed = stripToolRuff(parsed);
|
||||
|
||||
model.setValue(JSON.stringify(cleansed, null, 4));
|
||||
} catch (e) {
|
||||
// Turned out to not be TOML after all.
|
||||
console.warn("Failed to parse settings as TOML", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
<Editor
|
||||
options={{
|
||||
readOnly: false,
|
||||
minimap: { enabled: false },
|
||||
fontSize: 14,
|
||||
roundedSelection: false,
|
||||
scrollBeyondLastLine: false,
|
||||
contextmenu: true,
|
||||
contextmenu: false,
|
||||
}}
|
||||
onMount={handleMount}
|
||||
wrapperProps={visible ? {} : { style: { display: "none" } }}
|
||||
language="json"
|
||||
language={"json"}
|
||||
value={source}
|
||||
theme={theme === "light" ? "Ayu-Light" : "Ayu-Dark"}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function stripToolRuff(settings: object) {
|
||||
const { tool, ...nonToolSettings } = settings as any;
|
||||
|
||||
// Flatten out `tool.ruff.x` to just `x`
|
||||
if (typeof tool == "object" && !Array.isArray(tool)) {
|
||||
if (tool.ruff != null) {
|
||||
return { ...nonToolSettings, ...tool.ruff };
|
||||
}
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(settings).flatMap(([key, value]) => {
|
||||
if (key.startsWith("tool.ruff")) {
|
||||
const strippedKey = key.substring("tool.ruff".length);
|
||||
|
||||
if (strippedKey === "") {
|
||||
return Object.entries(value);
|
||||
}
|
||||
|
||||
return [[strippedKey.substring(1), value]];
|
||||
}
|
||||
|
||||
return [[key, value]];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function prefixWithRuffToml(settings: object) {
|
||||
const subTableEntries = [];
|
||||
const ruffTableEntries = [];
|
||||
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
if (typeof value === "object" && !Array.isArray(value)) {
|
||||
subTableEntries.push([`tool.ruff.${key}`, value]);
|
||||
} else {
|
||||
ruffTableEntries.push([key, value]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
["tool.ruff"]: Object.fromEntries(ruffTableEntries),
|
||||
...Object.fromEntries(subTableEntries),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,25 +2,11 @@
|
||||
* Editor for the Python source code.
|
||||
*/
|
||||
|
||||
import MonacoEditor, { Monaco, OnMount } from "@monaco-editor/react";
|
||||
import {
|
||||
editor,
|
||||
IDisposable,
|
||||
languages,
|
||||
MarkerSeverity,
|
||||
MarkerTag,
|
||||
Range,
|
||||
} from "monaco-editor";
|
||||
import Editor, { BeforeMount, Monaco } from "@monaco-editor/react";
|
||||
import { MarkerSeverity, MarkerTag } from "monaco-editor";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { Diagnostic } from "../pkg";
|
||||
import { Theme } from "./theme";
|
||||
import CodeActionProvider = languages.CodeActionProvider;
|
||||
|
||||
type MonacoEditorState = {
|
||||
monaco: Monaco;
|
||||
codeActionProvider: RuffCodeActionProvider;
|
||||
disposeCodeActionProvider: IDisposable;
|
||||
};
|
||||
|
||||
export default function SourceEditor({
|
||||
visible,
|
||||
@@ -35,32 +21,80 @@ export default function SourceEditor({
|
||||
theme: Theme;
|
||||
onChange: (pythonSource: string) => void;
|
||||
}) {
|
||||
const monacoRef = useRef<MonacoEditorState | null>(null);
|
||||
const monacoRef = useRef<Monaco | null>(null);
|
||||
const monaco = monacoRef.current;
|
||||
|
||||
// Update the diagnostics in the editor.
|
||||
useEffect(() => {
|
||||
const editorState = monacoRef.current;
|
||||
|
||||
if (editorState == null) {
|
||||
const editor = monaco?.editor;
|
||||
const model = editor?.getModels()[0];
|
||||
if (!editor || !model) {
|
||||
return;
|
||||
}
|
||||
|
||||
editorState.codeActionProvider.diagnostics = diagnostics;
|
||||
editor.setModelMarkers(
|
||||
model,
|
||||
"owner",
|
||||
diagnostics.map((diagnostic) => ({
|
||||
startLineNumber: diagnostic.location.row,
|
||||
startColumn: diagnostic.location.column,
|
||||
endLineNumber: diagnostic.end_location.row,
|
||||
endColumn: diagnostic.end_location.column,
|
||||
message: diagnostic.code
|
||||
? `${diagnostic.code}: ${diagnostic.message}`
|
||||
: diagnostic.message,
|
||||
severity: MarkerSeverity.Error,
|
||||
tags:
|
||||
diagnostic.code === "F401" || diagnostic.code === "F841"
|
||||
? [MarkerTag.Unnecessary]
|
||||
: [],
|
||||
})),
|
||||
);
|
||||
|
||||
updateMarkers(editorState.monaco, diagnostics);
|
||||
}, [diagnostics]);
|
||||
|
||||
// Dispose the code action provider on unmount.
|
||||
useEffect(() => {
|
||||
const disposeActionProvider = monacoRef.current?.disposeCodeActionProvider;
|
||||
if (disposeActionProvider == null) {
|
||||
return;
|
||||
}
|
||||
const codeActionProvider = monaco?.languages.registerCodeActionProvider(
|
||||
"python",
|
||||
{
|
||||
provideCodeActions: function (model, position) {
|
||||
const actions = diagnostics
|
||||
.filter((check) => position.startLineNumber === check.location.row)
|
||||
.filter(({ fix }) => fix)
|
||||
.map((check) => ({
|
||||
title: check.fix
|
||||
? check.fix.message
|
||||
? `${check.code}: ${check.fix.message}`
|
||||
: `Fix ${check.code}`
|
||||
: "Fix",
|
||||
id: `fix-${check.code}`,
|
||||
kind: "quickfix",
|
||||
edit: check.fix
|
||||
? {
|
||||
edits: check.fix.edits.map((edit) => ({
|
||||
resource: model.uri,
|
||||
versionId: model.getVersionId(),
|
||||
textEdit: {
|
||||
range: {
|
||||
startLineNumber: edit.location.row,
|
||||
startColumn: edit.location.column,
|
||||
endLineNumber: edit.end_location.row,
|
||||
endColumn: edit.end_location.column,
|
||||
},
|
||||
text: edit.content || "",
|
||||
},
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
return {
|
||||
actions,
|
||||
dispose: () => {},
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
disposeActionProvider.dispose();
|
||||
codeActionProvider?.dispose();
|
||||
};
|
||||
}, []);
|
||||
}, [diagnostics, monaco]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
@@ -69,30 +103,14 @@ export default function SourceEditor({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleMount: OnMount = useCallback(
|
||||
(_editor, instance) => {
|
||||
const ruffActionsProvider = new RuffCodeActionProvider(diagnostics);
|
||||
const disposeCodeActionProvider =
|
||||
instance.languages.registerCodeActionProvider(
|
||||
"python",
|
||||
ruffActionsProvider,
|
||||
);
|
||||
|
||||
updateMarkers(instance, diagnostics);
|
||||
|
||||
monacoRef.current = {
|
||||
monaco: instance,
|
||||
codeActionProvider: ruffActionsProvider,
|
||||
disposeCodeActionProvider,
|
||||
};
|
||||
},
|
||||
|
||||
[diagnostics],
|
||||
const handleMount: BeforeMount = useCallback(
|
||||
(instance) => (monacoRef.current = instance),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
onMount={handleMount}
|
||||
<Editor
|
||||
beforeMount={handleMount}
|
||||
options={{
|
||||
fixedOverflowWidgets: true,
|
||||
readOnly: false,
|
||||
@@ -110,76 +128,3 @@ export default function SourceEditor({
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
class RuffCodeActionProvider implements CodeActionProvider {
|
||||
constructor(public diagnostics: Array<Diagnostic>) {}
|
||||
|
||||
provideCodeActions(
|
||||
model: editor.ITextModel,
|
||||
range: Range,
|
||||
): languages.ProviderResult<languages.CodeActionList> {
|
||||
const actions = this.diagnostics
|
||||
.filter((check) => range.startLineNumber === check.location.row)
|
||||
.filter(({ fix }) => fix)
|
||||
.map((check) => ({
|
||||
title: check.fix
|
||||
? check.fix.message
|
||||
? `${check.code}: ${check.fix.message}`
|
||||
: `Fix ${check.code}`
|
||||
: "Fix",
|
||||
id: `fix-${check.code}`,
|
||||
kind: "quickfix",
|
||||
|
||||
edit: check.fix
|
||||
? {
|
||||
edits: check.fix.edits.map((edit) => ({
|
||||
resource: model.uri,
|
||||
versionId: model.getVersionId(),
|
||||
textEdit: {
|
||||
range: {
|
||||
startLineNumber: edit.location.row,
|
||||
startColumn: edit.location.column,
|
||||
endLineNumber: edit.end_location.row,
|
||||
endColumn: edit.end_location.column,
|
||||
},
|
||||
text: edit.content || "",
|
||||
},
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return {
|
||||
actions,
|
||||
dispose: () => {},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function updateMarkers(monaco: Monaco, diagnostics: Array<Diagnostic>) {
|
||||
const editor = monaco.editor;
|
||||
const model = editor?.getModels()[0];
|
||||
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.setModelMarkers(
|
||||
model,
|
||||
"owner",
|
||||
diagnostics.map((diagnostic) => ({
|
||||
startLineNumber: diagnostic.location.row,
|
||||
startColumn: diagnostic.location.column,
|
||||
endLineNumber: diagnostic.end_location.row,
|
||||
endColumn: diagnostic.end_location.column,
|
||||
message: diagnostic.code
|
||||
? `${diagnostic.code}: ${diagnostic.message}`
|
||||
: diagnostic.message,
|
||||
severity: MarkerSeverity.Error,
|
||||
tags:
|
||||
diagnostic.code === "F401" || diagnostic.code === "F841"
|
||||
? [MarkerTag.Unnecessary]
|
||||
: [],
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
*/
|
||||
|
||||
import { Monaco } from "@monaco-editor/react";
|
||||
import schema from "../../../ruff.schema.json";
|
||||
|
||||
export const WHITE = "#ffffff";
|
||||
export const RADIATE = "#d7ff64";
|
||||
@@ -32,16 +31,6 @@ export function setupMonaco(monaco: Monaco) {
|
||||
defineRustPythonTokensLanguage(monaco);
|
||||
defineRustPythonAstLanguage(monaco);
|
||||
defineCommentsLanguage(monaco);
|
||||
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
schemas: [
|
||||
{
|
||||
uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json",
|
||||
fileMatch: ["*"],
|
||||
schema,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function defineAyuThemes(monaco: Monaco) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user