Compare commits
27 Commits
micha/accu
...
0.7.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbf140a665 | ||
|
|
670f958525 | ||
|
|
fed35a25e8 | ||
|
|
d1ef418bb0 | ||
|
|
272d24bf3e | ||
|
|
2624249219 | ||
|
|
4b08d17088 | ||
|
|
5b6169b02d | ||
|
|
2040e93add | ||
|
|
794eb886e4 | ||
|
|
57ba25caaf | ||
|
|
4f74db5630 | ||
|
|
adc4216afb | ||
|
|
fe8e49de9a | ||
|
|
574eb3f4bd | ||
|
|
311b0bdf9a | ||
|
|
f2546c562c | ||
|
|
59c0dacea0 | ||
|
|
b8188b2262 | ||
|
|
136721e608 | ||
|
|
5b500b838b | ||
|
|
cb003ebe22 | ||
|
|
03a5788aa1 | ||
|
|
626f716de6 | ||
|
|
46c5a13103 | ||
|
|
31681f66c9 | ||
|
|
a56ee9268e |
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
@@ -16,7 +16,7 @@ env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_MAX_RETRIES: 10
|
||||
PACKAGE_NAME: ruff
|
||||
PYTHON_VERSION: "3.11"
|
||||
PYTHON_VERSION: "3.12"
|
||||
|
||||
jobs:
|
||||
determine_changes:
|
||||
|
||||
@@ -51,6 +51,10 @@ repos:
|
||||
- id: blacken-docs
|
||||
args: ["--pyi", "--line-length", "130"]
|
||||
files: '^crates/.*/resources/mdtest/.*\.md'
|
||||
exclude: |
|
||||
(?x)^(
|
||||
.*?invalid(_.+)_syntax.md
|
||||
)$
|
||||
additional_dependencies:
|
||||
- black==24.10.0
|
||||
|
||||
|
||||
37
CHANGELOG.md
37
CHANGELOG.md
@@ -1,5 +1,42 @@
|
||||
# Changelog
|
||||
|
||||
## 0.7.3
|
||||
|
||||
### Preview features
|
||||
|
||||
- Formatter: Disallow single-line implicit concatenated strings ([#13928](https://github.com/astral-sh/ruff/pull/13928))
|
||||
- \[`flake8-pyi`\] Include all Python file types for `PYI006` and `PYI066` ([#14059](https://github.com/astral-sh/ruff/pull/14059))
|
||||
- \[`flake8-simplify`\] Implement `split-of-static-string` (`SIM905`) ([#14008](https://github.com/astral-sh/ruff/pull/14008))
|
||||
- \[`refurb`\] Implement `subclass-builtin` (`FURB189`) ([#14105](https://github.com/astral-sh/ruff/pull/14105))
|
||||
- \[`ruff`\] Improve diagnostic messages and docs (`RUF031`, `RUF032`, `RUF034`) ([#14068](https://github.com/astral-sh/ruff/pull/14068))
|
||||
|
||||
### Rule changes
|
||||
|
||||
- Detect items that hash to same value in duplicate sets (`B033`, `PLC0208`) ([#14064](https://github.com/astral-sh/ruff/pull/14064))
|
||||
- \[`eradicate`\] Better detection of IntelliJ language injection comments (`ERA001`) ([#14094](https://github.com/astral-sh/ruff/pull/14094))
|
||||
- \[`flake8-pyi`\] Add autofix for `docstring-in-stub` (`PYI021`) ([#14150](https://github.com/astral-sh/ruff/pull/14150))
|
||||
- \[`flake8-pyi`\] Update `duplicate-literal-member` (`PYI062`) to alawys provide an autofix ([#14188](https://github.com/astral-sh/ruff/pull/14188))
|
||||
- \[`pyflakes`\] Detect items that hash to same value in duplicate dictionaries (`F601`) ([#14065](https://github.com/astral-sh/ruff/pull/14065))
|
||||
- \[`ruff`\] Fix false positive for decorators (`RUF028`) ([#14061](https://github.com/astral-sh/ruff/pull/14061))
|
||||
|
||||
### Bug fixes
|
||||
|
||||
- Avoid parsing joint rule codes as distinct codes in `# noqa` ([#12809](https://github.com/astral-sh/ruff/pull/12809))
|
||||
- \[`eradicate`\] ignore `# language=` in commented-out-code rule (ERA001) ([#14069](https://github.com/astral-sh/ruff/pull/14069))
|
||||
- \[`flake8-bugbear`\] - do not run `mutable-argument-default` on stubs (`B006`) ([#14058](https://github.com/astral-sh/ruff/pull/14058))
|
||||
- \[`flake8-builtins`\] Skip lambda expressions in `builtin-argument-shadowing (A002)` ([#14144](https://github.com/astral-sh/ruff/pull/14144))
|
||||
- \[`flake8-comprehension`\] Also remove trailing comma while fixing `C409` and `C419` ([#14097](https://github.com/astral-sh/ruff/pull/14097))
|
||||
- \[`flake8-simplify`\] Allow `open` without context manager in `return` statement (`SIM115`) ([#14066](https://github.com/astral-sh/ruff/pull/14066))
|
||||
- \[`pylint`\] Respect hash-equivalent literals in `iteration-over-set` (`PLC0208`) ([#14063](https://github.com/astral-sh/ruff/pull/14063))
|
||||
- \[`pylint`\] Update known dunder methods for Python 3.13 (`PLW3201`) ([#14146](https://github.com/astral-sh/ruff/pull/14146))
|
||||
- \[`pyupgrade`\] - ignore kwarg unpacking for `UP044` ([#14053](https://github.com/astral-sh/ruff/pull/14053))
|
||||
- \[`refurb`\] Parse more exotic decimal strings in `verbose-decimal-constructor` (`FURB157`) ([#14098](https://github.com/astral-sh/ruff/pull/14098))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Add links to missing related options within rule documentations ([#13971](https://github.com/astral-sh/ruff/pull/13971))
|
||||
- Add rule short code to mkdocs tags to allow searching via rule codes ([#14040](https://github.com/astral-sh/ruff/pull/14040))
|
||||
|
||||
## 0.7.2
|
||||
|
||||
### Preview features
|
||||
|
||||
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -2317,7 +2317,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argfile",
|
||||
@@ -2534,7 +2534,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"annotate-snippets 0.9.2",
|
||||
@@ -2849,7 +2849,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_wasm"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
dependencies = [
|
||||
"console_error_panic_hook",
|
||||
"console_log",
|
||||
|
||||
@@ -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.7.2/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.2/install.ps1 | iex"
|
||||
curl -LsSf https://astral.sh/ruff/0.7.3/install.sh | sh
|
||||
powershell -c "irm https://astral.sh/ruff/0.7.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.7.2
|
||||
rev: v0.7.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -5,8 +5,6 @@ use anyhow::{anyhow, Context};
|
||||
use clap::Parser;
|
||||
use colored::Colorize;
|
||||
use crossbeam::channel as crossbeam_channel;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
|
||||
use red_knot_python_semantic::SitePackages;
|
||||
use red_knot_server::run_server;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
@@ -14,7 +12,9 @@ use red_knot_workspace::watch;
|
||||
use red_knot_workspace::watch::WorkspaceWatcher;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::system::{OsSystem, System, SystemPath, SystemPathBuf};
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use target_version::TargetVersion;
|
||||
|
||||
use crate::logging::{setup_tracing, Verbosity};
|
||||
@@ -318,8 +318,9 @@ impl MainLoop {
|
||||
} => {
|
||||
let has_diagnostics = !result.is_empty();
|
||||
if check_revision == revision {
|
||||
#[allow(clippy::print_stdout)]
|
||||
for diagnostic in result {
|
||||
tracing::error!("{}", diagnostic);
|
||||
println!("{}", diagnostic.display(db));
|
||||
}
|
||||
} else {
|
||||
tracing::debug!(
|
||||
@@ -378,7 +379,10 @@ impl MainLoopCancellationToken {
|
||||
#[derive(Debug)]
|
||||
enum MainLoopMessage {
|
||||
CheckWorkspace,
|
||||
CheckCompleted { result: Vec<String>, revision: u64 },
|
||||
CheckCompleted {
|
||||
result: Vec<Box<dyn Diagnostic>>,
|
||||
revision: u64,
|
||||
},
|
||||
ApplyChanges(Vec<watch::ChangeEvent>),
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -18,3 +18,58 @@ class Unit: ...
|
||||
b = Unit()(3.0) # error: "Object of type `Unit` is not callable"
|
||||
reveal_type(b) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly unbound `__call__` method
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class PossiblyNotCallable:
|
||||
if flag():
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = PossiblyNotCallable()
|
||||
result = a() # error: "Object of type `PossiblyNotCallable` is not callable (possibly unbound `__call__` method)"
|
||||
reveal_type(result) # revealed: int
|
||||
```
|
||||
|
||||
## Possibly unbound callable
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
class PossiblyUnbound:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
a = PossiblyUnbound()
|
||||
reveal_type(a()) # revealed: int
|
||||
```
|
||||
|
||||
## Non-callable `__call__`
|
||||
|
||||
```py
|
||||
class NonCallable:
|
||||
__call__ = 1
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `NonCallable` is not callable"
|
||||
reveal_type(a()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Possibly non-callable `__call__`
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
class NonCallable:
|
||||
if flag():
|
||||
__call__ = 1
|
||||
else:
|
||||
def __call__(self) -> int: ...
|
||||
|
||||
a = NonCallable()
|
||||
# error: "Object of type `Literal[1] | Literal[__call__]` is not callable (due to union element `Literal[1]`)"
|
||||
reveal_type(a()) # revealed: Unknown | int
|
||||
```
|
||||
|
||||
@@ -44,3 +44,16 @@ reveal_type(bar()) # revealed: @Todo
|
||||
nonsense = 123
|
||||
x = nonsense() # error: "Object of type `Literal[123]` is not callable"
|
||||
```
|
||||
|
||||
## Potentially unbound function
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
if flag():
|
||||
def foo() -> int:
|
||||
return 42
|
||||
|
||||
# error: [possibly-unresolved-reference]
|
||||
reveal_type(foo()) # revealed: int
|
||||
```
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# Comparison: Intersections
|
||||
|
||||
## Positive contributions
|
||||
|
||||
If we have an intersection type `A & B` and we get a definitive true/false answer for one of the
|
||||
types, we can infer that the result for the intersection type is also true/false:
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
|
||||
class Child1(Base):
|
||||
def __eq__(self, other) -> Literal[True]:
|
||||
return True
|
||||
|
||||
class Child2(Base): ...
|
||||
|
||||
def get_base() -> Base: ...
|
||||
|
||||
x = get_base()
|
||||
c1 = Child1()
|
||||
|
||||
# Create an intersection type through narrowing:
|
||||
if isinstance(x, Child1):
|
||||
if isinstance(x, Child2):
|
||||
reveal_type(x) # revealed: Child1 & Child2
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[True]
|
||||
|
||||
# Other comparison operators fall back to the base type:
|
||||
reveal_type(x > 1) # revealed: bool
|
||||
reveal_type(x is c1) # revealed: bool
|
||||
```
|
||||
|
||||
## Negative contributions
|
||||
|
||||
Negative contributions to the intersection type only allow simplifications in a few special cases
|
||||
(equality and identity comparisons).
|
||||
|
||||
### Equality comparisons
|
||||
|
||||
#### Literal strings
|
||||
|
||||
```py
|
||||
x = "x" * 1_000_000_000
|
||||
y = "y" * 1_000_000_000
|
||||
reveal_type(x) # revealed: LiteralString
|
||||
|
||||
if x != "abc":
|
||||
reveal_type(x) # revealed: LiteralString & ~Literal["abc"]
|
||||
|
||||
reveal_type(x == "abc") # revealed: Literal[False]
|
||||
reveal_type("abc" == x) # revealed: Literal[False]
|
||||
reveal_type(x == "something else") # revealed: bool
|
||||
reveal_type("something else" == x) # revealed: bool
|
||||
|
||||
reveal_type(x != "abc") # revealed: Literal[True]
|
||||
reveal_type("abc" != x) # revealed: Literal[True]
|
||||
reveal_type(x != "something else") # revealed: bool
|
||||
reveal_type("something else" != x) # revealed: bool
|
||||
|
||||
reveal_type(x == y) # revealed: bool
|
||||
reveal_type(y == x) # revealed: bool
|
||||
reveal_type(x != y) # revealed: bool
|
||||
reveal_type(y != x) # revealed: bool
|
||||
|
||||
reveal_type(x >= "abc") # revealed: bool
|
||||
reveal_type("abc" >= x) # revealed: bool
|
||||
|
||||
reveal_type(x in "abc") # revealed: bool
|
||||
reveal_type("abc" in x) # revealed: bool
|
||||
```
|
||||
|
||||
#### Integers
|
||||
|
||||
```py
|
||||
def get_int() -> int: ...
|
||||
|
||||
x = get_int()
|
||||
|
||||
if x != 1:
|
||||
reveal_type(x) # revealed: int & ~Literal[1]
|
||||
|
||||
reveal_type(x != 1) # revealed: Literal[True]
|
||||
reveal_type(x != 2) # revealed: bool
|
||||
|
||||
reveal_type(x == 1) # revealed: Literal[False]
|
||||
reveal_type(x == 2) # revealed: bool
|
||||
```
|
||||
|
||||
### Identity comparisons
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
o = object()
|
||||
|
||||
a = A()
|
||||
n = None
|
||||
|
||||
if o is not None:
|
||||
reveal_type(o) # revealed: object & ~None
|
||||
|
||||
reveal_type(o is n) # revealed: Literal[False]
|
||||
reveal_type(o is not n) # revealed: Literal[True]
|
||||
```
|
||||
|
||||
## Diagnostics
|
||||
|
||||
### Unsupported operators for positive contributions
|
||||
|
||||
Raise an error if any of the positive contributions to the intersection type are unsupported for the
|
||||
given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & NonContainer
|
||||
|
||||
# error: [unsupported-operator] "Operator `in` is not supported for types `int` and `NonContainer`"
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
|
||||
### Unsupported operators for negative contributions
|
||||
|
||||
Do *not* raise an error if any of the negative contributions to the intersection type are
|
||||
unsupported for the given operator:
|
||||
|
||||
```py
|
||||
class Container:
|
||||
def __contains__(self, x) -> bool: ...
|
||||
|
||||
class NonContainer: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
x = get_object()
|
||||
|
||||
if isinstance(x, Container):
|
||||
if not isinstance(x, NonContainer):
|
||||
reveal_type(x) # revealed: Container & ~NonContainer
|
||||
|
||||
# No error here!
|
||||
reveal_type(2 in x) # revealed: bool
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
# Exception Handling
|
||||
|
||||
## Invalid syntax
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
print
|
||||
except as e: # error: [invalid-syntax]
|
||||
reveal_type(e) # revealed: Unknown
|
||||
|
||||
```
|
||||
@@ -238,7 +238,7 @@ class Test:
|
||||
def coinflip() -> bool:
|
||||
return True
|
||||
|
||||
# TODO: we should emit a diagnostic here (it might not be iterable)
|
||||
# error: [not-iterable] "Object of type `Test | Literal[42]` is not iterable because its `__iter__` method is possibly unbound"
|
||||
for x in Test() if coinflip() else 42:
|
||||
reveal_type(x) # revealed: int
|
||||
```
|
||||
|
||||
196
crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Normal file
196
crates/red_knot_python_semantic/resources/mdtest/metaclass.md
Normal file
@@ -0,0 +1,196 @@
|
||||
## Default
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
|
||||
reveal_type(M.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `object`
|
||||
|
||||
```py
|
||||
reveal_type(object.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## `type`
|
||||
|
||||
```py
|
||||
reveal_type(type.__class__) # revealed: Literal[type]
|
||||
```
|
||||
|
||||
## Basic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class B(metaclass=M): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Invalid metaclass
|
||||
|
||||
A class which doesn't inherit `type` (and/or doesn't implement a custom `__new__` accepting the same
|
||||
arguments as `type.__new__`) isn't a valid metaclass.
|
||||
|
||||
```py
|
||||
class M: ...
|
||||
class A(metaclass=M): ...
|
||||
|
||||
# TODO: emit a diagnostic for the invalid metaclass
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Linear inheritance
|
||||
|
||||
If a class is a subclass of a class with a custom metaclass, then the subclass will also have that
|
||||
metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Conflict (1)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`C`) must be a subclass of the metaclasses of all its bases, but `M1` and `M2` have no subclass relationship"
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Conflict (2)
|
||||
|
||||
The metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its
|
||||
bases. ("Strict subclass" is a synonym for "proper subclass"; a non-strict subclass can be a
|
||||
subclass or the class itself.)
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type): ...
|
||||
class A(metaclass=M1): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`B`) must be a subclass of the metaclasses of all its bases, but `M2` and `M1` have no subclass relationship"
|
||||
class B(A, metaclass=M2): ...
|
||||
|
||||
reveal_type(B.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Common metaclass
|
||||
|
||||
A class has two explicit bases, both of which have the same metaclass.
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(metaclass=M): ...
|
||||
class C(A, B): ...
|
||||
|
||||
reveal_type(C.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Metaclass metaclass
|
||||
|
||||
A class has an explicit base with a custom metaclass. That metaclass itself has a custom metaclass.
|
||||
|
||||
```py
|
||||
class M1(type): ...
|
||||
class M2(type, metaclass=M1): ...
|
||||
class M3(M2): ...
|
||||
class A(metaclass=M3): ...
|
||||
class B(A): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M3]
|
||||
```
|
||||
|
||||
## Diamond inheritance
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class M1(M): ...
|
||||
class M2(M): ...
|
||||
class M12(M1, M2): ...
|
||||
class A(metaclass=M1): ...
|
||||
class B(metaclass=M2): ...
|
||||
class C(metaclass=M12): ...
|
||||
|
||||
# error: [conflicting-metaclass] "The metaclass of a derived class (`D`) must be a subclass of the metaclasses of all its bases, but `M1` and `M2` have no subclass relationship"
|
||||
class D(A, B, C): ...
|
||||
|
||||
reveal_type(D.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Unknown
|
||||
|
||||
```py
|
||||
from nonexistent_module import UnknownClass # error: [unresolved-import]
|
||||
|
||||
class C(UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[type] & Unknown`
|
||||
reveal_type(C.__class__) # revealed: Literal[type]
|
||||
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, UnknownClass): ...
|
||||
|
||||
# TODO: should be `type[M] & Unknown`
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Duplicate
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A(metaclass=M): ...
|
||||
class B(A, A): ... # error: [duplicate-base] "Duplicate base class `A`"
|
||||
|
||||
reveal_type(B.__class__) # revealed: Literal[M]
|
||||
```
|
||||
|
||||
## Non-class
|
||||
|
||||
When a class has an explicit `metaclass` that is not a class, but is a callable that accepts
|
||||
`type.__new__` arguments, we should return the meta type of its return type.
|
||||
|
||||
```py
|
||||
def f(*args, **kwargs) -> int: ...
|
||||
|
||||
class A(metaclass=f): ...
|
||||
|
||||
# TODO should be `type[int]`
|
||||
reveal_type(A.__class__) # revealed: @Todo
|
||||
```
|
||||
|
||||
## Cyclic
|
||||
|
||||
Retrieving the metaclass of a cyclically defined class should not cause an infinite loop.
|
||||
|
||||
```py path=a.pyi
|
||||
class A(B): ... # error: [cyclic-class-def]
|
||||
class B(C): ... # error: [cyclic-class-def]
|
||||
class C(A): ... # error: [cyclic-class-def]
|
||||
|
||||
reveal_type(A.__class__) # revealed: Unknown
|
||||
```
|
||||
|
||||
## PEP 695 generic
|
||||
|
||||
```py
|
||||
class M(type): ...
|
||||
class A[T: str](metaclass=M): ...
|
||||
|
||||
reveal_type(A.__class__) # revealed: Literal[M]
|
||||
```
|
||||
@@ -0,0 +1,244 @@
|
||||
# Narrowing for `issubclass` checks
|
||||
|
||||
Narrowing for `issubclass(class, classinfo)` expressions.
|
||||
|
||||
## `classinfo` is a single type
|
||||
|
||||
### Basic example
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
if issubclass(t, bytes):
|
||||
reveal_type(t) # revealed: Never
|
||||
|
||||
if issubclass(t, object):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
|
||||
if issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Proper narrowing in `elif` and `else` branches
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str, bytes]
|
||||
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
elif issubclass(t, str):
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[bytes]
|
||||
```
|
||||
|
||||
### Multiple derived classes
|
||||
|
||||
```py
|
||||
class Base: ...
|
||||
class Derived1(Base): ...
|
||||
class Derived2(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t1 = Derived1 if flag() else Derived2
|
||||
|
||||
if issubclass(t1, Base):
|
||||
reveal_type(t1) # revealed: Literal[Derived1, Derived2]
|
||||
|
||||
if issubclass(t1, Derived1):
|
||||
reveal_type(t1) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t1) # revealed: Literal[Derived2]
|
||||
|
||||
t2 = Derived1 if flag() else Base
|
||||
|
||||
if issubclass(t2, Base):
|
||||
reveal_type(t2) # revealed: Literal[Derived1, Base]
|
||||
|
||||
t3 = Derived1 if flag() else Unrelated
|
||||
|
||||
if issubclass(t3, Base):
|
||||
reveal_type(t3) # revealed: Literal[Derived1]
|
||||
else:
|
||||
reveal_type(t3) # revealed: Literal[Unrelated]
|
||||
```
|
||||
|
||||
### Narrowing for non-literals
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
class B: ...
|
||||
|
||||
def get_class() -> type[object]: ...
|
||||
|
||||
t = get_class()
|
||||
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
if issubclass(t, B):
|
||||
reveal_type(t) # revealed: type[A] & type[B]
|
||||
else:
|
||||
reveal_type(t) # revealed: type[object] & ~type[A]
|
||||
```
|
||||
|
||||
### Handling of `None`
|
||||
|
||||
```py
|
||||
from types import NoneType
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else NoneType
|
||||
|
||||
if issubclass(t, NoneType):
|
||||
reveal_type(t) # revealed: Literal[NoneType]
|
||||
|
||||
if issubclass(t, type(None)):
|
||||
# TODO: this should be just `Literal[NoneType]`
|
||||
reveal_type(t) # revealed: Literal[int, NoneType]
|
||||
```
|
||||
|
||||
## `classinfo` contains multiple types
|
||||
|
||||
### (Nested) tuples of types
|
||||
|
||||
```py
|
||||
class Unrelated: ...
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str if flag() else bytes
|
||||
|
||||
if issubclass(t, (int, (Unrelated, (bytes,)))):
|
||||
reveal_type(t) # revealed: Literal[int, bytes]
|
||||
else:
|
||||
reveal_type(t) # revealed: Literal[str]
|
||||
```
|
||||
|
||||
## Special cases
|
||||
|
||||
### Emit a diagnostic if the first argument is of wrong type
|
||||
|
||||
#### Too wide
|
||||
|
||||
`type[object]` is a subtype of `object`, but not every `object` can be passed as the first argument
|
||||
to `issubclass`:
|
||||
|
||||
```py
|
||||
class A: ...
|
||||
|
||||
def get_object() -> object: ...
|
||||
|
||||
t = get_object()
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, A):
|
||||
reveal_type(t) # revealed: type[A]
|
||||
```
|
||||
|
||||
#### Wrong
|
||||
|
||||
`Literal[1]` and `type` are entirely disjoint, so the inferred type of `Literal[1] & type[int]` is
|
||||
eagerly simplified to `Never` as a result of the type narrowing in the `if issubclass(t, int)`
|
||||
branch:
|
||||
|
||||
```py
|
||||
t = 1
|
||||
|
||||
# TODO: we should emit a diagnostic here
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
### Do not use custom `issubclass` for narrowing
|
||||
|
||||
```py
|
||||
def issubclass(c, ci):
|
||||
return True
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is aliased
|
||||
|
||||
```py
|
||||
issubclass_alias = issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if issubclass_alias(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do support narrowing if `issubclass` is imported
|
||||
|
||||
```py
|
||||
from builtins import issubclass as imported_issubclass
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
if imported_issubclass(t, int):
|
||||
reveal_type(t) # revealed: Literal[int]
|
||||
```
|
||||
|
||||
### Do not narrow if second argument is not a proper `classinfo` argument
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, "str"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, (bytes, "str")):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic during
|
||||
# type checking
|
||||
if issubclass(t, Any):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
|
||||
### Do not narrow if there are keyword arguments
|
||||
|
||||
```py
|
||||
def flag() -> bool: ...
|
||||
|
||||
t = int if flag() else str
|
||||
|
||||
# TODO: this should cause us to emit a diagnostic
|
||||
# (`issubclass` has no `foo` parameter)
|
||||
if issubclass(t, int, foo="bar"):
|
||||
reveal_type(t) # revealed: Literal[int, str]
|
||||
```
|
||||
@@ -60,8 +60,7 @@ reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
|
||||
# TODO: understand properties
|
||||
reveal_type(typing.__class__) # revealed: Literal[__class__]
|
||||
reveal_type(typing.__class__) # revealed: Literal[type]
|
||||
|
||||
# TODO: needs support for attribute access on instances, properties and generics;
|
||||
# should be `dict[str, Any]`
|
||||
|
||||
@@ -145,13 +145,8 @@ reveal_type(f) # revealed: Unknown
|
||||
|
||||
### Non-iterable unpacking
|
||||
|
||||
TODO: Remove duplicate diagnostics. This is happening because for a sequence-like assignment target,
|
||||
multiple definitions are created and the inference engine runs on each of them which results in
|
||||
duplicate diagnostics.
|
||||
|
||||
```py
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
# error: "Object of type `Literal[1]` is not iterable"
|
||||
a, b = 1
|
||||
reveal_type(a) # revealed: Unknown
|
||||
reveal_type(b) # revealed: Unknown
|
||||
|
||||
@@ -281,8 +281,12 @@ impl<'db> SemanticIndexBuilder<'db> {
|
||||
debug_assert!(popped_assignment.is_some());
|
||||
}
|
||||
|
||||
fn current_assignment(&self) -> Option<&CurrentAssignment<'db>> {
|
||||
self.current_assignments.last()
|
||||
fn current_assignment(&self) -> Option<CurrentAssignment<'db>> {
|
||||
self.current_assignments.last().copied()
|
||||
}
|
||||
|
||||
fn current_assignment_mut(&mut self) -> Option<&mut CurrentAssignment<'db>> {
|
||||
self.current_assignments.last_mut()
|
||||
}
|
||||
|
||||
fn add_pattern_constraint(
|
||||
@@ -627,6 +631,7 @@ where
|
||||
ast::Expr::List(_) | ast::Expr::Tuple(_) => {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first: true,
|
||||
unpack: Some(Unpack::new(
|
||||
self.db,
|
||||
self.file,
|
||||
@@ -640,9 +645,11 @@ where
|
||||
)),
|
||||
})
|
||||
}
|
||||
ast::Expr::Name(_) => {
|
||||
Some(CurrentAssignment::Assign { node, unpack: None })
|
||||
}
|
||||
ast::Expr::Name(_) => Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
unpack: None,
|
||||
first: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -987,14 +994,19 @@ where
|
||||
}
|
||||
|
||||
if is_definition {
|
||||
match self.current_assignment().copied() {
|
||||
Some(CurrentAssignment::Assign { node, unpack }) => {
|
||||
match self.current_assignment() {
|
||||
Some(CurrentAssignment::Assign {
|
||||
node,
|
||||
first,
|
||||
unpack,
|
||||
}) => {
|
||||
self.add_definition(
|
||||
symbol,
|
||||
AssignmentDefinitionNodeRef {
|
||||
unpack,
|
||||
value: &node.value,
|
||||
name: name_node,
|
||||
first,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1045,6 +1057,11 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(CurrentAssignment::Assign { first, .. }) = self.current_assignment_mut()
|
||||
{
|
||||
*first = false;
|
||||
}
|
||||
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
ast::Expr::Named(node) => {
|
||||
@@ -1245,6 +1262,7 @@ where
|
||||
enum CurrentAssignment<'a> {
|
||||
Assign {
|
||||
node: &'a ast::StmtAssign,
|
||||
first: bool,
|
||||
unpack: Option<Unpack<'a>>,
|
||||
},
|
||||
AnnAssign(&'a ast::StmtAnnAssign),
|
||||
|
||||
@@ -183,6 +183,7 @@ pub(crate) struct AssignmentDefinitionNodeRef<'a> {
|
||||
pub(crate) unpack: Option<Unpack<'a>>,
|
||||
pub(crate) value: &'a ast::Expr,
|
||||
pub(crate) name: &'a ast::ExprName,
|
||||
pub(crate) first: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
@@ -250,10 +251,12 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
unpack,
|
||||
value,
|
||||
name,
|
||||
first,
|
||||
}) => DefinitionKind::Assignment(AssignmentDefinitionKind {
|
||||
target: TargetKind::from(unpack),
|
||||
value: AstNodeRef::new(parsed.clone(), value),
|
||||
name: AstNodeRef::new(parsed, name),
|
||||
first,
|
||||
}),
|
||||
DefinitionNodeRef::AnnotatedAssignment(assign) => {
|
||||
DefinitionKind::AnnotatedAssignment(AstNodeRef::new(parsed, assign))
|
||||
@@ -330,6 +333,7 @@ impl<'db> DefinitionNodeRef<'db> {
|
||||
value: _,
|
||||
unpack: _,
|
||||
name,
|
||||
first: _,
|
||||
}) => name.into(),
|
||||
Self::AnnotatedAssignment(node) => node.into(),
|
||||
Self::AugmentedAssignment(node) => node.into(),
|
||||
@@ -535,10 +539,11 @@ pub struct AssignmentDefinitionKind<'db> {
|
||||
target: TargetKind<'db>,
|
||||
value: AstNodeRef<ast::Expr>,
|
||||
name: AstNodeRef<ast::ExprName>,
|
||||
first: bool,
|
||||
}
|
||||
|
||||
impl AssignmentDefinitionKind<'_> {
|
||||
pub(crate) fn target(&self) -> TargetKind {
|
||||
impl<'db> AssignmentDefinitionKind<'db> {
|
||||
pub(crate) fn target(&self) -> TargetKind<'db> {
|
||||
self.target
|
||||
}
|
||||
|
||||
@@ -549,6 +554,10 @@ impl AssignmentDefinitionKind<'_> {
|
||||
pub(crate) fn name(&self) -> &ast::ExprName {
|
||||
self.name.node()
|
||||
}
|
||||
|
||||
pub(crate) fn is_first(&self) -> bool {
|
||||
self.first
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
Db,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Boundness {
|
||||
Bound,
|
||||
MayBeUnbound,
|
||||
@@ -44,17 +44,13 @@ impl<'db> Symbol<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or(&self, other: Type<'db>) -> Type<'db> {
|
||||
pub(crate) fn unwrap_or_unknown(&self) -> Type<'db> {
|
||||
match self {
|
||||
Symbol::Type(ty, _) => *ty,
|
||||
Symbol::Unbound => other,
|
||||
Symbol::Unbound => Type::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn unwrap_or_unknown(&self) -> Type<'db> {
|
||||
self.unwrap_or(Type::Unknown)
|
||||
}
|
||||
|
||||
pub(crate) fn as_type(&self) -> Option<Type<'db>> {
|
||||
match self {
|
||||
Symbol::Type(ty, _) => Some(*ty),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -246,7 +246,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
}
|
||||
} else {
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
if let Type::Instance(InstanceType { class, .. }) = new_positive {
|
||||
if let Type::Instance(InstanceType { class }) = new_positive {
|
||||
if class.is_known(db, KnownClass::Bool) {
|
||||
if let Some(&Type::BooleanLiteral(value)) = self
|
||||
.negative
|
||||
@@ -317,7 +317,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
|
||||
// Adding any of these types to the negative side of an intersection
|
||||
// is equivalent to adding it to the positive side. We do this to
|
||||
// simplify the representation.
|
||||
self.positive.insert(ty);
|
||||
self.add_positive(db, ty);
|
||||
}
|
||||
// ~Literal[True] & bool = Literal[False]
|
||||
Type::BooleanLiteral(bool)
|
||||
@@ -592,6 +592,22 @@ mod tests {
|
||||
assert_eq!(ta_not_i0.display(&db).to_string(), "int & Any | Literal[1]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_intersection_simplify_negative_any() {
|
||||
let db = setup_db();
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Any);
|
||||
|
||||
let ty = IntersectionBuilder::new(&db)
|
||||
.add_positive(Type::Never)
|
||||
.add_negative(Type::Any)
|
||||
.build();
|
||||
assert_eq!(ty, Type::Never);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersection_distributes_over_union() {
|
||||
let db = setup_db();
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
use crate::types::{ClassLiteralType, Type};
|
||||
use crate::Db;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_db::files::File;
|
||||
use ruff_python_ast::{self as ast, AnyNodeRef};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use std::borrow::Cow;
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::types::{ClassLiteralType, Type};
|
||||
use crate::Db;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
#[derive(Debug, Eq, PartialEq, Clone)]
|
||||
pub struct TypeCheckDiagnostic {
|
||||
// TODO: Don't use string keys for rules
|
||||
pub(super) rule: String,
|
||||
@@ -31,6 +32,28 @@ impl TypeCheckDiagnostic {
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for TypeCheckDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
TypeCheckDiagnostic::rule(self)
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
TypeCheckDiagnostic::message(self).into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
TypeCheckDiagnostic::file(self)
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(Ranged::range(self))
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TypeCheckDiagnostic {
|
||||
fn range(&self) -> TextRange {
|
||||
self.range
|
||||
@@ -141,6 +164,23 @@ impl<'db> TypeCheckDiagnosticsBuilder<'db> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that the object represented by `node` is not iterable
|
||||
/// because its `__iter__` method is possibly unbound.
|
||||
pub(super) fn add_not_iterable_possibly_unbound(
|
||||
&mut self,
|
||||
node: AnyNodeRef,
|
||||
element_ty: Type<'db>,
|
||||
) {
|
||||
self.add(
|
||||
node,
|
||||
"not-iterable",
|
||||
format_args!(
|
||||
"Object of type `{}` is not iterable because its `__iter__` method is possibly unbound",
|
||||
element_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Emit a diagnostic declaring that an index is out of bounds for a tuple.
|
||||
pub(super) fn add_index_out_of_bounds(
|
||||
&mut self,
|
||||
|
||||
@@ -6,7 +6,9 @@ use ruff_db::display::FormatterJoinExtension;
|
||||
use ruff_python_ast::str::Quote;
|
||||
use ruff_python_literal::escape::AsciiEscape;
|
||||
|
||||
use crate::types::{ClassLiteralType, InstanceType, IntersectionType, KnownClass, Type, UnionType};
|
||||
use crate::types::{
|
||||
ClassLiteralType, InstanceType, IntersectionType, KnownClass, SubclassOfType, Type, UnionType,
|
||||
};
|
||||
use crate::Db;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
@@ -64,7 +66,7 @@ impl Display for DisplayRepresentation<'_> {
|
||||
Type::Any => f.write_str("Any"),
|
||||
Type::Never => f.write_str("Never"),
|
||||
Type::Unknown => f.write_str("Unknown"),
|
||||
Type::Instance(InstanceType { class, .. })
|
||||
Type::Instance(InstanceType { class })
|
||||
if class.is_known(self.db, KnownClass::NoneType) =>
|
||||
{
|
||||
f.write_str("None")
|
||||
@@ -77,10 +79,11 @@ impl Display for DisplayRepresentation<'_> {
|
||||
}
|
||||
// TODO functions and classes should display using a fully qualified name
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => f.write_str(class.name(self.db)),
|
||||
Type::Instance(InstanceType { class, known }) => f.write_str(match known {
|
||||
Some(super::KnownInstance::Literal) => "Literal",
|
||||
_ => class.name(self.db),
|
||||
}),
|
||||
Type::SubclassOf(SubclassOfType { class }) => {
|
||||
write!(f, "type[{}]", class.name(self.db))
|
||||
}
|
||||
Type::Instance(InstanceType { class }) => f.write_str(class.name(self.db)),
|
||||
Type::KnownInstance(known_instance) => f.write_str(known_instance.as_str()),
|
||||
Type::FunctionLiteral(function) => f.write_str(function.name(self.db)),
|
||||
Type::Union(union) => union.display(self.db).fmt(f),
|
||||
Type::Intersection(intersection) => intersection.display(self.db).fmt(f),
|
||||
|
||||
@@ -41,7 +41,8 @@ use crate::module_name::ModuleName;
|
||||
use crate::module_resolver::{file_to_module, resolve_module};
|
||||
use crate::semantic_index::ast_ids::{HasScopedAstId, HasScopedUseId, ScopedExpressionId};
|
||||
use crate::semantic_index::definition::{
|
||||
Definition, DefinitionKind, DefinitionNodeKey, ExceptHandlerDefinitionKind, TargetKind,
|
||||
AssignmentDefinitionKind, Definition, DefinitionKind, DefinitionNodeKey,
|
||||
ExceptHandlerDefinitionKind, TargetKind,
|
||||
};
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::semantic_index;
|
||||
@@ -56,9 +57,9 @@ use crate::types::unpacker::{UnpackResult, Unpacker};
|
||||
use crate::types::{
|
||||
bindings_ty, builtins_symbol, declarations_ty, global_symbol, symbol, typing_extensions_symbol,
|
||||
Boundness, BytesLiteralType, Class, ClassLiteralType, FunctionType, InstanceType,
|
||||
IterationOutcome, KnownClass, KnownFunction, KnownInstance, SliceLiteralType,
|
||||
StringLiteralType, Symbol, Truthiness, TupleType, Type, TypeArrayDisplay, UnionBuilder,
|
||||
UnionType,
|
||||
IntersectionBuilder, IntersectionType, IterationOutcome, KnownClass, KnownFunction,
|
||||
KnownInstanceType, MetaclassErrorKind, SliceLiteralType, StringLiteralType, Symbol, Truthiness,
|
||||
TupleType, Type, TypeArrayDisplay, UnionBuilder, UnionType,
|
||||
};
|
||||
use crate::unpack::Unpack;
|
||||
use crate::util::subscript::{PyIndex, PySlice};
|
||||
@@ -265,6 +266,13 @@ impl<'db> TypeInference<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the intersection type is on the left or right side of the comparison.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum IntersectionOn {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
/// Builder to infer all types in a region.
|
||||
///
|
||||
/// A builder is used by creating it with [`new()`](TypeInferenceBuilder::new), and then calling
|
||||
@@ -449,9 +457,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
/// Iterate over all class definitions to check that Python will be able to create a
|
||||
/// consistent "[method resolution order]" for each class at runtime. If not, issue a diagnostic.
|
||||
/// consistent "[method resolution order]" and [metaclass] for each class at runtime. If not,
|
||||
/// issue a diagnostic.
|
||||
///
|
||||
/// [method resolution order]: https://docs.python.org/3/glossary.html#term-method-resolution-order
|
||||
/// [metaclass]: https://docs.python.org/3/reference/datamodel.html#metaclasses
|
||||
fn check_class_definitions(&mut self) {
|
||||
let class_definitions = self
|
||||
.types
|
||||
@@ -460,57 +470,73 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
.filter_map(|ty| ty.into_class_literal())
|
||||
.map(|class_ty| class_ty.class);
|
||||
|
||||
let invalid_mros = class_definitions.filter_map(|class| {
|
||||
class
|
||||
.try_mro(self.db)
|
||||
.as_ref()
|
||||
.err()
|
||||
.map(|mro_error| (class, mro_error))
|
||||
});
|
||||
for class in class_definitions {
|
||||
if let Err(mro_error) = class.try_mro(self.db).as_ref() {
|
||||
match mro_error.reason() {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class.node(self.db).bases();
|
||||
for (index, duplicate) in duplicates {
|
||||
self.diagnostics.add(
|
||||
(&base_nodes[*index]).into(),
|
||||
"duplicate-base",
|
||||
format_args!("Duplicate base class `{}`", duplicate.name(self.db)),
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::CyclicClassDefinition => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"cyclic-class-def",
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db),
|
||||
class.name(self.db)
|
||||
),
|
||||
),
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
let base_nodes = class.node(self.db).bases();
|
||||
for (index, base_ty) in bases {
|
||||
self.diagnostics.add(
|
||||
(&base_nodes[*index]).into(),
|
||||
"invalid-base",
|
||||
format_args!(
|
||||
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
base_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
MroErrorKind::UnresolvableMro { bases_list } => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"inconsistent-mro",
|
||||
format_args!(
|
||||
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
|
||||
class.name(self.db),
|
||||
bases_list.iter().map(|base| base.display(self.db)).join(", ")
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (class, mro_error) in invalid_mros {
|
||||
match mro_error.reason() {
|
||||
MroErrorKind::DuplicateBases(duplicates) => {
|
||||
let base_nodes = class.node(self.db).bases();
|
||||
for (index, duplicate) in duplicates {
|
||||
self.diagnostics.add(
|
||||
(&base_nodes[*index]).into(),
|
||||
"duplicate-base",
|
||||
format_args!("Duplicate base class `{}`", duplicate.name(self.db))
|
||||
);
|
||||
if let Err(metaclass_error) = class.try_metaclass(self.db) {
|
||||
match metaclass_error.reason() {
|
||||
MetaclassErrorKind::Conflict {
|
||||
metaclass1,
|
||||
metaclass2
|
||||
} => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"conflicting-metaclass",
|
||||
format_args!(
|
||||
"The metaclass of a derived class (`{}`) must be a subclass of the metaclasses of all its bases, but `{}` and `{}` have no subclass relationship",
|
||||
class.name(self.db),
|
||||
metaclass1.name(self.db),
|
||||
metaclass2.name(self.db),
|
||||
),
|
||||
),
|
||||
MetaclassErrorKind::CyclicDefinition => {
|
||||
// Cyclic class definition diagnostic will already have been emitted above
|
||||
// in MRO calculation.
|
||||
}
|
||||
}
|
||||
MroErrorKind::CyclicClassDefinition => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"cyclic-class-def",
|
||||
format_args!(
|
||||
"Cyclic definition of `{}` or bases of `{}` (class cannot inherit from itself)",
|
||||
class.name(self.db),
|
||||
class.name(self.db)
|
||||
)
|
||||
),
|
||||
MroErrorKind::InvalidBases(bases) => {
|
||||
let base_nodes = class.node(self.db).bases();
|
||||
for (index, base_ty) in bases {
|
||||
self.diagnostics.add(
|
||||
(&base_nodes[*index]).into(),
|
||||
"invalid-base",
|
||||
format_args!(
|
||||
"Invalid class base with type `{}` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
base_ty.display(self.db)
|
||||
)
|
||||
);
|
||||
}
|
||||
},
|
||||
MroErrorKind::UnresolvableMro{bases_list} => self.diagnostics.add(
|
||||
class.node(self.db).into(),
|
||||
"inconsistent-mro",
|
||||
format_args!(
|
||||
"Cannot create a consistent method resolution order (MRO) for class `{}` with bases list `[{}]`",
|
||||
class.name(self.db),
|
||||
bases_list.iter().map(|base| base.display(self.db)).join(", ")
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -532,12 +558,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
);
|
||||
}
|
||||
DefinitionKind::Assignment(assignment) => {
|
||||
self.infer_assignment_definition(
|
||||
assignment.target(),
|
||||
assignment.value(),
|
||||
assignment.name(),
|
||||
definition,
|
||||
);
|
||||
self.infer_assignment_definition(assignment, definition);
|
||||
}
|
||||
DefinitionKind::AnnotatedAssignment(annotated_assignment) => {
|
||||
self.infer_annotated_assignment_definition(annotated_assignment.node(), definition);
|
||||
@@ -613,10 +634,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
fn check_division_by_zero(&mut self, expr: &ast::ExprBinOp, left: Type<'db>) {
|
||||
match left {
|
||||
Type::BooleanLiteral(_) | Type::IntLiteral(_) => {}
|
||||
Type::Instance(InstanceType { class, .. })
|
||||
if [KnownClass::Float, KnownClass::Int, KnownClass::Bool]
|
||||
.iter()
|
||||
.any(|&k| class.is_known(self.db, k)) => {}
|
||||
Type::Instance(InstanceType { class })
|
||||
if matches!(
|
||||
class.known(self.db),
|
||||
Some(KnownClass::Float | KnownClass::Int | KnownClass::Bool)
|
||||
) => {}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
@@ -847,15 +869,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
let function_kind = match &**name {
|
||||
"reveal_type" if definition.is_typing_definition(self.db) => {
|
||||
Some(KnownFunction::RevealType)
|
||||
}
|
||||
"isinstance" if definition.is_builtin_definition(self.db) => {
|
||||
Some(KnownFunction::IsInstance)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let function_kind = KnownFunction::from_definition(self.db, definition, name);
|
||||
|
||||
let body_scope = self
|
||||
.index
|
||||
@@ -1191,9 +1205,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
context_expression.into(),
|
||||
"invalid-context-manager",
|
||||
format_args!("
|
||||
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable",
|
||||
context_expression = context_expression_ty.display(self.db),
|
||||
enter_ty = enter_ty.display(self.db)
|
||||
Object of type `{context_expression}` cannot be used with `with` because the method `__enter__` of type `{enter_ty}` is not callable", context_expression = context_expression_ty.display(self.db), enter_ty = enter_ty.display(self.db)
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
@@ -1285,13 +1297,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// anything else is invalid and should lead to a diagnostic being reported --Alex
|
||||
match node_ty {
|
||||
Type::Any | Type::Unknown => node_ty,
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Type::anonymous_instance(class),
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => {
|
||||
Type::Instance(InstanceType { class })
|
||||
}
|
||||
Type::Tuple(tuple) => UnionType::from_elements(
|
||||
self.db,
|
||||
tuple.elements(self.db).iter().map(|ty| {
|
||||
ty.into_class_literal()
|
||||
.map_or(Type::Todo, |ClassLiteralType { class }| {
|
||||
Type::anonymous_instance(class)
|
||||
Type::Instance(InstanceType { class })
|
||||
})
|
||||
}),
|
||||
),
|
||||
@@ -1427,20 +1441,26 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
fn infer_assignment_definition(
|
||||
&mut self,
|
||||
target: TargetKind<'db>,
|
||||
value: &ast::Expr,
|
||||
name: &ast::ExprName,
|
||||
assignment: &AssignmentDefinitionKind<'db>,
|
||||
definition: Definition<'db>,
|
||||
) {
|
||||
let value = assignment.value();
|
||||
let name = assignment.name();
|
||||
|
||||
self.infer_standalone_expression(value);
|
||||
|
||||
let value_ty = self.expression_ty(value);
|
||||
let name_ast_id = name.scoped_ast_id(self.db, self.scope());
|
||||
|
||||
let target_ty = match target {
|
||||
let target_ty = match assignment.target() {
|
||||
TargetKind::Sequence(unpack) => {
|
||||
let unpacked = infer_unpack_types(self.db, unpack);
|
||||
self.diagnostics.extend(unpacked.diagnostics());
|
||||
// Only copy the diagnostics if this is the first assignment to avoid duplicating the
|
||||
// unpack assignments.
|
||||
if assignment.is_first() {
|
||||
self.diagnostics.extend(unpacked.diagnostics());
|
||||
}
|
||||
|
||||
unpacked.get(name_ast_id).unwrap_or(Type::Unknown)
|
||||
}
|
||||
TargetKind::Name => value_ty,
|
||||
@@ -1485,14 +1505,16 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// If the declared variable is annotated with _SpecialForm class then we treat it differently
|
||||
// by assigning the known field to the instance.
|
||||
if let Type::Instance(InstanceType { class, .. }) = annotation_ty {
|
||||
if let Type::Instance(InstanceType { class }) = annotation_ty {
|
||||
if class.is_known(self.db, KnownClass::SpecialForm) {
|
||||
if let Some(name_expr) = target.as_name_expr() {
|
||||
let maybe_known_instance = file_to_module(self.db, self.file)
|
||||
if let Some(known_instance) = file_to_module(self.db, self.file)
|
||||
.as_ref()
|
||||
.and_then(|module| KnownInstance::maybe_from_module(module, &name_expr.id));
|
||||
if let Some(known_instance) = maybe_known_instance {
|
||||
annotation_ty = Type::Instance(InstanceType::known(class, known_instance));
|
||||
.and_then(|module| {
|
||||
KnownInstanceType::try_from_module_and_symbol(module, &name_expr.id)
|
||||
})
|
||||
{
|
||||
annotation_ty = Type::KnownInstance(known_instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1537,7 +1559,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
self.infer_augmented_op(assignment, target_type, value_type)
|
||||
})
|
||||
}
|
||||
Type::Instance(InstanceType { class, .. }) => {
|
||||
Type::Instance(InstanceType { class }) => {
|
||||
if let Symbol::Type(class_member, boundness) =
|
||||
class.class_member(self.db, op.in_place_dunder())
|
||||
{
|
||||
@@ -2699,7 +2721,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
(_, Type::Unknown) => Type::Unknown,
|
||||
(
|
||||
op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert),
|
||||
Type::Instance(InstanceType { class, .. }),
|
||||
Type::Instance(InstanceType { class }),
|
||||
) => {
|
||||
let unary_dunder_method = match op {
|
||||
UnaryOp::Invert => "__invert__",
|
||||
@@ -2946,37 +2968,26 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
op,
|
||||
),
|
||||
|
||||
(
|
||||
Type::Instance(InstanceType {
|
||||
class: left_class, ..
|
||||
}),
|
||||
Type::Instance(InstanceType {
|
||||
class: right_class, ..
|
||||
}),
|
||||
op,
|
||||
) => {
|
||||
if left_class != right_class && right_class.is_subclass_of(self.db, left_class) {
|
||||
(left_ty @ Type::Instance(left), right_ty @ Type::Instance(right), op) => {
|
||||
if left != right && right.is_instance_of(self.db, left.class) {
|
||||
let reflected_dunder = op.reflected_dunder();
|
||||
let rhs_reflected = right_class.class_member(self.db, reflected_dunder);
|
||||
let rhs_reflected = right.class.class_member(self.db, reflected_dunder);
|
||||
if !rhs_reflected.is_unbound()
|
||||
&& rhs_reflected != left_class.class_member(self.db, reflected_dunder)
|
||||
&& rhs_reflected != left.class.class_member(self.db, reflected_dunder)
|
||||
{
|
||||
return rhs_reflected
|
||||
.unwrap_or(Type::Never)
|
||||
.call(self.db, &[right_ty, left_ty])
|
||||
return right_ty
|
||||
.call_dunder(self.db, reflected_dunder, &[right_ty, left_ty])
|
||||
.return_ty(self.db)
|
||||
.or_else(|| {
|
||||
left_class
|
||||
.class_member(self.db, op.dunder())
|
||||
.unwrap_or(Type::Never)
|
||||
.call(self.db, &[left_ty, right_ty])
|
||||
left_ty
|
||||
.call_dunder(self.db, op.dunder(), &[left_ty, right_ty])
|
||||
.return_ty(self.db)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let call_on_left_instance = if let Symbol::Type(class_member, _) =
|
||||
left_class.class_member(self.db, op.dunder())
|
||||
left.class.class_member(self.db, op.dunder())
|
||||
{
|
||||
class_member
|
||||
.call(self.db, &[left_ty, right_ty])
|
||||
@@ -2986,11 +2997,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
};
|
||||
|
||||
call_on_left_instance.or_else(|| {
|
||||
if left_class == right_class {
|
||||
if left == right {
|
||||
None
|
||||
} else {
|
||||
if let Symbol::Type(class_member, _) =
|
||||
right_class.class_member(self.db, op.reflected_dunder())
|
||||
right.class.class_member(self.db, op.reflected_dunder())
|
||||
{
|
||||
class_member
|
||||
.call(self.db, &[right_ty, left_ty])
|
||||
@@ -3086,7 +3097,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
// https://docs.python.org/3/reference/expressions.html#comparisons
|
||||
// > Formally, if `a, b, c, …, y, z` are expressions and `op1, op2, …, opN` are comparison
|
||||
// > operators, then `a op1 b op2 c ... y opN z` is equivalent to a `op1 b and b op2 c and
|
||||
// > operators, then `a op1 b op2 c ... y opN z` is equivalent to `a op1 b and b op2 c and
|
||||
// ... > y opN z`, except that each expression is evaluated at most once.
|
||||
//
|
||||
// As some operators (==, !=, <, <=, >, >=) *can* return an arbitrary type, the logic below
|
||||
@@ -3140,6 +3151,101 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
)
|
||||
}
|
||||
|
||||
fn infer_binary_intersection_type_comparison(
|
||||
&mut self,
|
||||
intersection: IntersectionType<'db>,
|
||||
op: ast::CmpOp,
|
||||
other: Type<'db>,
|
||||
intersection_on: IntersectionOn,
|
||||
) -> Result<Type<'db>, CompareUnsupportedError<'db>> {
|
||||
// If a comparison yields a definitive true/false answer on a (positive) part
|
||||
// of an intersection type, it will also yield a definitive answer on the full
|
||||
// intersection type, which is even more specific.
|
||||
for pos in intersection.positive(self.db) {
|
||||
let result = match intersection_on {
|
||||
IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other)?,
|
||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos)?,
|
||||
};
|
||||
if let Type::BooleanLiteral(b) = result {
|
||||
return Ok(Type::BooleanLiteral(b));
|
||||
}
|
||||
}
|
||||
|
||||
// For negative contributions to the intersection type, there are only a few
|
||||
// special cases that allow us to narrow down the result type of the comparison.
|
||||
for neg in intersection.negative(self.db) {
|
||||
let result = match intersection_on {
|
||||
IntersectionOn::Left => self.infer_binary_type_comparison(*neg, op, other).ok(),
|
||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *neg).ok(),
|
||||
};
|
||||
|
||||
match (op, result) {
|
||||
(ast::CmpOp::Eq, Some(Type::BooleanLiteral(true))) => {
|
||||
return Ok(Type::BooleanLiteral(false));
|
||||
}
|
||||
(ast::CmpOp::NotEq, Some(Type::BooleanLiteral(false))) => {
|
||||
return Ok(Type::BooleanLiteral(true));
|
||||
}
|
||||
(ast::CmpOp::Is, Some(Type::BooleanLiteral(true))) => {
|
||||
return Ok(Type::BooleanLiteral(false));
|
||||
}
|
||||
(ast::CmpOp::IsNot, Some(Type::BooleanLiteral(false))) => {
|
||||
return Ok(Type::BooleanLiteral(true));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the simplifications above apply, we still need to return *some*
|
||||
// result type for the comparison 'T_inter `op` T_other' (or reversed), where
|
||||
//
|
||||
// T_inter = P1 & P2 & ... & Pn & ~N1 & ~N2 & ... & ~Nm
|
||||
//
|
||||
// is the intersection type. If f(T) is the function that computes the result
|
||||
// type of a `op`-comparison with `T_other`, we are interested in f(T_inter).
|
||||
// Since we can't compute it exactly, we return the following approximation:
|
||||
//
|
||||
// f(T_inter) = f(P1) & f(P2) & ... & f(Pn)
|
||||
//
|
||||
// The reason for this is the following: In general, for any function 'f', the
|
||||
// set f(A) & f(B) is *larger than or equal to* the set f(A & B). This means
|
||||
// that we will return a type that is possibly wider than it could be, but
|
||||
// never wrong.
|
||||
//
|
||||
// However, we do have to leave out the negative contributions. If we were to
|
||||
// add a contribution like ~f(N1), we would potentially infer result types
|
||||
// that are too narrow.
|
||||
//
|
||||
// As an example for this, consider the intersection type `int & ~Literal[1]`.
|
||||
// If 'f' would be the `==`-comparison with 2, we obviously can't tell if that
|
||||
// answer would be true or false, so we need to return `bool`. And indeed, we
|
||||
// we have (glossing over notational details):
|
||||
//
|
||||
// f(int & ~1)
|
||||
// = f({..., -1, 0, 2, 3, ...})
|
||||
// = {..., False, False, True, False, ...}
|
||||
// = bool
|
||||
//
|
||||
// On the other hand, if we were to compute
|
||||
//
|
||||
// f(int) & ~f(1)
|
||||
// = bool & ~False
|
||||
// = True
|
||||
//
|
||||
// we would get a result type `Literal[True]` which is too narrow.
|
||||
//
|
||||
let mut builder = IntersectionBuilder::new(self.db);
|
||||
for pos in intersection.positive(self.db) {
|
||||
let result = match intersection_on {
|
||||
IntersectionOn::Left => self.infer_binary_type_comparison(*pos, op, other)?,
|
||||
IntersectionOn::Right => self.infer_binary_type_comparison(other, op, *pos)?,
|
||||
};
|
||||
builder = builder.add_positive(result);
|
||||
}
|
||||
|
||||
Ok(builder.build())
|
||||
}
|
||||
|
||||
/// Infers the type of a binary comparison (e.g. 'left == right'). See
|
||||
/// `infer_compare_expression` for the higher level logic dealing with multi-comparison
|
||||
/// expressions.
|
||||
@@ -3172,6 +3278,21 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Ok(builder.build())
|
||||
}
|
||||
|
||||
(Type::Intersection(intersection), right) => self
|
||||
.infer_binary_intersection_type_comparison(
|
||||
intersection,
|
||||
op,
|
||||
right,
|
||||
IntersectionOn::Left,
|
||||
),
|
||||
(left, Type::Intersection(intersection)) => self
|
||||
.infer_binary_intersection_type_comparison(
|
||||
intersection,
|
||||
op,
|
||||
left,
|
||||
IntersectionOn::Right,
|
||||
),
|
||||
|
||||
(Type::IntLiteral(n), Type::IntLiteral(m)) => match op {
|
||||
ast::CmpOp::Eq => Ok(Type::BooleanLiteral(n == m)),
|
||||
ast::CmpOp::NotEq => Ok(Type::BooleanLiteral(n != m)),
|
||||
@@ -3624,20 +3745,20 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
|
||||
return dunder_getitem_method
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
value_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
});
|
||||
.call(self.db, &[slice_ty])
|
||||
.return_ty_result(self.db, value_node.into(), &mut self.diagnostics)
|
||||
.unwrap_or_else(|err| {
|
||||
self.diagnostics.add(
|
||||
value_node.into(),
|
||||
"call-non-callable",
|
||||
format_args!(
|
||||
"Method `__getitem__` of type `{}` is not callable on object of type `{}`",
|
||||
err.called_ty().display(self.db),
|
||||
value_ty.display(self.db),
|
||||
),
|
||||
);
|
||||
err.return_ty()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3731,7 +3852,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
Err(_) => SliceArg::Unsupported,
|
||||
},
|
||||
Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))),
|
||||
Some(Type::Instance(InstanceType { class, .. }))
|
||||
Some(Type::Instance(InstanceType { class }))
|
||||
if class.is_known(self.db, KnownClass::NoneType) =>
|
||||
{
|
||||
SliceArg::Arg(None)
|
||||
@@ -3864,15 +3985,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
let value_ty = self.infer_expression(value);
|
||||
|
||||
if value_ty
|
||||
.into_class_literal()
|
||||
.is_some_and(|ClassLiteralType { class }| {
|
||||
class.is_known(self.db, KnownClass::Tuple)
|
||||
})
|
||||
{
|
||||
self.infer_tuple_type_expression(slice)
|
||||
} else {
|
||||
self.infer_subscript_type_expression(subscript, value_ty)
|
||||
match value_ty {
|
||||
Type::ClassLiteral(class_literal_ty) => {
|
||||
match class_literal_ty.class.known(self.db) {
|
||||
Some(KnownClass::Tuple) => self.infer_tuple_type_expression(slice),
|
||||
Some(KnownClass::Type) => self.infer_subclass_of_type_expression(slice),
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
}
|
||||
}
|
||||
_ => self.infer_subscript_type_expression(subscript, value_ty),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4045,6 +4166,25 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given the slice of a `type[]` annotation, return the type that the annotation represents
|
||||
fn infer_subclass_of_type_expression(&mut self, slice: &ast::Expr) -> Type<'db> {
|
||||
match slice {
|
||||
ast::Expr::Name(name) => {
|
||||
let name_ty = self.infer_name_expression(name);
|
||||
if let Some(class_literal) = name_ty.into_class_literal() {
|
||||
Type::SubclassOf(class_literal.to_subclass_of_type())
|
||||
} else {
|
||||
Type::Todo
|
||||
}
|
||||
}
|
||||
// TODO: attributes, unions, subscripts, etc.
|
||||
_ => {
|
||||
self.infer_type_expression(slice);
|
||||
Type::Todo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_subscript_type_expression(
|
||||
&mut self,
|
||||
subscript: &ast::ExprSubscript,
|
||||
@@ -4058,10 +4198,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
} = subscript;
|
||||
|
||||
match value_ty {
|
||||
Type::Instance(InstanceType {
|
||||
class: _,
|
||||
known: Some(known_instance),
|
||||
}) => self.infer_parameterized_known_instance_type_expression(known_instance, slice),
|
||||
Type::KnownInstance(known_instance) => {
|
||||
self.infer_parameterized_known_instance_type_expression(known_instance, slice)
|
||||
}
|
||||
_ => {
|
||||
self.infer_type_expression(slice);
|
||||
Type::Todo // TODO: generics
|
||||
@@ -4071,11 +4210,11 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
fn infer_parameterized_known_instance_type_expression(
|
||||
&mut self,
|
||||
known_instance: KnownInstance,
|
||||
known_instance: KnownInstanceType,
|
||||
parameters: &ast::Expr,
|
||||
) -> Type<'db> {
|
||||
match known_instance {
|
||||
KnownInstance::Literal => match self.infer_literal_parameter_type(parameters) {
|
||||
KnownInstanceType::Literal => match self.infer_literal_parameter_type(parameters) {
|
||||
Ok(ty) => ty,
|
||||
Err(nodes) => {
|
||||
for node in nodes {
|
||||
@@ -4102,13 +4241,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// TODO handle type aliases
|
||||
ast::Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
|
||||
let value_ty = self.infer_expression(value);
|
||||
if matches!(
|
||||
value_ty,
|
||||
Type::Instance(InstanceType {
|
||||
known: Some(KnownInstance::Literal),
|
||||
..
|
||||
})
|
||||
) {
|
||||
if matches!(value_ty, Type::KnownInstance(KnownInstanceType::Literal)) {
|
||||
self.infer_literal_parameter_type(slice)?
|
||||
} else {
|
||||
return Err(vec![parameters]);
|
||||
@@ -4376,7 +4509,8 @@ fn perform_membership_test_comparison<'db>(
|
||||
// iteration-based membership test
|
||||
match Type::Instance(right).iterate(db) {
|
||||
IterationOutcome::Iterable { .. } => Some(KnownClass::Bool.to_instance(db)),
|
||||
IterationOutcome::NotIterable { .. } => None,
|
||||
IterationOutcome::NotIterable { .. }
|
||||
| IterationOutcome::PossiblyUnboundDunderIter { .. } => None,
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -4401,7 +4535,6 @@ fn perform_membership_test_comparison<'db>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use anyhow::Context;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
@@ -4516,7 +4649,7 @@ mod tests {
|
||||
let file = system_path_to_file(db, filename).unwrap();
|
||||
let diagnostics = check_types(db, file);
|
||||
|
||||
assert_diagnostic_messages(&diagnostics, expected);
|
||||
assert_diagnostic_messages(diagnostics, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -5061,27 +5194,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exception_handler_with_invalid_syntax() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
|
||||
db.write_dedented(
|
||||
"src/a.py",
|
||||
"
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
try:
|
||||
print
|
||||
except as e:
|
||||
reveal_type(e)
|
||||
",
|
||||
)?;
|
||||
|
||||
assert_file_diagnostics(&db, "src/a.py", &["Revealed type is `Unknown`"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn basic_comprehension() -> anyhow::Result<()> {
|
||||
let mut db = setup_db();
|
||||
@@ -5424,7 +5536,7 @@ mod tests {
|
||||
return 42
|
||||
|
||||
class Iterable:
|
||||
def __iter__(self) -> Iterator:
|
||||
def __iter__(self) -> Iterator: ...
|
||||
|
||||
x = [*NotIterable()]
|
||||
y = [*Iterable()]
|
||||
|
||||
@@ -5,7 +5,7 @@ use indexmap::IndexSet;
|
||||
use itertools::Either;
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use super::{Class, ClassLiteralType, KnownClass, Type};
|
||||
use super::{Class, ClassLiteralType, KnownClass, KnownInstanceType, Type};
|
||||
use crate::Db;
|
||||
|
||||
/// The inferred method resolution order of a given class.
|
||||
@@ -377,7 +377,11 @@ impl<'db> ClassBase<'db> {
|
||||
| Type::LiteralString
|
||||
| Type::Tuple(_)
|
||||
| Type::SliceLiteral(_)
|
||||
| Type::ModuleLiteral(_) => None,
|
||||
| Type::ModuleLiteral(_)
|
||||
| Type::SubclassOf(_) => None,
|
||||
Type::KnownInstance(known_instance) => match known_instance {
|
||||
KnownInstanceType::Literal => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ 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, ClassLiteralType, IntersectionBuilder, KnownClass, KnownFunction,
|
||||
Truthiness, Type, UnionBuilder,
|
||||
infer_expression_types, ClassLiteralType, InstanceType, IntersectionBuilder, KnownClass,
|
||||
KnownConstraintFunction, KnownFunction, Truthiness, Type, UnionBuilder,
|
||||
};
|
||||
use crate::Db;
|
||||
use itertools::Itertools;
|
||||
@@ -78,24 +78,27 @@ fn all_negative_narrowing_constraints_for_expression<'db>(
|
||||
NarrowingConstraintsBuilder::new(db, ConstraintNode::Expression(expression), false).finish()
|
||||
}
|
||||
|
||||
/// Generate a constraint from the *type* of the second argument of an `isinstance` call.
|
||||
/// Generate a constraint from the type of a `classinfo` argument to `isinstance` or `issubclass`.
|
||||
///
|
||||
/// Example: for `isinstance(…, str)`, we would infer `Type::ClassLiteral(str)` from the
|
||||
/// second argument, but we need to generate a `Type::Instance(str)` constraint that can
|
||||
/// be used to narrow down the type of the first argument.
|
||||
fn generate_isinstance_constraint<'db>(
|
||||
/// The `classinfo` argument can be a class literal, a tuple of (tuples of) class literals. PEP 604
|
||||
/// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type.
|
||||
fn generate_classinfo_constraint<'db, F>(
|
||||
db: &'db dyn Db,
|
||||
classinfo: &Type<'db>,
|
||||
) -> Option<Type<'db>> {
|
||||
to_constraint: F,
|
||||
) -> Option<Type<'db>>
|
||||
where
|
||||
F: Fn(ClassLiteralType<'db>) -> Type<'db> + Copy,
|
||||
{
|
||||
match classinfo {
|
||||
Type::ClassLiteral(ClassLiteralType { class }) => Some(Type::anonymous_instance(*class)),
|
||||
Type::Tuple(tuple) => {
|
||||
let mut builder = UnionBuilder::new(db);
|
||||
for element in tuple.elements(db) {
|
||||
builder = builder.add(generate_isinstance_constraint(db, element)?);
|
||||
builder = builder.add(generate_classinfo_constraint(db, element, to_constraint)?);
|
||||
}
|
||||
Some(builder.build())
|
||||
}
|
||||
Type::ClassLiteral(class_literal_type) => Some(to_constraint(*class_literal_type)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -330,34 +333,51 @@ impl<'db> NarrowingConstraintsBuilder<'db> {
|
||||
let scope = self.scope();
|
||||
let inference = infer_expression_types(self.db, expression);
|
||||
|
||||
if let Some(func_type) = inference
|
||||
// TODO: add support for PEP 604 union types on the right hand side of `isinstance`
|
||||
// and `issubclass`, for example `isinstance(x, str | (int | float))`.
|
||||
match inference
|
||||
.expression_ty(expr_call.func.scoped_ast_id(self.db, scope))
|
||||
.into_function_literal()
|
||||
.and_then(|f| f.known(self.db))
|
||||
.and_then(KnownFunction::constraint_function)
|
||||
{
|
||||
if func_type.is_known(self.db, KnownFunction::IsInstance)
|
||||
&& expr_call.arguments.keywords.is_empty()
|
||||
{
|
||||
if let [ast::Expr::Name(ast::ExprName { id, .. }), rhs] = &*expr_call.arguments.args
|
||||
Some(function) if expr_call.arguments.keywords.is_empty() => {
|
||||
if let [ast::Expr::Name(ast::ExprName { id, .. }), class_info] =
|
||||
&*expr_call.arguments.args
|
||||
{
|
||||
let symbol = self.symbols().symbol_id_by_name(id).unwrap();
|
||||
|
||||
let rhs_type = inference.expression_ty(rhs.scoped_ast_id(self.db, scope));
|
||||
let class_info_ty =
|
||||
inference.expression_ty(class_info.scoped_ast_id(self.db, scope));
|
||||
|
||||
// TODO: add support for PEP 604 union types on the right hand side:
|
||||
// isinstance(x, str | (int | float))
|
||||
if let Some(mut constraint) = generate_isinstance_constraint(self.db, &rhs_type)
|
||||
{
|
||||
if !is_positive {
|
||||
constraint = constraint.negate(self.db);
|
||||
let to_constraint = match function {
|
||||
KnownConstraintFunction::IsInstance => {
|
||||
|class_literal: ClassLiteralType<'db>| {
|
||||
Type::Instance(InstanceType {
|
||||
class: class_literal.class,
|
||||
})
|
||||
}
|
||||
}
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, constraint);
|
||||
return Some(constraints);
|
||||
}
|
||||
KnownConstraintFunction::IsSubclass => {
|
||||
|class_literal: ClassLiteralType<'db>| {
|
||||
Type::SubclassOf(class_literal.to_subclass_of_type())
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
generate_classinfo_constraint(self.db, &class_info_ty, to_constraint).map(
|
||||
|constraint| {
|
||||
let mut constraints = NarrowingConstraints::default();
|
||||
constraints.insert(symbol, constraint.negate_if(self.db, !is_positive));
|
||||
constraints
|
||||
},
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn evaluate_match_pattern_singleton(
|
||||
|
||||
@@ -6,7 +6,7 @@ mod text_document;
|
||||
|
||||
use lsp_types::{PositionEncodingKind, Url};
|
||||
pub use notebook::NotebookDocument;
|
||||
pub(crate) use range::RangeExt;
|
||||
pub(crate) use range::{RangeExt, ToRangeExt};
|
||||
pub(crate) use text_document::DocumentVersion;
|
||||
pub use text_document::TextDocument;
|
||||
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
use super::notebook;
|
||||
use super::PositionEncoding;
|
||||
use ruff_source_file::LineIndex;
|
||||
use lsp_types as types;
|
||||
use ruff_notebook::NotebookIndex;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_source_file::{LineIndex, SourceLocation};
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
|
||||
pub(crate) struct NotebookRange {
|
||||
pub(crate) cell: notebook::CellId,
|
||||
pub(crate) range: types::Range,
|
||||
}
|
||||
|
||||
pub(crate) trait RangeExt {
|
||||
fn to_text_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding)
|
||||
-> TextRange;
|
||||
}
|
||||
|
||||
pub(crate) trait ToRangeExt {
|
||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range;
|
||||
fn to_notebook_range(
|
||||
&self,
|
||||
text: &str,
|
||||
source_index: &LineIndex,
|
||||
notebook_index: &NotebookIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> NotebookRange;
|
||||
}
|
||||
|
||||
fn u32_index_to_usize(index: u32) -> usize {
|
||||
usize::try_from(index).expect("u32 fits in usize")
|
||||
}
|
||||
@@ -75,6 +94,61 @@ impl RangeExt for lsp_types::Range {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToRangeExt for TextRange {
|
||||
fn to_range(&self, text: &str, index: &LineIndex, encoding: PositionEncoding) -> types::Range {
|
||||
types::Range {
|
||||
start: source_location_to_position(&offset_to_source_location(
|
||||
self.start(),
|
||||
text,
|
||||
index,
|
||||
encoding,
|
||||
)),
|
||||
end: source_location_to_position(&offset_to_source_location(
|
||||
self.end(),
|
||||
text,
|
||||
index,
|
||||
encoding,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_notebook_range(
|
||||
&self,
|
||||
text: &str,
|
||||
source_index: &LineIndex,
|
||||
notebook_index: &NotebookIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> NotebookRange {
|
||||
let start = offset_to_source_location(self.start(), text, source_index, encoding);
|
||||
let mut end = offset_to_source_location(self.end(), text, source_index, encoding);
|
||||
let starting_cell = notebook_index.cell(start.row);
|
||||
|
||||
// weird edge case here - if the end of the range is where the newline after the cell got added (making it 'out of bounds')
|
||||
// we need to move it one character back (which should place it at the end of the last line).
|
||||
// we test this by checking if the ending offset is in a different (or nonexistent) cell compared to the cell of the starting offset.
|
||||
if notebook_index.cell(end.row) != starting_cell {
|
||||
end.row = end.row.saturating_sub(1);
|
||||
end.column = offset_to_source_location(
|
||||
self.end().checked_sub(1.into()).unwrap_or_default(),
|
||||
text,
|
||||
source_index,
|
||||
encoding,
|
||||
)
|
||||
.column;
|
||||
}
|
||||
|
||||
let start = source_location_to_position(¬ebook_index.translate_location(&start));
|
||||
let end = source_location_to_position(¬ebook_index.translate_location(&end));
|
||||
|
||||
NotebookRange {
|
||||
cell: starting_cell
|
||||
.map(OneIndexed::to_zero_indexed)
|
||||
.unwrap_or_default(),
|
||||
range: types::Range { start, end },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a UTF-16 code unit offset for a given line into a UTF-8 column number.
|
||||
fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize {
|
||||
let mut utf8_code_unit_offset = TextSize::new(0);
|
||||
@@ -96,3 +170,46 @@ fn utf8_column_offset(utf16_code_unit_offset: u32, line: &str) -> TextSize {
|
||||
|
||||
utf8_code_unit_offset
|
||||
}
|
||||
|
||||
fn offset_to_source_location(
|
||||
offset: TextSize,
|
||||
text: &str,
|
||||
index: &LineIndex,
|
||||
encoding: PositionEncoding,
|
||||
) -> SourceLocation {
|
||||
match encoding {
|
||||
PositionEncoding::UTF8 => {
|
||||
let row = index.line_index(offset);
|
||||
let column = offset - index.line_start(row, text);
|
||||
|
||||
SourceLocation {
|
||||
column: OneIndexed::from_zero_indexed(column.to_usize()),
|
||||
row,
|
||||
}
|
||||
}
|
||||
PositionEncoding::UTF16 => {
|
||||
let row = index.line_index(offset);
|
||||
|
||||
let column = if index.is_ascii() {
|
||||
(offset - index.line_start(row, text)).to_usize()
|
||||
} else {
|
||||
let up_to_line = &text[TextRange::new(index.line_start(row, text), offset)];
|
||||
up_to_line.encode_utf16().count()
|
||||
};
|
||||
|
||||
SourceLocation {
|
||||
column: OneIndexed::from_zero_indexed(column),
|
||||
row,
|
||||
}
|
||||
}
|
||||
PositionEncoding::UTF32 => index.source_location(offset, text),
|
||||
}
|
||||
}
|
||||
|
||||
fn source_location_to_position(location: &SourceLocation) -> types::Position {
|
||||
types::Position {
|
||||
line: u32::try_from(location.row.to_zero_indexed()).expect("row usize fits in u32"),
|
||||
character: u32::try_from(location.column.to_zero_indexed())
|
||||
.expect("character usize fits in u32"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,17 @@ use std::borrow::Cow;
|
||||
use lsp_types::request::DocumentDiagnosticRequest;
|
||||
use lsp_types::{
|
||||
Diagnostic, DiagnosticSeverity, DocumentDiagnosticParams, DocumentDiagnosticReport,
|
||||
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, Position, Range,
|
||||
DocumentDiagnosticReportResult, FullDocumentDiagnosticReport, NumberOrString, Range,
|
||||
RelatedFullDocumentDiagnosticReport, Url,
|
||||
};
|
||||
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
|
||||
use crate::edit::ToRangeExt;
|
||||
use crate::server::api::traits::{BackgroundDocumentRequestHandler, RequestHandler};
|
||||
use crate::server::{client::Notifier, Result};
|
||||
use crate::session::DocumentSnapshot;
|
||||
use red_knot_workspace::db::{Db, RootDatabase};
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
|
||||
pub(crate) struct DocumentDiagnosticRequestHandler;
|
||||
|
||||
@@ -64,36 +66,37 @@ fn compute_diagnostics(snapshot: &DocumentSnapshot, db: &RootDatabase) -> Vec<Di
|
||||
diagnostics
|
||||
.as_slice()
|
||||
.iter()
|
||||
.map(|message| to_lsp_diagnostic(message))
|
||||
.map(|message| to_lsp_diagnostic(db, message, snapshot.encoding()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn to_lsp_diagnostic(message: &str) -> Diagnostic {
|
||||
let words = message.split(':').collect::<Vec<_>>();
|
||||
fn to_lsp_diagnostic(
|
||||
db: &dyn Db,
|
||||
diagnostic: &dyn ruff_db::diagnostic::Diagnostic,
|
||||
encoding: crate::PositionEncoding,
|
||||
) -> Diagnostic {
|
||||
let range = if let Some(range) = diagnostic.range() {
|
||||
let index = line_index(db.upcast(), diagnostic.file());
|
||||
let source = source_text(db.upcast(), diagnostic.file());
|
||||
|
||||
let (range, message) = match words.as_slice() {
|
||||
[_, _, line, column, message] | [_, line, column, message] => {
|
||||
let line = line.parse::<u32>().unwrap_or_default().saturating_sub(1);
|
||||
let column = column.parse::<u32>().unwrap_or_default();
|
||||
(
|
||||
Range::new(
|
||||
Position::new(line, column.saturating_sub(1)),
|
||||
Position::new(line, column),
|
||||
),
|
||||
message.trim(),
|
||||
)
|
||||
}
|
||||
_ => (Range::default(), message),
|
||||
range.to_range(&source, &index, encoding)
|
||||
} else {
|
||||
Range::default()
|
||||
};
|
||||
|
||||
let severity = match diagnostic.severity() {
|
||||
Severity::Info => DiagnosticSeverity::INFORMATION,
|
||||
Severity::Error => DiagnosticSeverity::ERROR,
|
||||
};
|
||||
|
||||
Diagnostic {
|
||||
range,
|
||||
severity: Some(DiagnosticSeverity::ERROR),
|
||||
severity: Some(severity),
|
||||
tags: None,
|
||||
code: None,
|
||||
code: Some(NumberOrString::String(diagnostic.rule().to_string())),
|
||||
code_description: None,
|
||||
source: Some("red-knot".into()),
|
||||
message: message.to_string(),
|
||||
message: diagnostic.message().into_owned(),
|
||||
related_information: None,
|
||||
data: None,
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
//!
|
||||
//! We don't assume that we will get the diagnostics in source order.
|
||||
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::ops::{Deref, Range};
|
||||
|
||||
/// All diagnostics for one embedded Python file, sorted and grouped by start line number.
|
||||
@@ -19,13 +19,17 @@ pub(crate) struct SortedDiagnostics<T> {
|
||||
|
||||
impl<T> SortedDiagnostics<T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
T: Diagnostic,
|
||||
{
|
||||
pub(crate) fn new(diagnostics: impl IntoIterator<Item = T>, line_index: &LineIndex) -> Self {
|
||||
let mut diagnostics: Vec<_> = diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| DiagnosticWithLine {
|
||||
line_number: line_index.line_index(diagnostic.start()),
|
||||
line_number: diagnostic
|
||||
.range()
|
||||
.map_or(OneIndexed::from_zero_indexed(0), |range| {
|
||||
line_index.line_index(range.start())
|
||||
}),
|
||||
diagnostic,
|
||||
})
|
||||
.collect();
|
||||
@@ -94,7 +98,7 @@ pub(crate) struct LineDiagnosticsIterator<'a, T> {
|
||||
|
||||
impl<'a, T> Iterator for LineDiagnosticsIterator<'a, T>
|
||||
where
|
||||
T: Ranged + Clone,
|
||||
T: Diagnostic,
|
||||
{
|
||||
type Item = LineDiagnostics<'a, T>;
|
||||
|
||||
@@ -110,7 +114,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Clone + Ranged {}
|
||||
impl<T> std::iter::FusedIterator for LineDiagnosticsIterator<'_, T> where T: Diagnostic {}
|
||||
|
||||
/// All diagnostics that start on a single line of source code in one embedded Python file.
|
||||
#[derive(Debug)]
|
||||
@@ -139,11 +143,14 @@ struct DiagnosticWithLine<T> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::Db;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use crate::diagnostic::Diagnostic;
|
||||
use ruff_db::diagnostic::Severity;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::line_index;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{TextRange, TextSize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[test]
|
||||
fn sort_and_group() {
|
||||
@@ -152,13 +159,18 @@ mod tests {
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
let lines = line_index(&db, file);
|
||||
|
||||
let ranges = vec![
|
||||
let ranges = [
|
||||
TextRange::new(TextSize::new(0), TextSize::new(1)),
|
||||
TextRange::new(TextSize::new(5), TextSize::new(10)),
|
||||
TextRange::new(TextSize::new(1), TextSize::new(7)),
|
||||
];
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(&ranges, &lines);
|
||||
let diagnostics: Vec<_> = ranges
|
||||
.into_iter()
|
||||
.map(|range| DummyDiagnostic { range, file })
|
||||
.collect();
|
||||
|
||||
let sorted = super::SortedDiagnostics::new(diagnostics, &lines);
|
||||
let grouped = sorted.iter_lines().collect::<Vec<_>>();
|
||||
|
||||
let [line1, line2] = &grouped[..] else {
|
||||
@@ -170,4 +182,32 @@ mod tests {
|
||||
assert_eq!(line2.line_number, OneIndexed::from_zero_indexed(1));
|
||||
assert_eq!(line2.diagnostics.len(), 1);
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DummyDiagnostic {
|
||||
range: TextRange,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl Diagnostic for DummyDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"dummy"
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
"dummy".into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.range)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use colored::Colorize;
|
||||
use parser as test_parser;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic};
|
||||
use ruff_db::files::{system_path_to_file, File, Files};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
@@ -87,16 +88,24 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
.filter_map(|test_file| {
|
||||
let parsed = parsed_module(db, test_file.file);
|
||||
|
||||
// TODO allow testing against code with syntax errors
|
||||
assert!(
|
||||
parsed.errors().is_empty(),
|
||||
"Python syntax errors in {}, {}: {:?}",
|
||||
test.name(),
|
||||
test_file.file.path(db),
|
||||
parsed.errors()
|
||||
);
|
||||
let mut diagnostics: Vec<Box<_>> = parsed
|
||||
.errors()
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|error| {
|
||||
let diagnostic: Box<dyn Diagnostic> =
|
||||
Box::new(ParseDiagnostic::new(test_file.file, error));
|
||||
diagnostic
|
||||
})
|
||||
.collect();
|
||||
|
||||
match matcher::match_file(db, test_file.file, check_types(db, test_file.file)) {
|
||||
let type_diagnostics = check_types(db, test_file.file);
|
||||
diagnostics.extend(type_diagnostics.into_iter().map(|diagnostic| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new((*diagnostic).clone());
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
match matcher::match_file(db, test_file.file, diagnostics) {
|
||||
Ok(()) => None,
|
||||
Err(line_failures) => Some(FileFailures {
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
//! Match [`TypeCheckDiagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! Match [`Diagnostic`]s against [`Assertion`]s and produce test failure messages for any
|
||||
//! mismatches.
|
||||
use crate::assertion::{Assertion, ErrorAssertion, InlineFileAssertions};
|
||||
use crate::db::Db;
|
||||
use crate::diagnostic::SortedDiagnostics;
|
||||
use colored::Colorize;
|
||||
use red_knot_python_semantic::types::TypeCheckDiagnostic;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::File;
|
||||
use ruff_db::source::{line_index, source_text, SourceText};
|
||||
use ruff_source_file::{LineIndex, OneIndexed};
|
||||
use ruff_text_size::Ranged;
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Range;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub(super) struct FailuresByLine {
|
||||
@@ -55,7 +53,7 @@ pub(super) fn match_file<T>(
|
||||
diagnostics: impl IntoIterator<Item = T>,
|
||||
) -> Result<(), FailuresByLine>
|
||||
where
|
||||
T: Diagnostic + Clone,
|
||||
T: Diagnostic,
|
||||
{
|
||||
// Parse assertions from comments in the file, and get diagnostics from the file; both
|
||||
// ordered by line number.
|
||||
@@ -126,22 +124,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) trait Diagnostic: Ranged {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> &str;
|
||||
}
|
||||
|
||||
impl Diagnostic for Arc<TypeCheckDiagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
self.as_ref().rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.as_ref().message()
|
||||
}
|
||||
}
|
||||
|
||||
trait Unmatched {
|
||||
fn unmatched(&self) -> String;
|
||||
}
|
||||
@@ -253,10 +235,15 @@ impl Matcher {
|
||||
}
|
||||
}
|
||||
|
||||
fn column<T: Ranged>(&self, ranged: &T) -> OneIndexed {
|
||||
self.line_index
|
||||
.source_location(ranged.start(), &self.source)
|
||||
.column
|
||||
fn column<T: Diagnostic>(&self, diagnostic: &T) -> OneIndexed {
|
||||
diagnostic
|
||||
.range()
|
||||
.map(|range| {
|
||||
self.line_index
|
||||
.source_location(range.start(), &self.source)
|
||||
.column
|
||||
})
|
||||
.unwrap_or(OneIndexed::from_zero_indexed(0))
|
||||
}
|
||||
|
||||
/// Check if `assertion` matches any [`Diagnostic`]s in `unmatched`.
|
||||
@@ -323,20 +310,21 @@ impl Matcher {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::FailuresByLine;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::diagnostic::{Diagnostic, Severity};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPathBuf};
|
||||
use ruff_python_trivia::textwrap::dedent;
|
||||
use ruff_source_file::OneIndexed;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
use ruff_text_size::TextRange;
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct TestDiagnostic {
|
||||
struct ExpectedDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
}
|
||||
|
||||
impl TestDiagnostic {
|
||||
impl ExpectedDiagnostic {
|
||||
fn new(rule: &'static str, message: &'static str, offset: usize) -> Self {
|
||||
let offset: u32 = offset.try_into().unwrap();
|
||||
Self {
|
||||
@@ -345,32 +333,64 @@ mod tests {
|
||||
range: TextRange::new(offset.into(), (offset + 1).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn into_diagnostic(self, file: File) -> TestDiagnostic {
|
||||
TestDiagnostic {
|
||||
rule: self.rule,
|
||||
message: self.message,
|
||||
range: self.range,
|
||||
file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Diagnostic for TestDiagnostic {
|
||||
#[derive(Debug)]
|
||||
struct TestDiagnostic {
|
||||
rule: &'static str,
|
||||
message: &'static str,
|
||||
range: TextRange,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl Diagnostic for TestDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
self.rule
|
||||
}
|
||||
|
||||
fn message(&self) -> &str {
|
||||
self.message
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.message.into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.range)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for TestDiagnostic {
|
||||
fn range(&self) -> ruff_text_size::TextRange {
|
||||
self.range
|
||||
}
|
||||
}
|
||||
|
||||
fn get_result(source: &str, diagnostics: Vec<TestDiagnostic>) -> Result<(), FailuresByLine> {
|
||||
fn get_result(
|
||||
source: &str,
|
||||
diagnostics: Vec<ExpectedDiagnostic>,
|
||||
) -> Result<(), FailuresByLine> {
|
||||
colored::control::set_override(false);
|
||||
|
||||
let mut db = crate::db::Db::setup(SystemPathBuf::from("/src"));
|
||||
db.write_file("/src/test.py", source).unwrap();
|
||||
let file = system_path_to_file(&db, "/src/test.py").unwrap();
|
||||
|
||||
super::match_file(&db, file, diagnostics)
|
||||
super::match_file(
|
||||
&db,
|
||||
file,
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.into_diagnostic(file)),
|
||||
)
|
||||
}
|
||||
|
||||
fn assert_fail(result: Result<(), FailuresByLine>, messages: &[(usize, &[&str])]) {
|
||||
@@ -403,7 +423,7 @@ mod tests {
|
||||
fn revealed_match() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
@@ -417,7 +437,7 @@ mod tests {
|
||||
fn revealed_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new(
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"not-revealed-type",
|
||||
"Revealed type is `Foo`",
|
||||
0,
|
||||
@@ -440,7 +460,11 @@ mod tests {
|
||||
fn revealed_wrong_message() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("revealed-type", "Something else", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"revealed-type",
|
||||
"Something else",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -467,8 +491,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Foo`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -479,7 +503,11 @@ mod tests {
|
||||
fn revealed_match_with_only_undefined() {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"undefined-reveal",
|
||||
"Doesn't matter",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(result, &[(0, &["unmatched assertion: revealed: Foo"])]);
|
||||
@@ -490,8 +518,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"x # revealed: Foo",
|
||||
vec![
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
TestDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Bar`", 0),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "Doesn't matter", 0),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -512,8 +540,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1)",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -535,8 +563,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
"reveal_type(1) # error: [something-else]",
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "undefined reveal message", 0),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[1]`", 12),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -565,7 +593,7 @@ mod tests {
|
||||
fn error_match_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -575,7 +603,7 @@ mod tests {
|
||||
fn error_wrong_rule() {
|
||||
let result = get_result(
|
||||
"x # error: [some-rule]",
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -594,7 +622,11 @@ mod tests {
|
||||
fn error_match_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -604,7 +636,7 @@ mod tests {
|
||||
fn error_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("anything", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -623,7 +655,7 @@ mod tests {
|
||||
fn error_match_column_and_rule() {
|
||||
let result = get_result(
|
||||
"x # error: 1 [some-rule]",
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -633,7 +665,7 @@ mod tests {
|
||||
fn error_wrong_column() {
|
||||
let result = get_result(
|
||||
"x # error: 2 [rule]",
|
||||
vec![TestDiagnostic::new("rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -652,7 +684,11 @@ mod tests {
|
||||
fn error_match_column_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 "contains this""#,
|
||||
vec![TestDiagnostic::new("anything", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"anything",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -662,7 +698,11 @@ mod tests {
|
||||
fn error_match_rule_and_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -672,7 +712,11 @@ mod tests {
|
||||
fn error_match_all() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [a-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("a-rule", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"a-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_ok(&result);
|
||||
@@ -682,7 +726,11 @@ mod tests {
|
||||
fn error_match_all_wrong_column() {
|
||||
let result = get_result(
|
||||
r#"x # error: 2 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "message contains this", 0)],
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"some-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -701,7 +749,7 @@ mod tests {
|
||||
fn error_match_all_wrong_rule() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new(
|
||||
vec![ExpectedDiagnostic::new(
|
||||
"other-rule",
|
||||
"message contains this",
|
||||
0,
|
||||
@@ -724,7 +772,7 @@ mod tests {
|
||||
fn error_match_all_wrong_message() {
|
||||
let result = get_result(
|
||||
r#"x # error: 1 [some-rule] "contains this""#,
|
||||
vec![TestDiagnostic::new("some-rule", "Any message", 0)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "Any message", 0)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -757,9 +805,9 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
TestDiagnostic::new("line-three", "msg", three),
|
||||
TestDiagnostic::new("line-five", "msg", five),
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new("line-three", "msg", three),
|
||||
ExpectedDiagnostic::new("line-five", "msg", five),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -788,8 +836,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("line-one", "msg", one),
|
||||
TestDiagnostic::new("line-two", "msg", two),
|
||||
ExpectedDiagnostic::new("line-one", "msg", one),
|
||||
ExpectedDiagnostic::new("line-two", "msg", two),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -809,8 +857,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -830,8 +878,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -851,9 +899,9 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("one-rule", "msg", x),
|
||||
TestDiagnostic::new("other-rule", "msg", x),
|
||||
TestDiagnostic::new("third-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("one-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("other-rule", "msg", x),
|
||||
ExpectedDiagnostic::new("third-rule", "msg", x),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -877,8 +925,8 @@ mod tests {
|
||||
let result = get_result(
|
||||
&source,
|
||||
vec![
|
||||
TestDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
TestDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
ExpectedDiagnostic::new("undefined-reveal", "msg", reveal),
|
||||
ExpectedDiagnostic::new("revealed-type", "Revealed type is `Literal[5]`", reveal),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -891,7 +939,7 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
@@ -912,7 +960,7 @@ mod tests {
|
||||
let x = source.find('x').unwrap();
|
||||
let result = get_result(
|
||||
source,
|
||||
vec![TestDiagnostic::new("some-rule", "some message", x)],
|
||||
vec![ExpectedDiagnostic::new("some-rule", "some message", x)],
|
||||
);
|
||||
|
||||
assert_fail(
|
||||
|
||||
@@ -6,6 +6,7 @@ use wasm_bindgen::prelude::*;
|
||||
use red_knot_workspace::db::RootDatabase;
|
||||
use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
||||
use ruff_db::system::{
|
||||
@@ -110,14 +111,20 @@ impl Workspace {
|
||||
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<String>, Error> {
|
||||
let result = self.db.check_file(file_id.file).map_err(into_error)?;
|
||||
|
||||
Ok(result)
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&self.db).to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Checks all open files
|
||||
pub fn check(&self) -> Result<Vec<String>, Error> {
|
||||
let result = self.db.check().map_err(into_error)?;
|
||||
|
||||
Ok(result)
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&self.db).to_string())
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Returns the parsed AST for `path`
|
||||
|
||||
@@ -19,6 +19,6 @@ fn check() {
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
vec!["/test.py:1:8: Cannot resolve import `random22`"]
|
||||
vec!["error[unresolved-import] /test.py:1:8 Cannot resolve import `random22`"]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,14 @@ use std::sync::Arc;
|
||||
use salsa::plumbing::ZalsaDatabase;
|
||||
use salsa::{Cancelled, Event};
|
||||
|
||||
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::{Db as SemanticDb, Program};
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{File, Files};
|
||||
use ruff_db::system::System;
|
||||
use ruff_db::vendored::VendoredFileSystem;
|
||||
use ruff_db::{Db as SourceDb, Upcast};
|
||||
|
||||
use crate::workspace::{check_file, Workspace, WorkspaceMetadata};
|
||||
|
||||
mod changes;
|
||||
|
||||
#[salsa::db]
|
||||
@@ -51,11 +51,11 @@ impl RootDatabase {
|
||||
}
|
||||
|
||||
/// Checks all open files in the workspace and its dependencies.
|
||||
pub fn check(&self) -> Result<Vec<String>, Cancelled> {
|
||||
pub fn check(&self) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
self.with_db(|db| db.workspace().check(db))
|
||||
}
|
||||
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<String>, Cancelled> {
|
||||
pub fn check_file(&self, file: File) -> Result<Vec<Box<dyn Diagnostic>>, Cancelled> {
|
||||
let _span = tracing::debug_span!("check_file", file=%file.path(self)).entered();
|
||||
|
||||
self.with_db(|db| check_file(db, file))
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use rustc_hash::{FxBuildHasher, FxHashSet};
|
||||
use salsa::{Durability, Setter as _};
|
||||
use std::borrow::Cow;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::db::RootDatabase;
|
||||
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
pub use metadata::{PackageMetadata, WorkspaceMetadata};
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use red_knot_python_semantic::SearchPathSettings;
|
||||
use ruff_db::diagnostic::{Diagnostic, ParseDiagnostic, Severity};
|
||||
use ruff_db::parsed::parsed_module;
|
||||
use ruff_db::source::{line_index, source_text, SourceDiagnostic};
|
||||
use ruff_db::source::{source_text, SourceTextError};
|
||||
use ruff_db::{
|
||||
files::{system_path_to_file, File},
|
||||
system::{walk_directory::WalkState, SystemPath, SystemPathBuf},
|
||||
};
|
||||
use ruff_python_ast::{name::Name, PySourceType};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::db::Db;
|
||||
use crate::db::RootDatabase;
|
||||
use crate::workspace::files::{Index, Indexed, IndexedIter, PackageFiles};
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
mod files;
|
||||
mod metadata;
|
||||
@@ -188,7 +188,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
/// Checks all open files in the workspace and its dependencies.
|
||||
pub fn check(self, db: &RootDatabase) -> Vec<String> {
|
||||
pub fn check(self, db: &RootDatabase) -> Vec<Box<dyn Diagnostic>> {
|
||||
let workspace_span = tracing::debug_span!("check_workspace");
|
||||
let _span = workspace_span.enter();
|
||||
|
||||
@@ -378,47 +378,31 @@ impl Package {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::tracked]
|
||||
pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<String> {
|
||||
tracing::debug!("Checking file '{path}'", path = file.path(db));
|
||||
|
||||
let mut diagnostics = Vec::new();
|
||||
|
||||
let source_diagnostics = source_text::accumulated::<SourceDiagnostic>(db.upcast(), file);
|
||||
// TODO(micha): Consider using a single accumulator for all diagnostics
|
||||
diagnostics.extend(
|
||||
source_diagnostics
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string),
|
||||
);
|
||||
|
||||
pub(super) fn check_file(db: &dyn Db, file: File) -> Vec<Box<dyn Diagnostic>> {
|
||||
let mut diagnostics: Vec<Box<dyn Diagnostic>> = Vec::new();
|
||||
// Abort checking if there are IO errors.
|
||||
let source = source_text(db.upcast(), file);
|
||||
|
||||
if source.has_read_error() {
|
||||
if let Some(read_error) = source.read_error() {
|
||||
diagnostics.push(Box::new(IOErrorDiagnostic {
|
||||
file,
|
||||
error: read_error.clone(),
|
||||
}));
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
let parsed = parsed_module(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|error| {
|
||||
let diagnostic: Box<dyn Diagnostic> = Box::new(ParseDiagnostic::new(file, error.clone()));
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
if !parsed.errors().is_empty() {
|
||||
let path = file.path(db);
|
||||
let line_index = line_index(db.upcast(), file);
|
||||
diagnostics.extend(parsed.errors().iter().map(|err| {
|
||||
let source_location = line_index.source_location(err.location.start(), source.as_str());
|
||||
format!("{path}:{source_location}: {message}", message = err.error)
|
||||
}));
|
||||
}
|
||||
diagnostics.extend(check_types(db.upcast(), file).iter().map(|diagnostic| {
|
||||
let boxed: Box<dyn Diagnostic> = Box::new(diagnostic.clone());
|
||||
boxed
|
||||
}));
|
||||
|
||||
for diagnostic in check_types(db.upcast(), file) {
|
||||
let index = line_index(db.upcast(), diagnostic.file());
|
||||
let location = index.source_location(diagnostic.start(), source.as_str());
|
||||
diagnostics.push(format!(
|
||||
"{path}:{location}: {message}",
|
||||
path = file.path(db),
|
||||
message = diagnostic.message()
|
||||
));
|
||||
}
|
||||
diagnostics.sort_unstable_by_key(|diagnostic| diagnostic.range().unwrap_or_default().start());
|
||||
|
||||
diagnostics
|
||||
}
|
||||
@@ -533,17 +517,45 @@ impl Iterator for WorkspaceFilesIter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct IOErrorDiagnostic {
|
||||
file: File,
|
||||
error: SourceTextError,
|
||||
}
|
||||
|
||||
impl Diagnostic for IOErrorDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"io"
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
None
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::workspace::check_file;
|
||||
use red_knot_python_semantic::types::check_types;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::system_path_to_file;
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{DbWithTestSystem, SystemPath};
|
||||
use ruff_db::testing::assert_function_query_was_not_run;
|
||||
|
||||
use crate::db::tests::TestDb;
|
||||
use crate::workspace::check_file;
|
||||
|
||||
#[test]
|
||||
fn check_file_skips_type_checking_when_file_cant_be_read() -> ruff_db::system::Result<()> {
|
||||
let mut db = TestDb::new();
|
||||
@@ -558,7 +570,10 @@ mod tests {
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(
|
||||
check_file(&db, file),
|
||||
check_file(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["Failed to read file: No such file or directory".to_string()]
|
||||
);
|
||||
|
||||
@@ -570,7 +585,13 @@ mod tests {
|
||||
db.write_file(path, "").unwrap();
|
||||
|
||||
assert_eq!(source_text(&db, file).as_str(), "");
|
||||
assert_eq!(check_file(&db, file), vec![] as Vec<String>);
|
||||
assert_eq!(
|
||||
check_file(&db, file)
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.message().into_owned())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![] as Vec<String>
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
publish = true
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -8,6 +8,7 @@ use red_knot_workspace::workspace::settings::Configuration;
|
||||
use red_knot_workspace::workspace::WorkspaceMetadata;
|
||||
use ruff_benchmark::criterion::{criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use ruff_benchmark::TestFile;
|
||||
use ruff_db::diagnostic::Diagnostic;
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::source_text;
|
||||
use ruff_db::system::{MemoryFileSystem, SystemPath, SystemPathBuf, TestSystem};
|
||||
@@ -24,32 +25,32 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
|
||||
|
||||
static EXPECTED_DIAGNOSTICS: &[&str] = &[
|
||||
// We don't support `*` imports yet:
|
||||
"/src/tomllib/_parser.py:7:29: Module `collections.abc` has no member `Iterable`",
|
||||
"error[unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
|
||||
// We don't support terminal statements in control flow yet:
|
||||
"/src/tomllib/_parser.py:246:15: Method `__class_getitem__` of type `Literal[frozenset]` is possibly unbound",
|
||||
"/src/tomllib/_parser.py:692:8354: Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
"/src/tomllib/_parser.py:66:18: Name `s` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:98:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:101:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:104:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:108:17: Conflicting declared types for `second_char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:115:14: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:126:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:267:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:348:20: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:353:5: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:364:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:381:13: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:395:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:453:24: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:455:9: Name `nest` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:482:16: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:566:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:573:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:579:12: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:580:63: Name `char` used when possibly not defined",
|
||||
"/src/tomllib/_parser.py:590:9: Conflicting declared types for `char`: Unknown, str | None",
|
||||
"/src/tomllib/_parser.py:629:38: Name `datetime_obj` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:108:17 Conflicting declared types for `second_char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
|
||||
"error[call-possibly-unbound-method] /src/tomllib/_parser.py:246:15 Method `__class_getitem__` of type `Literal[frozenset]` is possibly unbound",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:267:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:364:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:381:13 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:395:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
|
||||
"error[conflicting-declarations] /src/tomllib/_parser.py:590:9 Conflicting declared types for `char`: Unknown, str | None",
|
||||
"error[possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
|
||||
"error[invalid-base] /src/tomllib/_parser.py:692:8354 Invalid class base with type `GenericAlias` (all bases must be a class, `Any`, `Unknown` or `Todo`)",
|
||||
];
|
||||
|
||||
fn get_test_file(name: &str) -> TestFile {
|
||||
@@ -123,7 +124,14 @@ fn setup_rayon() {
|
||||
fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
fn setup() -> Case {
|
||||
let case = setup_case();
|
||||
let result = case.db.check().unwrap();
|
||||
|
||||
let result: Vec<_> = case
|
||||
.db
|
||||
.check()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&case.db).to_string())
|
||||
.collect();
|
||||
|
||||
assert_eq!(result, EXPECTED_DIAGNOSTICS);
|
||||
|
||||
@@ -150,7 +158,7 @@ fn benchmark_incremental(criterion: &mut Criterion) {
|
||||
|
||||
let result = db.check().unwrap();
|
||||
|
||||
assert_eq!(result, EXPECTED_DIAGNOSTICS);
|
||||
assert_eq!(result.len(), EXPECTED_DIAGNOSTICS.len());
|
||||
}
|
||||
|
||||
setup_rayon();
|
||||
@@ -168,7 +176,12 @@ fn benchmark_cold(criterion: &mut Criterion) {
|
||||
setup_case,
|
||||
|case| {
|
||||
let Case { db, .. } = case;
|
||||
let result = db.check().unwrap();
|
||||
let result: Vec<_> = db
|
||||
.check()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(db).to_string())
|
||||
.collect();
|
||||
|
||||
assert_eq!(result, EXPECTED_DIAGNOSTICS);
|
||||
},
|
||||
|
||||
180
crates/ruff_db/src/diagnostic.rs
Normal file
180
crates/ruff_db/src/diagnostic.rs
Normal file
@@ -0,0 +1,180 @@
|
||||
use crate::{
|
||||
files::File,
|
||||
source::{line_index, source_text},
|
||||
Db,
|
||||
};
|
||||
use ruff_python_parser::ParseError;
|
||||
use ruff_text_size::TextRange;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub trait Diagnostic: Send + Sync + std::fmt::Debug {
|
||||
fn rule(&self) -> &str;
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str>;
|
||||
|
||||
fn file(&self) -> File;
|
||||
|
||||
fn range(&self) -> Option<TextRange>;
|
||||
|
||||
fn severity(&self) -> Severity;
|
||||
|
||||
fn display<'a>(&'a self, db: &'a dyn Db) -> DisplayDiagnostic<'a>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
DisplayDiagnostic {
|
||||
db,
|
||||
diagnostic: self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub struct DisplayDiagnostic<'db> {
|
||||
db: &'db dyn Db,
|
||||
diagnostic: &'db dyn Diagnostic,
|
||||
}
|
||||
|
||||
impl<'db> DisplayDiagnostic<'db> {
|
||||
pub fn new(db: &'db dyn Db, diagnostic: &'db dyn Diagnostic) -> Self {
|
||||
Self { db, diagnostic }
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DisplayDiagnostic<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.diagnostic.severity() {
|
||||
Severity::Info => f.write_str("info")?,
|
||||
Severity::Error => f.write_str("error")?,
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"[{rule}] {path}",
|
||||
rule = self.diagnostic.rule(),
|
||||
path = self.diagnostic.file().path(self.db)
|
||||
)?;
|
||||
|
||||
if let Some(range) = self.diagnostic.range() {
|
||||
let index = line_index(self.db, self.diagnostic.file());
|
||||
let source = source_text(self.db, self.diagnostic.file());
|
||||
|
||||
let start = index.source_location(range.start(), &source);
|
||||
|
||||
write!(f, ":{line}:{col}", line = start.row, col = start.column)?;
|
||||
}
|
||||
|
||||
write!(f, " {message}", message = self.diagnostic.message())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Diagnostic for Box<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Diagnostic for std::sync::Arc<T>
|
||||
where
|
||||
T: Diagnostic,
|
||||
{
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> std::borrow::Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for Box<dyn Diagnostic> {
|
||||
fn rule(&self) -> &str {
|
||||
(**self).rule()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseDiagnostic {
|
||||
file: File,
|
||||
error: ParseError,
|
||||
}
|
||||
|
||||
impl ParseDiagnostic {
|
||||
pub fn new(file: File, error: ParseError) -> Self {
|
||||
Self { file, error }
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for ParseDiagnostic {
|
||||
fn rule(&self) -> &str {
|
||||
"invalid-syntax"
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
self.error.error.to_string().into()
|
||||
}
|
||||
|
||||
fn file(&self) -> File {
|
||||
self.file
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
Some(self.error.location)
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
Severity::Error
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ use crate::files::Files;
|
||||
use crate::system::System;
|
||||
use crate::vendored::VendoredFileSystem;
|
||||
|
||||
pub mod diagnostic;
|
||||
pub mod display;
|
||||
pub mod file_revision;
|
||||
pub mod files;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
use std::fmt::Formatter;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use countme::Count;
|
||||
use salsa::Accumulator;
|
||||
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_python_ast::PySourceType;
|
||||
@@ -17,16 +15,14 @@ use crate::Db;
|
||||
pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
let path = file.path(db);
|
||||
let _span = tracing::trace_span!("source_text", file = %path).entered();
|
||||
let mut has_read_error = false;
|
||||
let mut read_error = None;
|
||||
|
||||
let kind = if is_notebook(file.path(db)) {
|
||||
file.read_to_notebook(db)
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::debug!("Failed to read notebook '{path}': {error}");
|
||||
|
||||
has_read_error = true;
|
||||
SourceDiagnostic(Arc::new(SourceTextError::FailedToReadNotebook(error)))
|
||||
.accumulate(db);
|
||||
read_error = Some(SourceTextError::FailedToReadNotebook(error.to_string()));
|
||||
Notebook::empty()
|
||||
})
|
||||
.into()
|
||||
@@ -35,8 +31,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
.unwrap_or_else(|error| {
|
||||
tracing::debug!("Failed to read file '{path}': {error}");
|
||||
|
||||
has_read_error = true;
|
||||
SourceDiagnostic(Arc::new(SourceTextError::FailedToReadFile(error))).accumulate(db);
|
||||
read_error = Some(SourceTextError::FailedToReadFile(error.to_string()));
|
||||
String::new()
|
||||
})
|
||||
.into()
|
||||
@@ -45,7 +40,7 @@ pub fn source_text(db: &dyn Db, file: File) -> SourceText {
|
||||
SourceText {
|
||||
inner: Arc::new(SourceTextInner {
|
||||
kind,
|
||||
has_read_error,
|
||||
read_error,
|
||||
count: Count::new(),
|
||||
}),
|
||||
}
|
||||
@@ -98,8 +93,8 @@ impl SourceText {
|
||||
}
|
||||
|
||||
/// Returns `true` if there was an error when reading the content of the file.
|
||||
pub fn has_read_error(&self) -> bool {
|
||||
self.inner.has_read_error
|
||||
pub fn read_error(&self) -> Option<&SourceTextError> {
|
||||
self.inner.read_error.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,7 +127,7 @@ impl std::fmt::Debug for SourceText {
|
||||
struct SourceTextInner {
|
||||
count: Count<SourceText>,
|
||||
kind: SourceTextKind,
|
||||
has_read_error: bool,
|
||||
read_error: Option<SourceTextError>,
|
||||
}
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
@@ -153,21 +148,12 @@ impl From<Notebook> for SourceTextKind {
|
||||
}
|
||||
}
|
||||
|
||||
#[salsa::accumulator]
|
||||
pub struct SourceDiagnostic(Arc<SourceTextError>);
|
||||
|
||||
impl std::fmt::Display for SourceDiagnostic {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)]
|
||||
pub enum SourceTextError {
|
||||
#[error("Failed to read notebook: {0}`")]
|
||||
FailedToReadNotebook(#[from] ruff_notebook::NotebookError),
|
||||
FailedToReadNotebook(String),
|
||||
#[error("Failed to read file: {0}")]
|
||||
FailedToReadFile(#[from] std::io::Error),
|
||||
FailedToReadFile(String),
|
||||
}
|
||||
|
||||
/// Computes the [`LineIndex`] for `file`.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff_linter"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
publish = false
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
@@ -10,6 +10,8 @@ async def func3(id, dir):
|
||||
pass
|
||||
|
||||
|
||||
# this is Ok for A002 (trigger A005 instead)
|
||||
# https://github.com/astral-sh/ruff/issues/14135
|
||||
map([], lambda float: ...)
|
||||
|
||||
from typing import override, overload
|
||||
|
||||
@@ -3,3 +3,8 @@ lambda x, float, y: x + y
|
||||
lambda min, max: min
|
||||
lambda id: id
|
||||
lambda dir: dir
|
||||
|
||||
# Ok for A006 - should trigger A002 instead
|
||||
# https://github.com/astral-sh/ruff/issues/14135
|
||||
def func1(str, /, type, *complex, Exception, **getattr):
|
||||
pass
|
||||
@@ -6,6 +6,19 @@ def foo():
|
||||
class Bar:
|
||||
"""bar""" # ERROR PYI021
|
||||
|
||||
class Qux:
|
||||
"""qux""" # ERROR PYI021
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
class Baz:
|
||||
"""Multiline docstring
|
||||
|
||||
Lorem ipsum dolor sit amet
|
||||
"""
|
||||
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
def bar():
|
||||
x = 1
|
||||
"""foo""" # OK, not a doc string
|
||||
|
||||
42
crates/ruff_linter/resources/test/fixtures/refurb/FURB189.py
vendored
Normal file
42
crates/ruff_linter/resources/test/fixtures/refurb/FURB189.py
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
# setup
|
||||
from enum import Enum, EnumMeta
|
||||
from collections import UserList as UL
|
||||
|
||||
class SetOnceMappingMixin:
|
||||
__slots__ = ()
|
||||
def __setitem__(self, key, value):
|
||||
if key in self:
|
||||
raise KeyError(str(key) + ' already set')
|
||||
return super().__setitem__(key, value)
|
||||
|
||||
|
||||
class CaseInsensitiveEnumMeta(EnumMeta):
|
||||
pass
|
||||
|
||||
# positives
|
||||
class D(dict):
|
||||
pass
|
||||
|
||||
class L(list):
|
||||
pass
|
||||
|
||||
class S(str):
|
||||
pass
|
||||
|
||||
# currently not detected
|
||||
class SetOnceDict(SetOnceMappingMixin, dict):
|
||||
pass
|
||||
|
||||
# negatives
|
||||
class C:
|
||||
pass
|
||||
|
||||
class I(int):
|
||||
pass
|
||||
|
||||
class ActivityState(str, Enum, metaclass=CaseInsensitiveEnumMeta):
|
||||
"""Activity state. This is an optional property and if not provided, the state will be Active by
|
||||
default.
|
||||
"""
|
||||
ACTIVE = "Active"
|
||||
INACTIVE = "Inactive"
|
||||
@@ -155,7 +155,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
|
||||
|
||||
// flake8-pyi
|
||||
if enforce_stubs {
|
||||
flake8_pyi::rules::docstring_in_stubs(checker, docstring);
|
||||
flake8_pyi::rules::docstring_in_stubs(checker, definition, docstring);
|
||||
}
|
||||
if enforce_stubs_and_runtime {
|
||||
flake8_pyi::rules::iter_method_return_iterable(checker, definition);
|
||||
|
||||
@@ -549,6 +549,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::WhitespaceAfterDecorator) {
|
||||
pycodestyle::rules::whitespace_after_decorator(checker, decorator_list);
|
||||
}
|
||||
if checker.enabled(Rule::SubclassBuiltin) {
|
||||
refurb::rules::subclass_builtin(checker, class_def);
|
||||
}
|
||||
}
|
||||
Stmt::Import(ast::StmtImport { names, range: _ }) => {
|
||||
if checker.enabled(Rule::MultipleImportsOnOneLine) {
|
||||
|
||||
@@ -1072,6 +1072,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(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, "189") => (RuleGroup::Preview, rules::refurb::rules::SubclassBuiltin),
|
||||
(Refurb, "192") => (RuleGroup::Preview, rules::refurb::rules::SortedMinMax),
|
||||
|
||||
// flake8-logging
|
||||
|
||||
@@ -16,8 +16,43 @@ static CODE_INDICATORS: LazyLock<AhoCorasick> = LazyLock::new(|| {
|
||||
|
||||
static ALLOWLIST_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(
|
||||
r"^(?i)(?:pylint|pyright|noqa|nosec|region|endregion|type:\s*ignore|fmt:\s*(on|off)|isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)|mypy:|SPDX-License-Identifier:|language=[a-zA-Z](?: ?[-_.a-zA-Z0-9]+)+(?:\s+prefix=\S+)?(?:\s+suffix=\S+)?|(?:en)?coding[:=][ \t]*([-_.a-zA-Z0-9]+))",
|
||||
).unwrap()
|
||||
r"(?x)
|
||||
^
|
||||
(?:
|
||||
# Case-sensitive
|
||||
pyright
|
||||
| mypy:
|
||||
| type:\s*ignore
|
||||
| SPDX-License-Identifier:
|
||||
| fmt:\s*(on|off|skip)
|
||||
| region|endregion
|
||||
|
||||
# Case-insensitive
|
||||
| (?i:
|
||||
noqa
|
||||
)
|
||||
|
||||
# Unknown case sensitivity
|
||||
| (?i:
|
||||
pylint
|
||||
| nosec
|
||||
| isort:\s*(on|off|skip|skip_file|split|dont-add-imports(:\s*\[.*?])?)
|
||||
| (?:en)?coding[:=][\x20\t]*([-_.A-Z0-9]+)
|
||||
)
|
||||
|
||||
# IntelliJ language injection comments:
|
||||
# * `language` must be lowercase.
|
||||
# * No spaces around `=`.
|
||||
# * Language IDs as used in comments must have no spaces,
|
||||
# though to IntelliJ they can be anything.
|
||||
# * May optionally contain `prefix=` and/or `suffix=`,
|
||||
# not declared here since we use `.is_match()`.
|
||||
| language=[-_.a-zA-Z0-9]+
|
||||
|
||||
)
|
||||
",
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
static HASH_NUMBER: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"#\d").unwrap());
|
||||
@@ -299,17 +334,42 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn comment_contains_language_injection() {
|
||||
assert!(comment_contains_code("# language=123", &[]));
|
||||
// `language` with bad casing
|
||||
assert!(comment_contains_code("# Language=C#", &[]));
|
||||
assert!(comment_contains_code("# lAngUAgE=inI", &[]));
|
||||
|
||||
// Unreasonable language IDs, possibly literals
|
||||
assert!(comment_contains_code("# language=\"pt\"", &[]));
|
||||
assert!(comment_contains_code("# language='en'", &[]));
|
||||
|
||||
assert!(!comment_contains_code("# language=xml", &[]));
|
||||
// Spaces around equal sign
|
||||
assert!(comment_contains_code("# language =xml", &[]));
|
||||
assert!(comment_contains_code("# language= html", &[]));
|
||||
assert!(comment_contains_code("# language = RegExp", &[]));
|
||||
|
||||
// Leading whitespace
|
||||
assert!(!comment_contains_code("#language=CSS", &[]));
|
||||
assert!(!comment_contains_code("# \t language=C++", &[]));
|
||||
|
||||
// Human language false negatives
|
||||
assert!(!comment_contains_code("# language=en", &[]));
|
||||
assert!(!comment_contains_code("# language=en-US", &[]));
|
||||
|
||||
// Casing (fine because such IDs cannot be validated)
|
||||
assert!(!comment_contains_code("# language=PytHoN", &[]));
|
||||
assert!(!comment_contains_code("# language=jaVaScrIpt", &[]));
|
||||
|
||||
// Space within ID (fine because `Shell` is considered the ID)
|
||||
assert!(!comment_contains_code("# language=Shell Script", &[]));
|
||||
|
||||
// With prefix and/or suffix
|
||||
assert!(!comment_contains_code("# language=HTML prefix=<body>", &[]));
|
||||
assert!(!comment_contains_code(
|
||||
"# language=HTML prefix=<body> suffix=</body>",
|
||||
r"# language=Requirements suffix=\n",
|
||||
&[]
|
||||
));
|
||||
assert!(!comment_contains_code(
|
||||
"# language=ecma script level 4",
|
||||
"language=javascript prefix=(function(){ suffix=})()",
|
||||
&[]
|
||||
));
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ impl Violation for MissingReturnTypeClassMethod {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type)
|
||||
/// - [Typing spec: `Any`](https://typing.readthedocs.io/en/latest/spec/special-types.html#any)
|
||||
/// - [Python documentation: `typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
|
||||
/// - [Mypy documentation: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
|
||||
#[violation]
|
||||
|
||||
@@ -10,16 +10,21 @@ use crate::checkers::ast::Checker;
|
||||
use crate::registry::AsRule;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for imports of the`telnetlib` module.
|
||||
/// Checks for imports of the `telnetlib` module.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Telnet is considered insecure. Instead, use SSH or another encrypted
|
||||
/// Telnet is considered insecure. It is deprecated since version 3.11, and
|
||||
/// was removed in version 3.13. Instead, use SSH or another encrypted
|
||||
/// protocol.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// import telnetlib
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `telnetlib` - Telnet client](https://docs.python.org/3.12/library/telnetlib.html#module-telnetlib)
|
||||
/// - [PEP 594: `telnetlib`](https://peps.python.org/pep-0594/#telnetlib)
|
||||
#[violation]
|
||||
pub struct SuspiciousTelnetlibImport;
|
||||
|
||||
@@ -41,6 +46,9 @@ impl Violation for SuspiciousTelnetlibImport {
|
||||
/// ```python
|
||||
/// import ftplib
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `ftplib` - FTP protocol client](https://docs.python.org/3/library/ftplib.html)
|
||||
#[violation]
|
||||
pub struct SuspiciousFtplibImport;
|
||||
|
||||
@@ -63,8 +71,9 @@ impl Violation for SuspiciousFtplibImport {
|
||||
/// ```python
|
||||
/// import pickle
|
||||
/// ```
|
||||
/// /// ## References
|
||||
/// - [Python Docs](https://docs.python.org/3/library/pickle.html)
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `pickle` — Python object serialization](https://docs.python.org/3/library/pickle.html)
|
||||
#[violation]
|
||||
pub struct SuspiciousPickleImport;
|
||||
|
||||
|
||||
@@ -33,8 +33,8 @@ use ruff_text_size::Ranged;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Common Weakness Enumeration: CWE-22](https://cwe.mitre.org/data/definitions/22.html)
|
||||
/// - [Python Documentation: `TarFile.extractall`](https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall)
|
||||
/// - [Python Documentation: Extraction filters](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter)
|
||||
/// - [Python documentation: `TarFile.extractall`](https://docs.python.org/3/library/tarfile.html#tarfile.TarFile.extractall)
|
||||
/// - [Python documentation: Extraction filters](https://docs.python.org/3/library/tarfile.html#tarfile-extraction-filter)
|
||||
///
|
||||
/// [PEP 706]: https://peps.python.org/pep-0706/#backporting-forward-compatibility
|
||||
#[violation]
|
||||
|
||||
@@ -60,7 +60,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ## References
|
||||
/// - [Python documentation: The `try` statement](https://docs.python.org/3/reference/compound_stmts.html#the-try-statement)
|
||||
/// - [Python documentation: Exception hierarchy](https://docs.python.org/3/library/exceptions.html#exception-hierarchy)
|
||||
/// - [PEP8 Programming Recommendations on bare `except`](https://peps.python.org/pep-0008/#programming-recommendations)
|
||||
/// - [PEP 8: Programming Recommendations on bare `except`](https://peps.python.org/pep-0008/#programming-recommendations)
|
||||
#[violation]
|
||||
pub struct BlindExcept {
|
||||
name: String,
|
||||
|
||||
@@ -88,7 +88,7 @@ impl Violation for AbstractBaseClassWithoutAbstractMethod {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: abc](https://docs.python.org/3/library/abc.html)
|
||||
/// - [Python documentation: `abc`](https://docs.python.org/3/library/abc.html)
|
||||
#[violation]
|
||||
pub struct EmptyMethodWithoutAbstractDecorator {
|
||||
name: String,
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 257](https://peps.python.org/pep-0257/)
|
||||
/// - [PEP 257 – Docstring Conventions](https://peps.python.org/pep-0257/)
|
||||
/// - [Python documentation: Formatted string literals](https://docs.python.org/3/reference/lexical_analysis.html#f-strings)
|
||||
#[violation]
|
||||
pub struct FStringDocstring;
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::checkers::ast::Checker;
|
||||
///
|
||||
/// ## References
|
||||
/// - [The Hitchhiker's Guide to Python: Late Binding Closures](https://docs.python-guide.org/writing/gotchas/#late-binding-closures)
|
||||
/// - [Python documentation: functools.partial](https://docs.python.org/3/library/functools.html#functools.partial)
|
||||
/// - [Python documentation: `functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial)
|
||||
#[violation]
|
||||
pub struct FunctionUsesLoopVariable {
|
||||
name: String,
|
||||
|
||||
@@ -26,6 +26,9 @@ use crate::checkers::ast::Checker;
|
||||
/// ```python
|
||||
/// warnings.warn("This is a warning", stacklevel=2)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `warnings.warn`](https://docs.python.org/3/library/warnings.html#warnings.warn)
|
||||
#[violation]
|
||||
pub struct NoExplicitStacklevel;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: contextlib.suppress](https://docs.python.org/3/library/contextlib.html#contextlib.suppress)
|
||||
/// - [Python documentation: `contextlib.suppress`](https://docs.python.org/3/library/contextlib.html#contextlib.suppress)
|
||||
#[violation]
|
||||
pub struct UselessContextlibSuppress;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use ruff_diagnostics::Diagnostic;
|
||||
use ruff_diagnostics::Violation;
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::Parameter;
|
||||
use ruff_python_ast::{Expr, Parameter};
|
||||
use ruff_python_semantic::analyze::visibility::{is_overload, is_override};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
@@ -58,7 +58,7 @@ impl Violation for BuiltinArgumentShadowing {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let BuiltinArgumentShadowing { name } = self;
|
||||
format!("Argument `{name}` is shadowing a Python builtin")
|
||||
format!("Function argument `{name}` is shadowing a Python builtin")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,15 @@ pub(crate) fn builtin_argument_shadowing(checker: &mut Checker, parameter: &Para
|
||||
&checker.settings.flake8_builtins.builtins_ignorelist,
|
||||
checker.settings.target_version,
|
||||
) {
|
||||
// Ignore parameters in lambda expressions.
|
||||
// (That is the domain of A006.)
|
||||
if checker
|
||||
.semantic()
|
||||
.current_expression()
|
||||
.is_some_and(Expr::is_lambda_expr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Ignore `@override` and `@overload` decorated functions.
|
||||
if checker
|
||||
.semantic()
|
||||
|
||||
@@ -1,66 +1,58 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
|
||||
---
|
||||
A002.py:1:11: A002 Argument `str` is shadowing a Python builtin
|
||||
A002.py:1:11: A002 Function argument `str` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:19: A002 Argument `type` is shadowing a Python builtin
|
||||
A002.py:1:19: A002 Function argument `type` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:26: A002 Argument `complex` is shadowing a Python builtin
|
||||
A002.py:1:26: A002 Function argument `complex` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:35: A002 Argument `Exception` is shadowing a Python builtin
|
||||
A002.py:1:35: A002 Function argument `Exception` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^^^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:48: A002 Argument `getattr` is shadowing a Python builtin
|
||||
A002.py:1:48: A002 Function argument `getattr` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:5:17: A002 Argument `bytes` is shadowing a Python builtin
|
||||
A002.py:5:17: A002 Function argument `bytes` is shadowing a Python builtin
|
||||
|
|
||||
5 | async def func2(bytes):
|
||||
| ^^^^^ A002
|
||||
6 | pass
|
||||
|
|
||||
|
||||
A002.py:9:17: A002 Argument `id` is shadowing a Python builtin
|
||||
A002.py:9:17: A002 Function argument `id` is shadowing a Python builtin
|
||||
|
|
||||
9 | async def func3(id, dir):
|
||||
| ^^ A002
|
||||
10 | pass
|
||||
|
|
||||
|
||||
A002.py:9:21: A002 Argument `dir` is shadowing a Python builtin
|
||||
A002.py:9:21: A002 Function argument `dir` is shadowing a Python builtin
|
||||
|
|
||||
9 | async def func3(id, dir):
|
||||
| ^^^ A002
|
||||
10 | pass
|
||||
|
|
||||
|
||||
A002.py:13:16: A002 Argument `float` is shadowing a Python builtin
|
||||
|
|
||||
13 | map([], lambda float: ...)
|
||||
| ^^^^^ A002
|
||||
14 |
|
||||
15 | from typing import override, overload
|
||||
|
|
||||
|
||||
@@ -1,52 +1,44 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_builtins/mod.rs
|
||||
---
|
||||
A002.py:1:11: A002 Argument `str` is shadowing a Python builtin
|
||||
A002.py:1:11: A002 Function argument `str` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:19: A002 Argument `type` is shadowing a Python builtin
|
||||
A002.py:1:19: A002 Function argument `type` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:26: A002 Argument `complex` is shadowing a Python builtin
|
||||
A002.py:1:26: A002 Function argument `complex` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:35: A002 Argument `Exception` is shadowing a Python builtin
|
||||
A002.py:1:35: A002 Function argument `Exception` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^^^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:1:48: A002 Argument `getattr` is shadowing a Python builtin
|
||||
A002.py:1:48: A002 Function argument `getattr` is shadowing a Python builtin
|
||||
|
|
||||
1 | def func1(str, /, type, *complex, Exception, **getattr):
|
||||
| ^^^^^^^ A002
|
||||
2 | pass
|
||||
|
|
||||
|
||||
A002.py:5:17: A002 Argument `bytes` is shadowing a Python builtin
|
||||
A002.py:5:17: A002 Function argument `bytes` is shadowing a Python builtin
|
||||
|
|
||||
5 | async def func2(bytes):
|
||||
| ^^^^^ A002
|
||||
6 | pass
|
||||
|
|
||||
|
||||
A002.py:13:16: A002 Argument `float` is shadowing a Python builtin
|
||||
|
|
||||
13 | map([], lambda float: ...)
|
||||
| ^^^^^ A002
|
||||
14 |
|
||||
15 | from typing import override, overload
|
||||
|
|
||||
|
||||
@@ -61,4 +61,6 @@ A006.py:5:8: A006 Lambda argument `dir` is shadowing a Python builtin
|
||||
4 | lambda id: id
|
||||
5 | lambda dir: dir
|
||||
| ^^^ A006
|
||||
6 |
|
||||
7 | # Ok for A006 - should trigger A002 instead
|
||||
|
|
||||
|
||||
@@ -30,6 +30,9 @@ use crate::checkers::ast::Checker;
|
||||
/// dict.fromkeys(iterable)
|
||||
/// dict.fromkeys(iterable, 1)
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys)
|
||||
#[violation]
|
||||
pub struct UnnecessaryDictComprehensionForIterable {
|
||||
is_value_none_literal: bool,
|
||||
@@ -53,7 +56,7 @@ impl Violation for UnnecessaryDictComprehensionForIterable {
|
||||
}
|
||||
}
|
||||
|
||||
/// RUF025
|
||||
/// C420
|
||||
pub(crate) fn unnecessary_dict_comprehension_for_iterable(
|
||||
checker: &mut Checker,
|
||||
dict_comp: &ast::ExprDictComp,
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
|
||||
/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html)
|
||||
#[violation]
|
||||
pub struct FStringInGetTextFuncCall;
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
|
||||
/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html)
|
||||
#[violation]
|
||||
pub struct FormatInGetTextFuncCall;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use ruff_text_size::Ranged;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: gettext](https://docs.python.org/3/library/gettext.html)
|
||||
/// - [Python documentation: `gettext` — Multilingual internationalization services](https://docs.python.org/3/library/gettext.html)
|
||||
#[violation]
|
||||
pub struct PrintfInGetTextFuncCall;
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ impl AlwaysFixableViolation for LoggingWarn {
|
||||
///
|
||||
/// username = "Maria"
|
||||
///
|
||||
/// logging.info("Something happened", extra=dict(user=username))
|
||||
/// logging.info("Something happened", extra=dict(user_id=username))
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::Locator;
|
||||
///
|
||||
/// Directories that lack an `__init__.py` file can still be imported, but
|
||||
/// they're indicative of a special kind of package, known as a "namespace
|
||||
/// package" (see: [PEP 420](https://www.python.org/dev/peps/pep-0420/)).
|
||||
/// package" (see: [PEP 420](https://peps.python.org/pep-0420/)).
|
||||
/// Namespace packages are less widely used, so a package that lacks an
|
||||
/// `__init__.py` file is typically meant to be a regular package, and
|
||||
/// the absence of the `__init__.py` file is probably an oversight.
|
||||
|
||||
@@ -30,7 +30,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// The [typing documentation on stub files](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks)
|
||||
/// - [Typing documentation: Version and platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks)
|
||||
#[violation]
|
||||
pub struct ComplexIfStatementInStub;
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use ruff_python_ast::ExprStringLiteral;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use ruff_python_semantic::Definition;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
@@ -29,18 +31,40 @@ use crate::checkers::ast::Checker;
|
||||
#[violation]
|
||||
pub struct DocstringInStub;
|
||||
|
||||
impl Violation for DocstringInStub {
|
||||
impl AlwaysFixableViolation for DocstringInStub {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
"Docstrings should not be included in stubs".to_string()
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove docstring".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI021
|
||||
pub(crate) fn docstring_in_stubs(checker: &mut Checker, docstring: Option<&ExprStringLiteral>) {
|
||||
if let Some(docstr) = docstring {
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(DocstringInStub, docstr.range()));
|
||||
}
|
||||
pub(crate) fn docstring_in_stubs(
|
||||
checker: &mut Checker,
|
||||
definition: &Definition,
|
||||
docstring: Option<&ExprStringLiteral>,
|
||||
) {
|
||||
let Some(docstring_range) = docstring.map(ExprStringLiteral::range) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let statements = match definition {
|
||||
Definition::Module(module) => module.python_ast,
|
||||
Definition::Member(member) => member.body(),
|
||||
};
|
||||
|
||||
let edit = if statements.len() == 1 {
|
||||
Edit::range_replacement("...".to_string(), docstring_range)
|
||||
} else {
|
||||
Edit::range_deletion(docstring_range)
|
||||
};
|
||||
|
||||
let fix = Fix::unsafe_edit(edit);
|
||||
let diagnostic = Diagnostic::new(DocstringInStub, docstring_range).with_fix(fix);
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@ use std::collections::HashSet;
|
||||
|
||||
use rustc_hash::FxHashSet;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, violation};
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::Expr;
|
||||
use ruff_python_ast::{self as ast, Expr, ExprContext};
|
||||
use ruff_python_semantic::analyze::typing::traverse_literal;
|
||||
use ruff_text_size::Ranged;
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -27,6 +27,10 @@ use crate::checkers::ast::Checker;
|
||||
/// foo: Literal["a", "b"]
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// This rule's fix is marked as safe; however, the fix will flatten nested
|
||||
/// literals into a single top-level literal.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `typing.Literal`](https://docs.python.org/3/library/typing.html#typing.Literal)
|
||||
#[violation]
|
||||
@@ -34,24 +38,29 @@ pub struct DuplicateLiteralMember {
|
||||
duplicate_name: String,
|
||||
}
|
||||
|
||||
impl Violation for DuplicateLiteralMember {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
impl AlwaysFixableViolation for DuplicateLiteralMember {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Duplicate literal member `{}`", self.duplicate_name)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
"Remove duplicates".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// PYI062
|
||||
pub(crate) fn duplicate_literal_member<'a>(checker: &mut Checker, expr: &'a Expr) {
|
||||
let mut seen_nodes: HashSet<ComparableExpr<'_>, _> = FxHashSet::default();
|
||||
let mut unique_nodes: Vec<&Expr> = Vec::new();
|
||||
let mut diagnostics: Vec<Diagnostic> = Vec::new();
|
||||
|
||||
// Adds a member to `literal_exprs` if it is a `Literal` annotation
|
||||
let mut check_for_duplicate_members = |expr: &'a Expr, _: &'a Expr| {
|
||||
// If we've already seen this literal member, raise a violation.
|
||||
if !seen_nodes.insert(expr.into()) {
|
||||
if seen_nodes.insert(expr.into()) {
|
||||
unique_nodes.push(expr);
|
||||
} else {
|
||||
diagnostics.push(Diagnostic::new(
|
||||
DuplicateLiteralMember {
|
||||
duplicate_name: checker.generator().expr(expr),
|
||||
@@ -61,7 +70,36 @@ pub(crate) fn duplicate_literal_member<'a>(checker: &mut Checker, expr: &'a Expr
|
||||
}
|
||||
};
|
||||
|
||||
// Traverse the literal, collect all diagnostic members
|
||||
// Traverse the literal, collect all diagnostic members.
|
||||
traverse_literal(&mut check_for_duplicate_members, checker.semantic(), expr);
|
||||
|
||||
// If there's at least one diagnostic, create a fix to remove the duplicate members.
|
||||
if !diagnostics.is_empty() {
|
||||
if let Expr::Subscript(subscript) = expr {
|
||||
let subscript = Expr::Subscript(ast::ExprSubscript {
|
||||
slice: Box::new(if let [elt] = unique_nodes.as_slice() {
|
||||
(*elt).clone()
|
||||
} else {
|
||||
Expr::Tuple(ast::ExprTuple {
|
||||
elts: unique_nodes.into_iter().cloned().collect(),
|
||||
range: TextRange::default(),
|
||||
ctx: ExprContext::Load,
|
||||
parenthesized: false,
|
||||
})
|
||||
}),
|
||||
value: subscript.value.clone(),
|
||||
range: TextRange::default(),
|
||||
ctx: ExprContext::Load,
|
||||
});
|
||||
let fix = Fix::safe_edit(Edit::range_replacement(
|
||||
checker.generator().expr(&subscript),
|
||||
expr.range(),
|
||||
));
|
||||
for diagnostic in &mut diagnostics {
|
||||
diagnostic.set_fix(fix.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checker.diagnostics.append(&mut diagnostics);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [The recommended style for stub functions and methods](https://typing.readthedocs.io/en/latest/source/stubs.html#id6)
|
||||
/// in the typing docs.
|
||||
/// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html)
|
||||
#[violation]
|
||||
pub struct NonEmptyStubBody;
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ use crate::checkers::ast::Checker;
|
||||
/// def __iadd__(self, other: Foo) -> Self: ...
|
||||
/// ```
|
||||
/// ## References
|
||||
/// - [`typing.Self` documentation](https://docs.python.org/3/library/typing.html#typing.Self)
|
||||
/// - [Python documentation: `typing.Self`](https://docs.python.org/3/library/typing.html#typing.Self)
|
||||
#[violation]
|
||||
pub struct NonSelfReturnType {
|
||||
class_name: String,
|
||||
|
||||
@@ -23,8 +23,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// The [recommended style for functions and methods](https://typing.readthedocs.io/en/latest/source/stubs.html#functions-and-methods)
|
||||
/// in the typing docs.
|
||||
/// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html)
|
||||
#[violation]
|
||||
pub struct PassStatementStubBody;
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Static Typing with Python: Type Stubs](https://typing.readthedocs.io/en/latest/source/stubs.html)
|
||||
/// - [Typing documentation - Writing and Maintaining Stub Files](https://typing.readthedocs.io/en/latest/guides/writing_stubs.html)
|
||||
#[violation]
|
||||
pub struct QuotedAnnotationInStub;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [The typing specification](https://docs.python.org/3/library/numbers.html#the-numeric-tower)
|
||||
/// - [Python documentation: The numeric tower](https://docs.python.org/3/library/numbers.html#the-numeric-tower)
|
||||
/// - [PEP 484: The numeric tower](https://peps.python.org/pep-0484/#the-numeric-tower)
|
||||
///
|
||||
/// [typing specification]: https://typing.readthedocs.io/en/latest/spec/special-types.html#special-cases-for-float-and-complex
|
||||
|
||||
@@ -40,7 +40,7 @@ use crate::registry::Rule;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks)
|
||||
/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks)
|
||||
#[violation]
|
||||
pub struct UnrecognizedPlatformCheck;
|
||||
|
||||
@@ -74,7 +74,7 @@ impl Violation for UnrecognizedPlatformCheck {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks)
|
||||
/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks)
|
||||
#[violation]
|
||||
pub struct UnrecognizedPlatformName {
|
||||
platform: String,
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::registry::Rule;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks)
|
||||
/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks)
|
||||
#[violation]
|
||||
pub struct UnrecognizedVersionInfoCheck;
|
||||
|
||||
@@ -70,7 +70,7 @@ impl Violation for UnrecognizedVersionInfoCheck {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks)
|
||||
/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks)
|
||||
#[violation]
|
||||
pub struct PatchVersionComparison;
|
||||
|
||||
@@ -106,7 +106,7 @@ impl Violation for PatchVersionComparison {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [Typing stubs documentation: Version and Platform Checks](https://typing.readthedocs.io/en/latest/source/stubs.html#version-and-platform-checks)
|
||||
/// - [Typing documentation: Version and Platform checking](https://typing.readthedocs.io/en/latest/spec/directives.html#version-and-platform-checks)
|
||||
#[violation]
|
||||
pub struct WrongTupleLengthVersionComparison {
|
||||
expected_length: usize,
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
PYI021.pyi:1:1: PYI021 Docstrings should not be included in stubs
|
||||
PYI021.pyi:1:1: PYI021 [*] Docstrings should not be included in stubs
|
||||
|
|
||||
1 | """foo""" # ERROR PYI021
|
||||
| ^^^^^^^^^ PYI021
|
||||
2 |
|
||||
3 | def foo():
|
||||
|
|
||||
= help: Remove docstring
|
||||
|
||||
PYI021.pyi:4:5: PYI021 Docstrings should not be included in stubs
|
||||
ℹ Unsafe fix
|
||||
1 |-"""foo""" # ERROR PYI021
|
||||
1 |+ # ERROR PYI021
|
||||
2 2 |
|
||||
3 3 | def foo():
|
||||
4 4 | """foo""" # ERROR PYI021
|
||||
|
||||
PYI021.pyi:4:5: PYI021 [*] Docstrings should not be included in stubs
|
||||
|
|
||||
3 | def foo():
|
||||
4 | """foo""" # ERROR PYI021
|
||||
@@ -17,14 +26,81 @@ PYI021.pyi:4:5: PYI021 Docstrings should not be included in stubs
|
||||
5 |
|
||||
6 | class Bar:
|
||||
|
|
||||
= help: Remove docstring
|
||||
|
||||
PYI021.pyi:7:5: PYI021 Docstrings should not be included in stubs
|
||||
ℹ Unsafe fix
|
||||
1 1 | """foo""" # ERROR PYI021
|
||||
2 2 |
|
||||
3 3 | def foo():
|
||||
4 |- """foo""" # ERROR PYI021
|
||||
4 |+ ... # ERROR PYI021
|
||||
5 5 |
|
||||
6 6 | class Bar:
|
||||
7 7 | """bar""" # ERROR PYI021
|
||||
|
||||
PYI021.pyi:7:5: PYI021 [*] Docstrings should not be included in stubs
|
||||
|
|
||||
6 | class Bar:
|
||||
7 | """bar""" # ERROR PYI021
|
||||
| ^^^^^^^^^ PYI021
|
||||
8 |
|
||||
9 | def bar():
|
||||
9 | class Qux:
|
||||
|
|
||||
= help: Remove docstring
|
||||
|
||||
ℹ Unsafe fix
|
||||
4 4 | """foo""" # ERROR PYI021
|
||||
5 5 |
|
||||
6 6 | class Bar:
|
||||
7 |- """bar""" # ERROR PYI021
|
||||
7 |+ ... # ERROR PYI021
|
||||
8 8 |
|
||||
9 9 | class Qux:
|
||||
10 10 | """qux""" # ERROR PYI021
|
||||
|
||||
PYI021.pyi:10:5: PYI021 [*] Docstrings should not be included in stubs
|
||||
|
|
||||
9 | class Qux:
|
||||
10 | """qux""" # ERROR PYI021
|
||||
| ^^^^^^^^^ PYI021
|
||||
11 |
|
||||
12 | def __init__(self) -> None: ...
|
||||
|
|
||||
= help: Remove docstring
|
||||
|
||||
ℹ Unsafe fix
|
||||
7 7 | """bar""" # ERROR PYI021
|
||||
8 8 |
|
||||
9 9 | class Qux:
|
||||
10 |- """qux""" # ERROR PYI021
|
||||
10 |+ # ERROR PYI021
|
||||
11 11 |
|
||||
12 12 | def __init__(self) -> None: ...
|
||||
13 13 |
|
||||
|
||||
PYI021.pyi:15:5: PYI021 [*] Docstrings should not be included in stubs
|
||||
|
|
||||
14 | class Baz:
|
||||
15 | """Multiline docstring
|
||||
| _____^
|
||||
16 | |
|
||||
17 | | Lorem ipsum dolor sit amet
|
||||
18 | | """
|
||||
| |_______^ PYI021
|
||||
19 |
|
||||
20 | def __init__(self) -> None: ...
|
||||
|
|
||||
= help: Remove docstring
|
||||
|
||||
ℹ Unsafe fix
|
||||
12 12 | def __init__(self) -> None: ...
|
||||
13 13 |
|
||||
14 14 | class Baz:
|
||||
15 |- """Multiline docstring
|
||||
16 |-
|
||||
17 |- Lorem ipsum dolor sit amet
|
||||
18 |- """
|
||||
15 |+
|
||||
19 16 |
|
||||
20 17 | def __init__(self) -> None: ...
|
||||
21 18 |
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI062.py:5:25: PYI062 Duplicate literal member `True`
|
||||
PYI062.py:5:25: PYI062 [*] Duplicate literal member `True`
|
||||
|
|
||||
3 | import typing_extensions
|
||||
4 |
|
||||
@@ -10,8 +10,19 @@ PYI062.py:5:25: PYI062 Duplicate literal member `True`
|
||||
6 |
|
||||
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:5:31: PYI062 Duplicate literal member `False`
|
||||
ℹ Safe fix
|
||||
2 2 | import typing as t
|
||||
3 3 | import typing_extensions
|
||||
4 4 |
|
||||
5 |-x: Literal[True, False, True, False] # PYI062 twice here
|
||||
5 |+x: Literal[True, False] # PYI062 twice here
|
||||
6 6 |
|
||||
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
8 8 |
|
||||
|
||||
PYI062.py:5:31: PYI062 [*] Duplicate literal member `False`
|
||||
|
|
||||
3 | import typing_extensions
|
||||
4 |
|
||||
@@ -20,8 +31,19 @@ PYI062.py:5:31: PYI062 Duplicate literal member `False`
|
||||
6 |
|
||||
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:7:45: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
2 2 | import typing as t
|
||||
3 3 | import typing_extensions
|
||||
4 4 |
|
||||
5 |-x: Literal[True, False, True, False] # PYI062 twice here
|
||||
5 |+x: Literal[True, False] # PYI062 twice here
|
||||
6 6 |
|
||||
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
8 8 |
|
||||
|
||||
PYI062.py:7:45: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
5 | x: Literal[True, False, True, False] # PYI062 twice here
|
||||
6 |
|
||||
@@ -30,8 +52,19 @@ PYI062.py:7:45: PYI062 Duplicate literal member `1`
|
||||
8 |
|
||||
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | x: Literal[True, False, True, False] # PYI062 twice here
|
||||
6 6 |
|
||||
7 |-y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
7 |+y: Literal[1, print("hello"), 3, 4] # PYI062 on the last 1
|
||||
8 8 |
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
10 10 |
|
||||
|
||||
PYI062.py:9:33: PYI062 [*] Duplicate literal member `{1, 3, 5}`
|
||||
|
|
||||
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
8 |
|
||||
@@ -40,8 +73,19 @@ PYI062.py:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
|
||||
10 |
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:11:20: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PYI062 on the last 1
|
||||
8 8 |
|
||||
9 |-z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
9 |+z: Literal[{1, 3, 5}, "foobar"] # PYI062 on the set literal
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
|
||||
PYI062.py:11:20: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
10 |
|
||||
@@ -50,8 +94,19 @@ PYI062.py:11:20: PYI062 Duplicate literal member `1`
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:12:23: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
8 8 |
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
10 10 |
|
||||
11 |-Literal[1, Literal[1]] # once
|
||||
11 |+Literal[1] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
|
||||
PYI062.py:12:23: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -59,8 +114,19 @@ PYI062.py:12:23: PYI062 Duplicate literal member `1`
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:12:26: PYI062 Duplicate literal member `2`
|
||||
ℹ Safe fix
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 |-Literal[1, 2, Literal[1, 2]] # twice
|
||||
12 |+Literal[1, 2] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
||||
PYI062.py:12:26: PYI062 [*] Duplicate literal member `2`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -68,8 +134,19 @@ PYI062.py:12:26: PYI062 Duplicate literal member `2`
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:13:20: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PYI062 on the set literal
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 |-Literal[1, 2, Literal[1, 2]] # twice
|
||||
12 |+Literal[1, 2] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
||||
PYI062.py:13:20: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -78,8 +155,19 @@ PYI062.py:13:20: PYI062 Duplicate literal member `1`
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:13:32: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 |-Literal[1, Literal[1], Literal[1]] # twice
|
||||
13 |+Literal[1] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
||||
PYI062.py:13:32: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -88,8 +176,19 @@ PYI062.py:13:32: PYI062 Duplicate literal member `1`
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:14:32: PYI062 Duplicate literal member `2`
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 |-Literal[1, Literal[1], Literal[1]] # twice
|
||||
13 |+Literal[1] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
||||
PYI062.py:14:32: PYI062 [*] Duplicate literal member `2`
|
||||
|
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
@@ -98,8 +197,19 @@ PYI062.py:14:32: PYI062 Duplicate literal member `2`
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:15:37: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 |-Literal[1, Literal[2], Literal[2]] # once
|
||||
14 |+Literal[1, 2] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
17 17 |
|
||||
|
||||
PYI062.py:15:37: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
@@ -107,8 +217,19 @@ PYI062.py:15:37: PYI062 Duplicate literal member `1`
|
||||
| ^ PYI062
|
||||
16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:16:30: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 |-t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
15 |+t.Literal[1, 2] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
|
||||
PYI062.py:16:30: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
@@ -117,8 +238,19 @@ PYI062.py:16:30: PYI062 Duplicate literal member `1`
|
||||
17 |
|
||||
18 | # Ensure issue is only raised once, even on nested literals
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:16:33: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 |-typing_extensions.Literal[1, 1, 1] # twice
|
||||
16 |+typing_extensions.Literal[1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
|
||||
PYI062.py:16:33: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
@@ -127,8 +259,19 @@ PYI062.py:16:33: PYI062 Duplicate literal member `1`
|
||||
17 |
|
||||
18 | # Ensure issue is only raised once, even on nested literals
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.py:19:46: PYI062 Duplicate literal member `True`
|
||||
ℹ Safe fix
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 |-typing_extensions.Literal[1, 1, 1] # twice
|
||||
16 |+typing_extensions.Literal[1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
|
||||
PYI062.py:19:46: PYI062 [*] Duplicate literal member `True`
|
||||
|
|
||||
18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
@@ -136,3 +279,13 @@ PYI062.py:19:46: PYI062 Duplicate literal member `True`
|
||||
20 |
|
||||
21 | n: Literal["No", "duplicates", "here", 1, "1"]
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 |-MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
19 |+MyType = Literal["foo", True, False, "bar"] # PYI062
|
||||
20 20 |
|
||||
21 21 | n: Literal["No", "duplicates", "here", 1, "1"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI062.pyi:5:25: PYI062 Duplicate literal member `True`
|
||||
PYI062.pyi:5:25: PYI062 [*] Duplicate literal member `True`
|
||||
|
|
||||
3 | import typing_extensions
|
||||
4 |
|
||||
@@ -10,8 +10,19 @@ PYI062.pyi:5:25: PYI062 Duplicate literal member `True`
|
||||
6 |
|
||||
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:5:31: PYI062 Duplicate literal member `False`
|
||||
ℹ Safe fix
|
||||
2 2 | import typing as t
|
||||
3 3 | import typing_extensions
|
||||
4 4 |
|
||||
5 |-x: Literal[True, False, True, False] # PY062 twice here
|
||||
5 |+x: Literal[True, False] # PY062 twice here
|
||||
6 6 |
|
||||
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
8 8 |
|
||||
|
||||
PYI062.pyi:5:31: PYI062 [*] Duplicate literal member `False`
|
||||
|
|
||||
3 | import typing_extensions
|
||||
4 |
|
||||
@@ -20,8 +31,19 @@ PYI062.pyi:5:31: PYI062 Duplicate literal member `False`
|
||||
6 |
|
||||
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:7:45: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
2 2 | import typing as t
|
||||
3 3 | import typing_extensions
|
||||
4 4 |
|
||||
5 |-x: Literal[True, False, True, False] # PY062 twice here
|
||||
5 |+x: Literal[True, False] # PY062 twice here
|
||||
6 6 |
|
||||
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
8 8 |
|
||||
|
||||
PYI062.pyi:7:45: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
5 | x: Literal[True, False, True, False] # PY062 twice here
|
||||
6 |
|
||||
@@ -30,8 +52,19 @@ PYI062.pyi:7:45: PYI062 Duplicate literal member `1`
|
||||
8 |
|
||||
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | x: Literal[True, False, True, False] # PY062 twice here
|
||||
6 6 |
|
||||
7 |-y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
7 |+y: Literal[1, print("hello"), 3, 4] # PY062 on the last 1
|
||||
8 8 |
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
10 10 |
|
||||
|
||||
PYI062.pyi:9:33: PYI062 [*] Duplicate literal member `{1, 3, 5}`
|
||||
|
|
||||
7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
8 |
|
||||
@@ -40,8 +73,19 @@ PYI062.pyi:9:33: PYI062 Duplicate literal member `{1, 3, 5}`
|
||||
10 |
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:11:20: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
7 7 | y: Literal[1, print("hello"), 3, Literal[4, 1]] # PY062 on the last 1
|
||||
8 8 |
|
||||
9 |-z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
9 |+z: Literal[{1, 3, 5}, "foobar"] # PY062 on the set literal
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
|
||||
PYI062.pyi:11:20: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
10 |
|
||||
@@ -50,8 +94,19 @@ PYI062.pyi:11:20: PYI062 Duplicate literal member `1`
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:12:23: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
8 8 |
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
10 10 |
|
||||
11 |-Literal[1, Literal[1]] # once
|
||||
11 |+Literal[1] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
|
||||
PYI062.pyi:12:23: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -59,8 +114,19 @@ PYI062.pyi:12:23: PYI062 Duplicate literal member `1`
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:12:26: PYI062 Duplicate literal member `2`
|
||||
ℹ Safe fix
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 |-Literal[1, 2, Literal[1, 2]] # twice
|
||||
12 |+Literal[1, 2] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
||||
PYI062.pyi:12:26: PYI062 [*] Duplicate literal member `2`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -68,8 +134,19 @@ PYI062.pyi:12:26: PYI062 Duplicate literal member `2`
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:13:20: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
9 9 | z: Literal[{1, 3, 5}, "foobar", {1,3,5}] # PY062 on the set literal
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 |-Literal[1, 2, Literal[1, 2]] # twice
|
||||
12 |+Literal[1, 2] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
||||
PYI062.pyi:13:20: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -78,8 +155,19 @@ PYI062.pyi:13:20: PYI062 Duplicate literal member `1`
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:13:32: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 |-Literal[1, Literal[1], Literal[1]] # twice
|
||||
13 |+Literal[1] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
||||
PYI062.pyi:13:32: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
11 | Literal[1, Literal[1]] # once
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
@@ -88,8 +176,19 @@ PYI062.pyi:13:32: PYI062 Duplicate literal member `1`
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:14:32: PYI062 Duplicate literal member `2`
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 |-Literal[1, Literal[1], Literal[1]] # twice
|
||||
13 |+Literal[1] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
||||
PYI062.pyi:14:32: PYI062 [*] Duplicate literal member `2`
|
||||
|
|
||||
12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
@@ -98,8 +197,19 @@ PYI062.pyi:14:32: PYI062 Duplicate literal member `2`
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:15:37: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
11 11 | Literal[1, Literal[1]] # once
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 |-Literal[1, Literal[2], Literal[2]] # once
|
||||
14 |+Literal[1, 2] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
17 17 |
|
||||
|
||||
PYI062.pyi:15:37: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
@@ -107,8 +217,19 @@ PYI062.pyi:15:37: PYI062 Duplicate literal member `1`
|
||||
| ^ PYI062
|
||||
16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:16:30: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
12 12 | Literal[1, 2, Literal[1, 2]] # twice
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 |-t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
15 |+t.Literal[1, 2] # once
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
|
||||
PYI062.pyi:16:30: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
@@ -117,8 +238,19 @@ PYI062.pyi:16:30: PYI062 Duplicate literal member `1`
|
||||
17 |
|
||||
18 | # Ensure issue is only raised once, even on nested literals
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:16:33: PYI062 Duplicate literal member `1`
|
||||
ℹ Safe fix
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 |-typing_extensions.Literal[1, 1, 1] # twice
|
||||
16 |+typing_extensions.Literal[1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
|
||||
PYI062.pyi:16:33: PYI062 [*] Duplicate literal member `1`
|
||||
|
|
||||
14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
@@ -127,8 +259,19 @@ PYI062.pyi:16:33: PYI062 Duplicate literal member `1`
|
||||
17 |
|
||||
18 | # Ensure issue is only raised once, even on nested literals
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
PYI062.pyi:19:46: PYI062 Duplicate literal member `True`
|
||||
ℹ Safe fix
|
||||
13 13 | Literal[1, Literal[1], Literal[1]] # twice
|
||||
14 14 | Literal[1, Literal[2], Literal[2]] # once
|
||||
15 15 | t.Literal[1, t.Literal[2, t.Literal[1]]] # once
|
||||
16 |-typing_extensions.Literal[1, 1, 1] # twice
|
||||
16 |+typing_extensions.Literal[1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
|
||||
PYI062.pyi:19:46: PYI062 [*] Duplicate literal member `True`
|
||||
|
|
||||
18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 | MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
@@ -136,3 +279,13 @@ PYI062.pyi:19:46: PYI062 Duplicate literal member `True`
|
||||
20 |
|
||||
21 | n: Literal["No", "duplicates", "here", 1, "1"]
|
||||
|
|
||||
= help: Remove duplicates
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | typing_extensions.Literal[1, 1, 1] # twice
|
||||
17 17 |
|
||||
18 18 | # Ensure issue is only raised once, even on nested literals
|
||||
19 |-MyType = Literal["foo", Literal[True, False, True], "bar"] # PYI062
|
||||
19 |+MyType = Literal["foo", True, False, "bar"] # PYI062
|
||||
20 20 |
|
||||
21 21 | n: Literal["No", "duplicates", "here", 1, "1"]
|
||||
|
||||
@@ -590,7 +590,7 @@ impl AlwaysFixableViolation for PytestErroneousUseFixturesOnFixture {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [`pytest-asyncio`](https://pypi.org/project/pytest-asyncio/)
|
||||
/// - [PyPI: `pytest-asyncio`](https://pypi.org/project/pytest-asyncio/)
|
||||
#[violation]
|
||||
pub struct PytestUnnecessaryAsyncioMarkOnFixture;
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ use ruff_text_size::Ranged;
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `unittest.mock.patch`](https://docs.python.org/3/library/unittest.mock.html#unittest.mock.patch)
|
||||
/// - [`pytest-mock`](https://pypi.org/project/pytest-mock/)
|
||||
/// - [PyPI: `pytest-mock`](https://pypi.org/project/pytest-mock/)
|
||||
#[violation]
|
||||
pub struct PytestPatchWithLambda;
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ use crate::fix;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
#[violation]
|
||||
pub struct EmptyTypeCheckingBlock;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ use crate::rules::flake8_type_checking::imports::ImportBinding;
|
||||
/// - `lint.flake8-type-checking.quote-annotations`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 535](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
#[violation]
|
||||
pub struct RuntimeImportInTypeCheckingBlock {
|
||||
qualified_name: String,
|
||||
|
||||
@@ -37,8 +37,8 @@ use crate::checkers::ast::Checker;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 535](https://peps.python.org/pep-0563/)
|
||||
/// - [PEP 604](https://peps.python.org/pep-0604/)
|
||||
/// - [PEP 563 - Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/)
|
||||
/// - [PEP 604 – Allow writing union types as `X | Y`](https://peps.python.org/pep-0604/)
|
||||
///
|
||||
/// [PEP 604]: https://peps.python.org/pep-0604/
|
||||
#[violation]
|
||||
|
||||
@@ -71,7 +71,7 @@ use crate::rules::isort::{categorize, ImportSection, ImportType};
|
||||
/// - `lint.typing-modules`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
#[violation]
|
||||
pub struct TypingOnlyFirstPartyImport {
|
||||
qualified_name: String,
|
||||
@@ -146,7 +146,7 @@ impl Violation for TypingOnlyFirstPartyImport {
|
||||
/// - `lint.typing-modules`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
#[violation]
|
||||
pub struct TypingOnlyThirdPartyImport {
|
||||
qualified_name: String,
|
||||
@@ -221,7 +221,7 @@ impl Violation for TypingOnlyThirdPartyImport {
|
||||
/// - `lint.typing-modules`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 536](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
/// - [PEP 563: Runtime annotation resolution and `TYPE_CHECKING`](https://peps.python.org/pep-0563/#runtime-annotation-resolution-and-type-checking)
|
||||
#[violation]
|
||||
pub struct TypingOnlyStandardLibraryImport {
|
||||
qualified_name: String,
|
||||
|
||||
@@ -19,6 +19,10 @@ use crate::registry::Rule;
|
||||
/// An argument that is defined but not used is likely a mistake, and should
|
||||
/// be removed to avoid confusion.
|
||||
///
|
||||
/// If a variable is intentionally defined-but-not-used, it should be
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def foo(bar, baz):
|
||||
@@ -30,6 +34,9 @@ use crate::registry::Rule;
|
||||
/// def foo(bar):
|
||||
/// return bar * 2
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
#[violation]
|
||||
pub struct UnusedFunctionArgument {
|
||||
name: String,
|
||||
@@ -50,6 +57,10 @@ impl Violation for UnusedFunctionArgument {
|
||||
/// An argument that is defined but not used is likely a mistake, and should
|
||||
/// be removed to avoid confusion.
|
||||
///
|
||||
/// If a variable is intentionally defined-but-not-used, it should be
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -63,6 +74,9 @@ impl Violation for UnusedFunctionArgument {
|
||||
/// def foo(self, arg1):
|
||||
/// print(arg1)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
#[violation]
|
||||
pub struct UnusedMethodArgument {
|
||||
name: String,
|
||||
@@ -83,6 +97,10 @@ impl Violation for UnusedMethodArgument {
|
||||
/// An argument that is defined but not used is likely a mistake, and should
|
||||
/// be removed to avoid confusion.
|
||||
///
|
||||
/// If a variable is intentionally defined-but-not-used, it should be
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -98,6 +116,9 @@ impl Violation for UnusedMethodArgument {
|
||||
/// def foo(cls, arg1):
|
||||
/// print(arg1)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
#[violation]
|
||||
pub struct UnusedClassMethodArgument {
|
||||
name: String,
|
||||
@@ -118,6 +139,10 @@ impl Violation for UnusedClassMethodArgument {
|
||||
/// An argument that is defined but not used is likely a mistake, and should
|
||||
/// be removed to avoid confusion.
|
||||
///
|
||||
/// If a variable is intentionally defined-but-not-used, it should be
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -133,6 +158,9 @@ impl Violation for UnusedClassMethodArgument {
|
||||
/// def foo(arg1):
|
||||
/// print(arg1)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
#[violation]
|
||||
pub struct UnusedStaticMethodArgument {
|
||||
name: String,
|
||||
@@ -154,6 +182,10 @@ impl Violation for UnusedStaticMethodArgument {
|
||||
/// An argument that is defined but not used is likely a mistake, and should
|
||||
/// be removed to avoid confusion.
|
||||
///
|
||||
/// If a variable is intentionally defined-but-not-used, it should be
|
||||
/// prefixed with an underscore, or some other value that adheres to the
|
||||
/// [`lint.dummy-variable-rgx`] pattern.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// my_list = [1, 2, 3, 4, 5]
|
||||
@@ -165,6 +197,9 @@ impl Violation for UnusedStaticMethodArgument {
|
||||
/// my_list = [1, 2, 3, 4, 5]
|
||||
/// squares = map(lambda x: x**2, my_list)
|
||||
/// ```
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.dummy-variable-rgx`
|
||||
#[violation]
|
||||
pub struct UnusedLambdaArgument {
|
||||
name: String,
|
||||
|
||||
@@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getatime`](https://docs.python.org/3/library/os.path.html#os.path.getatime)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
|
||||
@@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getctime`](https://docs.python.org/3/library/os.path.html#os.path.getctime)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
|
||||
@@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getmtime`](https://docs.python.org/3/library/os.path.html#os.path.getmtime)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
|
||||
@@ -32,7 +32,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.stat`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.stat)
|
||||
/// - [Python documentation: `os.path.getsize`](https://docs.python.org/3/library/os.path.html#os.path.getsize)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
|
||||
@@ -30,7 +30,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.resolve`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.resolve)
|
||||
/// - [Python documentation: `os.path.abspath`](https://docs.python.org/3/library/os.path.html#os.path.abspath)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -73,7 +73,7 @@ impl Violation for OsPathAbspath {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.chmod`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.chmod)
|
||||
/// - [Python documentation: `os.chmod`](https://docs.python.org/3/library/os.html#os.chmod)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -116,7 +116,7 @@ impl Violation for OsChmod {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
|
||||
/// - [Python documentation: `os.makedirs`](https://docs.python.org/3/library/os.html#os.makedirs)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -159,7 +159,7 @@ impl Violation for OsMakedirs {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.mkdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.mkdir)
|
||||
/// - [Python documentation: `os.mkdir`](https://docs.python.org/3/library/os.html#os.mkdir)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -202,7 +202,7 @@ impl Violation for OsMkdir {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.rename`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rename)
|
||||
/// - [Python documentation: `os.rename`](https://docs.python.org/3/library/os.html#os.rename)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -245,7 +245,7 @@ impl Violation for OsRename {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.replace`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.replace)
|
||||
/// - [Python documentation: `os.replace`](https://docs.python.org/3/library/os.html#os.replace)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -288,7 +288,7 @@ impl Violation for OsReplace {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.rmdir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.rmdir)
|
||||
/// - [Python documentation: `os.rmdir`](https://docs.python.org/3/library/os.html#os.rmdir)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -331,7 +331,7 @@ impl Violation for OsRmdir {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
|
||||
/// - [Python documentation: `os.remove`](https://docs.python.org/3/library/os.html#os.remove)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -374,7 +374,7 @@ impl Violation for OsRemove {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.unlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.unlink)
|
||||
/// - [Python documentation: `os.unlink`](https://docs.python.org/3/library/os.html#os.unlink)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -418,7 +418,7 @@ impl Violation for OsUnlink {
|
||||
/// - [Python documentation: `Path.cwd`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.cwd)
|
||||
/// - [Python documentation: `os.getcwd`](https://docs.python.org/3/library/os.html#os.getcwd)
|
||||
/// - [Python documentation: `os.getcwdb`](https://docs.python.org/3/library/os.html#os.getcwdb)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -461,7 +461,7 @@ impl Violation for OsGetcwd {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.exists`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.exists)
|
||||
/// - [Python documentation: `os.path.exists`](https://docs.python.org/3/library/os.path.html#os.path.exists)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -504,7 +504,7 @@ impl Violation for OsPathExists {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.expanduser`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.expanduser)
|
||||
/// - [Python documentation: `os.path.expanduser`](https://docs.python.org/3/library/os.path.html#os.path.expanduser)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -547,7 +547,7 @@ impl Violation for OsPathExpanduser {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.is_dir`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_dir)
|
||||
/// - [Python documentation: `os.path.isdir`](https://docs.python.org/3/library/os.path.html#os.path.isdir)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -590,7 +590,7 @@ impl Violation for OsPathIsdir {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.is_file`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_file)
|
||||
/// - [Python documentation: `os.path.isfile`](https://docs.python.org/3/library/os.path.html#os.path.isfile)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -633,7 +633,7 @@ impl Violation for OsPathIsfile {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.is_symlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.is_symlink)
|
||||
/// - [Python documentation: `os.path.islink`](https://docs.python.org/3/library/os.path.html#os.path.islink)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -676,7 +676,7 @@ impl Violation for OsPathIslink {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.readlink`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.readline)
|
||||
/// - [Python documentation: `os.readlink`](https://docs.python.org/3/library/os.html#os.readlink)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -728,7 +728,7 @@ impl Violation for OsReadlink {
|
||||
/// - [Python documentation: `Path.group`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.group)
|
||||
/// - [Python documentation: `Path.owner`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.owner)
|
||||
/// - [Python documentation: `os.stat`](https://docs.python.org/3/library/os.html#os.stat)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -774,7 +774,7 @@ impl Violation for OsStat {
|
||||
/// ## References
|
||||
/// - [Python documentation: `PurePath.is_absolute`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.is_absolute)
|
||||
/// - [Python documentation: `os.path.isabs`](https://docs.python.org/3/library/os.path.html#os.path.isabs)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -817,7 +817,7 @@ impl Violation for OsPathIsabs {
|
||||
/// ## References
|
||||
/// - [Python documentation: `PurePath.joinpath`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.joinpath)
|
||||
/// - [Python documentation: `os.path.join`](https://docs.python.org/3/library/os.path.html#os.path.join)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -877,7 +877,7 @@ pub(crate) enum Joiner {
|
||||
/// ## References
|
||||
/// - [Python documentation: `PurePath.name`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.name)
|
||||
/// - [Python documentation: `os.path.basename`](https://docs.python.org/3/library/os.path.html#os.path.basename)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -920,7 +920,7 @@ impl Violation for OsPathBasename {
|
||||
/// ## References
|
||||
/// - [Python documentation: `PurePath.parent`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.parent)
|
||||
/// - [Python documentation: `os.path.dirname`](https://docs.python.org/3/library/os.path.html#os.path.dirname)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -963,7 +963,7 @@ impl Violation for OsPathDirname {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.samefile`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.samefile)
|
||||
/// - [Python documentation: `os.path.samefile`](https://docs.python.org/3/library/os.path.html#os.path.samefile)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -1015,7 +1015,7 @@ impl Violation for OsPathSamefile {
|
||||
/// - [Python documentation: `Path.suffix`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix)
|
||||
/// - [Python documentation: `Path.suffixes`](https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffixes)
|
||||
/// - [Python documentation: `os.path.splitext`](https://docs.python.org/3/library/os.path.html#os.path.splitext)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
@@ -1055,7 +1055,7 @@ impl Violation for OsPathSplitext {
|
||||
/// ## References
|
||||
/// - [Python documentation: `Path.open`](https://docs.python.org/3/library/pathlib.html#pathlib.Path.open)
|
||||
/// - [Python documentation: `open`](https://docs.python.org/3/library/functions.html#open)
|
||||
/// - [PEP 428](https://peps.python.org/pep-0428/)
|
||||
/// - [PEP 428 – The pathlib module – object-oriented filesystem paths](https://peps.python.org/pep-0428/)
|
||||
/// - [Correspondence between `os` and `pathlib`](https://docs.python.org/3/library/pathlib.html#correspondence-to-tools-in-the-os-module)
|
||||
/// - [Why you should be using pathlib](https://treyhunner.com/2018/12/why-you-should-be-using-pathlib/)
|
||||
/// - [No really, pathlib is great](https://treyhunner.com/2019/01/no-really-pathlib-is-great/)
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::Locator;
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [_Why You Should Probably Never Use pandas inplace=True_](https://towardsdatascience.com/why-you-should-probably-never-use-pandas-inplace-true-9f9f211849e4)
|
||||
/// - [_Why You Should Probably Never Use pandas `inplace=True`_](https://towardsdatascience.com/why-you-should-probably-never-use-pandas-inplace-true-9f9f211849e4)
|
||||
#[violation]
|
||||
pub struct PandasUseOfInplaceArgument;
|
||||
|
||||
|
||||
@@ -35,10 +35,10 @@ use crate::rules::pep8_naming::helpers;
|
||||
/// from example import MyClassName
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
///
|
||||
/// ## Options
|
||||
/// - `lint.flake8-import-conventions.aliases`
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/
|
||||
#[violation]
|
||||
pub struct CamelcaseImportedAsAcronym {
|
||||
name: String,
|
||||
|
||||
@@ -61,7 +61,7 @@ const BLANK_LINES_NESTED_LEVEL: u32 = 1;
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E301.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
@@ -114,7 +114,7 @@ impl AlwaysFixableViolation for BlankLineBetweenMethods {
|
||||
/// - `lint.isort.lines-after-imports`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E302.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
@@ -181,7 +181,7 @@ impl AlwaysFixableViolation for BlankLinesTopLevel {
|
||||
/// - `lint.isort.lines-between-types`
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E303.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
@@ -228,7 +228,7 @@ impl AlwaysFixableViolation for TooManyBlankLines {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E304.html)
|
||||
#[violation]
|
||||
pub struct BlankLineAfterDecorator {
|
||||
@@ -278,7 +278,7 @@ impl AlwaysFixableViolation for BlankLineAfterDecorator {
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E305.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
@@ -332,7 +332,7 @@ impl AlwaysFixableViolation for BlankLinesAfterFunctionOrClass {
|
||||
/// them. That's why this rule is not enabled in typing stub files.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 8](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [PEP 8: Blank Lines](https://peps.python.org/pep-0008/#blank-lines)
|
||||
/// - [Flake 8 rule](https://www.flake8rules.com/rules/E306.html)
|
||||
/// - [Typing Style Guide](https://typing.readthedocs.io/en/latest/source/stubs.html#blank-lines)
|
||||
#[violation]
|
||||
|
||||
@@ -52,7 +52,7 @@ use crate::Locator;
|
||||
/// See [#10885](https://github.com/astral-sh/ruff/issues/10885) for more.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 498](https://www.python.org/dev/peps/pep-0498/)
|
||||
/// - [PEP 498 – Literal String Interpolation](https://peps.python.org/pep-0498/)
|
||||
#[violation]
|
||||
pub struct FStringMissingPlaceholders;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 563](https://www.python.org/dev/peps/pep-0563/)
|
||||
/// - [PEP 563 – Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/)
|
||||
#[violation]
|
||||
pub struct ForwardAnnotationSyntaxError {
|
||||
pub body: String,
|
||||
|
||||
@@ -8,7 +8,7 @@ use ruff_text_size::Ranged;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for `if statements that use non-empty tuples as test conditions.
|
||||
/// Checks for `if` statements that use non-empty tuples as test conditions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Non-empty tuples are always `True`, so an `if` statement with a non-empty
|
||||
|
||||
@@ -13,6 +13,9 @@ use ruff_macros::{derive_message_formats, violation};
|
||||
/// In Python 3, no more than 1 << 8 assignments are allowed before a starred
|
||||
/// expression, and no more than 1 << 24 expressions are allowed after a starred
|
||||
/// expression.
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 3132 – Extended Iterable Unpacking](https://peps.python.org/pep-3132/)
|
||||
#[violation]
|
||||
pub struct ExpressionsInStarAssignment;
|
||||
|
||||
@@ -38,7 +41,7 @@ impl Violation for ExpressionsInStarAssignment {
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// - [PEP 3132](https://peps.python.org/pep-3132/)
|
||||
/// - [PEP 3132 – Extended Iterable Unpacking](https://peps.python.org/pep-3132/)
|
||||
#[violation]
|
||||
pub struct MultipleStarredExpressions;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user