Compare commits

...

27 Commits

Author SHA1 Message Date
Dhruv Manilawala
fbf140a665 Bump version to 0.7.3 (#14197) 2024-11-08 16:39:37 +05:30
David Peter
670f958525 [red-knot] Fix intersection simplification for ~Any/~Unknown (#14195)
## Summary

Another bug found using [property
testing](https://github.com/astral-sh/ruff/pull/14178).

## Test Plan

New unit test
2024-11-08 10:54:13 +01:00
David Peter
fed35a25e8 [red-knot] Fix is_assignable_to for unions (#14196)
## Summary

Fix `Type::is_assignable_to` for union types on the left hand side (of
`.is_assignable_to`; or the right hand side of the `… = …` assignment):

`Literal[1, 2]` should be assignable to `int`.

## Test Plan

New unit tests that were previously failing.
2024-11-08 10:53:48 +01:00
Simon Brugman
d1ef418bb0 Docs: tweak rules documentation (#14180)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-08 09:01:53 +00:00
Charlie Marsh
272d24bf3e [flake8-pyi] Add a fix for duplicate-literal-member (#14188)
## Summary

Closes https://github.com/astral-sh/ruff/issues/14187.
2024-11-08 03:45:19 +00:00
David Peter
2624249219 [red-knot] Minor: fix Literal[True] <: int (#14177)
## Summary

Minor fix to `Type::is_subtype_of` to make sure that Boolean literals
are subtypes of `int`, to match runtime semantics.

Found this while doing some property-testing experiments [1].

[1] https://github.com/astral-sh/ruff/pull/14178

## Test Plan

New unit test.
2024-11-07 23:23:35 +01:00
Alex Waygood
4b08d17088 [red-knot] Add a new Type::KnownInstanceType variant (#14155)
## Summary

Fixes #14114. I don't think I can really describe the problems with our
current architecture (and therefore the motivations for this PR) any
better than @carljm did in that issue, so I'll just copy it out here!

---

We currently represent "known instances" (e.g. special forms like
`typing.Literal`, which are an instance of `typing._SpecialForm`, but
need to be handled differently from other instances of
`typing._SpecialForm`) as an `InstanceType` with a `known` field that is
`Some(...)`.

This makes it easy to handle a known instance as if it were a regular
instance type (by ignoring the `known` field), and in some cases (e.g.
`Type::member`) that is correct and convenient. But in other cases (e.g.
`Type::is_equivalent_to`) it is not correct, and we currently have a bug
that we would consider the known-instance type of `typing.Literal` as
equivalent to the general instance type for `typing._SpecialForm`, and
we would fail to consider it a singleton type or a single-valued type
(even though it is both.)

An instance type with `known.is_some()` is semantically quite different
from an instance type with `known.is_none()`. The former is a singleton
type that represents exactly one runtime object; the latter is an open
type that represents many runtime objects, including instances of
unknown subclasses. It is too error-prone to represent these
very-different types as a single `Type` variant. We should instead
introduce a dedicated `Type::KnownInstance` variant and force ourselves
to handle these explicitly in all `Type` variant matches.

## Possible followups

There is still a little bit of awkwardness in our current design in some
places, in that we first infer the symbol `typing.Literal` as a
`_SpecialForm` instance, and then later convert that instance-type into
a known-instance-type. We could also use this `KnownInstanceType` enum
to account for other special runtime symbols such as `builtins.Ellipsis`
or `builtins.NotImplemented`.

I think these might be worth pursuing, but I didn't do them here as they
didn't seem essential right now, and I wanted to keep the diff
relatively minimal.

## Test Plan

`cargo test -p red_knot_python_semantic`. New unit tests added for
`Type::is_subtype_of`.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-11-07 22:07:27 +00:00
David Peter
5b6169b02d [red-knot] Minor fix in intersection type comment (#14176)
## Summary

Minor fix in intersection type comment introduced in #14138
2024-11-07 20:23:06 +00:00
Simon Brugman
2040e93add [flake8-logging-format] Fix invalid formatting value in docs of logging-extra-attr-clash (G101) (#14165) 2024-11-07 21:00:05 +01:00
Simon Brugman
794eb886e4 [flake8-bandit] Typo in docs suspicious-pickle-import (S403) (#14175) 2024-11-07 20:59:18 +01:00
David Peter
57ba25caaf [red-knot] Type inference for comparisons involving intersection types (#14138)
## Summary

This adds type inference for comparison expressions involving
intersection types.

For example:
```py
x = get_random_int()

if x != 42:
    reveal_type(x == 42)  # revealed: Literal[False]
    reveal_type(x == 43)  # bool
```

closes #13854

## Test Plan

New Markdown-based tests.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
2024-11-07 20:51:14 +01:00
David Peter
4f74db5630 [red-knot] Improve Symbol API for callable types (#14137)
## Summary

- Get rid of `Symbol::unwrap_or` (unclear semantics, not needed anymore)
- Introduce `Type::call_dunder`
- Emit new diagnostic for possibly-unbound `__iter__` methods
- Better diagnostics for callables with possibly-unbound /
possibly-non-callable `__call__` methods

part of: #14022 

closes #14016

## Test Plan

- Updated test for iterables with possibly-unbound `__iter__` methods.
- New tests for callables
2024-11-07 19:58:31 +01:00
Alex Waygood
adc4216afb Use Python 3.12 for fuzz-parser in CI (#14159) 2024-11-07 15:51:04 +00:00
Simon Brugman
fe8e49de9a [pyflakes] Typo in docs for if-tuple (F634) (#14158) 2024-11-07 15:28:20 +00:00
Alex Waygood
574eb3f4bd Upgrade locked dependencies for the fuzz-parser script (#14156) 2024-11-07 15:10:17 +00:00
Alex Waygood
311b0bdf9a [red-knot] Cleanup handling of InstanceTypes in a couple of places (#14154) 2024-11-07 14:08:31 +00:00
David Peter
f2546c562c [red-knot] Add narrowing for issubclass checks (#14128)
## Summary

- Adds basic support for `type[C]` as a red knot `Type`. Some things
  might not be supported yet, like `type[Any]`.
- Adds type narrowing for `issubclass` checks.

closes #14117 

## Test Plan

New Markdown-based tests

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-07 14:15:39 +01:00
Micha Reiser
59c0dacea0 Introduce Diagnostic trait (#14130) 2024-11-07 13:26:21 +01:00
InSync
b8188b2262 [flake8-pyi] Add autofix for docstring-in-stub (PYI021) (#14150)
Co-authored-by: Micha Reiser <micha@reiser.io>
Co-authored-by: Alex Waygood <alex.waygood@gmail.com>
2024-11-07 12:00:19 +00:00
Simon Brugman
136721e608 [refurb] Implement subclass-builtin (FURB189) (#14105)
## Summary

Implementation for one of the rules in
https://github.com/astral-sh/ruff/issues/1348
Refurb only deals only with classes with a single base, however the rule
is valid for any base.
(`str, Enum` is common prior to `StrEnum`)

## Test Plan

`cargo test`

---------

Co-authored-by: Dhruv Manilawala <dhruvmanila@gmail.com>
2024-11-07 17:26:19 +05:30
Dhruv Manilawala
5b500b838b Update known dunder methods for Python 3.13 (#14146)
## Summary

Closes: #14145
2024-11-07 11:39:00 +05:30
Dylan
cb003ebe22 [flake8-builtins] Skip lambda expressions in builtin-argument-shadowing (A002) (#14144)
Flake8-builtins provides two checks for arguments (really, parameters)
of a function shadowing builtins: A002 checks function definitions, and
A006 checks lambda expressions. This PR ensures that A002 is restricted
to functions rather than lambda expressions.

Closes #14135 .
2024-11-07 05:34:09 +00:00
Carl Meyer
03a5788aa1 [red-knot] a few metaclass cleanups (#14142)
Just cleaning up a few small things I noticed in post-land review.
2024-11-06 22:13:39 +00:00
Charlie Marsh
626f716de6 Add support for resolving metaclasses (#14120)
## Summary

I mirrored some of the idioms that @AlexWaygood used in the MRO work.

Closes https://github.com/astral-sh/ruff/issues/14096.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-06 15:41:35 -05:00
InSync
46c5a13103 [eradicate] Better detection of IntelliJ language injection comments (ERA001) (#14094) 2024-11-06 18:24:15 +00:00
Micha Reiser
31681f66c9 Fix duplicate unpack diagnostics (#14125)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2024-11-06 11:28:29 +00:00
Micha Reiser
a56ee9268e Add mdtest support for files with invalid syntax (#14126) 2024-11-06 12:25:52 +01:00
127 changed files with 3582 additions and 869 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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
View File

@@ -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",

View File

@@ -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

View File

@@ -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,
}

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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
```

View File

@@ -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
```

View 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]
```

View File

@@ -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]
```

View File

@@ -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]`

View File

@@ -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

View File

@@ -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),

View File

@@ -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)]

View File

@@ -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

View File

@@ -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();

View File

@@ -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,

View File

@@ -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),

View File

@@ -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()]

View File

@@ -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,
},
}
}

View File

@@ -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(

View File

@@ -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;

View File

@@ -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(&notebook_index.translate_location(&start));
let end = source_location_to_position(&notebook_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"),
}
}

View File

@@ -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,
}

View File

@@ -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
}
}
}

View File

@@ -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,

View File

@@ -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(

View File

@@ -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`

View File

@@ -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`"]
);
}

View File

@@ -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))

View 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(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.7.2"
version = "0.7.3"
publish = true
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -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);
},

View 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
}
}

View File

@@ -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;

View File

@@ -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`.

View 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 }

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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"

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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

View File

@@ -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=})()",
&[]
));
}

View File

@@ -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]

View File

@@ -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;

View File

@@ -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]

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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()

View File

@@ -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
|

View File

@@ -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
|

View File

@@ -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
|

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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.

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;

View File

@@ -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

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 |

View File

@@ -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"]

View File

@@ -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"]

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,

View File

@@ -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]

View File

@@ -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,

View File

@@ -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,

View File

@@ -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/)

View File

@@ -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/)

View File

@@ -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/)

View File

@@ -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/)

View File

@@ -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/)

View File

@@ -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;

View File

@@ -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,

View File

@@ -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]

View File

@@ -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;

View File

@@ -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,

View File

@@ -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

View File

@@ -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