Compare commits
29 Commits
david/make
...
zb/fix-win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70cf8a94d5 | ||
|
|
d47088c8f8 | ||
|
|
1f0ad675d3 | ||
|
|
a84b27e679 | ||
|
|
8d4679b3ae | ||
|
|
b40a7cce15 | ||
|
|
54b3849dfb | ||
|
|
ffd94e9ace | ||
|
|
c816542704 | ||
|
|
3f958a9d4c | ||
|
|
2ebb5e8d4b | ||
|
|
c69b19fe1d | ||
|
|
076d35fb93 | ||
|
|
16f2a93fca | ||
|
|
eb08345fd5 | ||
|
|
7ca778f492 | ||
|
|
827a076a2f | ||
|
|
4855e0b288 | ||
|
|
44ddd98d7e | ||
|
|
82cb8675dd | ||
|
|
5852217198 | ||
|
|
700e969c56 | ||
|
|
4c15d7a559 | ||
|
|
e15419396c | ||
|
|
444b055cec | ||
|
|
6bb32355ef | ||
|
|
cb71393332 | ||
|
|
64e64d2681 | ||
|
|
9d83e76a3b |
7
.github/workflows/ci.yaml
vendored
7
.github/workflows/ci.yaml
vendored
@@ -217,6 +217,11 @@ jobs:
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: "Install Rust toolchain"
|
||||
run: rustup show
|
||||
# There are spurious CRL server offline errors when downloading
|
||||
# `cargo-bloat` with curl below, so we just disable them for now
|
||||
- name: "Disable SChannel CRL checks"
|
||||
run: |
|
||||
reg add "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v EnableCRLCheck /t REG_DWORD /d 0 /f
|
||||
- name: "Install cargo nextest"
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
@@ -280,7 +285,7 @@ jobs:
|
||||
|
||||
cargo-build-msrv:
|
||||
name: "cargo build (msrv)"
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-latest-8
|
||||
needs: determine_changes
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-test') && (needs.determine_changes.outputs.code == 'true' || github.ref == 'refs/heads/main') }}
|
||||
timeout-minutes: 20
|
||||
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2518,6 +2518,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"camino",
|
||||
"colored 3.0.0",
|
||||
"insta",
|
||||
"memchr",
|
||||
"red_knot_python_semantic",
|
||||
"red_knot_vendored",
|
||||
|
||||
@@ -103,10 +103,10 @@ fn cli_arguments_are_relative_to_the_current_directory() -> anyhow::Result<()> {
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
error: lint:unresolved-import
|
||||
--> <temp_dir>/child/test.py:2:1
|
||||
--> <temp_dir>/child/test.py:2:6
|
||||
|
|
||||
2 | from utils import add
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `utils`
|
||||
| ^^^^^ Cannot resolve import `utils`
|
||||
3 |
|
||||
4 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
@@ -210,6 +210,8 @@ def get_str() -> str:
|
||||
return "a"
|
||||
|
||||
class C:
|
||||
z: int
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.x = get_int()
|
||||
self.y: int = 1
|
||||
@@ -220,12 +222,14 @@ class C:
|
||||
# TODO: this redeclaration should be an error
|
||||
self.y: str = "a"
|
||||
|
||||
# TODO: this redeclaration should be an error
|
||||
self.z: str = "a"
|
||||
|
||||
c_instance = C()
|
||||
|
||||
reveal_type(c_instance.x) # revealed: Unknown | int | str
|
||||
|
||||
# TODO: We should probably infer `int | str` here.
|
||||
reveal_type(c_instance.y) # revealed: int
|
||||
reveal_type(c_instance.z) # revealed: int
|
||||
```
|
||||
|
||||
#### Attributes defined in tuple unpackings
|
||||
@@ -354,6 +358,77 @@ class C:
|
||||
reveal_type(C().declared_and_bound) # revealed: Unknown
|
||||
```
|
||||
|
||||
#### Static methods do not influence implicitly defined attributes
|
||||
|
||||
```py
|
||||
class Other:
|
||||
x: int
|
||||
|
||||
class C:
|
||||
@staticmethod
|
||||
def f(other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
|
||||
# This also works if `staticmethod` is aliased:
|
||||
|
||||
my_staticmethod = staticmethod
|
||||
|
||||
class D:
|
||||
@my_staticmethod
|
||||
def f(other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(D.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(D().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
If `staticmethod` is something else, that should not influence the behavior:
|
||||
|
||||
`other.py`:
|
||||
|
||||
```py
|
||||
def staticmethod(f):
|
||||
return f
|
||||
|
||||
class C:
|
||||
@staticmethod
|
||||
def f(self) -> None:
|
||||
self.x = 1
|
||||
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
And if `staticmethod` is fully qualified, that should also be recognized:
|
||||
|
||||
`fully_qualified.py`:
|
||||
|
||||
```py
|
||||
import builtins
|
||||
|
||||
class Other:
|
||||
x: int
|
||||
|
||||
class C:
|
||||
@builtins.staticmethod
|
||||
def f(other: Other) -> None:
|
||||
other.x = 1
|
||||
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.x) # revealed: Unknown
|
||||
|
||||
# TODO: this should raise `unresolved-attribute` as well, and the type should be `Unknown`
|
||||
reveal_type(C().x) # revealed: Unknown | Literal[1]
|
||||
```
|
||||
|
||||
#### Attributes defined in statically-known-to-be-false branches
|
||||
|
||||
```py
|
||||
@@ -440,12 +515,12 @@ reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
C.pure_class_variable = "overwritten on class"
|
||||
|
||||
# TODO: should be `Literal["overwritten on class"]`
|
||||
# TODO: should be `Unknown | Literal["value set in class method"]` or
|
||||
# Literal["overwritten on class"]`, once/if we support local narrowing.
|
||||
# error: [unresolved-attribute]
|
||||
reveal_type(C.pure_class_variable) # revealed: Unknown
|
||||
|
||||
c_instance = C()
|
||||
# TODO: should be `Literal["overwritten on class"]`
|
||||
reveal_type(c_instance.pure_class_variable) # revealed: Unknown | Literal["value set in class method"]
|
||||
|
||||
# TODO: should raise an error.
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
# Descriptor protocol
|
||||
|
||||
[Descriptors] let objects customize attribute lookup, storage, and deletion.
|
||||
|
||||
A descriptor is an attribute value that has one of the methods in the descriptor protocol. Those
|
||||
methods are `__get__()`, `__set__()`, and `__delete__()`. If any of those methods are defined for an
|
||||
attribute, it is said to be a descriptor.
|
||||
|
||||
## Basic example
|
||||
|
||||
An introductory example, modeled after a [simple example] in the primer on descriptors, involving a
|
||||
descriptor that returns a constant value:
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Ten:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
|
||||
return 10
|
||||
|
||||
def __set__(self, instance: object, value: Literal[10]) -> None:
|
||||
pass
|
||||
|
||||
class C:
|
||||
ten = Ten()
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: this should be `Literal[10]`
|
||||
reveal_type(c.ten) # revealed: Unknown | Ten
|
||||
|
||||
# TODO: This should `Literal[10]`
|
||||
reveal_type(C.ten) # revealed: Unknown | Ten
|
||||
|
||||
# These are fine:
|
||||
c.ten = 10
|
||||
C.ten = 10
|
||||
|
||||
# TODO: Both of these should be errors
|
||||
c.ten = 11
|
||||
C.ten = 11
|
||||
```
|
||||
|
||||
## Different types for `__get__` and `__set__`
|
||||
|
||||
The return type of `__get__` and the value type of `__set__` can be different:
|
||||
|
||||
```py
|
||||
class FlexibleInt:
|
||||
def __init__(self):
|
||||
self._value: int | None = None
|
||||
|
||||
def __get__(self, instance: object, owner: type | None = None) -> int | None:
|
||||
return self._value
|
||||
|
||||
def __set__(self, instance: object, value: int | str) -> None:
|
||||
self._value = int(value)
|
||||
|
||||
class C:
|
||||
flexible_int = FlexibleInt()
|
||||
|
||||
c = C()
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
|
||||
c.flexible_int = 42 # okay
|
||||
c.flexible_int = "42" # also okay!
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
|
||||
# TODO: should be an error
|
||||
c.flexible_int = None # not okay
|
||||
|
||||
# TODO: should be `int | None`
|
||||
reveal_type(c.flexible_int) # revealed: Unknown | FlexibleInt
|
||||
```
|
||||
|
||||
## Built-in `property` descriptor
|
||||
|
||||
The built-in `property` decorator creates a descriptor. The names for attribute reads/writes are
|
||||
determined by the return type of the `name` method and the parameter type of the setter,
|
||||
respectively.
|
||||
|
||||
```py
|
||||
class C:
|
||||
_name: str | None = None
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self._name or "Unset"
|
||||
# TODO: No diagnostic should be emitted here
|
||||
# error: [unresolved-attribute] "Type `Literal[name]` has no attribute `setter`"
|
||||
@name.setter
|
||||
def name(self, value: str | None) -> None:
|
||||
self._value = value
|
||||
|
||||
c = C()
|
||||
|
||||
reveal_type(c._name) # revealed: str | None
|
||||
|
||||
# Should be `str`
|
||||
reveal_type(c.name) # revealed: @Todo(bound method)
|
||||
|
||||
# Should be `builtins.property`
|
||||
reveal_type(C.name) # revealed: Literal[name]
|
||||
|
||||
# This is fine:
|
||||
c.name = "new"
|
||||
|
||||
c.name = None
|
||||
|
||||
# TODO: this should be an error
|
||||
c.name = 42
|
||||
```
|
||||
|
||||
## Built-in `classmethod` descriptor
|
||||
|
||||
Similarly to `property`, `classmethod` decorator creates an implicit descriptor that binds the first
|
||||
argument to the class instead of the instance.
|
||||
|
||||
```py
|
||||
class C:
|
||||
def __init__(self, value: str) -> None:
|
||||
self._name: str = value
|
||||
|
||||
@classmethod
|
||||
def factory(cls, value: str) -> "C":
|
||||
return cls(value)
|
||||
|
||||
@classmethod
|
||||
def get_name(cls) -> str:
|
||||
return cls.__name__
|
||||
|
||||
c1 = C.factory("test") # okay
|
||||
|
||||
# TODO: should be `C`
|
||||
reveal_type(c1) # revealed: @Todo(return type)
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C.get_name()) # revealed: @Todo(return type)
|
||||
|
||||
# TODO: should be `str`
|
||||
reveal_type(C("42").get_name()) # revealed: @Todo(bound method)
|
||||
```
|
||||
|
||||
## Descriptors only work when used as class variables
|
||||
|
||||
From the descriptor guide:
|
||||
|
||||
> Descriptors only work when used as class variables. When put in instances, they have no effect.
|
||||
|
||||
```py
|
||||
from typing import Literal
|
||||
|
||||
class Ten:
|
||||
def __get__(self, instance: object, owner: type | None = None) -> Literal[10]:
|
||||
return 10
|
||||
|
||||
class C:
|
||||
def __init__(self):
|
||||
self.ten = Ten()
|
||||
|
||||
reveal_type(C().ten) # revealed: Unknown | Ten
|
||||
```
|
||||
|
||||
## Descriptors distinguishing between class and instance access
|
||||
|
||||
Overloads can be used to distinguish between when a descriptor is accessed on a class object and
|
||||
when it is accessed on an instance. A real-world example of this is the `__get__` method on
|
||||
`types.FunctionType`.
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString, overload
|
||||
|
||||
class Descriptor:
|
||||
@overload
|
||||
def __get__(self, instance: None, owner: type, /) -> Literal["called on class object"]: ...
|
||||
@overload
|
||||
def __get__(self, instance: object, owner: type | None = None, /) -> Literal["called on instance"]: ...
|
||||
def __get__(self, instance, owner=None, /) -> LiteralString:
|
||||
if instance:
|
||||
return "called on instance"
|
||||
else:
|
||||
return "called on class object"
|
||||
|
||||
class C:
|
||||
d = Descriptor()
|
||||
|
||||
# TODO: should be `Literal["called on class object"]
|
||||
reveal_type(C.d) # revealed: Unknown | Descriptor
|
||||
|
||||
# TODO: should be `Literal["called on instance"]
|
||||
reveal_type(C().d) # revealed: Unknown | Descriptor
|
||||
```
|
||||
|
||||
[descriptors]: https://docs.python.org/3/howto/descriptor.html
|
||||
[simple example]: https://docs.python.org/3/howto/descriptor.html#simple-example-a-descriptor-that-returns-a-constant
|
||||
@@ -0,0 +1,87 @@
|
||||
# Unresolved import diagnostics
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
## Using `from` with an unresolvable module
|
||||
|
||||
This example demonstrates the diagnostic when a `from` style import is used with a module that could
|
||||
not be found:
|
||||
|
||||
```py
|
||||
from does_not_exist import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with too many leading dots
|
||||
|
||||
This example demonstrates the diagnostic when a `from` style import is used with a presumptively
|
||||
valid path, but where there are too many leading dots.
|
||||
|
||||
`package/__init__.py`:
|
||||
|
||||
```py
|
||||
```
|
||||
|
||||
`package/foo.py`:
|
||||
|
||||
```py
|
||||
def add(x, y):
|
||||
return x + y
|
||||
```
|
||||
|
||||
`package/subpackage/subsubpackage/__init__.py`:
|
||||
|
||||
```py
|
||||
from ....foo import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with an unknown current module
|
||||
|
||||
This is another case handled separately in Red Knot, where a `.` provokes relative module name
|
||||
resolution, but where the module name is not resolvable.
|
||||
|
||||
```py
|
||||
from .does_not_exist import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with an unknown nested module
|
||||
|
||||
Like the previous test, but with sub-modules to ensure the span is correct.
|
||||
|
||||
```py
|
||||
from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
|
||||
stat = add(10, 15)
|
||||
```
|
||||
|
||||
## Using `from` with a resolvable module but unresolvable item
|
||||
|
||||
This ensures that diagnostics for an unresolvable item inside a resolvable import highlight the item
|
||||
and not the entire `from ... import ...` statement.
|
||||
|
||||
`a.py`:
|
||||
|
||||
```py
|
||||
does_exist1 = 1
|
||||
does_exist2 = 2
|
||||
```
|
||||
|
||||
```py
|
||||
from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
```
|
||||
|
||||
## An unresolvable import that does not use `from`
|
||||
|
||||
This ensures that an unresolvable `import ...` statement highlights just the module name and not the
|
||||
entire statement.
|
||||
|
||||
```py
|
||||
import does_not_exist # error: [unresolved-import]
|
||||
|
||||
x = does_not_exist.foo
|
||||
```
|
||||
@@ -116,8 +116,18 @@ reveal_type(c.C) # revealed: Literal[C]
|
||||
class C: ...
|
||||
```
|
||||
|
||||
## Unresolvable module import
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
```
|
||||
|
||||
## Unresolvable submodule imports
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
# Topmost component resolvable, submodule not resolvable:
|
||||
import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
|
||||
@@ -13,7 +13,7 @@ if returns_bool():
|
||||
chr: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
reveal_type(chr) # revealed: int | Literal[chr]
|
||||
```
|
||||
|
||||
## Conditionally global or builtin, with annotation
|
||||
@@ -28,5 +28,5 @@ if returns_bool():
|
||||
chr: int = 1
|
||||
|
||||
def f():
|
||||
reveal_type(chr) # revealed: Literal[chr] | int
|
||||
reveal_type(chr) # revealed: int | Literal[chr]
|
||||
```
|
||||
|
||||
@@ -56,10 +56,10 @@ inside the module:
|
||||
import typing
|
||||
|
||||
reveal_type(typing.__name__) # revealed: str
|
||||
reveal_type(typing.__init__) # revealed: Literal[__init__]
|
||||
reveal_type(typing.__init__) # revealed: @Todo(bound method)
|
||||
|
||||
# These come from `builtins.object`, not `types.ModuleType`:
|
||||
reveal_type(typing.__eq__) # revealed: Literal[__eq__]
|
||||
reveal_type(typing.__eq__) # revealed: @Todo(bound method)
|
||||
|
||||
reveal_type(typing.__class__) # revealed: Literal[ModuleType]
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: basic.md - Structures - Unresolvable module import
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:1:8
|
||||
|
|
||||
1 | import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `zqzqzqzqzqzqzq`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: basic.md - Structures - Unresolvable submodule imports
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/import/basic.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | # Topmost component resolvable, submodule not resolvable:
|
||||
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
3 |
|
||||
4 | # Topmost component unresolvable:
|
||||
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
```
|
||||
|
||||
## a/__init__.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:2:8
|
||||
|
|
||||
1 | # Topmost component resolvable, submodule not resolvable:
|
||||
2 | import a.foo # error: [unresolved-import] "Cannot resolve import `a.foo`"
|
||||
| ^^^^^ Cannot resolve import `a.foo`
|
||||
3 |
|
||||
4 | # Topmost component unresolvable:
|
||||
|
|
||||
|
||||
```
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:5:8
|
||||
|
|
||||
4 | # Topmost component unresolvable:
|
||||
5 | import b.foo # error: [unresolved-import] "Cannot resolve import `b.foo`"
|
||||
| ^^^^^ Cannot resolve import `b.foo`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - An unresolvable import that does not use `from`
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | import does_not_exist # error: [unresolved-import]
|
||||
2 |
|
||||
3 | x = does_not_exist.foo
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:1:8
|
||||
|
|
||||
1 | import does_not_exist # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
2 |
|
||||
3 | x = does_not_exist.foo
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with a resolvable module but unresolvable item
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## a.py
|
||||
|
||||
```
|
||||
1 | does_exist1 = 1
|
||||
2 | does_exist2 = 2
|
||||
```
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:1:28
|
||||
|
|
||||
1 | from a import does_exist1, does_not_exist, does_exist2 # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Module `a` has no member `does_not_exist`
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown current module
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from .does_not_exist import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:1:7
|
||||
|
|
||||
1 | from .does_not_exist import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unknown nested module
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:1:7
|
||||
|
|
||||
1 | from .does_not_exist.foo.bar import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ Cannot resolve import `.does_not_exist.foo.bar`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with an unresolvable module
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## mdtest_snippet__1.py
|
||||
|
||||
```
|
||||
1 | from does_not_exist import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/mdtest_snippet__1.py:1:6
|
||||
|
|
||||
1 | from does_not_exist import add # error: [unresolved-import]
|
||||
| ^^^^^^^^^^^^^^ Cannot resolve import `does_not_exist`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
---
|
||||
source: crates/red_knot_test/src/lib.rs
|
||||
expression: snapshot
|
||||
---
|
||||
---
|
||||
mdtest name: unresolved_import.md - Unresolved import diagnostics - Using `from` with too many leading dots
|
||||
mdtest path: crates/red_knot_python_semantic/resources/mdtest/diagnostics/unresolved_import.md
|
||||
---
|
||||
|
||||
# Python source files
|
||||
|
||||
## package/__init__.py
|
||||
|
||||
```
|
||||
```
|
||||
|
||||
## package/foo.py
|
||||
|
||||
```
|
||||
1 | def add(x, y):
|
||||
2 | return x + y
|
||||
```
|
||||
|
||||
## package/subpackage/subsubpackage/__init__.py
|
||||
|
||||
```
|
||||
1 | from ....foo import add # error: [unresolved-import]
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
```
|
||||
|
||||
# Diagnostics
|
||||
|
||||
```
|
||||
error: lint:unresolved-import
|
||||
--> /src/package/subpackage/subsubpackage/__init__.py:1:10
|
||||
|
|
||||
1 | from ....foo import add # error: [unresolved-import]
|
||||
| ^^^ Cannot resolve import `....foo`
|
||||
2 |
|
||||
3 | stat = add(10, 15)
|
||||
|
|
||||
|
||||
```
|
||||
@@ -1509,37 +1509,6 @@ if True:
|
||||
from module import symbol
|
||||
```
|
||||
|
||||
## Known limitations
|
||||
|
||||
We currently have a limitation in the complexity (depth) of the visibility constraints that are
|
||||
supported. This is to avoid pathological cases that would require us to recurse deeply.
|
||||
|
||||
```py
|
||||
x = 1
|
||||
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or (x := 2) # fmt: skip
|
||||
|
||||
# This still works fine:
|
||||
reveal_type(x) # revealed: Literal[2]
|
||||
|
||||
y = 1
|
||||
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or False or \
|
||||
False or False or False or (y := 2) # fmt: skip
|
||||
|
||||
# TODO: This should ideally be `Literal[2]` as well:
|
||||
reveal_type(y) # revealed: Literal[1, 2]
|
||||
```
|
||||
|
||||
## Unsupported features
|
||||
|
||||
We do not support full unreachable code analysis yet. We also raise diagnostics from
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Truthiness
|
||||
|
||||
## Literals
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal, LiteralString
|
||||
from knot_extensions import AlwaysFalsy, AlwaysTruthy
|
||||
@@ -45,3 +47,31 @@ def _(
|
||||
reveal_type(bool(c)) # revealed: bool
|
||||
reveal_type(bool(d)) # revealed: bool
|
||||
```
|
||||
|
||||
## Instances
|
||||
|
||||
Checks that we don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
|
||||
### __bool__ is bool
|
||||
|
||||
```py
|
||||
class BoolIsBool:
|
||||
__bool__ = bool
|
||||
|
||||
reveal_type(bool(BoolIsBool())) # revealed: bool
|
||||
```
|
||||
|
||||
### Conditional __bool__ method
|
||||
|
||||
```py
|
||||
def flag() -> bool:
|
||||
return True
|
||||
|
||||
class Boom:
|
||||
if flag():
|
||||
__bool__ = bool
|
||||
else:
|
||||
__bool__ = int
|
||||
|
||||
reveal_type(bool(Boom())) # revealed: bool
|
||||
```
|
||||
|
||||
@@ -141,15 +141,6 @@ class AlwaysFalse:
|
||||
# revealed: Literal[True]
|
||||
reveal_type(not AlwaysFalse())
|
||||
|
||||
# We don't get into a cycle if someone sets their `__bool__` method to the `bool` builtin:
|
||||
class BoolIsBool:
|
||||
# TODO: The `type[bool]` declaration here is a workaround to avoid running into
|
||||
# https://github.com/astral-sh/ruff/issues/15672
|
||||
__bool__: type[bool] = bool
|
||||
|
||||
# revealed: bool
|
||||
reveal_type(not BoolIsBool())
|
||||
|
||||
# At runtime, no `__bool__` and no `__len__` means truthy, but we can't rely on that, because
|
||||
# a subclass could add a `__bool__` method.
|
||||
class NoBoolMethod: ...
|
||||
|
||||
@@ -5,20 +5,20 @@ use crate::db::Db;
|
||||
use crate::semantic_index::expression::Expression;
|
||||
use crate::semantic_index::symbol::{FileScopeId, ScopeId};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) struct Constraint<'db> {
|
||||
pub(crate) node: ConstraintNode<'db>,
|
||||
pub(crate) is_positive: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub(crate) enum ConstraintNode<'db> {
|
||||
Expression(Expression<'db>),
|
||||
Pattern(PatternConstraint<'db>),
|
||||
}
|
||||
|
||||
/// Pattern kinds for which we support type narrowing and/or static visibility analysis.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, Hash, PartialEq)]
|
||||
pub(crate) enum PatternConstraintKind<'db> {
|
||||
Singleton(Singleton, Option<Expression<'db>>),
|
||||
Value(Expression<'db>, Option<Expression<'db>>),
|
||||
|
||||
@@ -9,15 +9,6 @@ pub(crate) enum Boundness {
|
||||
PossiblyUnbound,
|
||||
}
|
||||
|
||||
impl Boundness {
|
||||
pub(crate) fn or(self, other: Boundness) -> Boundness {
|
||||
match (self, other) {
|
||||
(Boundness::Bound, _) | (_, Boundness::Bound) => Boundness::Bound,
|
||||
(Boundness::PossiblyUnbound, Boundness::PossiblyUnbound) => Boundness::PossiblyUnbound,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of a symbol lookup, which can either be a (possibly unbound) type
|
||||
/// or a completely unbound symbol.
|
||||
///
|
||||
@@ -46,13 +37,6 @@ impl<'db> Symbol<'db> {
|
||||
matches!(self, Symbol::Unbound)
|
||||
}
|
||||
|
||||
pub(crate) fn possibly_unbound(&self) -> bool {
|
||||
match self {
|
||||
Symbol::Type(_, Boundness::PossiblyUnbound) | Symbol::Unbound => true,
|
||||
Symbol::Type(_, Boundness::Bound) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type of the symbol, ignoring possible unboundness.
|
||||
///
|
||||
/// If the symbol is *definitely* unbound, this function will return `None`. Otherwise,
|
||||
@@ -71,18 +55,32 @@ impl<'db> Symbol<'db> {
|
||||
.expect("Expected a (possibly unbound) type, not an unbound symbol")
|
||||
}
|
||||
|
||||
/// Fallback (partially or fully) to another symbol if `self` is partially or fully unbound.
|
||||
///
|
||||
/// 1. If `self` is definitely bound, return `self` without evaluating `fallback_fn()`.
|
||||
/// 2. Else, evaluate `fallback_fn()`:
|
||||
/// a. If `self` is definitely unbound, return the result of `fallback_fn()`.
|
||||
/// b. Else, if `fallback` is definitely unbound, return `self`.
|
||||
/// c. Else, if `self` is possibly unbound and `fallback` is definitely bound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::Bound)`
|
||||
/// d. Else, if `self` is possibly unbound and `fallback` is possibly unbound,
|
||||
/// return `Symbol(<union of self-type and fallback-type>, Boundness::PossiblyUnbound)`
|
||||
#[must_use]
|
||||
pub(crate) fn or_fall_back_to(self, db: &'db dyn Db, fallback: &Symbol<'db>) -> Symbol<'db> {
|
||||
match fallback {
|
||||
Symbol::Type(fallback_ty, fallback_boundness) => match self {
|
||||
Symbol::Type(_, Boundness::Bound) => self,
|
||||
Symbol::Type(ty, boundness @ Boundness::PossiblyUnbound) => Symbol::Type(
|
||||
UnionType::from_elements(db, [*fallback_ty, ty]),
|
||||
fallback_boundness.or(boundness),
|
||||
pub(crate) fn or_fall_back_to(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
fallback_fn: impl FnOnce() -> Self,
|
||||
) -> Self {
|
||||
match self {
|
||||
Symbol::Type(_, Boundness::Bound) => self,
|
||||
Symbol::Unbound => fallback_fn(),
|
||||
Symbol::Type(self_ty, Boundness::PossiblyUnbound) => match fallback_fn() {
|
||||
Symbol::Unbound => self,
|
||||
Symbol::Type(fallback_ty, fallback_boundness) => Symbol::Type(
|
||||
UnionType::from_elements(db, [self_ty, fallback_ty]),
|
||||
fallback_boundness,
|
||||
),
|
||||
Symbol::Unbound => fallback.clone(),
|
||||
},
|
||||
Symbol::Unbound => self,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,44 +108,44 @@ mod tests {
|
||||
|
||||
// Start from an unbound symbol
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Unbound),
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Unbound
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, PossiblyUnbound)),
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Unbound.or_fall_back_to(&db, &Symbol::Type(ty1, Bound)),
|
||||
Symbol::Unbound.or_fall_back_to(&db, || Symbol::Type(ty1, Bound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
|
||||
// Start from a possibly unbound symbol
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Unbound),
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound)
|
||||
.or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), PossiblyUnbound)
|
||||
.or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), PossiblyUnbound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty2, ty1]), Bound)
|
||||
Symbol::Type(ty1, PossiblyUnbound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(UnionType::from_elements(&db, [ty1, ty2]), Bound)
|
||||
);
|
||||
|
||||
// Start from a definitely bound symbol
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Unbound),
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Unbound),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, PossiblyUnbound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
assert_eq!(
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, &Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(ty1, Bound).or_fall_back_to(&db, || Symbol::Type(ty2, Bound)),
|
||||
Symbol::Type(ty1, Bound)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -256,26 +256,19 @@ fn module_type_symbols<'db>(db: &'db dyn Db) -> smallvec::SmallVec<[ast::name::N
|
||||
|
||||
/// Looks up a module-global symbol by name in a file.
|
||||
pub(crate) fn global_symbol<'db>(db: &'db dyn Db, file: File, name: &str) -> Symbol<'db> {
|
||||
let explicit_symbol = symbol(db, global_scope(db, file), name);
|
||||
|
||||
if !explicit_symbol.possibly_unbound() {
|
||||
return explicit_symbol;
|
||||
}
|
||||
|
||||
// Not defined explicitly in the global scope?
|
||||
// All modules are instances of `types.ModuleType`;
|
||||
// look it up there (with a few very special exceptions)
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
// TODO: this should use `.to_instance(db)`. but we don't understand attribute access
|
||||
// on instance types yet.
|
||||
let module_type_member = KnownClass::ModuleType.to_class_literal(db).member(db, name);
|
||||
return explicit_symbol.or_fall_back_to(db, &module_type_member);
|
||||
}
|
||||
|
||||
explicit_symbol
|
||||
symbol(db, global_scope(db, file), name).or_fall_back_to(db, || {
|
||||
if module_type_symbols(db)
|
||||
.iter()
|
||||
.any(|module_type_member| &**module_type_member == name)
|
||||
{
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Infer the type of a binding.
|
||||
@@ -670,6 +663,10 @@ impl<'db> Type<'db> {
|
||||
matches!(self, Type::ClassLiteral(..))
|
||||
}
|
||||
|
||||
pub const fn is_instance(&self) -> bool {
|
||||
matches!(self, Type::Instance(..))
|
||||
}
|
||||
|
||||
pub fn module_literal(db: &'db dyn Db, importing_file: File, submodule: Module) -> Self {
|
||||
Self::ModuleLiteral(ModuleLiteralType::new(db, importing_file, submodule))
|
||||
}
|
||||
@@ -1843,19 +1840,8 @@ impl<'db> Type<'db> {
|
||||
return Truthiness::Ambiguous;
|
||||
};
|
||||
|
||||
// Check if the class has `__bool__ = bool` and avoid infinite recursion, since
|
||||
// `Type::call` on `bool` will call `Type::bool` on the argument.
|
||||
if bool_method
|
||||
.into_class_literal()
|
||||
.is_some_and(|ClassLiteralType { class }| {
|
||||
class.is_known(db, KnownClass::Bool)
|
||||
})
|
||||
{
|
||||
return Truthiness::Ambiguous;
|
||||
}
|
||||
|
||||
if let Some(Type::BooleanLiteral(bool_val)) = bool_method
|
||||
.call(db, &CallArguments::positional([*instance_ty]))
|
||||
.call_bound(db, instance_ty, &CallArguments::positional([]))
|
||||
.return_type(db)
|
||||
{
|
||||
bool_val.into()
|
||||
@@ -2148,6 +2134,52 @@ impl<'db> Type<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the outcome of calling an class/instance attribute of this type
|
||||
/// using descriptor protocol.
|
||||
///
|
||||
/// `receiver_ty` must be `Type::Instance(_)` or `Type::ClassLiteral`.
|
||||
///
|
||||
/// TODO: handle `super()` objects properly
|
||||
#[must_use]
|
||||
fn call_bound(
|
||||
self,
|
||||
db: &'db dyn Db,
|
||||
receiver_ty: &Type<'db>,
|
||||
arguments: &CallArguments<'_, 'db>,
|
||||
) -> CallOutcome<'db> {
|
||||
debug_assert!(receiver_ty.is_instance() || receiver_ty.is_class_literal());
|
||||
|
||||
match self {
|
||||
Type::FunctionLiteral(..) => {
|
||||
// Functions are always descriptors, so this would effectively call
|
||||
// the function with the instance as the first argument
|
||||
self.call(db, &arguments.with_self(*receiver_ty))
|
||||
}
|
||||
|
||||
Type::Instance(_) | Type::ClassLiteral(_) => {
|
||||
// TODO descriptor protocol. For now, assume non-descriptor and call without `self` argument.
|
||||
self.call(db, arguments)
|
||||
}
|
||||
|
||||
Type::Union(union) => CallOutcome::union(
|
||||
self,
|
||||
union
|
||||
.elements(db)
|
||||
.iter()
|
||||
.map(|elem| elem.call_bound(db, receiver_ty, arguments)),
|
||||
),
|
||||
|
||||
Type::Intersection(_) => CallOutcome::callable(CallBinding::from_return_type(
|
||||
todo_type!("Type::Intersection.call_bound()"),
|
||||
)),
|
||||
|
||||
// Cases that duplicate, and thus must be kept in sync with, `Type::call()`
|
||||
Type::Dynamic(_) => CallOutcome::callable(CallBinding::from_return_type(self)),
|
||||
|
||||
_ => CallOutcome::not_callable(self),
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up a dunder method on the meta type of `self` and call it.
|
||||
fn call_dunder(
|
||||
self,
|
||||
@@ -3757,10 +3789,8 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
}
|
||||
}
|
||||
|
||||
let global_lookup = symbol(db, global_scope(db, self.module(db).file()), name);
|
||||
|
||||
// If it's unbound, check if it's present as an instance on `types.ModuleType`
|
||||
// or `builtins.object`.
|
||||
// If it's not found in the global scope, check if it's present as an instance
|
||||
// on `types.ModuleType` or `builtins.object`.
|
||||
//
|
||||
// We do a more limited version of this in `global_symbol_ty`,
|
||||
// but there are two crucial differences here:
|
||||
@@ -3774,14 +3804,13 @@ impl<'db> ModuleLiteralType<'db> {
|
||||
// ignore `__getattr__`. Typeshed has a fake `__getattr__` on `types.ModuleType`
|
||||
// to help out with dynamic imports; we shouldn't use it for `ModuleLiteral` types
|
||||
// where we know exactly which module we're dealing with.
|
||||
if name != "__getattr__" && global_lookup.possibly_unbound() {
|
||||
// TODO: this should use `.to_instance()`, but we don't understand instance attribute yet
|
||||
let module_type_instance_member =
|
||||
KnownClass::ModuleType.to_class_literal(db).member(db, name);
|
||||
global_lookup.or_fall_back_to(db, &module_type_instance_member)
|
||||
} else {
|
||||
global_lookup
|
||||
}
|
||||
symbol(db, global_scope(db, self.module(db).file()), name).or_fall_back_to(db, || {
|
||||
if name == "__getattr__" {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
KnownClass::ModuleType.to_instance(db).member(db, name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2538,6 +2538,12 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// - Absolute `*` imports (`from collections import *`)
|
||||
// - Relative `*` imports (`from ...foo import *`)
|
||||
let ast::StmtImportFrom { module, level, .. } = import_from;
|
||||
// For diagnostics, we want to highlight the unresolvable
|
||||
// module and not the entire `from ... import ...` statement.
|
||||
let module_ref = module
|
||||
.as_ref()
|
||||
.map(AnyNodeRef::from)
|
||||
.unwrap_or_else(|| AnyNodeRef::from(import_from));
|
||||
let module = module.as_deref();
|
||||
|
||||
let module_name = if let Some(level) = NonZeroU32::new(*level) {
|
||||
@@ -2572,7 +2578,7 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
"Relative module resolution `{}` failed: too many leading dots",
|
||||
format_import_from_module(*level, module),
|
||||
);
|
||||
report_unresolved_module(&self.context, import_from, *level, module);
|
||||
report_unresolved_module(&self.context, module_ref, *level, module);
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
}
|
||||
@@ -2582,14 +2588,14 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
format_import_from_module(*level, module),
|
||||
self.file().path(self.db())
|
||||
);
|
||||
report_unresolved_module(&self.context, import_from, *level, module);
|
||||
report_unresolved_module(&self.context, module_ref, *level, module);
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let Some(module_ty) = self.module_type_from_name(&module_name) else {
|
||||
report_unresolved_module(&self.context, import_from, *level, module);
|
||||
report_unresolved_module(&self.context, module_ref, *level, module);
|
||||
self.add_unknown_declaration_with_binding(alias.into(), definition);
|
||||
return;
|
||||
};
|
||||
@@ -3290,8 +3296,9 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
|
||||
/// Look up a name reference that isn't bound in the local scope.
|
||||
fn lookup_name(&mut self, name_node: &ast::ExprName) -> Symbol<'db> {
|
||||
let db = self.db();
|
||||
let ast::ExprName { id: name, .. } = name_node;
|
||||
let file_scope_id = self.scope().file_scope_id(self.db());
|
||||
let file_scope_id = self.scope().file_scope_id(db);
|
||||
let is_bound =
|
||||
if let Some(symbol) = self.index.symbol_table(file_scope_id).symbol_by_name(name) {
|
||||
symbol.is_bound()
|
||||
@@ -3306,16 +3313,15 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// In function-like scopes, any local variable (symbol that is bound in this scope) can
|
||||
// only have a definition in this scope, or error; it never references another scope.
|
||||
// (At runtime, it would use the `LOAD_FAST` opcode.)
|
||||
if !is_bound || !self.scope().is_function_like(self.db()) {
|
||||
if !is_bound || !self.scope().is_function_like(db) {
|
||||
// Walk up parent scopes looking for a possible enclosing scope that may have a
|
||||
// definition of this name visible to us (would be `LOAD_DEREF` at runtime.)
|
||||
for (enclosing_scope_file_id, _) in self.index.ancestor_scopes(file_scope_id) {
|
||||
// Class scopes are not visible to nested scopes, and we need to handle global
|
||||
// scope differently (because an unbound name there falls back to builtins), so
|
||||
// check only function-like scopes.
|
||||
let enclosing_scope_id =
|
||||
enclosing_scope_file_id.to_scope_id(self.db(), self.file());
|
||||
if !enclosing_scope_id.is_function_like(self.db()) {
|
||||
let enclosing_scope_id = enclosing_scope_file_id.to_scope_id(db, self.file());
|
||||
if !enclosing_scope_id.is_function_like(db) {
|
||||
continue;
|
||||
}
|
||||
let enclosing_symbol_table = self.index.symbol_table(enclosing_scope_file_id);
|
||||
@@ -3328,37 +3334,45 @@ impl<'db> TypeInferenceBuilder<'db> {
|
||||
// runtime, it is the scope that creates the cell for our closure.) If the name
|
||||
// isn't bound in that scope, we should get an unbound name, not continue
|
||||
// falling back to other scopes / globals / builtins.
|
||||
return symbol(self.db(), enclosing_scope_id, name);
|
||||
return symbol(db, enclosing_scope_id, name);
|
||||
}
|
||||
}
|
||||
|
||||
// No nonlocal binding, check module globals. Avoid infinite recursion if `self.scope`
|
||||
// already is module globals.
|
||||
let global_symbol = if file_scope_id.is_global() {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
global_symbol(self.db(), self.file(), name)
|
||||
};
|
||||
|
||||
// Fallback to builtins (without infinite recursion if we're already in builtins.)
|
||||
if global_symbol.possibly_unbound()
|
||||
&& Some(self.scope()) != builtins_module_scope(self.db())
|
||||
{
|
||||
let mut builtins_symbol = builtins_symbol(self.db(), name);
|
||||
if builtins_symbol.is_unbound() && name == "reveal_type" {
|
||||
self.context.report_lint(
|
||||
&UNDEFINED_REVEAL,
|
||||
name_node.into(),
|
||||
format_args!(
|
||||
"`reveal_type` used without importing it; this is allowed for debugging convenience but will fail at runtime"),
|
||||
);
|
||||
builtins_symbol = typing_extensions_symbol(self.db(), name);
|
||||
}
|
||||
|
||||
global_symbol.or_fall_back_to(self.db(), &builtins_symbol)
|
||||
} else {
|
||||
global_symbol
|
||||
}
|
||||
Symbol::Unbound
|
||||
// No nonlocal binding? Check the module's globals.
|
||||
// Avoid infinite recursion if `self.scope` already is the module's global scope.
|
||||
.or_fall_back_to(db, || {
|
||||
if file_scope_id.is_global() {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
global_symbol(db, self.file(), name)
|
||||
}
|
||||
})
|
||||
// Not found in globals? Fallback to builtins
|
||||
// (without infinite recursion if we're already in builtins.)
|
||||
.or_fall_back_to(db, || {
|
||||
if Some(self.scope()) == builtins_module_scope(db) {
|
||||
Symbol::Unbound
|
||||
} else {
|
||||
builtins_symbol(db, name)
|
||||
}
|
||||
})
|
||||
// Still not found? It might be `reveal_type`...
|
||||
.or_fall_back_to(db, || {
|
||||
if name == "reveal_type" {
|
||||
self.context.report_lint(
|
||||
&UNDEFINED_REVEAL,
|
||||
name_node.into(),
|
||||
format_args!(
|
||||
"`reveal_type` used without importing it; \
|
||||
this is allowed for debugging convenience but will fail at runtime"
|
||||
),
|
||||
);
|
||||
typing_extensions_symbol(db, name)
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Symbol::Unbound
|
||||
}
|
||||
|
||||
@@ -137,20 +137,46 @@
|
||||
//! create a state where the `x = <unbound>` binding is always visible.
|
||||
//!
|
||||
//!
|
||||
//! ### Properties
|
||||
//! ### Representing formulas
|
||||
//!
|
||||
//! The ternary `AND` and `OR` operations have the property that `~a OR ~b = ~(a AND b)`. This
|
||||
//! means we could, in principle, get rid of either of these two to simplify the representation.
|
||||
//! Given everything above, we can represent a visibility constraint as a _ternary formula_. This
|
||||
//! is like a boolean formula (which maps several true/false variables to a single true/false
|
||||
//! result), but which allows the third "ambiguous" value in addition to "true" and "false".
|
||||
//!
|
||||
//! However, we already apply negative constraints `~test1` and `~test2` to the "branches not
|
||||
//! taken" in the example above. This means that the tree-representation `~test1 OR ~test2` is much
|
||||
//! cheaper/shallower than basically creating `~(~(~test1) AND ~(~test2))`. Similarly, if we wanted
|
||||
//! to get rid of `AND`, we would also have to create additional nodes. So for performance reasons,
|
||||
//! there is a small "duplication" in the code between those two constraint types.
|
||||
//! [_Binary decision diagrams_][bdd] (BDDs) are a common way to represent boolean formulas when
|
||||
//! doing program analysis. We extend this to a _ternary decision diagram_ (TDD) to support
|
||||
//! ambiguous values.
|
||||
//!
|
||||
//! A TDD is a graph, and a ternary formula is represented by a node in this graph. There are three
|
||||
//! possible leaf nodes representing the "true", "false", and "ambiguous" constant functions.
|
||||
//! Interior nodes consist of a ternary variable to evaluate, and outgoing edges for whether the
|
||||
//! variable evaluates to true, false, or ambiguous.
|
||||
//!
|
||||
//! Our TDDs are _reduced_ and _ordered_ (as is typical for BDDs).
|
||||
//!
|
||||
//! An ordered TDD means that variables appear in the same order in all paths within the graph.
|
||||
//!
|
||||
//! A reduced TDD means two things: First, we intern the graph nodes, so that we only keep a single
|
||||
//! copy of interior nodes with the same contents. Second, we eliminate any nodes that are "noops",
|
||||
//! where the "true" and "false" outgoing edges lead to the same node. (This implies that it
|
||||
//! doesn't matter what value that variable has when evaluating the formula, and we can leave it
|
||||
//! out of the evaluation chain completely.)
|
||||
//!
|
||||
//! Reduced and ordered decision diagrams are _normal forms_, which means that two equivalent
|
||||
//! formulas (which have the same outputs for every combination of inputs) are represented by
|
||||
//! exactly the same graph node. (Because of interning, this is not _equal_ nodes, but _identical_
|
||||
//! ones.) That means that we can compare formulas for equivalence in constant time, and in
|
||||
//! particular, can check whether a visibility constraint is statically always true or false,
|
||||
//! regardless of any Python program state, by seeing if the constraint's formula is the "true" or
|
||||
//! "false" leaf node.
|
||||
//!
|
||||
//! [Kleene]: <https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics>
|
||||
//! [bdd]: https://en.wikipedia.org/wiki/Binary_decision_diagram
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use ruff_index::{Idx, IndexVec};
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::semantic_index::{
|
||||
ast_ids::HasScopedExpressionId,
|
||||
@@ -159,14 +185,6 @@ use crate::semantic_index::{
|
||||
use crate::types::{infer_expression_types, Truthiness};
|
||||
use crate::Db;
|
||||
|
||||
/// The maximum depth of recursion when evaluating visibility constraints.
|
||||
///
|
||||
/// This is a performance optimization that prevents us from descending deeply in case of
|
||||
/// pathological cases. The actual limit here has been derived from performance testing on
|
||||
/// the `black` codebase. When increasing the limit beyond 32, we see a 5x runtime increase
|
||||
/// resulting from a few files with a lot of boolean expressions and `if`-statements.
|
||||
const MAX_RECURSION_DEPTH: usize = 24;
|
||||
|
||||
/// A ternary formula that defines under what conditions a binding is visible. (A ternary formula
|
||||
/// is just like a boolean formula, but with `Ambiguous` as a third potential result. See the
|
||||
/// module documentation for more details.)
|
||||
@@ -182,211 +200,416 @@ const MAX_RECURSION_DEPTH: usize = 24;
|
||||
/// That means that when you are constructing a formula, you might need to create distinct atoms
|
||||
/// for a particular [`Constraint`], if your formula needs to consider how a particular runtime
|
||||
/// property might be different at different points in the execution of the program.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct VisibilityConstraint<'db>(VisibilityConstraintInner<'db>);
|
||||
///
|
||||
/// Visibility constraints are normalized, so equivalent constraints are guaranteed to have equal
|
||||
/// IDs.
|
||||
#[derive(Clone, Copy, Eq, Hash, PartialEq)]
|
||||
pub(crate) struct ScopedVisibilityConstraintId(u32);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum VisibilityConstraintInner<'db> {
|
||||
AlwaysTrue,
|
||||
AlwaysFalse,
|
||||
Ambiguous,
|
||||
VisibleIf(Constraint<'db>, u8),
|
||||
VisibleIfNot(ScopedVisibilityConstraintId),
|
||||
KleeneAnd(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
|
||||
KleeneOr(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
|
||||
impl std::fmt::Debug for ScopedVisibilityConstraintId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut f = f.debug_tuple("ScopedVisibilityConstraintId");
|
||||
match *self {
|
||||
// We use format_args instead of rendering the strings directly so that we don't get
|
||||
// any quotes in the output: ScopedVisibilityConstraintId(AlwaysTrue) instead of
|
||||
// ScopedVisibilityConstraintId("AlwaysTrue").
|
||||
ALWAYS_TRUE => f.field(&format_args!("AlwaysTrue")),
|
||||
AMBIGUOUS => f.field(&format_args!("Ambiguous")),
|
||||
ALWAYS_FALSE => f.field(&format_args!("AlwaysFalse")),
|
||||
_ => f.field(&self.0),
|
||||
};
|
||||
f.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A newtype-index for a visibility constraint in a particular scope.
|
||||
#[newtype_index]
|
||||
pub(crate) struct ScopedVisibilityConstraintId;
|
||||
// Internal details:
|
||||
//
|
||||
// There are 3 terminals, with hard-coded constraint IDs: true, ambiguous, and false.
|
||||
//
|
||||
// _Atoms_ are the underlying Constraints, which are the variables that are evaluated by the
|
||||
// ternary function.
|
||||
//
|
||||
// _Interior nodes_ provide the TDD structure for the formula. Interior nodes are stored in an
|
||||
// arena Vec, with the constraint ID providing an index into the arena.
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
struct InteriorNode {
|
||||
atom: Atom,
|
||||
if_true: ScopedVisibilityConstraintId,
|
||||
if_ambiguous: ScopedVisibilityConstraintId,
|
||||
if_false: ScopedVisibilityConstraintId,
|
||||
}
|
||||
|
||||
/// A "variable" that is evaluated as part of a TDD ternary function. For visibility constraints,
|
||||
/// this is a `Constraint` that represents some runtime property of the Python code that we are
|
||||
/// evaluating. We intern these constraints in an arena ([`VisibilityConstraints::constraints`]).
|
||||
/// An atom is then an index into this arena.
|
||||
///
|
||||
/// By using a 32-bit index, we would typically allow 4 billion distinct constraints within a
|
||||
/// scope. However, we sometimes have to model how a `Constraint` can have a different runtime
|
||||
/// value at different points in the execution of the program. To handle this, we reserve the top
|
||||
/// byte of an atom to represent a "copy number". This is just an opaque value that allows
|
||||
/// different `Atom`s to evaluate the same `Constraint`. This yields a maximum of 16 million
|
||||
/// distinct `Constraint`s in a scope, and 256 possible copies of each of those constraints.
|
||||
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
struct Atom(u32);
|
||||
|
||||
impl Atom {
|
||||
/// Deconstruct an atom into a constraint index and a copy number.
|
||||
#[inline]
|
||||
fn into_index_and_copy(self) -> (u32, u8) {
|
||||
let copy = self.0 >> 24;
|
||||
let index = self.0 & 0x00ff_ffff;
|
||||
(index, copy as u8)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn copy_of(mut self, copy: u8) -> Self {
|
||||
// Clear out the previous copy number
|
||||
self.0 &= 0x00ff_ffff;
|
||||
// OR in the new one
|
||||
self.0 |= u32::from(copy) << 24;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// A custom Debug implementation that prints out the constraint index and copy number as distinct
|
||||
// fields.
|
||||
impl std::fmt::Debug for Atom {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let (index, copy) = self.into_index_and_copy();
|
||||
f.debug_tuple("Atom").field(&index).field(©).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Idx for Atom {
|
||||
#[inline]
|
||||
fn new(value: usize) -> Self {
|
||||
assert!(value <= 0x00ff_ffff);
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Self(value as u32)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
let (index, _) = self.into_index_and_copy();
|
||||
index as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl ScopedVisibilityConstraintId {
|
||||
/// A special ID that is used for an "always true" / "always visible" constraint.
|
||||
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
|
||||
/// present at index 0.
|
||||
pub(crate) const ALWAYS_TRUE: ScopedVisibilityConstraintId =
|
||||
ScopedVisibilityConstraintId::from_u32(0);
|
||||
|
||||
/// A special ID that is used for an "always false" / "never visible" constraint.
|
||||
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
|
||||
/// present at index 1.
|
||||
pub(crate) const ALWAYS_FALSE: ScopedVisibilityConstraintId =
|
||||
ScopedVisibilityConstraintId::from_u32(1);
|
||||
ScopedVisibilityConstraintId(0xffff_ffff);
|
||||
|
||||
/// A special ID that is used for an ambiguous constraint.
|
||||
/// When we create a new [`VisibilityConstraints`] object, this constraint is always
|
||||
/// present at index 2.
|
||||
pub(crate) const AMBIGUOUS: ScopedVisibilityConstraintId =
|
||||
ScopedVisibilityConstraintId::from_u32(2);
|
||||
ScopedVisibilityConstraintId(0xffff_fffe);
|
||||
|
||||
/// A special ID that is used for an "always false" / "never visible" constraint.
|
||||
pub(crate) const ALWAYS_FALSE: ScopedVisibilityConstraintId =
|
||||
ScopedVisibilityConstraintId(0xffff_fffd);
|
||||
|
||||
fn is_terminal(self) -> bool {
|
||||
self.0 >= SMALLEST_TERMINAL.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Idx for ScopedVisibilityConstraintId {
|
||||
#[inline]
|
||||
fn new(value: usize) -> Self {
|
||||
assert!(value <= (SMALLEST_TERMINAL.0 as usize));
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
Self(value as u32)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn index(self) -> usize {
|
||||
debug_assert!(!self.is_terminal());
|
||||
self.0 as usize
|
||||
}
|
||||
}
|
||||
|
||||
// Rebind some constants locally so that we don't need as many qualifiers below.
|
||||
const ALWAYS_TRUE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_TRUE;
|
||||
const AMBIGUOUS: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::AMBIGUOUS;
|
||||
const ALWAYS_FALSE: ScopedVisibilityConstraintId = ScopedVisibilityConstraintId::ALWAYS_FALSE;
|
||||
const SMALLEST_TERMINAL: ScopedVisibilityConstraintId = ALWAYS_FALSE;
|
||||
|
||||
/// A collection of visibility constraints. This is currently stored in `UseDefMap`, which means we
|
||||
/// maintain a separate set of visibility constraints for each scope in file.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct VisibilityConstraints<'db> {
|
||||
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
|
||||
constraints: IndexVec<Atom, Constraint<'db>>,
|
||||
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub(crate) struct VisibilityConstraintsBuilder<'db> {
|
||||
constraints: IndexVec<ScopedVisibilityConstraintId, VisibilityConstraint<'db>>,
|
||||
}
|
||||
|
||||
impl Default for VisibilityConstraintsBuilder<'_> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
constraints: IndexVec::from_iter([
|
||||
VisibilityConstraint(VisibilityConstraintInner::AlwaysTrue),
|
||||
VisibilityConstraint(VisibilityConstraintInner::AlwaysFalse),
|
||||
VisibilityConstraint(VisibilityConstraintInner::Ambiguous),
|
||||
]),
|
||||
}
|
||||
}
|
||||
constraints: IndexVec<Atom, Constraint<'db>>,
|
||||
interiors: IndexVec<ScopedVisibilityConstraintId, InteriorNode>,
|
||||
constraint_cache: FxHashMap<Constraint<'db>, Atom>,
|
||||
interior_cache: FxHashMap<InteriorNode, ScopedVisibilityConstraintId>,
|
||||
not_cache: FxHashMap<ScopedVisibilityConstraintId, ScopedVisibilityConstraintId>,
|
||||
and_cache: FxHashMap<
|
||||
(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
|
||||
ScopedVisibilityConstraintId,
|
||||
>,
|
||||
or_cache: FxHashMap<
|
||||
(ScopedVisibilityConstraintId, ScopedVisibilityConstraintId),
|
||||
ScopedVisibilityConstraintId,
|
||||
>,
|
||||
}
|
||||
|
||||
impl<'db> VisibilityConstraintsBuilder<'db> {
|
||||
pub(crate) fn build(self) -> VisibilityConstraints<'db> {
|
||||
VisibilityConstraints {
|
||||
constraints: self.constraints,
|
||||
interiors: self.interiors,
|
||||
}
|
||||
}
|
||||
|
||||
fn add(&mut self, constraint: VisibilityConstraintInner<'db>) -> ScopedVisibilityConstraintId {
|
||||
self.constraints.push(VisibilityConstraint(constraint))
|
||||
/// Returns whether `a` or `b` has a "larger" atom. TDDs are ordered such that interior nodes
|
||||
/// can only have edges to "larger" nodes. Terminals are considered to have a larger atom than
|
||||
/// any internal node, since they are leaf nodes.
|
||||
fn cmp_atoms(
|
||||
&self,
|
||||
a: ScopedVisibilityConstraintId,
|
||||
b: ScopedVisibilityConstraintId,
|
||||
) -> Ordering {
|
||||
if a == b || (a.is_terminal() && b.is_terminal()) {
|
||||
Ordering::Equal
|
||||
} else if a.is_terminal() {
|
||||
Ordering::Greater
|
||||
} else if b.is_terminal() {
|
||||
Ordering::Less
|
||||
} else {
|
||||
self.interiors[a].atom.cmp(&self.interiors[b].atom)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a constraint, ensuring that we only store any particular constraint once.
|
||||
fn add_constraint(&mut self, constraint: Constraint<'db>, copy: u8) -> Atom {
|
||||
self.constraint_cache
|
||||
.entry(constraint)
|
||||
.or_insert_with(|| self.constraints.push(constraint))
|
||||
.copy_of(copy)
|
||||
}
|
||||
|
||||
/// Adds an interior node, ensuring that we always use the same visibility constraint ID for
|
||||
/// equal nodes.
|
||||
fn add_interior(&mut self, node: InteriorNode) -> ScopedVisibilityConstraintId {
|
||||
// If the true and false branches lead to the same node, we can override the ambiguous
|
||||
// branch to go there too. And this node is then redundant and can be reduced.
|
||||
if node.if_true == node.if_false {
|
||||
return node.if_true;
|
||||
}
|
||||
|
||||
*self
|
||||
.interior_cache
|
||||
.entry(node)
|
||||
.or_insert_with(|| self.interiors.push(node))
|
||||
}
|
||||
|
||||
/// Adds a new visibility constraint that checks a single [`Constraint`]. Provide different
|
||||
/// values for `copy` if you need to model that the constraint can evaluate to different
|
||||
/// results at different points in the execution of the program being modeled.
|
||||
pub(crate) fn add_atom(
|
||||
&mut self,
|
||||
constraint: Constraint<'db>,
|
||||
copy: u8,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
self.add(VisibilityConstraintInner::VisibleIf(constraint, copy))
|
||||
let atom = self.add_constraint(constraint, copy);
|
||||
self.add_interior(InteriorNode {
|
||||
atom,
|
||||
if_true: ALWAYS_TRUE,
|
||||
if_ambiguous: AMBIGUOUS,
|
||||
if_false: ALWAYS_FALSE,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a new visibility constraint that is the ternary NOT of an existing one.
|
||||
pub(crate) fn add_not_constraint(
|
||||
&mut self,
|
||||
a: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
if a == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
} else if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
|
||||
ScopedVisibilityConstraintId::ALWAYS_FALSE
|
||||
} else if a == ScopedVisibilityConstraintId::AMBIGUOUS {
|
||||
ScopedVisibilityConstraintId::AMBIGUOUS
|
||||
} else {
|
||||
self.add(VisibilityConstraintInner::VisibleIfNot(a))
|
||||
if a == ALWAYS_TRUE {
|
||||
return ALWAYS_FALSE;
|
||||
} else if a == AMBIGUOUS {
|
||||
return AMBIGUOUS;
|
||||
} else if a == ALWAYS_FALSE {
|
||||
return ALWAYS_TRUE;
|
||||
}
|
||||
|
||||
if let Some(cached) = self.not_cache.get(&a) {
|
||||
return *cached;
|
||||
}
|
||||
let a_node = self.interiors[a];
|
||||
let if_true = self.add_not_constraint(a_node.if_true);
|
||||
let if_ambiguous = self.add_not_constraint(a_node.if_ambiguous);
|
||||
let if_false = self.add_not_constraint(a_node.if_false);
|
||||
let result = self.add_interior(InteriorNode {
|
||||
atom: a_node.atom,
|
||||
if_true,
|
||||
if_ambiguous,
|
||||
if_false,
|
||||
});
|
||||
self.not_cache.insert(a, result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Adds a new visibility constraint that is the ternary OR of two existing ones.
|
||||
pub(crate) fn add_or_constraint(
|
||||
&mut self,
|
||||
a: ScopedVisibilityConstraintId,
|
||||
b: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
if a == ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
|| b == ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
{
|
||||
return ScopedVisibilityConstraintId::ALWAYS_TRUE;
|
||||
} else if a == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
return b;
|
||||
} else if b == ScopedVisibilityConstraintId::ALWAYS_FALSE {
|
||||
return a;
|
||||
match (a, b) {
|
||||
(ALWAYS_TRUE, _) | (_, ALWAYS_TRUE) => return ALWAYS_TRUE,
|
||||
(ALWAYS_FALSE, other) | (other, ALWAYS_FALSE) => return other,
|
||||
(AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS,
|
||||
_ => {}
|
||||
}
|
||||
match (&self.constraints[a], &self.constraints[b]) {
|
||||
(_, VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id))) if a == *id => {
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
}
|
||||
(VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id)), _) if *id == b => {
|
||||
ScopedVisibilityConstraintId::ALWAYS_TRUE
|
||||
}
|
||||
_ => self.add(VisibilityConstraintInner::KleeneOr(a, b)),
|
||||
|
||||
// OR is commutative, which lets us halve the cache requirements
|
||||
let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
|
||||
if let Some(cached) = self.or_cache.get(&(a, b)) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) {
|
||||
Ordering::Equal => {
|
||||
let a_node = self.interiors[a];
|
||||
let b_node = self.interiors[b];
|
||||
let if_true = self.add_or_constraint(a_node.if_true, b_node.if_true);
|
||||
let if_false = self.add_or_constraint(a_node.if_false, b_node.if_false);
|
||||
let if_ambiguous = if if_true == if_false {
|
||||
if_true
|
||||
} else {
|
||||
self.add_or_constraint(a_node.if_ambiguous, b_node.if_ambiguous)
|
||||
};
|
||||
(a_node.atom, if_true, if_ambiguous, if_false)
|
||||
}
|
||||
Ordering::Less => {
|
||||
let a_node = self.interiors[a];
|
||||
let if_true = self.add_or_constraint(a_node.if_true, b);
|
||||
let if_false = self.add_or_constraint(a_node.if_false, b);
|
||||
let if_ambiguous = if if_true == if_false {
|
||||
if_true
|
||||
} else {
|
||||
self.add_or_constraint(a_node.if_ambiguous, b)
|
||||
};
|
||||
(a_node.atom, if_true, if_ambiguous, if_false)
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let b_node = self.interiors[b];
|
||||
let if_true = self.add_or_constraint(a, b_node.if_true);
|
||||
let if_false = self.add_or_constraint(a, b_node.if_false);
|
||||
let if_ambiguous = if if_true == if_false {
|
||||
if_true
|
||||
} else {
|
||||
self.add_or_constraint(a, b_node.if_ambiguous)
|
||||
};
|
||||
(b_node.atom, if_true, if_ambiguous, if_false)
|
||||
}
|
||||
};
|
||||
|
||||
let result = self.add_interior(InteriorNode {
|
||||
atom,
|
||||
if_true,
|
||||
if_ambiguous,
|
||||
if_false,
|
||||
});
|
||||
self.or_cache.insert((a, b), result);
|
||||
result
|
||||
}
|
||||
|
||||
/// Adds a new visibility constraint that is the ternary AND of two existing ones.
|
||||
pub(crate) fn add_and_constraint(
|
||||
&mut self,
|
||||
a: ScopedVisibilityConstraintId,
|
||||
b: ScopedVisibilityConstraintId,
|
||||
) -> ScopedVisibilityConstraintId {
|
||||
if a == ScopedVisibilityConstraintId::ALWAYS_FALSE
|
||||
|| b == ScopedVisibilityConstraintId::ALWAYS_FALSE
|
||||
{
|
||||
return ScopedVisibilityConstraintId::ALWAYS_FALSE;
|
||||
} else if a == ScopedVisibilityConstraintId::ALWAYS_TRUE {
|
||||
return b;
|
||||
} else if b == ScopedVisibilityConstraintId::ALWAYS_TRUE {
|
||||
return a;
|
||||
match (a, b) {
|
||||
(ALWAYS_FALSE, _) | (_, ALWAYS_FALSE) => return ALWAYS_FALSE,
|
||||
(ALWAYS_TRUE, other) | (other, ALWAYS_TRUE) => return other,
|
||||
(AMBIGUOUS, AMBIGUOUS) => return AMBIGUOUS,
|
||||
_ => {}
|
||||
}
|
||||
match (&self.constraints[a], &self.constraints[b]) {
|
||||
(_, VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id))) if a == *id => {
|
||||
ScopedVisibilityConstraintId::ALWAYS_FALSE
|
||||
}
|
||||
(VisibilityConstraint(VisibilityConstraintInner::VisibleIfNot(id)), _) if *id == b => {
|
||||
ScopedVisibilityConstraintId::ALWAYS_FALSE
|
||||
}
|
||||
_ => self.add(VisibilityConstraintInner::KleeneAnd(a, b)),
|
||||
|
||||
// AND is commutative, which lets us halve the cache requirements
|
||||
let (a, b) = if b.0 < a.0 { (b, a) } else { (a, b) };
|
||||
if let Some(cached) = self.and_cache.get(&(a, b)) {
|
||||
return *cached;
|
||||
}
|
||||
|
||||
let (atom, if_true, if_ambiguous, if_false) = match self.cmp_atoms(a, b) {
|
||||
Ordering::Equal => {
|
||||
let a_node = self.interiors[a];
|
||||
let b_node = self.interiors[b];
|
||||
let if_true = self.add_and_constraint(a_node.if_true, b_node.if_true);
|
||||
let if_false = self.add_and_constraint(a_node.if_false, b_node.if_false);
|
||||
let if_ambiguous = if if_true == if_false {
|
||||
if_true
|
||||
} else {
|
||||
self.add_and_constraint(a_node.if_ambiguous, b_node.if_ambiguous)
|
||||
};
|
||||
(a_node.atom, if_true, if_ambiguous, if_false)
|
||||
}
|
||||
Ordering::Less => {
|
||||
let a_node = self.interiors[a];
|
||||
let if_true = self.add_and_constraint(a_node.if_true, b);
|
||||
let if_false = self.add_and_constraint(a_node.if_false, b);
|
||||
let if_ambiguous = if if_true == if_false {
|
||||
if_true
|
||||
} else {
|
||||
self.add_and_constraint(a_node.if_ambiguous, b)
|
||||
};
|
||||
(a_node.atom, if_true, if_ambiguous, if_false)
|
||||
}
|
||||
Ordering::Greater => {
|
||||
let b_node = self.interiors[b];
|
||||
let if_true = self.add_and_constraint(a, b_node.if_true);
|
||||
let if_false = self.add_and_constraint(a, b_node.if_false);
|
||||
let if_ambiguous = if if_true == if_false {
|
||||
if_true
|
||||
} else {
|
||||
self.add_and_constraint(a, b_node.if_ambiguous)
|
||||
};
|
||||
(b_node.atom, if_true, if_ambiguous, if_false)
|
||||
}
|
||||
};
|
||||
|
||||
let result = self.add_interior(InteriorNode {
|
||||
atom,
|
||||
if_true,
|
||||
if_ambiguous,
|
||||
if_false,
|
||||
});
|
||||
self.and_cache.insert((a, b), result);
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl<'db> VisibilityConstraints<'db> {
|
||||
/// Analyze the statically known visibility for a given visibility constraint.
|
||||
pub(crate) fn evaluate(&self, db: &'db dyn Db, id: ScopedVisibilityConstraintId) -> Truthiness {
|
||||
self.evaluate_impl(db, id, MAX_RECURSION_DEPTH)
|
||||
}
|
||||
|
||||
fn evaluate_impl(
|
||||
pub(crate) fn evaluate(
|
||||
&self,
|
||||
db: &'db dyn Db,
|
||||
id: ScopedVisibilityConstraintId,
|
||||
max_depth: usize,
|
||||
mut id: ScopedVisibilityConstraintId,
|
||||
) -> Truthiness {
|
||||
if max_depth == 0 {
|
||||
return Truthiness::Ambiguous;
|
||||
}
|
||||
|
||||
let VisibilityConstraint(visibility_constraint) = &self.constraints[id];
|
||||
match visibility_constraint {
|
||||
VisibilityConstraintInner::AlwaysTrue => Truthiness::AlwaysTrue,
|
||||
VisibilityConstraintInner::AlwaysFalse => Truthiness::AlwaysFalse,
|
||||
VisibilityConstraintInner::Ambiguous => Truthiness::Ambiguous,
|
||||
VisibilityConstraintInner::VisibleIf(constraint, _) => {
|
||||
Self::analyze_single(db, constraint)
|
||||
}
|
||||
VisibilityConstraintInner::VisibleIfNot(negated) => {
|
||||
self.evaluate_impl(db, *negated, max_depth - 1).negate()
|
||||
}
|
||||
VisibilityConstraintInner::KleeneAnd(lhs, rhs) => {
|
||||
let lhs = self.evaluate_impl(db, *lhs, max_depth - 1);
|
||||
|
||||
if lhs == Truthiness::AlwaysFalse {
|
||||
return Truthiness::AlwaysFalse;
|
||||
}
|
||||
|
||||
let rhs = self.evaluate_impl(db, *rhs, max_depth - 1);
|
||||
|
||||
if rhs == Truthiness::AlwaysFalse {
|
||||
Truthiness::AlwaysFalse
|
||||
} else if lhs == Truthiness::AlwaysTrue && rhs == Truthiness::AlwaysTrue {
|
||||
Truthiness::AlwaysTrue
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
}
|
||||
VisibilityConstraintInner::KleeneOr(lhs_id, rhs_id) => {
|
||||
let lhs = self.evaluate_impl(db, *lhs_id, max_depth - 1);
|
||||
|
||||
if lhs == Truthiness::AlwaysTrue {
|
||||
return Truthiness::AlwaysTrue;
|
||||
}
|
||||
|
||||
let rhs = self.evaluate_impl(db, *rhs_id, max_depth - 1);
|
||||
|
||||
if rhs == Truthiness::AlwaysTrue {
|
||||
Truthiness::AlwaysTrue
|
||||
} else if lhs == Truthiness::AlwaysFalse && rhs == Truthiness::AlwaysFalse {
|
||||
Truthiness::AlwaysFalse
|
||||
} else {
|
||||
Truthiness::Ambiguous
|
||||
}
|
||||
loop {
|
||||
let node = match id {
|
||||
ALWAYS_TRUE => return Truthiness::AlwaysTrue,
|
||||
AMBIGUOUS => return Truthiness::Ambiguous,
|
||||
ALWAYS_FALSE => return Truthiness::AlwaysFalse,
|
||||
_ => self.interiors[id],
|
||||
};
|
||||
let constraint = &self.constraints[node.atom];
|
||||
match Self::analyze_single(db, constraint) {
|
||||
Truthiness::AlwaysTrue => id = node.if_true,
|
||||
Truthiness::Ambiguous => id = node.if_ambiguous,
|
||||
Truthiness::AlwaysFalse => id = node.if_false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use camino::Utf8Path;
|
||||
use dir_test::{dir_test, Fixture};
|
||||
use std::path::Path;
|
||||
|
||||
/// See `crates/red_knot_test/README.md` for documentation on these tests.
|
||||
#[dir_test(
|
||||
@@ -9,16 +8,23 @@ use std::path::Path;
|
||||
)]
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn mdtest(fixture: Fixture<&str>) {
|
||||
let fixture_path = Utf8Path::new(fixture.path());
|
||||
let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let absolute_fixture_path = Utf8Path::new(fixture.path());
|
||||
let crate_dir = Utf8Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let snapshot_path = crate_dir.join("resources").join("mdtest").join("snapshots");
|
||||
let workspace_root = crate_dir.ancestors().nth(2).unwrap();
|
||||
|
||||
let long_title = fixture_path.strip_prefix(workspace_root).unwrap();
|
||||
let short_title = fixture_path.file_name().unwrap();
|
||||
let relative_fixture_path = absolute_fixture_path.strip_prefix(workspace_root).unwrap();
|
||||
let short_title = absolute_fixture_path.file_name().unwrap();
|
||||
|
||||
let test_name = test_name("mdtest", fixture_path);
|
||||
let test_name = test_name("mdtest", absolute_fixture_path);
|
||||
|
||||
red_knot_test::run(fixture_path, long_title.as_str(), short_title, &test_name);
|
||||
red_knot_test::run(
|
||||
absolute_fixture_path,
|
||||
relative_fixture_path,
|
||||
&snapshot_path,
|
||||
short_title,
|
||||
&test_name,
|
||||
);
|
||||
}
|
||||
|
||||
/// Constructs the test name used for individual markdown files
|
||||
|
||||
@@ -22,6 +22,7 @@ ruff_text_size = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
camino = { workspace = true }
|
||||
colored = { workspace = true }
|
||||
insta = { workspace = true, features = ["filters"] }
|
||||
memchr = { workspace = true }
|
||||
regex = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
|
||||
@@ -126,6 +126,34 @@ Intervening empty lines or non-assertion comments are not allowed; an assertion
|
||||
assertion per line, immediately following each other, with the line immediately following the last
|
||||
assertion as the line of source code on which the matched diagnostics are emitted.
|
||||
|
||||
## Diagnostic Snapshotting
|
||||
|
||||
In addition to inline assertions, one can also snapshot the full diagnostic
|
||||
output of a test. This is done by adding a `<!-- snapshot-diagnostics -->` directive
|
||||
in the corresponding section. For example:
|
||||
|
||||
````markdown
|
||||
## Unresolvable module import
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
import zqzqzqzqzqzqzq # error: [unresolved-import] "Cannot resolve import `zqzqzqzqzqzqzq`"
|
||||
```
|
||||
````
|
||||
|
||||
The `snapshot-diagnostics` directive must appear before anything else in
|
||||
the section.
|
||||
|
||||
This will use `insta` to manage an external file snapshot of all diagnostic
|
||||
output generated.
|
||||
|
||||
Inline assertions, as described above, may be used in conjunction with diagnostic
|
||||
snapshotting.
|
||||
|
||||
At present, there is no way to do inline snapshotting or to request more granular
|
||||
snapshotting of specific diagnostics.
|
||||
|
||||
## Multi-file tests
|
||||
|
||||
Some tests require multiple files, with imports from one file into another. Multiple fenced code
|
||||
@@ -345,6 +373,11 @@ I/O error on read.
|
||||
|
||||
### Asserting on full diagnostic output
|
||||
|
||||
> [!NOTE]
|
||||
> At present, one can opt into diagnostic snapshotting that is managed via external files. See
|
||||
> the section above for more details. The feature outlined below, *inline* diagnostic snapshotting,
|
||||
> is still desirable.
|
||||
|
||||
The inline comment diagnostic assertions are useful for making quick, readable assertions about
|
||||
diagnostics in a particular location. But sometimes we will want to assert on the full diagnostic
|
||||
output of checking an embedded Python file. Or sometimes (see “incremental tests” below) we will
|
||||
|
||||
@@ -27,12 +27,18 @@ const MDTEST_TEST_FILTER: &str = "MDTEST_TEST_FILTER";
|
||||
///
|
||||
/// Panic on test failure, and print failure details.
|
||||
#[allow(clippy::print_stdout)]
|
||||
pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str) {
|
||||
let source = std::fs::read_to_string(path).unwrap();
|
||||
pub fn run(
|
||||
absolute_fixture_path: &Utf8Path,
|
||||
relative_fixture_path: &Utf8Path,
|
||||
snapshot_path: &Utf8Path,
|
||||
short_title: &str,
|
||||
test_name: &str,
|
||||
) {
|
||||
let source = std::fs::read_to_string(absolute_fixture_path).unwrap();
|
||||
let suite = match test_parser::parse(short_title, &source) {
|
||||
Ok(suite) => suite,
|
||||
Err(err) => {
|
||||
panic!("Error parsing `{path}`: {err:?}")
|
||||
panic!("Error parsing `{absolute_fixture_path}`: {err:?}")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,7 +60,7 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
db.memory_file_system().remove_all();
|
||||
Files::sync_all(&mut db);
|
||||
|
||||
if let Err(failures) = run_test(&mut db, &test) {
|
||||
if let Err(failures) = run_test(&mut db, relative_fixture_path, snapshot_path, &test) {
|
||||
any_failures = true;
|
||||
println!("\n{}\n", test.name().bold().underline());
|
||||
|
||||
@@ -67,7 +73,8 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
for failure in failures {
|
||||
let absolute_line_number =
|
||||
backtick_line.checked_add(relative_line_number).unwrap();
|
||||
let line_info = format!("{long_title}:{absolute_line_number}").cyan();
|
||||
let line_info =
|
||||
format!("{relative_fixture_path}:{absolute_line_number}").cyan();
|
||||
println!(" {line_info} {failure}");
|
||||
}
|
||||
}
|
||||
@@ -89,7 +96,12 @@ pub fn run(path: &Utf8Path, long_title: &str, short_title: &str, test_name: &str
|
||||
assert!(!any_failures, "Some tests failed.");
|
||||
}
|
||||
|
||||
fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures> {
|
||||
fn run_test(
|
||||
db: &mut db::Db,
|
||||
relative_fixture_path: &Utf8Path,
|
||||
snapshot_path: &Utf8Path,
|
||||
test: &parser::MarkdownTest,
|
||||
) -> Result<(), Failures> {
|
||||
let project_root = db.project_root().to_path_buf();
|
||||
let src_path = SystemPathBuf::from("/src");
|
||||
let custom_typeshed_path = test.configuration().typeshed().map(SystemPathBuf::from);
|
||||
@@ -176,6 +188,10 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
)
|
||||
.expect("Failed to update Program settings in TestDb");
|
||||
|
||||
// When snapshot testing is enabled, this is populated with
|
||||
// all diagnostics. Otherwise it remains empty.
|
||||
let mut snapshot_diagnostics = vec![];
|
||||
|
||||
let failures: Failures = test_files
|
||||
.into_iter()
|
||||
.filter_map(|test_file| {
|
||||
@@ -224,16 +240,36 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
diagnostic
|
||||
}));
|
||||
|
||||
match matcher::match_file(db, test_file.file, diagnostics) {
|
||||
Ok(()) => None,
|
||||
Err(line_failures) => Some(FileFailures {
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
by_line: line_failures,
|
||||
}),
|
||||
let failure =
|
||||
match matcher::match_file(db, test_file.file, diagnostics.iter().map(|d| &**d)) {
|
||||
Ok(()) => None,
|
||||
Err(line_failures) => Some(FileFailures {
|
||||
backtick_offset: test_file.backtick_offset,
|
||||
by_line: line_failures,
|
||||
}),
|
||||
};
|
||||
if test.should_snapshot_diagnostics() {
|
||||
snapshot_diagnostics.extend(diagnostics);
|
||||
}
|
||||
failure
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !snapshot_diagnostics.is_empty() {
|
||||
let snapshot =
|
||||
create_diagnostic_snapshot(db, relative_fixture_path, test, snapshot_diagnostics);
|
||||
let name = test.name().replace(' ', "_");
|
||||
insta::with_settings!(
|
||||
{
|
||||
snapshot_path => snapshot_path,
|
||||
input_file => name.clone(),
|
||||
filters => vec![(r"\\", "/")],
|
||||
prepend_module_to_snapshot => false,
|
||||
},
|
||||
{ insta::assert_snapshot!(name, snapshot) }
|
||||
);
|
||||
}
|
||||
|
||||
if failures.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -244,6 +280,7 @@ fn run_test(db: &mut db::Db, test: &parser::MarkdownTest) -> Result<(), Failures
|
||||
type Failures = Vec<FileFailures>;
|
||||
|
||||
/// The failures for a single file in a test by line number.
|
||||
#[derive(Debug)]
|
||||
struct FileFailures {
|
||||
/// The offset of the backticks that starts the code block in the Markdown file
|
||||
backtick_offset: TextSize,
|
||||
@@ -258,3 +295,55 @@ struct TestFile {
|
||||
// Offset of the backticks that starts the code block in the Markdown file
|
||||
backtick_offset: TextSize,
|
||||
}
|
||||
|
||||
fn create_diagnostic_snapshot<D: Diagnostic>(
|
||||
db: &mut db::Db,
|
||||
relative_fixture_path: &Utf8Path,
|
||||
test: &parser::MarkdownTest,
|
||||
diagnostics: impl IntoIterator<Item = D>,
|
||||
) -> String {
|
||||
// TODO(ag): Do something better than requiring this
|
||||
// global state to be twiddled everywhere.
|
||||
colored::control::set_override(false);
|
||||
|
||||
let mut snapshot = String::new();
|
||||
writeln!(snapshot).unwrap();
|
||||
writeln!(snapshot, "---").unwrap();
|
||||
writeln!(snapshot, "mdtest name: {}", test.name()).unwrap();
|
||||
writeln!(snapshot, "mdtest path: {relative_fixture_path}").unwrap();
|
||||
writeln!(snapshot, "---").unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
|
||||
writeln!(snapshot, "# Python source files").unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
for file in test.files() {
|
||||
writeln!(snapshot, "## {}", file.path).unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
// Note that we don't use ```py here because the line numbering
|
||||
// we add makes it invalid Python. This sacrifices syntax
|
||||
// highlighting when you look at the snapshot on GitHub,
|
||||
// but the line numbers are extremely useful for analyzing
|
||||
// snapshots. So we keep them.
|
||||
writeln!(snapshot, "```").unwrap();
|
||||
|
||||
let line_number_width = file.code.lines().count().to_string().len();
|
||||
for (i, line) in file.code.lines().enumerate() {
|
||||
let line_number = i + 1;
|
||||
writeln!(snapshot, "{line_number:>line_number_width$} | {line}").unwrap();
|
||||
}
|
||||
writeln!(snapshot, "```").unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
}
|
||||
|
||||
writeln!(snapshot, "# Diagnostics").unwrap();
|
||||
writeln!(snapshot).unwrap();
|
||||
for (i, diag) in diagnostics.into_iter().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(snapshot).unwrap();
|
||||
}
|
||||
writeln!(snapshot, "```").unwrap();
|
||||
writeln!(snapshot, "{}", diag.display(db)).unwrap();
|
||||
writeln!(snapshot, "```").unwrap();
|
||||
}
|
||||
snapshot
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::bail;
|
||||
use rustc_hash::FxHashSet;
|
||||
use rustc_hash::{FxHashMap, FxHashSet};
|
||||
|
||||
use ruff_index::{newtype_index, IndexVec};
|
||||
use ruff_python_trivia::Cursor;
|
||||
@@ -73,6 +73,10 @@ impl<'m, 's> MarkdownTest<'m, 's> {
|
||||
pub(crate) fn configuration(&self) -> &MarkdownTestConfig {
|
||||
&self.section.config
|
||||
}
|
||||
|
||||
pub(super) fn should_snapshot_diagnostics(&self) -> bool {
|
||||
self.section.snapshot_diagnostics
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator yielding all [`MarkdownTest`]s in a [`MarkdownTestSuite`].
|
||||
@@ -122,6 +126,7 @@ struct Section<'s> {
|
||||
level: u8,
|
||||
parent_id: Option<SectionId>,
|
||||
config: MarkdownTestConfig,
|
||||
snapshot_diagnostics: bool,
|
||||
}
|
||||
|
||||
#[newtype_index]
|
||||
@@ -189,7 +194,12 @@ struct Parser<'s> {
|
||||
|
||||
/// [`EmbeddedFile`]s of the final [`MarkdownTestSuite`].
|
||||
files: IndexVec<EmbeddedFileId, EmbeddedFile<'s>>,
|
||||
unnamed_file_count: usize,
|
||||
|
||||
/// The counts are done by section. This gives each code block a
|
||||
/// somewhat locally derived name. That is, adding new sections
|
||||
/// won't change the names of files in other sections. This is
|
||||
/// important for avoiding snapshot churn.
|
||||
unnamed_file_count: FxHashMap<SectionId, usize>,
|
||||
|
||||
/// The unparsed remainder of the Markdown source.
|
||||
cursor: Cursor<'s>,
|
||||
@@ -221,12 +231,13 @@ impl<'s> Parser<'s> {
|
||||
level: 0,
|
||||
parent_id: None,
|
||||
config: MarkdownTestConfig::default(),
|
||||
snapshot_diagnostics: false,
|
||||
});
|
||||
Self {
|
||||
sections,
|
||||
source,
|
||||
files: IndexVec::default(),
|
||||
unnamed_file_count: 0,
|
||||
unnamed_file_count: FxHashMap::default(),
|
||||
cursor: Cursor::new(source),
|
||||
preceding_blank_lines: 0,
|
||||
explicit_path: None,
|
||||
@@ -279,10 +290,27 @@ impl<'s> Parser<'s> {
|
||||
}
|
||||
|
||||
fn parse_impl(&mut self) -> anyhow::Result<()> {
|
||||
const SECTION_CONFIG_SNAPSHOT: &str = "<!-- snapshot-diagnostics -->";
|
||||
const CODE_BLOCK_END: &[u8] = b"```";
|
||||
|
||||
while let Some(first) = self.cursor.bump() {
|
||||
match first {
|
||||
'<' => {
|
||||
self.explicit_path = None;
|
||||
self.preceding_blank_lines = 0;
|
||||
// If we want to support more comment directives, then we should
|
||||
// probably just parse the directive generically first. But it's
|
||||
// not clear if we'll want to add more, since comments are hidden
|
||||
// from GitHub Markdown rendering.
|
||||
if self
|
||||
.cursor
|
||||
.as_str()
|
||||
.starts_with(&SECTION_CONFIG_SNAPSHOT[1..])
|
||||
{
|
||||
self.cursor.skip_bytes(SECTION_CONFIG_SNAPSHOT.len() - 1);
|
||||
self.process_snapshot_diagnostics()?;
|
||||
}
|
||||
}
|
||||
'#' => {
|
||||
self.explicit_path = None;
|
||||
self.preceding_blank_lines = 0;
|
||||
@@ -397,6 +425,7 @@ impl<'s> Parser<'s> {
|
||||
level: header_level.try_into()?,
|
||||
parent_id: Some(parent),
|
||||
config: self.sections[parent].config.clone(),
|
||||
snapshot_diagnostics: self.sections[parent].snapshot_diagnostics,
|
||||
};
|
||||
|
||||
if self.current_section_files.is_some() {
|
||||
@@ -444,11 +473,12 @@ impl<'s> Parser<'s> {
|
||||
let path = match self.explicit_path {
|
||||
Some(path) => path.to_string(),
|
||||
None => {
|
||||
self.unnamed_file_count += 1;
|
||||
let unnamed_file_count = self.unnamed_file_count.entry(section).or_default();
|
||||
*unnamed_file_count += 1;
|
||||
|
||||
match lang {
|
||||
"py" | "pyi" => format!("mdtest_snippet__{}.{lang}", self.unnamed_file_count),
|
||||
"" => format!("mdtest_snippet__{}.py", self.unnamed_file_count),
|
||||
"py" | "pyi" => format!("mdtest_snippet__{unnamed_file_count}.{lang}"),
|
||||
"" => format!("mdtest_snippet__{unnamed_file_count}.py"),
|
||||
_ => {
|
||||
bail!(
|
||||
"Cannot generate name for `lang={}`: Unsupported extension",
|
||||
@@ -494,6 +524,32 @@ impl<'s> Parser<'s> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_snapshot_diagnostics(&mut self) -> anyhow::Result<()> {
|
||||
if self.current_section_has_config {
|
||||
bail!(
|
||||
"Section config to enable snapshotting diagnostics must come before \
|
||||
everything else (including TOML configuration blocks).",
|
||||
);
|
||||
}
|
||||
if self.current_section_files.is_some() {
|
||||
bail!(
|
||||
"Section config to enable snapshotting diagnostics must come before \
|
||||
everything else (including embedded files).",
|
||||
);
|
||||
}
|
||||
|
||||
let current_section = &mut self.sections[self.stack.top()];
|
||||
if current_section.snapshot_diagnostics {
|
||||
bail!(
|
||||
"Section config to enable snapshotting diagnostics should appear \
|
||||
at most once.",
|
||||
);
|
||||
}
|
||||
current_section.snapshot_diagnostics = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn pop_sections_to_level(&mut self, level: usize) {
|
||||
while level <= self.sections[self.stack.top()].level.into() {
|
||||
self.stack.pop();
|
||||
@@ -620,7 +676,7 @@ mod tests {
|
||||
panic!("expected one file");
|
||||
};
|
||||
|
||||
assert_eq!(file.path, "mdtest_snippet__2.py");
|
||||
assert_eq!(file.path, "mdtest_snippet__1.py");
|
||||
assert_eq!(file.lang, "py");
|
||||
assert_eq!(file.code, "y = 2");
|
||||
|
||||
@@ -628,11 +684,11 @@ mod tests {
|
||||
panic!("expected two files");
|
||||
};
|
||||
|
||||
assert_eq!(file_1.path, "mdtest_snippet__3.pyi");
|
||||
assert_eq!(file_1.path, "mdtest_snippet__1.pyi");
|
||||
assert_eq!(file_1.lang, "pyi");
|
||||
assert_eq!(file_1.code, "a: int");
|
||||
|
||||
assert_eq!(file_2.path, "mdtest_snippet__4.pyi");
|
||||
assert_eq!(file_2.path, "mdtest_snippet__2.pyi");
|
||||
assert_eq!(file_2.lang, "pyi");
|
||||
assert_eq!(file_2.code, "b: str");
|
||||
}
|
||||
@@ -1289,4 +1345,74 @@ mod tests {
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(err.to_string(), "Trailing code-block metadata is not supported. Only the code block language can be specified.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_section_directive_not_allowed() {
|
||||
let source = dedent(
|
||||
"
|
||||
# Some header
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Section config to enable snapshotting diagnostics should appear at most once.",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_directive_must_appear_before_config() {
|
||||
let source = dedent(
|
||||
"
|
||||
# Some header
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = \"/typeshed\"
|
||||
```
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Section config to enable snapshotting diagnostics must \
|
||||
come before everything else \
|
||||
(including TOML configuration blocks).",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn section_directive_must_appear_before_embedded_files() {
|
||||
let source = dedent(
|
||||
"
|
||||
# Some header
|
||||
|
||||
```py
|
||||
x = 1
|
||||
```
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
",
|
||||
);
|
||||
let err = super::parse("file.md", &source).expect_err("Should fail to parse");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"Section config to enable snapshotting diagnostics must \
|
||||
come before everything else \
|
||||
(including embedded files).",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2174,3 +2174,68 @@ fn flake8_import_convention_unused_aliased_import() {
|
||||
.arg("-")
|
||||
.pass_stdin("1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flake8_import_convention_unused_aliased_import_no_conflict() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.arg("--config")
|
||||
.arg(r#"lint.isort.required-imports = ["import pandas as pd"]"#)
|
||||
.args(["--select", "I002,ICN001,F401"])
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("--unsafe-fixes")
|
||||
.arg("--fix")
|
||||
.arg("-")
|
||||
.pass_stdin("1"));
|
||||
}
|
||||
|
||||
/// Test that private, old-style `TypeVar` generics
|
||||
/// 1. Get replaced with PEP 695 type parameters (UP046, UP047)
|
||||
/// 2. Get renamed to remove leading underscores (UP049)
|
||||
/// 3. Emit a warning that the standalone type variable is now unused (PYI018)
|
||||
/// 4. Remove the now-unused `Generic` import
|
||||
#[test]
|
||||
fn pep695_generic_rename() {
|
||||
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
|
||||
.args(STDIN_BASE_OPTIONS)
|
||||
.args(["--select", "F401,PYI018,UP046,UP047,UP049"])
|
||||
.args(["--stdin-filename", "test.py"])
|
||||
.arg("--unsafe-fixes")
|
||||
.arg("--fix")
|
||||
.arg("--preview")
|
||||
.arg("--target-version=py312")
|
||||
.arg("-")
|
||||
.pass_stdin(
|
||||
r#"
|
||||
from typing import Generic, TypeVar
|
||||
_T = TypeVar("_T")
|
||||
|
||||
class OldStyle(Generic[_T]):
|
||||
var: _T
|
||||
|
||||
def func(t: _T) -> _T:
|
||||
x: _T
|
||||
return x
|
||||
"#
|
||||
),
|
||||
@r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
|
||||
from typing import TypeVar
|
||||
_T = TypeVar("_T")
|
||||
|
||||
class OldStyle[T]:
|
||||
var: T
|
||||
|
||||
def func[T](t: T) -> T:
|
||||
x: T
|
||||
return x
|
||||
|
||||
----- stderr -----
|
||||
test.py:3:1: PYI018 Private TypeVar `_T` is never used
|
||||
Found 6 errors (5 fixed, 1 remaining).
|
||||
"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
source: crates/ruff/tests/lint.rs
|
||||
info:
|
||||
program: ruff
|
||||
args:
|
||||
- check
|
||||
- "--no-cache"
|
||||
- "--output-format"
|
||||
- concise
|
||||
- "--config"
|
||||
- "lint.isort.required-imports = [\"import pandas as pd\"]"
|
||||
- "--select"
|
||||
- "I002,ICN001,F401"
|
||||
- "--stdin-filename"
|
||||
- test.py
|
||||
- "--unsafe-fixes"
|
||||
- "--fix"
|
||||
- "-"
|
||||
stdin: "1"
|
||||
---
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
import pandas as pd
|
||||
1
|
||||
----- stderr -----
|
||||
Found 1 error (1 fixed, 0 remaining).
|
||||
@@ -375,6 +375,50 @@ impl Diagnostic for Box<dyn Diagnostic> {
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for &'_ dyn Diagnostic {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
impl Diagnostic for std::sync::Arc<dyn Diagnostic> {
|
||||
fn id(&self) -> DiagnosticId {
|
||||
(**self).id()
|
||||
}
|
||||
|
||||
fn message(&self) -> Cow<str> {
|
||||
(**self).message()
|
||||
}
|
||||
|
||||
fn file(&self) -> Option<File> {
|
||||
(**self).file()
|
||||
}
|
||||
|
||||
fn range(&self) -> Option<TextRange> {
|
||||
(**self).range()
|
||||
}
|
||||
|
||||
fn severity(&self) -> Severity {
|
||||
(**self).severity()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParseDiagnostic {
|
||||
file: File,
|
||||
|
||||
@@ -16,6 +16,17 @@ list((2 * x for x in range(3)))
|
||||
list(((2 * x for x in range(3))))
|
||||
list((((2 * x for x in range(3)))))
|
||||
|
||||
# Account for trailing comma in fix
|
||||
# See https://github.com/astral-sh/ruff/issues/15852
|
||||
list((0 for _ in []),)
|
||||
list(
|
||||
(0 for _ in [])
|
||||
# some comments
|
||||
,
|
||||
# some more
|
||||
)
|
||||
|
||||
|
||||
# Not built-in list.
|
||||
def list(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@@ -26,6 +26,16 @@ set((2 * x for x in range(3)))
|
||||
set(((2 * x for x in range(3))))
|
||||
set((((2 * x for x in range(3)))))
|
||||
|
||||
# Account for trailing comma in fix
|
||||
# See https://github.com/astral-sh/ruff/issues/15852
|
||||
set((0 for _ in []),)
|
||||
set(
|
||||
(0 for _ in [])
|
||||
# some comments
|
||||
,
|
||||
# some more
|
||||
)
|
||||
|
||||
# Not built-in set.
|
||||
def set(*args, **kwargs):
|
||||
return None
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import TypeVar, Self, Type
|
||||
from typing import TypeVar, Self, Type, cast
|
||||
|
||||
_S = TypeVar("_S", bound=BadClass)
|
||||
_S2 = TypeVar("_S2", BadClass, GoodClass)
|
||||
@@ -56,7 +56,7 @@ class CustomClassMethod:
|
||||
|
||||
_S695 = TypeVar("_S695", bound="PEP695Fix")
|
||||
|
||||
# Only .pyi gets fixes, no fixes for .py
|
||||
|
||||
class PEP695Fix:
|
||||
def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
|
||||
@@ -139,3 +139,38 @@ class NoReturnAnnotations:
|
||||
class MultipleBoundParameters:
|
||||
def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
|
||||
class MethodsWithBody:
|
||||
def m[S](self: S, other: S) -> S:
|
||||
x: S = other
|
||||
return x
|
||||
|
||||
@classmethod
|
||||
def n[S](cls: type[S], other: S) -> S:
|
||||
x: type[S] = type(other)
|
||||
return x()
|
||||
|
||||
class StringizedReferencesCanBeFixed:
|
||||
def m[S](self: S) -> S:
|
||||
x = cast("list[tuple[S, S]]", self)
|
||||
return x
|
||||
|
||||
class ButStrangeStringizedReferencesCannotBeFixed:
|
||||
def m[_T](self: _T) -> _T:
|
||||
x = cast('list[_\x54]', self)
|
||||
return x
|
||||
|
||||
class DeletionsAreNotTouched:
|
||||
def m[S](self: S) -> S:
|
||||
# `S` is not a local variable here, and `del` can only be used with local variables,
|
||||
# so `del S` here is not actually a reference to the type variable `S`.
|
||||
# This `del` statement is therefore not touched by the autofix (it raises `UnboundLocalError`
|
||||
# both before and after the autofix)
|
||||
del S
|
||||
return self
|
||||
|
||||
class NamesShadowingTypeVarAreNotTouched:
|
||||
def m[S](self: S) -> S:
|
||||
type S = int
|
||||
print(S) # not a reference to the type variable, so not touched by the autofix
|
||||
return 42
|
||||
|
||||
@@ -56,7 +56,7 @@ class CustomClassMethod:
|
||||
|
||||
_S695 = TypeVar("_S695", bound="PEP695Fix")
|
||||
|
||||
# Only .pyi gets fixes, no fixes for .py
|
||||
|
||||
class PEP695Fix:
|
||||
def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
|
||||
|
||||
@@ -9,3 +9,21 @@ class Class:
|
||||
|
||||
def func(_, setUp):
|
||||
return _, setUp
|
||||
|
||||
|
||||
from typing import override
|
||||
|
||||
class Extended(Class):
|
||||
@override
|
||||
def method(self, _, a, A): ...
|
||||
|
||||
|
||||
@override # Incorrect usage
|
||||
def func(_, a, A): ...
|
||||
|
||||
|
||||
func = lambda _, a, A: ...
|
||||
|
||||
|
||||
class Extended(Class):
|
||||
method = override(lambda self, _, a, A: ...) # Incorrect usage
|
||||
|
||||
30
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP049_0.py
vendored
Normal file
30
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP049_0.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
# simple case, replace _T in signature and body
|
||||
class Generic[_T]:
|
||||
buf: list[_T]
|
||||
|
||||
def append(self, t: _T):
|
||||
self.buf.append(t)
|
||||
|
||||
|
||||
# simple case, replace _T in signature and body
|
||||
def second[_T](var: tuple[_T]) -> _T:
|
||||
y: _T = var[1]
|
||||
return y
|
||||
|
||||
|
||||
# one diagnostic for each variable, comments are preserved
|
||||
def many_generics[
|
||||
_T, # first generic
|
||||
_U, # second generic
|
||||
](args):
|
||||
return args
|
||||
|
||||
|
||||
# neither of these are currently renamed
|
||||
from typing import Literal, cast
|
||||
|
||||
|
||||
def f[_T](v):
|
||||
cast("_T", v)
|
||||
cast("Literal['_T']")
|
||||
cast("list[_T]", v)
|
||||
56
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP049_1.py
vendored
Normal file
56
crates/ruff_linter/resources/test/fixtures/pyupgrade/UP049_1.py
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
# bound
|
||||
class Foo[_T: str]:
|
||||
var: _T
|
||||
|
||||
|
||||
# constraint
|
||||
class Foo[_T: (str, bytes)]:
|
||||
var: _T
|
||||
|
||||
|
||||
# python 3.13+ default
|
||||
class Foo[_T = int]:
|
||||
var: _T
|
||||
|
||||
|
||||
# tuple
|
||||
class Foo[*_Ts]:
|
||||
var: tuple[*_Ts]
|
||||
|
||||
|
||||
# paramspec
|
||||
class C[**_P]:
|
||||
var: _P
|
||||
|
||||
|
||||
from typing import Callable
|
||||
|
||||
|
||||
# each of these will get a separate diagnostic, but at least they'll all get
|
||||
# fixed
|
||||
class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
@staticmethod
|
||||
def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
return None
|
||||
|
||||
|
||||
# this should not be fixed because the new name is a keyword, but we still
|
||||
# offer a diagnostic
|
||||
class F[_async]: ...
|
||||
|
||||
|
||||
# and this should not be fixed because of the conflict with the outer X, but it
|
||||
# also gets a diagnostic
|
||||
def f():
|
||||
type X = int
|
||||
|
||||
class ScopeConflict[_X]:
|
||||
var: _X
|
||||
x: X
|
||||
|
||||
|
||||
# these cases should be skipped entirely
|
||||
def f[_](x: _) -> _: ...
|
||||
def g[__](x: __) -> __: ...
|
||||
def h[_T_](x: _T_) -> _T_: ...
|
||||
def i[__T__](x: __T__) -> __T__: ...
|
||||
@@ -76,6 +76,27 @@ def _():
|
||||
f.write(())
|
||||
|
||||
|
||||
def _():
|
||||
# https://github.com/astral-sh/ruff/issues/15936
|
||||
with open("file", "w") as f:
|
||||
for char in "a", "b":
|
||||
f.write(char)
|
||||
|
||||
def _():
|
||||
# https://github.com/astral-sh/ruff/issues/15936
|
||||
with open("file", "w") as f:
|
||||
for char in "a", "b":
|
||||
f.write(f"{char}")
|
||||
|
||||
def _():
|
||||
with open("file", "w") as f:
|
||||
for char in (
|
||||
"a", # Comment
|
||||
"b"
|
||||
):
|
||||
f.write(f"{char}")
|
||||
|
||||
|
||||
# OK
|
||||
|
||||
def _():
|
||||
|
||||
@@ -31,6 +31,20 @@ for x in (1, 2, 3):
|
||||
for x in (1, 2, 3):
|
||||
s.add(x + num)
|
||||
|
||||
# https://github.com/astral-sh/ruff/issues/15936
|
||||
for x in 1, 2, 3:
|
||||
s.add(x)
|
||||
|
||||
for x in 1, 2, 3:
|
||||
s.add(f"{x}")
|
||||
|
||||
for x in (
|
||||
1, # Comment
|
||||
2, 3
|
||||
):
|
||||
s.add(f"{x}")
|
||||
|
||||
|
||||
# False negative
|
||||
|
||||
class C:
|
||||
@@ -41,6 +55,7 @@ c = C()
|
||||
for x in (1, 2, 3):
|
||||
c.s.add(x)
|
||||
|
||||
|
||||
# Ok
|
||||
|
||||
s.update(x for x in (1, 2, 3))
|
||||
|
||||
@@ -26,6 +26,23 @@ type(None) != type(foo)
|
||||
|
||||
type(None) != type(None)
|
||||
|
||||
type(a.b) is type(None)
|
||||
|
||||
type(
|
||||
a(
|
||||
# Comment
|
||||
)
|
||||
) != type(None)
|
||||
|
||||
type(
|
||||
a := 1
|
||||
) == type(None)
|
||||
|
||||
type(
|
||||
a for a in range(0)
|
||||
) is not type(None)
|
||||
|
||||
|
||||
# Ok.
|
||||
|
||||
foo is None
|
||||
|
||||
@@ -23,3 +23,14 @@ class B:
|
||||
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
|
||||
perfectly_fine: list[int] = field(default_factory=list)
|
||||
class_variable: ClassVar[list[int]] = []
|
||||
|
||||
# Lint should account for deferred annotations
|
||||
# See https://github.com/astral-sh/ruff/issues/15857
|
||||
@dataclass
|
||||
class AWithQuotes:
|
||||
mutable_default: 'list[int]' = []
|
||||
immutable_annotation: 'typing.Sequence[int]' = []
|
||||
without_annotation = []
|
||||
correct_code: 'list[int]' = KNOWINGLY_MUTABLE_DEFAULT
|
||||
perfectly_fine: 'list[int]' = field(default_factory=list)
|
||||
class_variable: 'typing.ClassVar[list[int]]'= []
|
||||
|
||||
19
crates/ruff_linter/resources/test/fixtures/ruff/RUF008_deferred.py
vendored
Normal file
19
crates/ruff_linter/resources/test/fixtures/ruff/RUF008_deferred.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
# Lint should account for deferred annotations
|
||||
# See https://github.com/astral-sh/ruff/issues/15857
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class Example():
|
||||
"""Class that uses ClassVar."""
|
||||
|
||||
options: ClassVar[dict[str, str]] = {}
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import ClassVar
|
||||
|
||||
15
crates/ruff_linter/resources/test/fixtures/ruff/RUF009_deferred.py
vendored
Normal file
15
crates/ruff_linter/resources/test/fixtures/ruff/RUF009_deferred.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
def default_function() ->list[int]:
|
||||
return []
|
||||
|
||||
@dataclass()
|
||||
class A:
|
||||
hidden_mutable_default: list[int] = default_function()
|
||||
class_variable: typing.ClassVar[list[int]] = default_function()
|
||||
another_class_var: ClassVar[list[int]] = default_function()
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import ClassVar
|
||||
@@ -103,3 +103,18 @@ class K(SQLModel):
|
||||
class L(SQLModel):
|
||||
id: int
|
||||
i_j: list[K] = list()
|
||||
|
||||
# Lint should account for deferred annotations
|
||||
# See https://github.com/astral-sh/ruff/issues/15857
|
||||
class AWithQuotes:
|
||||
__slots__ = {
|
||||
"mutable_default": "A mutable default value",
|
||||
}
|
||||
|
||||
mutable_default: 'list[int]' = []
|
||||
immutable_annotation: 'Sequence[int]'= []
|
||||
without_annotation = []
|
||||
class_variable: 'ClassVar[list[int]]' = []
|
||||
final_variable: 'Final[list[int]]' = []
|
||||
class_variable_without_subscript: 'ClassVar' = []
|
||||
final_variable_without_subscript: 'Final' = []
|
||||
|
||||
16
crates/ruff_linter/resources/test/fixtures/ruff/RUF012_deferred.py
vendored
Normal file
16
crates/ruff_linter/resources/test/fixtures/ruff/RUF012_deferred.py
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
# Lint should account for deferred annotations
|
||||
# See https://github.com/astral-sh/ruff/issues/15857
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
class Example():
|
||||
"""Class that uses ClassVar."""
|
||||
|
||||
options: ClassVar[dict[str, str]] = {}
|
||||
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import ClassVar
|
||||
@@ -5,7 +5,7 @@ use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{
|
||||
flake8_import_conventions, flake8_pyi, flake8_pytest_style, flake8_type_checking, pyflakes,
|
||||
pylint, refurb, ruff,
|
||||
pylint, pyupgrade, refurb, ruff,
|
||||
};
|
||||
|
||||
/// Run lint rules over the [`Binding`]s.
|
||||
@@ -24,6 +24,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
Rule::PytestUnittestRaisesAssertion,
|
||||
Rule::ForLoopWrites,
|
||||
Rule::CustomTypeVarForSelf,
|
||||
Rule::PrivateTypeParameter,
|
||||
]) {
|
||||
return;
|
||||
}
|
||||
@@ -123,5 +124,10 @@ pub(crate) fn bindings(checker: &mut Checker) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::PrivateTypeParameter) {
|
||||
if let Some(diagnostic) = pyupgrade::rules::private_type_parameter(checker, binding) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
Rule::AsyncioDanglingTask,
|
||||
Rule::BadStaticmethodArgument,
|
||||
Rule::BuiltinAttributeShadowing,
|
||||
Rule::FunctionCallInDataclassDefaultArgument,
|
||||
Rule::GlobalVariableNotAssigned,
|
||||
Rule::ImportPrivateName,
|
||||
Rule::ImportShadowedByLoopVar,
|
||||
Rule::InvalidFirstArgumentNameForClassMethod,
|
||||
Rule::InvalidFirstArgumentNameForMethod,
|
||||
Rule::MutableClassDefault,
|
||||
Rule::MutableDataclassDefault,
|
||||
Rule::NoSelfUse,
|
||||
Rule::RedefinedArgumentFromLocal,
|
||||
Rule::RedefinedWhileUnused,
|
||||
@@ -380,6 +383,19 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) {
|
||||
ruff::rules::function_call_in_dataclass_default(
|
||||
checker,
|
||||
class_def,
|
||||
&mut diagnostics,
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::MutableClassDefault) {
|
||||
ruff::rules::mutable_class_default(checker, class_def, &mut diagnostics);
|
||||
}
|
||||
if checker.enabled(Rule::MutableDataclassDefault) {
|
||||
ruff::rules::mutable_dataclass_default(checker, class_def, &mut diagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
|
||||
|
||||
@@ -1746,9 +1746,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
|
||||
ruff::rules::parenthesize_chained_logical_operators(checker, bool_op);
|
||||
}
|
||||
}
|
||||
Expr::Lambda(lambda_expr) => {
|
||||
Expr::Lambda(lambda) => {
|
||||
if checker.enabled(Rule::ReimplementedOperator) {
|
||||
refurb::rules::reimplemented_operator(checker, &lambda_expr.into());
|
||||
refurb::rules::reimplemented_operator(checker, &lambda.into());
|
||||
}
|
||||
if checker.enabled(Rule::InvalidArgumentName) {
|
||||
pep8_naming::rules::invalid_argument_name_lambda(checker, lambda);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -3,7 +3,7 @@ use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::codes::Rule;
|
||||
use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
|
||||
use crate::rules::{flake8_builtins, pycodestyle};
|
||||
|
||||
/// Run lint rules over a [`Parameter`] syntax node.
|
||||
pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
|
||||
@@ -14,15 +14,6 @@ pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
|
||||
parameter.name.range(),
|
||||
);
|
||||
}
|
||||
if checker.enabled(Rule::InvalidArgumentName) {
|
||||
if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name(
|
||||
¶meter.name,
|
||||
parameter,
|
||||
&checker.settings.pep8_naming.ignore_names,
|
||||
) {
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
if checker.enabled(Rule::BuiltinArgumentShadowing) {
|
||||
flake8_builtins::rules::builtin_argument_shadowing(checker, parameter);
|
||||
}
|
||||
|
||||
@@ -379,6 +379,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NonPEP695GenericFunction) {
|
||||
pyupgrade::rules::non_pep695_generic_function(checker, function_def);
|
||||
}
|
||||
if checker.enabled(Rule::InvalidArgumentName) {
|
||||
pep8_naming::rules::invalid_argument_name_function(checker, function_def);
|
||||
}
|
||||
}
|
||||
Stmt::Return(_) => {
|
||||
if checker.enabled(Rule::ReturnOutsideFunction) {
|
||||
@@ -512,15 +515,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
|
||||
if checker.enabled(Rule::NonUniqueEnums) {
|
||||
flake8_pie::rules::non_unique_enums(checker, stmt, body);
|
||||
}
|
||||
if checker.enabled(Rule::MutableClassDefault) {
|
||||
ruff::rules::mutable_class_default(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::MutableDataclassDefault) {
|
||||
ruff::rules::mutable_dataclass_default(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::FunctionCallInDataclassDefaultArgument) {
|
||||
ruff::rules::function_call_in_dataclass_default(checker, class_def);
|
||||
}
|
||||
if checker.enabled(Rule::FStringDocstring) {
|
||||
flake8_bugbear::rules::f_string_docstring(checker, body);
|
||||
}
|
||||
|
||||
@@ -542,6 +542,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
|
||||
(Pyupgrade, "045") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP604AnnotationOptional),
|
||||
(Pyupgrade, "046") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericClass),
|
||||
(Pyupgrade, "047") => (RuleGroup::Preview, rules::pyupgrade::rules::NonPEP695GenericFunction),
|
||||
(Pyupgrade, "049") => (RuleGroup::Preview, rules::pyupgrade::rules::PrivateTypeParameter),
|
||||
|
||||
// pydocstyle
|
||||
(Pydocstyle, "100") => (RuleGroup::Stable, rules::pydocstyle::rules::UndocumentedPublicModule),
|
||||
|
||||
@@ -7,8 +7,11 @@ use ruff_diagnostics::Edit;
|
||||
use ruff_python_ast as ast;
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
|
||||
use ruff_python_stdlib::{builtins::is_python_builtin, keyword::is_keyword};
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
pub(crate) struct Renamer;
|
||||
|
||||
impl Renamer {
|
||||
@@ -369,3 +372,52 @@ impl Renamer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enumeration of various ways in which a binding can shadow other variables
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub(crate) enum ShadowedKind {
|
||||
/// The variable shadows a global, nonlocal or local symbol
|
||||
Some,
|
||||
/// The variable shadows a builtin symbol
|
||||
BuiltIn,
|
||||
/// The variable shadows a keyword
|
||||
Keyword,
|
||||
/// The variable does not shadow any other symbols
|
||||
None,
|
||||
}
|
||||
|
||||
impl ShadowedKind {
|
||||
/// Determines the kind of shadowing or conflict for a given variable name.
|
||||
///
|
||||
/// This function is useful for checking whether or not the `target` of a [`Rename::rename`]
|
||||
/// will shadow another binding.
|
||||
pub(crate) fn new(name: &str, checker: &Checker, scope_id: ScopeId) -> ShadowedKind {
|
||||
// Check the kind in order of precedence
|
||||
if is_keyword(name) {
|
||||
return ShadowedKind::Keyword;
|
||||
}
|
||||
|
||||
if is_python_builtin(
|
||||
name,
|
||||
checker.settings.target_version.minor(),
|
||||
checker.source_type.is_ipynb(),
|
||||
) {
|
||||
return ShadowedKind::BuiltIn;
|
||||
}
|
||||
|
||||
if !checker.semantic().is_available_in_scope(name, scope_id) {
|
||||
return ShadowedKind::Some;
|
||||
}
|
||||
|
||||
// Default to no shadowing
|
||||
ShadowedKind::None
|
||||
}
|
||||
|
||||
/// Returns `true` if `self` shadows any global, nonlocal, or local symbol, keyword, or builtin.
|
||||
pub(crate) const fn shadows_any(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
ShadowedKind::Some | ShadowedKind::BuiltIn | ShadowedKind::Keyword
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,8 @@ use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::ExprGenerator;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
@@ -123,11 +124,14 @@ pub(crate) fn unnecessary_generator_list(checker: &mut Checker, call: &ast::Expr
|
||||
);
|
||||
|
||||
// Replace `)` with `]`.
|
||||
let call_end = Edit::replacement(
|
||||
"]".to_string(),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
call.end(),
|
||||
);
|
||||
// Place `]` at argument's end or at trailing comma if present
|
||||
let mut tokenizer =
|
||||
SimpleTokenizer::new(checker.source(), TextRange::new(argument.end(), call.end()));
|
||||
let right_bracket_loc = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Comma)
|
||||
.map_or(call.arguments.end(), |comma| comma.end())
|
||||
- TextSize::from(1);
|
||||
let call_end = Edit::replacement("]".to_string(), right_bracket_loc, call.end());
|
||||
|
||||
// Remove the inner parentheses, if the expression is a generator. The easiest way to do
|
||||
// this reliably is to use the printer.
|
||||
|
||||
@@ -4,7 +4,8 @@ use ruff_python_ast as ast;
|
||||
use ruff_python_ast::comparable::ComparableExpr;
|
||||
use ruff_python_ast::parenthesize::parenthesized_range;
|
||||
use ruff_python_ast::ExprGenerator;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::flake8_comprehensions::fixes::{pad_end, pad_start};
|
||||
@@ -126,9 +127,16 @@ pub(crate) fn unnecessary_generator_set(checker: &mut Checker, call: &ast::ExprC
|
||||
);
|
||||
|
||||
// Replace `)` with `}`.
|
||||
// Place `}` at argument's end or at trailing comma if present
|
||||
let mut tokenizer =
|
||||
SimpleTokenizer::new(checker.source(), TextRange::new(argument.end(), call.end()));
|
||||
let right_brace_loc = tokenizer
|
||||
.find(|token| token.kind == SimpleTokenKind::Comma)
|
||||
.map_or(call.arguments.end(), |comma| comma.end())
|
||||
- TextSize::from(1);
|
||||
let call_end = Edit::replacement(
|
||||
pad_end("}", call.range(), checker.locator(), checker.semantic()),
|
||||
call.arguments.end() - TextSize::from(1),
|
||||
right_brace_loc,
|
||||
call.end(),
|
||||
);
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ C400.py:16:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
||||
16 |+[2 * x for x in range(3)]
|
||||
17 17 | list((((2 * x for x in range(3)))))
|
||||
18 18 |
|
||||
19 19 | # Not built-in list.
|
||||
19 19 | # Account for trailing comma in fix
|
||||
|
||||
C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
||||
|
|
||||
@@ -136,7 +136,7 @@ C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
||||
17 | list((((2 * x for x in range(3)))))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
18 |
|
||||
19 | # Not built-in list.
|
||||
19 | # Account for trailing comma in fix
|
||||
|
|
||||
= help: Rewrite as a list comprehension
|
||||
|
||||
@@ -147,5 +147,57 @@ C400.py:17:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
||||
17 |-list((((2 * x for x in range(3)))))
|
||||
17 |+[2 * x for x in range(3)]
|
||||
18 18 |
|
||||
19 19 | # Not built-in list.
|
||||
20 20 | def list(*args, **kwargs):
|
||||
19 19 | # Account for trailing comma in fix
|
||||
20 20 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
|
||||
C400.py:21:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
||||
|
|
||||
19 | # Account for trailing comma in fix
|
||||
20 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
21 | list((0 for _ in []),)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^ C400
|
||||
22 | list(
|
||||
23 | (0 for _ in [])
|
||||
|
|
||||
= help: Rewrite as a list comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
18 18 |
|
||||
19 19 | # Account for trailing comma in fix
|
||||
20 20 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
21 |-list((0 for _ in []),)
|
||||
21 |+[0 for _ in []]
|
||||
22 22 | list(
|
||||
23 23 | (0 for _ in [])
|
||||
24 24 | # some comments
|
||||
|
||||
C400.py:22:1: C400 [*] Unnecessary generator (rewrite as a list comprehension)
|
||||
|
|
||||
20 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
21 | list((0 for _ in []),)
|
||||
22 | / list(
|
||||
23 | | (0 for _ in [])
|
||||
24 | | # some comments
|
||||
25 | | ,
|
||||
26 | | # some more
|
||||
27 | | )
|
||||
| |__^ C400
|
||||
|
|
||||
= help: Rewrite as a list comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
19 19 | # Account for trailing comma in fix
|
||||
20 20 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
21 21 | list((0 for _ in []),)
|
||||
22 |-list(
|
||||
23 |- (0 for _ in [])
|
||||
22 |+[
|
||||
23 |+ 0 for _ in []
|
||||
24 24 | # some comments
|
||||
25 |- ,
|
||||
26 |- # some more
|
||||
27 |- )
|
||||
25 |+ ]
|
||||
28 26 |
|
||||
29 27 |
|
||||
30 28 | # Not built-in list.
|
||||
|
||||
@@ -290,7 +290,7 @@ C401.py:26:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
||||
26 |+{2 * x for x in range(3)}
|
||||
27 27 | set((((2 * x for x in range(3)))))
|
||||
28 28 |
|
||||
29 29 | # Not built-in set.
|
||||
29 29 | # Account for trailing comma in fix
|
||||
|
||||
C401.py:27:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
||||
|
|
||||
@@ -299,7 +299,7 @@ C401.py:27:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
||||
27 | set((((2 * x for x in range(3)))))
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ C401
|
||||
28 |
|
||||
29 | # Not built-in set.
|
||||
29 | # Account for trailing comma in fix
|
||||
|
|
||||
= help: Rewrite as a set comprehension
|
||||
|
||||
@@ -310,5 +310,59 @@ C401.py:27:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
||||
27 |-set((((2 * x for x in range(3)))))
|
||||
27 |+{2 * x for x in range(3)}
|
||||
28 28 |
|
||||
29 29 | # Not built-in set.
|
||||
30 30 | def set(*args, **kwargs):
|
||||
29 29 | # Account for trailing comma in fix
|
||||
30 30 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
|
||||
C401.py:31:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
||||
|
|
||||
29 | # Account for trailing comma in fix
|
||||
30 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
31 | set((0 for _ in []),)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ C401
|
||||
32 | set(
|
||||
33 | (0 for _ in [])
|
||||
|
|
||||
= help: Rewrite as a set comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
28 28 |
|
||||
29 29 | # Account for trailing comma in fix
|
||||
30 30 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
31 |-set((0 for _ in []),)
|
||||
31 |+{0 for _ in []}
|
||||
32 32 | set(
|
||||
33 33 | (0 for _ in [])
|
||||
34 34 | # some comments
|
||||
|
||||
C401.py:32:1: C401 [*] Unnecessary generator (rewrite as a set comprehension)
|
||||
|
|
||||
30 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
31 | set((0 for _ in []),)
|
||||
32 | / set(
|
||||
33 | | (0 for _ in [])
|
||||
34 | | # some comments
|
||||
35 | | ,
|
||||
36 | | # some more
|
||||
37 | | )
|
||||
| |_^ C401
|
||||
38 |
|
||||
39 | # Not built-in set.
|
||||
|
|
||||
= help: Rewrite as a set comprehension
|
||||
|
||||
ℹ Unsafe fix
|
||||
29 29 | # Account for trailing comma in fix
|
||||
30 30 | # See https://github.com/astral-sh/ruff/issues/15852
|
||||
31 31 | set((0 for _ in []),)
|
||||
32 |-set(
|
||||
33 |- (0 for _ in [])
|
||||
32 |+{
|
||||
33 |+ 0 for _ in []
|
||||
34 34 | # some comments
|
||||
35 |- ,
|
||||
36 |- # some more
|
||||
37 |-)
|
||||
35 |+ }
|
||||
38 36 |
|
||||
39 37 | # Not built-in set.
|
||||
40 38 | def set(*args, **kwargs):
|
||||
|
||||
@@ -14,8 +14,8 @@ use crate::importer::{ImportRequest, ResolutionError};
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for methods that use custom `TypeVar`s in their annotations
|
||||
/// when they could use `Self` instead.
|
||||
/// Checks for methods that use custom [`TypeVar`s][typing_TypeVar] in their
|
||||
/// annotations when they could use [`Self`][Self] instead.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// While the semantics are often identical, using `Self` is more intuitive
|
||||
@@ -49,10 +49,11 @@ use crate::settings::types::PythonVersion;
|
||||
/// def bar(cls, arg: int) -> Self: ...
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix is only available in stub files.
|
||||
/// It will try to remove all usages and declarations of the custom type variable.
|
||||
/// Pre-[PEP-695]-style declarations will not be removed.
|
||||
/// ## Fix behaviour and safety
|
||||
/// The fix removes all usages and declarations of the custom type variable.
|
||||
/// [PEP-695]-style `TypeVar` declarations are also removed from the [type parameter list];
|
||||
/// however, old-style `TypeVar`s do not have their declarations removed. See
|
||||
/// [`unused-private-type-var`][PYI018] for a rule to clean up unused private type variables.
|
||||
///
|
||||
/// If there are any comments within the fix ranges, it will be marked as unsafe.
|
||||
/// Otherwise, it will be marked as safe.
|
||||
@@ -71,6 +72,10 @@ use crate::settings::types::PythonVersion;
|
||||
///
|
||||
/// [PEP 673]: https://peps.python.org/pep-0673/#motivation
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
/// [type parameter list]: https://docs.python.org/3/reference/compound_stmts.html#type-params
|
||||
/// [Self]: https://docs.python.org/3/library/typing.html#typing.Self
|
||||
/// [typing_TypeVar]: https://docs.python.org/3/library/typing.html#typing.TypeVar
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct CustomTypeVarForSelf {
|
||||
typevar_name: String,
|
||||
@@ -199,7 +204,7 @@ pub(crate) fn custom_type_var_instead_of_self(
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
CustomTypeVarForSelf {
|
||||
typevar_name: custom_typevar.name(checker).to_string(),
|
||||
typevar_name: custom_typevar.name(checker.source()).to_string(),
|
||||
},
|
||||
diagnostic_range,
|
||||
);
|
||||
@@ -211,7 +216,6 @@ pub(crate) fn custom_type_var_instead_of_self(
|
||||
custom_typevar,
|
||||
self_or_cls_parameter,
|
||||
self_or_cls_annotation,
|
||||
function_header_end,
|
||||
)
|
||||
});
|
||||
|
||||
@@ -466,7 +470,7 @@ fn custom_typevar_preview<'a>(
|
||||
///
|
||||
/// * Import `Self` if necessary
|
||||
/// * Remove the first parameter's annotation
|
||||
/// * Replace other uses of the original type variable elsewhere in the signature with `Self`
|
||||
/// * Replace other uses of the original type variable elsewhere in the function with `Self`
|
||||
/// * If it was a PEP-695 type variable, removes that `TypeVar` from the PEP-695 type-parameter list
|
||||
fn replace_custom_typevar_with_self(
|
||||
checker: &Checker,
|
||||
@@ -474,19 +478,11 @@ fn replace_custom_typevar_with_self(
|
||||
custom_typevar: TypeVar,
|
||||
self_or_cls_parameter: &ast::ParameterWithDefault,
|
||||
self_or_cls_annotation: &ast::Expr,
|
||||
function_header_end: TextSize,
|
||||
) -> anyhow::Result<Option<Fix>> {
|
||||
if checker.settings.preview.is_disabled() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// This fix cannot be suggested for non-stubs,
|
||||
// as a non-stub fix would have to deal with references in body/at runtime as well,
|
||||
// which is substantially harder and requires a type-aware backend.
|
||||
if !checker.source_type.is_stub() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// (1) Import `Self` (if necessary)
|
||||
let (import_edit, self_symbol_binding) = import_self(checker, function_def.start())?;
|
||||
|
||||
@@ -506,18 +502,18 @@ fn replace_custom_typevar_with_self(
|
||||
other_edits.push(deletion_edit);
|
||||
}
|
||||
|
||||
// (4) Replace all other references to the original type variable elsewhere in the function's header
|
||||
// with `Self`
|
||||
let replace_references_range =
|
||||
TextRange::new(self_or_cls_annotation.end(), function_header_end);
|
||||
// (4) Replace all other references to the original type variable elsewhere in the function with `Self`
|
||||
let replace_references_range = TextRange::new(self_or_cls_annotation.end(), function_def.end());
|
||||
|
||||
other_edits.extend(replace_typevar_usages_with_self(
|
||||
replace_typevar_usages_with_self(
|
||||
custom_typevar,
|
||||
checker.source(),
|
||||
self_or_cls_annotation.range(),
|
||||
&self_symbol_binding,
|
||||
replace_references_range,
|
||||
checker.semantic(),
|
||||
));
|
||||
&mut other_edits,
|
||||
)?;
|
||||
|
||||
// (5) Determine the safety of the fixes as a whole
|
||||
let comment_ranges = checker.comment_ranges();
|
||||
@@ -562,21 +558,35 @@ fn import_self(checker: &Checker, position: TextSize) -> Result<(Edit, String),
|
||||
/// This ensures that no edit in this series will overlap with other edits.
|
||||
fn replace_typevar_usages_with_self<'a>(
|
||||
typevar: TypeVar<'a>,
|
||||
source: &'a str,
|
||||
self_or_cls_annotation_range: TextRange,
|
||||
self_symbol_binding: &'a str,
|
||||
editable_range: TextRange,
|
||||
semantic: &'a SemanticModel<'a>,
|
||||
) -> impl Iterator<Item = Edit> + 'a {
|
||||
typevar
|
||||
.references(semantic)
|
||||
.map(Ranged::range)
|
||||
.filter(move |reference_range| editable_range.contains_range(*reference_range))
|
||||
.filter(move |reference_range| {
|
||||
!self_or_cls_annotation_range.contains_range(*reference_range)
|
||||
})
|
||||
.map(|reference_range| {
|
||||
Edit::range_replacement(self_symbol_binding.to_string(), reference_range)
|
||||
})
|
||||
edits: &mut Vec<Edit>,
|
||||
) -> anyhow::Result<()> {
|
||||
let tvar_name = typevar.name(source);
|
||||
for reference in typevar.references(semantic) {
|
||||
let reference_range = reference.range();
|
||||
if &source[reference_range] != tvar_name {
|
||||
bail!(
|
||||
"Cannot autofix: feference in the source code (`{}`) is not equal to the typevar name (`{}`)",
|
||||
&source[reference_range],
|
||||
tvar_name
|
||||
);
|
||||
}
|
||||
if !editable_range.contains_range(reference_range) {
|
||||
continue;
|
||||
}
|
||||
if self_or_cls_annotation_range.contains_range(reference_range) {
|
||||
continue;
|
||||
}
|
||||
edits.push(Edit::range_replacement(
|
||||
self_symbol_binding.to_string(),
|
||||
reference_range,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create an [`Edit`] removing the `TypeVar` binding from the PEP 695 type parameter list.
|
||||
@@ -624,8 +634,8 @@ impl<'a> TypeVar<'a> {
|
||||
self.0.kind.is_type_param()
|
||||
}
|
||||
|
||||
fn name(self, checker: &'a Checker) -> &'a str {
|
||||
self.0.name(checker.source())
|
||||
fn name(self, source: &'a str) -> &'a str {
|
||||
self.0.name(source)
|
||||
}
|
||||
|
||||
fn references(
|
||||
|
||||
@@ -59,7 +59,6 @@ PYI019_0.py:54:32: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
||||
PYI019_0.py:61:48: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
59 | # Only .pyi gets fixes, no fixes for .py
|
||||
60 | class PEP695Fix:
|
||||
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
| ^ PYI019
|
||||
@@ -251,5 +250,67 @@ PYI019_0.py:141:63: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
140 | def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
| ^ PYI019
|
||||
142 |
|
||||
143 | class MethodsWithBody:
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:144:36: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
143 | class MethodsWithBody:
|
||||
144 | def m[S](self: S, other: S) -> S:
|
||||
| ^ PYI019
|
||||
145 | x: S = other
|
||||
146 | return x
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:149:41: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
148 | @classmethod
|
||||
149 | def n[S](cls: type[S], other: S) -> S:
|
||||
| ^ PYI019
|
||||
150 | x: type[S] = type(other)
|
||||
151 | return x()
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:154:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
153 | class StringizedReferencesCanBeFixed:
|
||||
154 | def m[S](self: S) -> S:
|
||||
| ^ PYI019
|
||||
155 | x = cast("list[tuple[S, S]]", self)
|
||||
156 | return x
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:159:28: PYI019 Use `Self` instead of custom TypeVar `_T`
|
||||
|
|
||||
158 | class ButStrangeStringizedReferencesCannotBeFixed:
|
||||
159 | def m[_T](self: _T) -> _T:
|
||||
| ^^ PYI019
|
||||
160 | x = cast('list[_\x54]', self)
|
||||
161 | return x
|
||||
|
|
||||
= help: Replace TypeVar `_T` with `Self`
|
||||
|
||||
PYI019_0.py:164:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
163 | class DeletionsAreNotTouched:
|
||||
164 | def m[S](self: S) -> S:
|
||||
| ^ PYI019
|
||||
165 | # `S` is not a local variable here, and `del` can only be used with local variables,
|
||||
166 | # so `del S` here is not actually a reference to the type variable `S`.
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:173:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
172 | class NamesShadowingTypeVarAreNotTouched:
|
||||
173 | def m[S](self: S) -> S:
|
||||
| ^ PYI019
|
||||
174 | type S = int
|
||||
175 | print(S) # not a reference to the type variable, so not touched by the autofix
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
@@ -59,7 +59,6 @@ PYI019_0.pyi:54:32: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
||||
PYI019_0.pyi:61:48: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
59 | # Only .pyi gets fixes, no fixes for .py
|
||||
60 | class PEP695Fix:
|
||||
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
| ^ PYI019
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
|
||||
---
|
||||
PYI019_0.py:7:16: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
PYI019_0.py:7:16: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
6 | class BadClass:
|
||||
7 | def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
|
||||
@@ -9,14 +9,34 @@ PYI019_0.py:7:16: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
= help: Replace TypeVar `_S` with `Self`
|
||||
|
||||
PYI019_0.py:10:28: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
ℹ Safe fix
|
||||
4 4 | _S2 = TypeVar("_S2", BadClass, GoodClass)
|
||||
5 5 |
|
||||
6 6 | class BadClass:
|
||||
7 |- def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
|
||||
7 |+ def __new__(cls, *args: str, **kwargs: int) -> Self: ... # PYI019
|
||||
8 8 |
|
||||
9 9 |
|
||||
10 10 | def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
|
||||
|
||||
PYI019_0.py:10:28: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
10 | def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
|
|
||||
= help: Replace TypeVar `_S` with `Self`
|
||||
|
||||
PYI019_0.py:14:25: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
ℹ Safe fix
|
||||
7 7 | def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
|
||||
8 8 |
|
||||
9 9 |
|
||||
10 |- def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
|
||||
10 |+ def bad_instance_method(self, arg: bytes) -> Self: ... # PYI019
|
||||
11 11 |
|
||||
12 12 |
|
||||
13 13 | @classmethod
|
||||
|
||||
PYI019_0.py:14:25: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
13 | @classmethod
|
||||
14 | def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
|
||||
@@ -24,7 +44,17 @@ PYI019_0.py:14:25: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
= help: Replace TypeVar `_S` with `Self`
|
||||
|
||||
PYI019_0.py:18:33: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
ℹ Safe fix
|
||||
11 11 |
|
||||
12 12 |
|
||||
13 13 | @classmethod
|
||||
14 |- def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
|
||||
14 |+ def bad_class_method(cls, arg: int) -> Self: ... # PYI019
|
||||
15 15 |
|
||||
16 16 |
|
||||
17 17 | @classmethod
|
||||
|
||||
PYI019_0.py:18:33: PYI019 [*] Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
17 | @classmethod
|
||||
18 | def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
|
||||
@@ -32,7 +62,17 @@ PYI019_0.py:18:33: PYI019 Use `Self` instead of custom TypeVar `_S`
|
||||
|
|
||||
= help: Replace TypeVar `_S` with `Self`
|
||||
|
||||
PYI019_0.py:39:14: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
15 15 |
|
||||
16 16 |
|
||||
17 17 | @classmethod
|
||||
18 |- def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
|
||||
18 |+ def bad_posonly_class_method(cls, /) -> Self: ... # PYI019
|
||||
19 19 |
|
||||
20 20 |
|
||||
21 21 | @classmethod
|
||||
|
||||
PYI019_0.py:39:14: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
37 | # Python > 3.12
|
||||
38 | class PEP695BadDunderNew[T]:
|
||||
@@ -41,14 +81,34 @@ PYI019_0.py:39:14: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:42:30: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
36 36 |
|
||||
37 37 | # Python > 3.12
|
||||
38 38 | class PEP695BadDunderNew[T]:
|
||||
39 |- def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
|
||||
39 |+ def __new__(cls, *args: Any, ** kwargs: Any) -> Self: ... # PYI019
|
||||
40 40 |
|
||||
41 41 |
|
||||
42 42 | def generic_instance_method[S](self: S) -> S: ... # PYI019
|
||||
|
||||
PYI019_0.py:42:30: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
42 | def generic_instance_method[S](self: S) -> S: ... # PYI019
|
||||
| ^^^^^^^^^^^^^^^^^ PYI019
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:54:11: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
39 39 | def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
|
||||
40 40 |
|
||||
41 41 |
|
||||
42 |- def generic_instance_method[S](self: S) -> S: ... # PYI019
|
||||
42 |+ def generic_instance_method(self) -> Self: ... # PYI019
|
||||
43 43 |
|
||||
44 44 |
|
||||
45 45 | class PEP695GoodDunderNew[T]:
|
||||
|
||||
PYI019_0.py:54:11: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
52 | # in the settings for this test:
|
||||
53 | @foo_classmethod
|
||||
@@ -57,9 +117,18 @@ PYI019_0.py:54:11: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:61:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
51 51 | # due to `foo_classmethod being listed in `pep8_naming.classmethod-decorators`
|
||||
52 52 | # in the settings for this test:
|
||||
53 53 | @foo_classmethod
|
||||
54 |- def foo[S](cls: type[S]) -> S: ... # PYI019
|
||||
54 |+ def foo(cls) -> Self: ... # PYI019
|
||||
55 55 |
|
||||
56 56 |
|
||||
57 57 | _S695 = TypeVar("_S695", bound="PEP695Fix")
|
||||
|
||||
PYI019_0.py:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
59 | # Only .pyi gets fixes, no fixes for .py
|
||||
60 | class PEP695Fix:
|
||||
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
@@ -68,7 +137,17 @@ PYI019_0.py:61:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:63:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
58 58 |
|
||||
59 59 |
|
||||
60 60 | class PEP695Fix:
|
||||
61 |- def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
61 |+ def __new__(cls) -> Self: ...
|
||||
62 62 |
|
||||
63 63 | def __init_subclass__[S](cls: type[S]) -> S: ...
|
||||
64 64 |
|
||||
|
||||
PYI019_0.py:63:26: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
62 |
|
||||
@@ -79,7 +158,17 @@ PYI019_0.py:63:26: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:65:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
60 60 | class PEP695Fix:
|
||||
61 61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
62 62 |
|
||||
63 |- def __init_subclass__[S](cls: type[S]) -> S: ...
|
||||
63 |+ def __init_subclass__(cls) -> Self: ...
|
||||
64 64 |
|
||||
65 65 | def __neg__[S: PEP695Fix](self: S) -> S: ...
|
||||
66 66 |
|
||||
|
||||
PYI019_0.py:65:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
63 | def __init_subclass__[S](cls: type[S]) -> S: ...
|
||||
64 |
|
||||
@@ -90,7 +179,17 @@ PYI019_0.py:65:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:67:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
62 62 |
|
||||
63 63 | def __init_subclass__[S](cls: type[S]) -> S: ...
|
||||
64 64 |
|
||||
65 |- def __neg__[S: PEP695Fix](self: S) -> S: ...
|
||||
65 |+ def __neg__(self) -> Self: ...
|
||||
66 66 |
|
||||
67 67 | def __pos__[S](self: S) -> S: ...
|
||||
68 68 |
|
||||
|
||||
PYI019_0.py:67:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
65 | def __neg__[S: PEP695Fix](self: S) -> S: ...
|
||||
66 |
|
||||
@@ -101,7 +200,17 @@ PYI019_0.py:67:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:69:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
64 64 |
|
||||
65 65 | def __neg__[S: PEP695Fix](self: S) -> S: ...
|
||||
66 66 |
|
||||
67 |- def __pos__[S](self: S) -> S: ...
|
||||
67 |+ def __pos__(self) -> Self: ...
|
||||
68 68 |
|
||||
69 69 | def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
70 70 |
|
||||
|
||||
PYI019_0.py:69:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
67 | def __pos__[S](self: S) -> S: ...
|
||||
68 |
|
||||
@@ -112,7 +221,17 @@ PYI019_0.py:69:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:71:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
66 66 |
|
||||
67 67 | def __pos__[S](self: S) -> S: ...
|
||||
68 68 |
|
||||
69 |- def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
69 |+ def __add__(self, other: Self) -> Self: ...
|
||||
70 70 |
|
||||
71 71 | def __sub__[S](self: S, other: S) -> S: ...
|
||||
72 72 |
|
||||
|
||||
PYI019_0.py:71:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
69 | def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
70 |
|
||||
@@ -123,7 +242,17 @@ PYI019_0.py:71:16: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:74:27: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
68 68 |
|
||||
69 69 | def __add__[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
70 70 |
|
||||
71 |- def __sub__[S](self: S, other: S) -> S: ...
|
||||
71 |+ def __sub__(self, other: Self) -> Self: ...
|
||||
72 72 |
|
||||
73 73 | @classmethod
|
||||
74 74 | def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
|
||||
PYI019_0.py:74:27: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
73 | @classmethod
|
||||
74 | def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
@@ -133,7 +262,17 @@ PYI019_0.py:74:27: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:77:29: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
71 71 | def __sub__[S](self: S, other: S) -> S: ...
|
||||
72 72 |
|
||||
73 73 | @classmethod
|
||||
74 |- def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
74 |+ def class_method_bound(cls) -> Self: ...
|
||||
75 75 |
|
||||
76 76 | @classmethod
|
||||
77 77 | def class_method_unbound[S](cls: type[S]) -> S: ...
|
||||
|
||||
PYI019_0.py:77:29: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
76 | @classmethod
|
||||
77 | def class_method_unbound[S](cls: type[S]) -> S: ...
|
||||
@@ -143,7 +282,17 @@ PYI019_0.py:77:29: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:79:30: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
74 74 | def class_method_bound[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
75 75 |
|
||||
76 76 | @classmethod
|
||||
77 |- def class_method_unbound[S](cls: type[S]) -> S: ...
|
||||
77 |+ def class_method_unbound(cls) -> Self: ...
|
||||
78 78 |
|
||||
79 79 | def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
|
||||
80 80 |
|
||||
|
||||
PYI019_0.py:79:30: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
77 | def class_method_unbound[S](cls: type[S]) -> S: ...
|
||||
78 |
|
||||
@@ -154,7 +303,17 @@ PYI019_0.py:79:30: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:81:32: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
76 76 | @classmethod
|
||||
77 77 | def class_method_unbound[S](cls: type[S]) -> S: ...
|
||||
78 78 |
|
||||
79 |- def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
|
||||
79 |+ def instance_method_bound(self) -> Self: ...
|
||||
80 80 |
|
||||
81 81 | def instance_method_unbound[S](self: S) -> S: ...
|
||||
82 82 |
|
||||
|
||||
PYI019_0.py:81:32: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
79 | def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
|
||||
80 |
|
||||
@@ -165,7 +324,17 @@ PYI019_0.py:81:32: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:83:53: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
78 78 |
|
||||
79 79 | def instance_method_bound[S: PEP695Fix](self: S) -> S: ...
|
||||
80 80 |
|
||||
81 |- def instance_method_unbound[S](self: S) -> S: ...
|
||||
81 |+ def instance_method_unbound(self) -> Self: ...
|
||||
82 82 |
|
||||
83 83 | def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
84 84 |
|
||||
|
||||
PYI019_0.py:83:53: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
81 | def instance_method_unbound[S](self: S) -> S: ...
|
||||
82 |
|
||||
@@ -176,7 +345,17 @@ PYI019_0.py:83:53: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:85:55: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
80 80 |
|
||||
81 81 | def instance_method_unbound[S](self: S) -> S: ...
|
||||
82 82 |
|
||||
83 |- def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
83 |+ def instance_method_bound_with_another_parameter(self, other: Self) -> Self: ...
|
||||
84 84 |
|
||||
85 85 | def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
|
||||
86 86 |
|
||||
|
||||
PYI019_0.py:85:55: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
83 | def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
84 |
|
||||
@@ -187,7 +366,17 @@ PYI019_0.py:85:55: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:87:27: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
82 82 |
|
||||
83 83 | def instance_method_bound_with_another_parameter[S: PEP695Fix](self: S, other: S) -> S: ...
|
||||
84 84 |
|
||||
85 |- def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
|
||||
85 |+ def instance_method_unbound_with_another_parameter(self, other: Self) -> Self: ...
|
||||
86 86 |
|
||||
87 87 | def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
|
||||
88 88 |
|
||||
|
||||
PYI019_0.py:87:27: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
85 | def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
|
||||
86 |
|
||||
@@ -198,7 +387,17 @@ PYI019_0.py:87:27: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:89:43: PYI019 Use `Self` instead of custom TypeVar `_S695`
|
||||
ℹ Safe fix
|
||||
84 84 |
|
||||
85 85 | def instance_method_unbound_with_another_parameter[S](self: S, other: S) -> S: ...
|
||||
86 86 |
|
||||
87 |- def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
|
||||
87 |+ def multiple_type_vars[*Ts, T](self, other: Self, /, *args: *Ts, a: T, b: list[T]) -> Self: ...
|
||||
88 88 |
|
||||
89 89 | def mixing_old_and_new_style_type_vars[T](self: _S695, a: T, b: T) -> _S695: ...
|
||||
90 90 |
|
||||
|
||||
PYI019_0.py:89:43: PYI019 [*] Use `Self` instead of custom TypeVar `_S695`
|
||||
|
|
||||
87 | def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
|
||||
88 |
|
||||
@@ -207,7 +406,17 @@ PYI019_0.py:89:43: PYI019 Use `Self` instead of custom TypeVar `_S695`
|
||||
|
|
||||
= help: Replace TypeVar `_S695` with `Self`
|
||||
|
||||
PYI019_0.py:94:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
86 86 |
|
||||
87 87 | def multiple_type_vars[S, *Ts, T](self: S, other: S, /, *args: *Ts, a: T, b: list[T]) -> S: ...
|
||||
88 88 |
|
||||
89 |- def mixing_old_and_new_style_type_vars[T](self: _S695, a: T, b: T) -> _S695: ...
|
||||
89 |+ def mixing_old_and_new_style_type_vars[T](self, a: T, b: T) -> Self: ...
|
||||
90 90 |
|
||||
91 91 |
|
||||
92 92 | class InvalidButWeDoNotPanic:
|
||||
|
||||
PYI019_0.py:94:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
92 | class InvalidButWeDoNotPanic:
|
||||
93 | @classmethod
|
||||
@@ -217,7 +426,17 @@ PYI019_0.py:94:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:114:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
91 91 |
|
||||
92 92 | class InvalidButWeDoNotPanic:
|
||||
93 93 | @classmethod
|
||||
94 |- def m[S](cls: type[S], /) -> S[int]: ...
|
||||
94 |+ def m(cls, /) -> Self[int]: ...
|
||||
95 95 | def n(self: S) -> S[int]: ...
|
||||
96 96 |
|
||||
97 97 |
|
||||
|
||||
PYI019_0.py:114:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
112 | class SubscriptReturnType:
|
||||
113 | @classmethod
|
||||
@@ -226,7 +445,17 @@ PYI019_0.py:114:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:118:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
111 111 |
|
||||
112 112 | class SubscriptReturnType:
|
||||
113 113 | @classmethod
|
||||
114 |- def m[S](cls: type[S]) -> type[S]: ... # PYI019
|
||||
114 |+ def m(cls) -> type[Self]: ... # PYI019
|
||||
115 115 |
|
||||
116 116 |
|
||||
117 117 | class SelfNotUsedInReturnAnnotation:
|
||||
|
||||
PYI019_0.py:118:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
117 | class SelfNotUsedInReturnAnnotation:
|
||||
118 | def m[S](self: S, other: S) -> int: ...
|
||||
@@ -236,7 +465,17 @@ PYI019_0.py:118:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:120:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
115 115 |
|
||||
116 116 |
|
||||
117 117 | class SelfNotUsedInReturnAnnotation:
|
||||
118 |- def m[S](self: S, other: S) -> int: ...
|
||||
118 |+ def m(self, other: Self) -> int: ...
|
||||
119 119 | @classmethod
|
||||
120 120 | def n[S](cls: type[S], other: S) -> int: ...
|
||||
121 121 |
|
||||
|
||||
PYI019_0.py:120:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
118 | def m[S](self: S, other: S) -> int: ...
|
||||
119 | @classmethod
|
||||
@@ -245,7 +484,17 @@ PYI019_0.py:120:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:135:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
117 117 | class SelfNotUsedInReturnAnnotation:
|
||||
118 118 | def m[S](self: S, other: S) -> int: ...
|
||||
119 119 | @classmethod
|
||||
120 |- def n[S](cls: type[S], other: S) -> int: ...
|
||||
120 |+ def n(cls, other: Self) -> int: ...
|
||||
121 121 |
|
||||
122 122 |
|
||||
123 123 | class _NotATypeVar: ...
|
||||
|
||||
PYI019_0.py:135:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
134 | class NoReturnAnnotations:
|
||||
135 | def m[S](self: S, other: S): ...
|
||||
@@ -255,7 +504,17 @@ PYI019_0.py:135:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:137:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
132 132 |
|
||||
133 133 |
|
||||
134 134 | class NoReturnAnnotations:
|
||||
135 |- def m[S](self: S, other: S): ...
|
||||
135 |+ def m(self, other: Self): ...
|
||||
136 136 | @classmethod
|
||||
137 137 | def n[S](cls: type[S], other: S): ...
|
||||
138 138 |
|
||||
|
||||
PYI019_0.py:137:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
135 | def m[S](self: S, other: S): ...
|
||||
136 | @classmethod
|
||||
@@ -266,7 +525,17 @@ PYI019_0.py:137:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:140:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
134 134 | class NoReturnAnnotations:
|
||||
135 135 | def m[S](self: S, other: S): ...
|
||||
136 136 | @classmethod
|
||||
137 |- def n[S](cls: type[S], other: S): ...
|
||||
137 |+ def n(cls, other: Self): ...
|
||||
138 138 |
|
||||
139 139 | class MultipleBoundParameters:
|
||||
140 140 | def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
|
||||
PYI019_0.py:140:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
139 | class MultipleBoundParameters:
|
||||
140 | def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
@@ -275,11 +544,149 @@ PYI019_0.py:140:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
PYI019_0.py:141:10: PYI019 Use `Self` instead of custom TypeVar `S`
|
||||
ℹ Safe fix
|
||||
137 137 | def n[S](cls: type[S], other: S): ...
|
||||
138 138 |
|
||||
139 139 | class MultipleBoundParameters:
|
||||
140 |- def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
140 |+ def m[T: int](self, other: T) -> Self: ...
|
||||
141 141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
142 142 |
|
||||
143 143 | class MethodsWithBody:
|
||||
|
||||
PYI019_0.py:141:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
139 | class MultipleBoundParameters:
|
||||
140 | def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
142 |
|
||||
143 | class MethodsWithBody:
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
ℹ Safe fix
|
||||
138 138 |
|
||||
139 139 | class MultipleBoundParameters:
|
||||
140 140 | def m[S: int, T: int](self: S, other: T) -> S: ...
|
||||
141 |- def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
141 |+ def n[T: (int, str)](self, other: T) -> Self: ...
|
||||
142 142 |
|
||||
143 143 | class MethodsWithBody:
|
||||
144 144 | def m[S](self: S, other: S) -> S:
|
||||
|
||||
PYI019_0.py:144:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
143 | class MethodsWithBody:
|
||||
144 | def m[S](self: S, other: S) -> S:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
145 | x: S = other
|
||||
146 | return x
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
ℹ Safe fix
|
||||
141 141 | def n[T: (int, str), S: (int, str)](self: S, other: T) -> S: ...
|
||||
142 142 |
|
||||
143 143 | class MethodsWithBody:
|
||||
144 |- def m[S](self: S, other: S) -> S:
|
||||
145 |- x: S = other
|
||||
144 |+ def m(self, other: Self) -> Self:
|
||||
145 |+ x: Self = other
|
||||
146 146 | return x
|
||||
147 147 |
|
||||
148 148 | @classmethod
|
||||
|
||||
PYI019_0.py:149:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
148 | @classmethod
|
||||
149 | def n[S](cls: type[S], other: S) -> S:
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
150 | x: type[S] = type(other)
|
||||
151 | return x()
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
ℹ Safe fix
|
||||
146 146 | return x
|
||||
147 147 |
|
||||
148 148 | @classmethod
|
||||
149 |- def n[S](cls: type[S], other: S) -> S:
|
||||
150 |- x: type[S] = type(other)
|
||||
149 |+ def n(cls, other: Self) -> Self:
|
||||
150 |+ x: type[Self] = type(other)
|
||||
151 151 | return x()
|
||||
152 152 |
|
||||
153 153 | class StringizedReferencesCanBeFixed:
|
||||
|
||||
PYI019_0.py:154:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
153 | class StringizedReferencesCanBeFixed:
|
||||
154 | def m[S](self: S) -> S:
|
||||
| ^^^^^^^^^^^^^^^^^ PYI019
|
||||
155 | x = cast("list[tuple[S, S]]", self)
|
||||
156 | return x
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
ℹ Safe fix
|
||||
151 151 | return x()
|
||||
152 152 |
|
||||
153 153 | class StringizedReferencesCanBeFixed:
|
||||
154 |- def m[S](self: S) -> S:
|
||||
155 |- x = cast("list[tuple[S, S]]", self)
|
||||
154 |+ def m(self) -> Self:
|
||||
155 |+ x = cast("list[tuple[Self, Self]]", self)
|
||||
156 156 | return x
|
||||
157 157 |
|
||||
158 158 | class ButStrangeStringizedReferencesCannotBeFixed:
|
||||
|
||||
PYI019_0.py:159:10: PYI019 Use `Self` instead of custom TypeVar `_T`
|
||||
|
|
||||
158 | class ButStrangeStringizedReferencesCannotBeFixed:
|
||||
159 | def m[_T](self: _T) -> _T:
|
||||
| ^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
160 | x = cast('list[_\x54]', self)
|
||||
161 | return x
|
||||
|
|
||||
= help: Replace TypeVar `_T` with `Self`
|
||||
|
||||
PYI019_0.py:164:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
163 | class DeletionsAreNotTouched:
|
||||
164 | def m[S](self: S) -> S:
|
||||
| ^^^^^^^^^^^^^^^^^ PYI019
|
||||
165 | # `S` is not a local variable here, and `del` can only be used with local variables,
|
||||
166 | # so `del S` here is not actually a reference to the type variable `S`.
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
ℹ Safe fix
|
||||
161 161 | return x
|
||||
162 162 |
|
||||
163 163 | class DeletionsAreNotTouched:
|
||||
164 |- def m[S](self: S) -> S:
|
||||
164 |+ def m(self) -> Self:
|
||||
165 165 | # `S` is not a local variable here, and `del` can only be used with local variables,
|
||||
166 166 | # so `del S` here is not actually a reference to the type variable `S`.
|
||||
167 167 | # This `del` statement is therefore not touched by the autofix (it raises `UnboundLocalError`
|
||||
|
||||
PYI019_0.py:173:10: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
172 | class NamesShadowingTypeVarAreNotTouched:
|
||||
173 | def m[S](self: S) -> S:
|
||||
| ^^^^^^^^^^^^^^^^^ PYI019
|
||||
174 | type S = int
|
||||
175 | print(S) # not a reference to the type variable, so not touched by the autofix
|
||||
|
|
||||
= help: Replace TypeVar `S` with `Self`
|
||||
|
||||
ℹ Safe fix
|
||||
170 170 | return self
|
||||
171 171 |
|
||||
172 172 | class NamesShadowingTypeVarAreNotTouched:
|
||||
173 |- def m[S](self: S) -> S:
|
||||
173 |+ def m(self) -> Self:
|
||||
174 174 | type S = int
|
||||
175 175 | print(S) # not a reference to the type variable, so not touched by the autofix
|
||||
176 176 | return 42
|
||||
|
||||
@@ -129,7 +129,6 @@ PYI019_0.pyi:54:11: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
||||
PYI019_0.pyi:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
|
||||
59 | # Only .pyi gets fixes, no fixes for .py
|
||||
60 | class PEP695Fix:
|
||||
61 | def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI019
|
||||
@@ -140,7 +139,7 @@ PYI019_0.pyi:61:16: PYI019 [*] Use `Self` instead of custom TypeVar `S`
|
||||
|
||||
ℹ Safe fix
|
||||
58 58 |
|
||||
59 59 | # Only .pyi gets fixes, no fixes for .py
|
||||
59 59 |
|
||||
60 60 | class PEP695Fix:
|
||||
61 |- def __new__[S: PEP695Fix](cls: type[S]) -> S: ...
|
||||
61 |+ def __new__(cls) -> Self: ...
|
||||
|
||||
@@ -14,6 +14,7 @@ mod tests {
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
use crate::rules::{flake8_import_conventions, pep8_naming};
|
||||
use crate::settings::types::PreviewMode;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -88,6 +89,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::InvalidArgumentName, Path::new("N803.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pep8_naming").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn camelcase_imported_as_incorrect_convention() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use ruff_python_ast::Parameter;
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{ExprLambda, Parameters, StmtFunctionDef};
|
||||
use ruff_python_semantic::analyze::visibility::is_override;
|
||||
use ruff_python_semantic::ScopeKind;
|
||||
use ruff_python_stdlib::str;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for argument names that do not follow the `snake_case` convention.
|
||||
@@ -22,6 +23,8 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// > mixedCase is allowed only in contexts where that’s already the
|
||||
/// > prevailing style (e.g. threading.py), to retain backwards compatibility.
|
||||
///
|
||||
/// In [preview], overridden methods are ignored.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def my_function(A, myArg):
|
||||
@@ -35,6 +38,7 @@ use crate::rules::pep8_naming::settings::IgnoreNames;
|
||||
/// ```
|
||||
///
|
||||
/// [PEP 8]: https://peps.python.org/pep-0008/#function-and-method-arguments
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct InvalidArgumentName {
|
||||
name: String,
|
||||
@@ -49,22 +53,54 @@ impl Violation for InvalidArgumentName {
|
||||
}
|
||||
|
||||
/// N803
|
||||
pub(crate) fn invalid_argument_name(
|
||||
name: &str,
|
||||
parameter: &Parameter,
|
||||
ignore_names: &IgnoreNames,
|
||||
) -> Option<Diagnostic> {
|
||||
if !str::is_lowercase(name) {
|
||||
// Ignore any explicitly-allowed names.
|
||||
if ignore_names.matches(name) {
|
||||
return None;
|
||||
pub(crate) fn invalid_argument_name_function(
|
||||
checker: &mut Checker,
|
||||
function_def: &StmtFunctionDef,
|
||||
) {
|
||||
let semantic = checker.semantic();
|
||||
let scope = semantic.current_scope();
|
||||
|
||||
if checker.settings.preview.is_enabled()
|
||||
&& matches!(scope.kind, ScopeKind::Class(_))
|
||||
&& is_override(&function_def.decorator_list, semantic)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
invalid_argument_name(checker, &function_def.parameters);
|
||||
}
|
||||
|
||||
/// N803
|
||||
pub(crate) fn invalid_argument_name_lambda(checker: &mut Checker, lambda: &ExprLambda) {
|
||||
let Some(parameters) = &lambda.parameters else {
|
||||
return;
|
||||
};
|
||||
|
||||
invalid_argument_name(checker, parameters);
|
||||
}
|
||||
|
||||
/// N803
|
||||
fn invalid_argument_name(checker: &mut Checker, parameters: &Parameters) {
|
||||
let ignore_names = &checker.settings.pep8_naming.ignore_names;
|
||||
|
||||
for parameter in parameters {
|
||||
let name = parameter.name().as_str();
|
||||
|
||||
if str::is_lowercase(name) {
|
||||
continue;
|
||||
}
|
||||
return Some(Diagnostic::new(
|
||||
|
||||
if ignore_names.matches(name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let diagnostic = Diagnostic::new(
|
||||
InvalidArgumentName {
|
||||
name: name.to_string(),
|
||||
},
|
||||
parameter.range(),
|
||||
));
|
||||
);
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pep8_naming/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
N803.py:1:16: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
@@ -16,3 +15,31 @@ N803.py:6:28: N803 Argument name `A` should be lowercase
|
||||
| ^ N803
|
||||
7 | return _, a, A
|
||||
|
|
||||
|
||||
N803.py:18:28: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
16 | class Extended(Class):
|
||||
17 | @override
|
||||
18 | def method(self, _, a, A): ...
|
||||
| ^ N803
|
||||
|
|
||||
|
||||
N803.py:22:16: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
21 | @override # Incorrect usage
|
||||
22 | def func(_, a, A): ...
|
||||
| ^ N803
|
||||
|
|
||||
|
||||
N803.py:25:21: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
25 | func = lambda _, a, A: ...
|
||||
| ^ N803
|
||||
|
|
||||
|
||||
N803.py:29:42: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
28 | class Extended(Class):
|
||||
29 | method = override(lambda self, _, a, A: ...) # Incorrect usage
|
||||
| ^ N803
|
||||
|
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pep8_naming/mod.rs
|
||||
---
|
||||
N803.py:1:16: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
1 | def func(_, a, A):
|
||||
| ^ N803
|
||||
2 | return _, a, A
|
||||
|
|
||||
|
||||
N803.py:6:28: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
5 | class Class:
|
||||
6 | def method(self, _, a, A):
|
||||
| ^ N803
|
||||
7 | return _, a, A
|
||||
|
|
||||
|
||||
N803.py:22:16: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
21 | @override # Incorrect usage
|
||||
22 | def func(_, a, A): ...
|
||||
| ^ N803
|
||||
|
|
||||
|
||||
N803.py:25:21: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
25 | func = lambda _, a, A: ...
|
||||
| ^ N803
|
||||
|
|
||||
|
||||
N803.py:29:42: N803 Argument name `A` should be lowercase
|
||||
|
|
||||
28 | class Extended(Class):
|
||||
29 | method = override(lambda self, _, a, A: ...) # Incorrect usage
|
||||
| ^ N803
|
||||
|
|
||||
@@ -121,37 +121,32 @@ pub(crate) fn if_stmt_min_max(checker: &mut Checker, stmt_if: &ast::StmtIf) {
|
||||
let left_is_value = left_cmp == body_value_cmp;
|
||||
let right_is_value = right_cmp == body_value_cmp;
|
||||
|
||||
// Determine whether to use `min()` or `max()`, and whether to flip the
|
||||
// order of the arguments, which is relevant for breaking ties.
|
||||
// Also ensure that we understand the operation we're trying to do,
|
||||
// by checking both sides of the comparison and assignment.
|
||||
let (min_max, flip_args) = match (
|
||||
let min_max = match (
|
||||
left_is_target,
|
||||
right_is_target,
|
||||
left_is_value,
|
||||
right_is_value,
|
||||
) {
|
||||
(true, false, false, true) => match op {
|
||||
CmpOp::Lt => (MinMax::Max, true),
|
||||
CmpOp::LtE => (MinMax::Max, false),
|
||||
CmpOp::Gt => (MinMax::Min, true),
|
||||
CmpOp::GtE => (MinMax::Min, false),
|
||||
CmpOp::Lt | CmpOp::LtE => MinMax::Max,
|
||||
CmpOp::Gt | CmpOp::GtE => MinMax::Min,
|
||||
_ => return,
|
||||
},
|
||||
(false, true, true, false) => match op {
|
||||
CmpOp::Lt => (MinMax::Min, true),
|
||||
CmpOp::LtE => (MinMax::Min, false),
|
||||
CmpOp::Gt => (MinMax::Max, true),
|
||||
CmpOp::GtE => (MinMax::Max, false),
|
||||
CmpOp::Lt | CmpOp::LtE => MinMax::Min,
|
||||
CmpOp::Gt | CmpOp::GtE => MinMax::Max,
|
||||
_ => return,
|
||||
},
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let (arg1, arg2) = if flip_args {
|
||||
(left.as_ref(), right)
|
||||
// Determine whether to use `min()` or `max()`, and make sure that the first
|
||||
// arg of the `min()` or `max()` method is equal to the target of the comparison.
|
||||
// This is to be consistent with the Python implementation of the methods `min()` and `max()`.
|
||||
let (arg1, arg2) = if left_is_target {
|
||||
(&**left, right)
|
||||
} else {
|
||||
(right, left.as_ref())
|
||||
(right, &**left)
|
||||
};
|
||||
|
||||
let replacement = format!(
|
||||
|
||||
@@ -23,7 +23,7 @@ if_stmt_min_max.py:8:1: PLR1730 [*] Replace `if` statement with `value = max(val
|
||||
11 10 | if value <= 10: # [max-instead-of-if]
|
||||
12 11 | value = 10
|
||||
|
||||
if_stmt_min_max.py:11:1: PLR1730 [*] Replace `if` statement with `value = max(10, value)`
|
||||
if_stmt_min_max.py:11:1: PLR1730 [*] Replace `if` statement with `value = max(value, 10)`
|
||||
|
|
||||
9 | value = 10
|
||||
10 |
|
||||
@@ -33,7 +33,7 @@ if_stmt_min_max.py:11:1: PLR1730 [*] Replace `if` statement with `value = max(10
|
||||
13 |
|
||||
14 | if value < value2: # [max-instead-of-if]
|
||||
|
|
||||
= help: Replace with `value = max(10, value)`
|
||||
= help: Replace with `value = max(value, 10)`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | if value < 10: # [max-instead-of-if]
|
||||
@@ -41,7 +41,7 @@ if_stmt_min_max.py:11:1: PLR1730 [*] Replace `if` statement with `value = max(10
|
||||
10 10 |
|
||||
11 |-if value <= 10: # [max-instead-of-if]
|
||||
12 |- value = 10
|
||||
11 |+value = max(10, value)
|
||||
11 |+value = max(value, 10)
|
||||
13 12 |
|
||||
14 13 | if value < value2: # [max-instead-of-if]
|
||||
15 14 | value = value2
|
||||
@@ -92,7 +92,7 @@ if_stmt_min_max.py:17:1: PLR1730 [*] Replace `if` statement with `value = min(va
|
||||
20 19 | if value >= 10: # [min-instead-of-if]
|
||||
21 20 | value = 10
|
||||
|
||||
if_stmt_min_max.py:20:1: PLR1730 [*] Replace `if` statement with `value = min(10, value)`
|
||||
if_stmt_min_max.py:20:1: PLR1730 [*] Replace `if` statement with `value = min(value, 10)`
|
||||
|
|
||||
18 | value = 10
|
||||
19 |
|
||||
@@ -102,7 +102,7 @@ if_stmt_min_max.py:20:1: PLR1730 [*] Replace `if` statement with `value = min(10
|
||||
22 |
|
||||
23 | if value > value2: # [min-instead-of-if]
|
||||
|
|
||||
= help: Replace with `value = min(10, value)`
|
||||
= help: Replace with `value = min(value, 10)`
|
||||
|
||||
ℹ Safe fix
|
||||
17 17 | if value > 10: # [min-instead-of-if]
|
||||
@@ -110,7 +110,7 @@ if_stmt_min_max.py:20:1: PLR1730 [*] Replace `if` statement with `value = min(10
|
||||
19 19 |
|
||||
20 |-if value >= 10: # [min-instead-of-if]
|
||||
21 |- value = 10
|
||||
20 |+value = min(10, value)
|
||||
20 |+value = min(value, 10)
|
||||
22 21 |
|
||||
23 22 | if value > value2: # [min-instead-of-if]
|
||||
24 23 | value = value2
|
||||
@@ -202,7 +202,7 @@ if_stmt_min_max.py:60:1: PLR1730 [*] Replace `if` statement with `A2 = max(A2, A
|
||||
63 62 | if A2 <= A1: # [max-instead-of-if]
|
||||
64 63 | A2 = A1
|
||||
|
||||
if_stmt_min_max.py:63:1: PLR1730 [*] Replace `if` statement with `A2 = max(A1, A2)`
|
||||
if_stmt_min_max.py:63:1: PLR1730 [*] Replace `if` statement with `A2 = max(A2, A1)`
|
||||
|
|
||||
61 | A2 = A1
|
||||
62 |
|
||||
@@ -212,7 +212,7 @@ if_stmt_min_max.py:63:1: PLR1730 [*] Replace `if` statement with `A2 = max(A1, A
|
||||
65 |
|
||||
66 | if A2 > A1: # [min-instead-of-if]
|
||||
|
|
||||
= help: Replace with `A2 = max(A1, A2)`
|
||||
= help: Replace with `A2 = max(A2, A1)`
|
||||
|
||||
ℹ Safe fix
|
||||
60 60 | if A2 < A1: # [max-instead-of-if]
|
||||
@@ -220,7 +220,7 @@ if_stmt_min_max.py:63:1: PLR1730 [*] Replace `if` statement with `A2 = max(A1, A
|
||||
62 62 |
|
||||
63 |-if A2 <= A1: # [max-instead-of-if]
|
||||
64 |- A2 = A1
|
||||
63 |+A2 = max(A1, A2)
|
||||
63 |+A2 = max(A2, A1)
|
||||
65 64 |
|
||||
66 65 | if A2 > A1: # [min-instead-of-if]
|
||||
67 66 | A2 = A1
|
||||
@@ -248,7 +248,7 @@ if_stmt_min_max.py:66:1: PLR1730 [*] Replace `if` statement with `A2 = min(A2, A
|
||||
69 68 | if A2 >= A1: # [min-instead-of-if]
|
||||
70 69 | A2 = A1
|
||||
|
||||
if_stmt_min_max.py:69:1: PLR1730 [*] Replace `if` statement with `A2 = min(A1, A2)`
|
||||
if_stmt_min_max.py:69:1: PLR1730 [*] Replace `if` statement with `A2 = min(A2, A1)`
|
||||
|
|
||||
67 | A2 = A1
|
||||
68 |
|
||||
@@ -258,7 +258,7 @@ if_stmt_min_max.py:69:1: PLR1730 [*] Replace `if` statement with `A2 = min(A1, A
|
||||
71 |
|
||||
72 | # Negative
|
||||
|
|
||||
= help: Replace with `A2 = min(A1, A2)`
|
||||
= help: Replace with `A2 = min(A2, A1)`
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | if A2 > A1: # [min-instead-of-if]
|
||||
@@ -266,7 +266,7 @@ if_stmt_min_max.py:69:1: PLR1730 [*] Replace `if` statement with `A2 = min(A1, A
|
||||
68 68 |
|
||||
69 |-if A2 >= A1: # [min-instead-of-if]
|
||||
70 |- A2 = A1
|
||||
69 |+A2 = min(A1, A2)
|
||||
69 |+A2 = min(A2, A1)
|
||||
71 70 |
|
||||
72 71 | # Negative
|
||||
73 72 | if value < 10:
|
||||
@@ -300,7 +300,7 @@ if_stmt_min_max.py:132:1: PLR1730 [*] Replace `if` statement with `min` call
|
||||
138 137 | class Foo:
|
||||
139 138 | _min = 0
|
||||
|
||||
if_stmt_min_max.py:143:9: PLR1730 [*] Replace `if` statement with `self._min = min(value, self._min)`
|
||||
if_stmt_min_max.py:143:9: PLR1730 [*] Replace `if` statement with `self._min = min(self._min, value)`
|
||||
|
|
||||
142 | def foo(self, value) -> None:
|
||||
143 | / if value < self._min:
|
||||
@@ -309,7 +309,7 @@ if_stmt_min_max.py:143:9: PLR1730 [*] Replace `if` statement with `self._min = m
|
||||
145 | if value > self._max:
|
||||
146 | self._max = value
|
||||
|
|
||||
= help: Replace with `self._min = min(value, self._min)`
|
||||
= help: Replace with `self._min = min(self._min, value)`
|
||||
|
||||
ℹ Safe fix
|
||||
140 140 | _max = 0
|
||||
@@ -317,12 +317,12 @@ if_stmt_min_max.py:143:9: PLR1730 [*] Replace `if` statement with `self._min = m
|
||||
142 142 | def foo(self, value) -> None:
|
||||
143 |- if value < self._min:
|
||||
144 |- self._min = value
|
||||
143 |+ self._min = min(value, self._min)
|
||||
143 |+ self._min = min(self._min, value)
|
||||
145 144 | if value > self._max:
|
||||
146 145 | self._max = value
|
||||
147 146 |
|
||||
|
||||
if_stmt_min_max.py:145:9: PLR1730 [*] Replace `if` statement with `self._max = max(value, self._max)`
|
||||
if_stmt_min_max.py:145:9: PLR1730 [*] Replace `if` statement with `self._max = max(self._max, value)`
|
||||
|
|
||||
143 | if value < self._min:
|
||||
144 | self._min = value
|
||||
@@ -332,7 +332,7 @@ if_stmt_min_max.py:145:9: PLR1730 [*] Replace `if` statement with `self._max = m
|
||||
147 |
|
||||
148 | if self._min < value:
|
||||
|
|
||||
= help: Replace with `self._max = max(value, self._max)`
|
||||
= help: Replace with `self._max = max(self._max, value)`
|
||||
|
||||
ℹ Safe fix
|
||||
142 142 | def foo(self, value) -> None:
|
||||
@@ -340,7 +340,7 @@ if_stmt_min_max.py:145:9: PLR1730 [*] Replace `if` statement with `self._max = m
|
||||
144 144 | self._min = value
|
||||
145 |- if value > self._max:
|
||||
146 |- self._max = value
|
||||
145 |+ self._max = max(value, self._max)
|
||||
145 |+ self._max = max(self._max, value)
|
||||
147 146 |
|
||||
148 147 | if self._min < value:
|
||||
149 148 | self._min = value
|
||||
@@ -437,7 +437,7 @@ if_stmt_min_max.py:155:9: PLR1730 [*] Replace `if` statement with `self._max = m
|
||||
158 157 | if self._min <= value:
|
||||
159 158 | self._min = value
|
||||
|
||||
if_stmt_min_max.py:158:9: PLR1730 [*] Replace `if` statement with `self._min = max(value, self._min)`
|
||||
if_stmt_min_max.py:158:9: PLR1730 [*] Replace `if` statement with `self._min = max(self._min, value)`
|
||||
|
|
||||
156 | self._max = value
|
||||
157 |
|
||||
@@ -447,7 +447,7 @@ if_stmt_min_max.py:158:9: PLR1730 [*] Replace `if` statement with `self._min = m
|
||||
160 | if self._max >= value:
|
||||
161 | self._max = value
|
||||
|
|
||||
= help: Replace with `self._min = max(value, self._min)`
|
||||
= help: Replace with `self._min = max(self._min, value)`
|
||||
|
||||
ℹ Safe fix
|
||||
155 155 | if value >= self._max:
|
||||
@@ -455,11 +455,11 @@ if_stmt_min_max.py:158:9: PLR1730 [*] Replace `if` statement with `self._min = m
|
||||
157 157 |
|
||||
158 |- if self._min <= value:
|
||||
159 |- self._min = value
|
||||
158 |+ self._min = max(value, self._min)
|
||||
158 |+ self._min = max(self._min, value)
|
||||
160 159 | if self._max >= value:
|
||||
161 160 | self._max = value
|
||||
|
||||
if_stmt_min_max.py:160:9: PLR1730 [*] Replace `if` statement with `self._max = min(value, self._max)`
|
||||
if_stmt_min_max.py:160:9: PLR1730 [*] Replace `if` statement with `self._max = min(self._max, value)`
|
||||
|
|
||||
158 | if self._min <= value:
|
||||
159 | self._min = value
|
||||
@@ -467,7 +467,7 @@ if_stmt_min_max.py:160:9: PLR1730 [*] Replace `if` statement with `self._max = m
|
||||
161 | | self._max = value
|
||||
| |_____________________________^ PLR1730
|
||||
|
|
||||
= help: Replace with `self._max = min(value, self._max)`
|
||||
= help: Replace with `self._max = min(self._max, value)`
|
||||
|
||||
ℹ Safe fix
|
||||
157 157 |
|
||||
@@ -475,4 +475,4 @@ if_stmt_min_max.py:160:9: PLR1730 [*] Replace `if` statement with `self._max = m
|
||||
159 159 | self._min = value
|
||||
160 |- if self._max >= value:
|
||||
161 |- self._max = value
|
||||
160 |+ self._max = min(value, self._max)
|
||||
160 |+ self._max = min(self._max, value)
|
||||
|
||||
@@ -106,6 +106,8 @@ mod tests {
|
||||
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_0.py"))]
|
||||
#[test_case(Rule::NonPEP695GenericClass, Path::new("UP046_1.py"))]
|
||||
#[test_case(Rule::NonPEP695GenericFunction, Path::new("UP047.py"))]
|
||||
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_0.py"))]
|
||||
#[test_case(Rule::PrivateTypeParameter, Path::new("UP049_1.py"))]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = path.to_string_lossy().to_string();
|
||||
let diagnostics = test_path(
|
||||
@@ -116,6 +118,25 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::RedundantOpenModes, Path::new("UP015.py"))]
|
||||
#[test_case(Rule::RedundantOpenModes, Path::new("UP015_1.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("pyupgrade").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn up007_preview() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -18,12 +18,14 @@ use ruff_text_size::{Ranged, TextRange};
|
||||
pub(crate) use non_pep695_generic_class::*;
|
||||
pub(crate) use non_pep695_generic_function::*;
|
||||
pub(crate) use non_pep695_type_alias::*;
|
||||
pub(crate) use private_type_parameter::*;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
mod non_pep695_generic_class;
|
||||
mod non_pep695_generic_function;
|
||||
mod non_pep695_type_alias;
|
||||
mod private_type_parameter;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum TypeVarRestriction<'a> {
|
||||
|
||||
@@ -65,19 +65,24 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc
|
||||
/// [`unused-private-type-var`][PYI018] for a rule to clean up unused
|
||||
/// private type variables.
|
||||
///
|
||||
/// This rule will not rename private type variables to remove leading underscores, even though the
|
||||
/// new type parameters are restricted in scope to their associated class. See
|
||||
/// [`private-type-parameter`][UP049] for a rule to update these names.
|
||||
///
|
||||
/// This rule will correctly handle classes with multiple base classes, as long as the single
|
||||
/// `Generic` base class is at the end of the argument list, as checked by
|
||||
/// [`generic-not-last-base-class`][PYI059]. If a `Generic` base class is
|
||||
/// found outside of the last position, a diagnostic is emitted without a suggested fix.
|
||||
///
|
||||
/// This rule only applies to generic classes and does not include generic functions. See
|
||||
/// [`non-pep695-generic-function`][PYI059] for the function version.
|
||||
/// [`non-pep695-generic-function`][UP047] for the function version.
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [PEP 696]: https://peps.python.org/pep-0696/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
/// [PYI059]: https://docs.astral.sh/ruff/rules/generic-not-last-base-class/
|
||||
/// [PYI059]: https://docs.astral.sh/ruff/rules/non-pep695-generic-function/
|
||||
/// [UP047]: https://docs.astral.sh/ruff/rules/non-pep695-generic-function/
|
||||
/// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct NonPEP695GenericClass {
|
||||
name: String,
|
||||
|
||||
@@ -64,6 +64,10 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc
|
||||
/// [`unused-private-type-var`][PYI018] for a rule to clean up unused
|
||||
/// private type variables.
|
||||
///
|
||||
/// This rule will not rename private type variables to remove leading underscores, even though the
|
||||
/// new type parameters are restricted in scope to their associated function. See
|
||||
/// [`private-type-parameter`][UP049] for a rule to update these names.
|
||||
///
|
||||
/// This rule only applies to generic functions and does not include generic classes. See
|
||||
/// [`non-pep695-generic-class`][UP046] for the class version.
|
||||
///
|
||||
@@ -71,6 +75,7 @@ use super::{check_type_vars, in_nested_context, DisplayTypeVars, TypeVarReferenc
|
||||
/// [PEP 696]: https://peps.python.org/pep-0696/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
/// [UP046]: https://docs.astral.sh/ruff/rules/non-pep695-generic-class/
|
||||
/// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct NonPEP695GenericFunction {
|
||||
name: String,
|
||||
|
||||
@@ -57,7 +57,25 @@ use super::{
|
||||
/// `TypeAliasType` assignments if there are any comments in the replacement range that would be
|
||||
/// deleted.
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// This rule only applies to `TypeAlias`es and `TypeAliasType`s. See
|
||||
/// [`non-pep695-generic-class`][UP046] and [`non-pep695-generic-function`][UP047] for similar
|
||||
/// transformations for generic classes and functions.
|
||||
///
|
||||
/// This rule replaces standalone type variables in aliases but doesn't remove the corresponding
|
||||
/// type variables even if they are unused after the fix. See [`unused-private-type-var`][PYI018]
|
||||
/// for a rule to clean up unused private type variables.
|
||||
///
|
||||
/// This rule will not rename private type variables to remove leading underscores, even though the
|
||||
/// new type parameters are restricted in scope to their associated aliases. See
|
||||
/// [`private-type-parameter`][UP049] for a rule to update these names.
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [PYI018]: https://docs.astral.sh/ruff/rules/unused-private-type-var/
|
||||
/// [UP046]: https://docs.astral.sh/ruff/rules/non-pep695-generic-class/
|
||||
/// [UP047]: https://docs.astral.sh/ruff/rules/non-pep695-generic-function/
|
||||
/// [UP049]: https://docs.astral.sh/ruff/rules/private-type-parameter/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct NonPEP695TypeAlias {
|
||||
name: String,
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::Stmt;
|
||||
use ruff_python_semantic::Binding;
|
||||
|
||||
use crate::{
|
||||
checkers::ast::Checker,
|
||||
renamer::{Renamer, ShadowedKind},
|
||||
};
|
||||
|
||||
/// ## What it does
|
||||
///
|
||||
/// Checks for use of [PEP 695] type parameters with leading underscores in generic classes and
|
||||
/// functions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
///
|
||||
/// [PEP 695] type parameters are already restricted in scope to the class or function in which they
|
||||
/// appear, so leading underscores just hurt readability without the usual privacy benefits.
|
||||
///
|
||||
/// However, neither a diagnostic nor a fix will be emitted for "sunder" (`_T_`) or "dunder"
|
||||
/// (`__T__`) type parameter names as these are not considered private.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```python
|
||||
/// class GenericClass[_T]:
|
||||
/// var: _T
|
||||
///
|
||||
///
|
||||
/// def generic_function[_T](var: _T) -> list[_T]:
|
||||
/// return var[0]
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
///
|
||||
/// ```python
|
||||
/// class GenericClass[T]:
|
||||
/// var: T
|
||||
///
|
||||
///
|
||||
/// def generic_function[T](var: T) -> list[T]:
|
||||
/// return var[0]
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix availability
|
||||
///
|
||||
/// If the name without an underscore would shadow a builtin or another variable, would be a
|
||||
/// keyword, or would otherwise be an invalid identifier, a fix will not be available. In these
|
||||
/// situations, you can consider using a trailing underscore or a different name entirely to satisfy
|
||||
/// the lint rule.
|
||||
///
|
||||
/// ## See also
|
||||
///
|
||||
/// This rule renames private [PEP 695] type parameters but doesn't convert pre-[PEP 695] generics
|
||||
/// to the new format. See [`non-pep695-generic-function`] and [`non-pep695-generic-class`] for
|
||||
/// rules that will make this transformation. Those rules do not remove unused type variables after
|
||||
/// their changes, so you may also want to consider enabling [`unused-private-type-var`] to complete
|
||||
/// the transition to [PEP 695] generics.
|
||||
///
|
||||
/// [PEP 695]: https://peps.python.org/pep-0695/
|
||||
/// [non-pep695-generic-function]: https://docs.astral.sh/ruff/rules/non-pep695-generic-function
|
||||
/// [non-pep695-generic-class]: https://docs.astral.sh/ruff/rules/non-pep695-generic-class
|
||||
/// [unused-private-type-var]: https://docs.astral.sh/ruff/rules/unused-private-type-var
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct PrivateTypeParameter {
|
||||
kind: ParamKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
enum ParamKind {
|
||||
Class,
|
||||
Function,
|
||||
}
|
||||
|
||||
impl ParamKind {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ParamKind::Class => "class",
|
||||
ParamKind::Function => "function",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Violation for PrivateTypeParameter {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"Generic {} uses private type parameters",
|
||||
self.kind.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
Some("Remove the leading underscores".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// UP049
|
||||
pub(crate) fn private_type_parameter(checker: &Checker, binding: &Binding) -> Option<Diagnostic> {
|
||||
let semantic = checker.semantic();
|
||||
let stmt = binding.statement(semantic)?;
|
||||
if !binding.kind.is_type_param() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let kind = match stmt {
|
||||
Stmt::FunctionDef(_) => ParamKind::Function,
|
||||
Stmt::ClassDef(_) => ParamKind::Class,
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let old_name = binding.name(checker.source());
|
||||
|
||||
if !old_name.starts_with('_') {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Sunder `_T_`, dunder `__T__`, and all all-under `_` or `__` cases should all be skipped, as
|
||||
// these are not "private" names
|
||||
if old_name.ends_with('_') {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(PrivateTypeParameter { kind }, binding.range);
|
||||
|
||||
let new_name = old_name.trim_start_matches('_');
|
||||
|
||||
// if the new name would shadow another variable, keyword, or builtin, emit a diagnostic without
|
||||
// a suggested fix
|
||||
if ShadowedKind::new(new_name, checker, binding.scope).shadows_any() {
|
||||
return Some(diagnostic);
|
||||
}
|
||||
|
||||
diagnostic.try_set_fix(|| {
|
||||
let (first, rest) = Renamer::rename(
|
||||
old_name,
|
||||
new_name,
|
||||
&semantic.scopes[binding.scope],
|
||||
checker.semantic(),
|
||||
checker.stylist(),
|
||||
)?;
|
||||
|
||||
Ok(Fix::safe_edits(first, rest))
|
||||
});
|
||||
|
||||
Some(diagnostic)
|
||||
}
|
||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_codegen::Stylist;
|
||||
use ruff_python_parser::{TokenKind, Tokens};
|
||||
use ruff_python_stdlib::open_mode::OpenMode;
|
||||
use ruff_text_size::{Ranged, TextSize};
|
||||
@@ -10,10 +9,10 @@ use ruff_text_size::{Ranged, TextSize};
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for redundant `open` mode parameters.
|
||||
/// Checks for redundant `open` mode arguments.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Redundant `open` mode parameters are unnecessary and should be removed to
|
||||
/// Redundant `open` mode arguments are unnecessary and should be removed to
|
||||
/// avoid confusion.
|
||||
///
|
||||
/// ## Example
|
||||
@@ -40,18 +39,18 @@ impl AlwaysFixableViolation for RedundantOpenModes {
|
||||
fn message(&self) -> String {
|
||||
let RedundantOpenModes { replacement } = self;
|
||||
if replacement.is_empty() {
|
||||
"Unnecessary open mode parameters".to_string()
|
||||
"Unnecessary mode argument".to_string()
|
||||
} else {
|
||||
format!("Unnecessary open mode parameters, use \"{replacement}\"")
|
||||
format!("Unnecessary modes, use `{replacement}`")
|
||||
}
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> String {
|
||||
let RedundantOpenModes { replacement } = self;
|
||||
if replacement.is_empty() {
|
||||
"Remove open mode parameters".to_string()
|
||||
"Remove mode argument".to_string()
|
||||
} else {
|
||||
format!("Replace with \"{replacement}\"")
|
||||
format!("Replace with `{replacement}`")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,10 +70,10 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall)
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(mode_param) = call.arguments.find_argument_value("mode", 1) else {
|
||||
let Some(mode_arg) = call.arguments.find_argument_value("mode", 1) else {
|
||||
return;
|
||||
};
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &mode_param else {
|
||||
let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &mode_arg else {
|
||||
return;
|
||||
};
|
||||
let Ok(mode) = OpenMode::from_chars(value.chars()) else {
|
||||
@@ -82,57 +81,60 @@ pub(crate) fn redundant_open_modes(checker: &mut Checker, call: &ast::ExprCall)
|
||||
};
|
||||
let reduced = mode.reduce();
|
||||
if reduced != mode {
|
||||
checker.diagnostics.push(create_diagnostic(
|
||||
call,
|
||||
mode_param,
|
||||
reduced,
|
||||
checker.tokens(),
|
||||
checker.stylist(),
|
||||
));
|
||||
checker
|
||||
.diagnostics
|
||||
.push(create_diagnostic(call, mode_arg, reduced, checker));
|
||||
}
|
||||
}
|
||||
|
||||
fn create_diagnostic(
|
||||
call: &ast::ExprCall,
|
||||
mode_param: &Expr,
|
||||
mode_arg: &Expr,
|
||||
mode: OpenMode,
|
||||
tokens: &Tokens,
|
||||
stylist: &Stylist,
|
||||
checker: &Checker,
|
||||
) -> Diagnostic {
|
||||
let range = if checker.settings.preview.is_enabled() {
|
||||
mode_arg.range()
|
||||
} else {
|
||||
call.range
|
||||
};
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
RedundantOpenModes {
|
||||
replacement: mode.to_string(),
|
||||
},
|
||||
call.range(),
|
||||
range,
|
||||
);
|
||||
|
||||
if mode.is_empty() {
|
||||
diagnostic
|
||||
.try_set_fix(|| create_remove_param_fix(call, mode_param, tokens).map(Fix::safe_edit));
|
||||
diagnostic.try_set_fix(|| {
|
||||
create_remove_argument_fix(call, mode_arg, checker.tokens()).map(Fix::safe_edit)
|
||||
});
|
||||
} else {
|
||||
let stylist = checker.stylist();
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
format!("{}{mode}{}", stylist.quote(), stylist.quote()),
|
||||
mode_param.range(),
|
||||
mode_arg.range(),
|
||||
)));
|
||||
}
|
||||
|
||||
diagnostic
|
||||
}
|
||||
|
||||
fn create_remove_param_fix(
|
||||
fn create_remove_argument_fix(
|
||||
call: &ast::ExprCall,
|
||||
mode_param: &Expr,
|
||||
mode_arg: &Expr,
|
||||
tokens: &Tokens,
|
||||
) -> Result<Edit> {
|
||||
// Find the last comma before mode_param and create a deletion fix
|
||||
// starting from the comma and ending after mode_param.
|
||||
// Find the last comma before mode_arg and create a deletion fix
|
||||
// starting from the comma and ending after mode_arg.
|
||||
let mut fix_start: Option<TextSize> = None;
|
||||
let mut fix_end: Option<TextSize> = None;
|
||||
let mut is_first_arg: bool = false;
|
||||
let mut delete_first_arg: bool = false;
|
||||
|
||||
for token in tokens.in_range(call.range()) {
|
||||
if token.start() == mode_param.start() {
|
||||
if token.start() == mode_arg.start() {
|
||||
if is_first_arg {
|
||||
delete_first_arg = true;
|
||||
continue;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP015.py:1:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:1:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
1 | open("foo", "U")
|
||||
| ^^^^^^^^^^^^^^^^ UP015
|
||||
2 | open("foo", "Ur")
|
||||
3 | open("foo", "Ub")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-open("foo", "U")
|
||||
@@ -17,7 +17,7 @@ UP015.py:1:1: UP015 [*] Unnecessary open mode parameters
|
||||
3 3 | open("foo", "Ub")
|
||||
4 4 | open("foo", "rUb")
|
||||
|
||||
UP015.py:2:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:2:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
1 | open("foo", "U")
|
||||
2 | open("foo", "Ur")
|
||||
@@ -25,7 +25,7 @@ UP015.py:2:1: UP015 [*] Unnecessary open mode parameters
|
||||
3 | open("foo", "Ub")
|
||||
4 | open("foo", "rUb")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | open("foo", "U")
|
||||
@@ -35,7 +35,7 @@ UP015.py:2:1: UP015 [*] Unnecessary open mode parameters
|
||||
4 4 | open("foo", "rUb")
|
||||
5 5 | open("foo", "r")
|
||||
|
||||
UP015.py:3:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:3:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
1 | open("foo", "U")
|
||||
2 | open("foo", "Ur")
|
||||
@@ -44,7 +44,7 @@ UP015.py:3:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
4 | open("foo", "rUb")
|
||||
5 | open("foo", "r")
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | open("foo", "U")
|
||||
@@ -55,7 +55,7 @@ UP015.py:3:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
5 5 | open("foo", "r")
|
||||
6 6 | open("foo", "rt")
|
||||
|
||||
UP015.py:4:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:4:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
2 | open("foo", "Ur")
|
||||
3 | open("foo", "Ub")
|
||||
@@ -64,7 +64,7 @@ UP015.py:4:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
5 | open("foo", "r")
|
||||
6 | open("foo", "rt")
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | open("foo", "U")
|
||||
@@ -76,7 +76,7 @@ UP015.py:4:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
6 6 | open("foo", "rt")
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
|
||||
UP015.py:5:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:5:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
3 | open("foo", "Ub")
|
||||
4 | open("foo", "rUb")
|
||||
@@ -85,7 +85,7 @@ UP015.py:5:1: UP015 [*] Unnecessary open mode parameters
|
||||
6 | open("foo", "rt")
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | open("foo", "Ur")
|
||||
@@ -97,7 +97,7 @@ UP015.py:5:1: UP015 [*] Unnecessary open mode parameters
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
8 8 | open("f", "wt")
|
||||
|
||||
UP015.py:6:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:6:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
4 | open("foo", "rUb")
|
||||
5 | open("foo", "r")
|
||||
@@ -106,7 +106,7 @@ UP015.py:6:1: UP015 [*] Unnecessary open mode parameters
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
8 | open("f", "wt")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | open("foo", "Ub")
|
||||
@@ -118,7 +118,7 @@ UP015.py:6:1: UP015 [*] Unnecessary open mode parameters
|
||||
8 8 | open("f", "wt")
|
||||
9 9 | open("f", "tw")
|
||||
|
||||
UP015.py:7:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:7:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
5 | open("foo", "r")
|
||||
6 | open("foo", "rt")
|
||||
@@ -127,7 +127,7 @@ UP015.py:7:1: UP015 [*] Unnecessary open mode parameters
|
||||
8 | open("f", "wt")
|
||||
9 | open("f", "tw")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | open("foo", "rUb")
|
||||
@@ -139,7 +139,7 @@ UP015.py:7:1: UP015 [*] Unnecessary open mode parameters
|
||||
9 9 | open("f", "tw")
|
||||
10 10 |
|
||||
|
||||
UP015.py:8:1: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
UP015.py:8:1: UP015 [*] Unnecessary modes, use `w`
|
||||
|
|
||||
6 | open("foo", "rt")
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
@@ -147,7 +147,7 @@ UP015.py:8:1: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
| ^^^^^^^^^^^^^^^ UP015
|
||||
9 | open("f", "tw")
|
||||
|
|
||||
= help: Replace with "w"
|
||||
= help: Replace with `w`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | open("foo", "r")
|
||||
@@ -159,7 +159,7 @@ UP015.py:8:1: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
10 10 |
|
||||
11 11 | with open("foo", "U") as f:
|
||||
|
||||
UP015.py:9:1: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
UP015.py:9:1: UP015 [*] Unnecessary modes, use `w`
|
||||
|
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
8 | open("f", "wt")
|
||||
@@ -168,7 +168,7 @@ UP015.py:9:1: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
10 |
|
||||
11 | with open("foo", "U") as f:
|
||||
|
|
||||
= help: Replace with "w"
|
||||
= help: Replace with `w`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | open("foo", "rt")
|
||||
@@ -180,7 +180,7 @@ UP015.py:9:1: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
11 11 | with open("foo", "U") as f:
|
||||
12 12 | pass
|
||||
|
||||
UP015.py:11:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:11:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
9 | open("f", "tw")
|
||||
10 |
|
||||
@@ -189,7 +189,7 @@ UP015.py:11:6: UP015 [*] Unnecessary open mode parameters
|
||||
12 | pass
|
||||
13 | with open("foo", "Ur") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | open("f", "wt")
|
||||
@@ -201,7 +201,7 @@ UP015.py:11:6: UP015 [*] Unnecessary open mode parameters
|
||||
13 13 | with open("foo", "Ur") as f:
|
||||
14 14 | pass
|
||||
|
||||
UP015.py:13:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:13:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
11 | with open("foo", "U") as f:
|
||||
12 | pass
|
||||
@@ -210,7 +210,7 @@ UP015.py:13:6: UP015 [*] Unnecessary open mode parameters
|
||||
14 | pass
|
||||
15 | with open("foo", "Ub") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
@@ -222,7 +222,7 @@ UP015.py:13:6: UP015 [*] Unnecessary open mode parameters
|
||||
15 15 | with open("foo", "Ub") as f:
|
||||
16 16 | pass
|
||||
|
||||
UP015.py:15:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:15:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
13 | with open("foo", "Ur") as f:
|
||||
14 | pass
|
||||
@@ -231,7 +231,7 @@ UP015.py:15:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
16 | pass
|
||||
17 | with open("foo", "rUb") as f:
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | pass
|
||||
@@ -243,7 +243,7 @@ UP015.py:15:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
17 17 | with open("foo", "rUb") as f:
|
||||
18 18 | pass
|
||||
|
||||
UP015.py:17:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:17:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
15 | with open("foo", "Ub") as f:
|
||||
16 | pass
|
||||
@@ -252,7 +252,7 @@ UP015.py:17:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
18 | pass
|
||||
19 | with open("foo", "r") as f:
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | pass
|
||||
@@ -264,7 +264,7 @@ UP015.py:17:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
19 19 | with open("foo", "r") as f:
|
||||
20 20 | pass
|
||||
|
||||
UP015.py:19:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:19:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
17 | with open("foo", "rUb") as f:
|
||||
18 | pass
|
||||
@@ -273,7 +273,7 @@ UP015.py:19:6: UP015 [*] Unnecessary open mode parameters
|
||||
20 | pass
|
||||
21 | with open("foo", "rt") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | pass
|
||||
@@ -285,7 +285,7 @@ UP015.py:19:6: UP015 [*] Unnecessary open mode parameters
|
||||
21 21 | with open("foo", "rt") as f:
|
||||
22 22 | pass
|
||||
|
||||
UP015.py:21:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:21:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
19 | with open("foo", "r") as f:
|
||||
20 | pass
|
||||
@@ -294,7 +294,7 @@ UP015.py:21:6: UP015 [*] Unnecessary open mode parameters
|
||||
22 | pass
|
||||
23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | pass
|
||||
@@ -306,7 +306,7 @@ UP015.py:21:6: UP015 [*] Unnecessary open mode parameters
|
||||
23 23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
24 24 | pass
|
||||
|
||||
UP015.py:23:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:23:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
21 | with open("foo", "rt") as f:
|
||||
22 | pass
|
||||
@@ -315,7 +315,7 @@ UP015.py:23:6: UP015 [*] Unnecessary open mode parameters
|
||||
24 | pass
|
||||
25 | with open("foo", "wt") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 | pass
|
||||
@@ -327,7 +327,7 @@ UP015.py:23:6: UP015 [*] Unnecessary open mode parameters
|
||||
25 25 | with open("foo", "wt") as f:
|
||||
26 26 | pass
|
||||
|
||||
UP015.py:25:6: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
UP015.py:25:6: UP015 [*] Unnecessary modes, use `w`
|
||||
|
|
||||
23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
24 | pass
|
||||
@@ -335,7 +335,7 @@ UP015.py:25:6: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
| ^^^^^^^^^^^^^^^^^ UP015
|
||||
26 | pass
|
||||
|
|
||||
= help: Replace with "w"
|
||||
= help: Replace with `w`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 | pass
|
||||
@@ -347,7 +347,7 @@ UP015.py:25:6: UP015 [*] Unnecessary open mode parameters, use "w"
|
||||
27 27 |
|
||||
28 28 | open(f("a", "b", "c"), "U")
|
||||
|
||||
UP015.py:28:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:28:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
26 | pass
|
||||
27 |
|
||||
@@ -355,7 +355,7 @@ UP015.py:28:1: UP015 [*] Unnecessary open mode parameters
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
29 | open(f("a", "b", "c"), "Ub")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
25 25 | with open("foo", "wt") as f:
|
||||
@@ -367,7 +367,7 @@ UP015.py:28:1: UP015 [*] Unnecessary open mode parameters
|
||||
30 30 |
|
||||
31 31 | with open(f("a", "b", "c"), "U") as f:
|
||||
|
||||
UP015.py:29:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:29:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
28 | open(f("a", "b", "c"), "U")
|
||||
29 | open(f("a", "b", "c"), "Ub")
|
||||
@@ -375,7 +375,7 @@ UP015.py:29:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
30 |
|
||||
31 | with open(f("a", "b", "c"), "U") as f:
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | pass
|
||||
@@ -387,7 +387,7 @@ UP015.py:29:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
31 31 | with open(f("a", "b", "c"), "U") as f:
|
||||
32 32 | pass
|
||||
|
||||
UP015.py:31:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:31:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
29 | open(f("a", "b", "c"), "Ub")
|
||||
30 |
|
||||
@@ -396,7 +396,7 @@ UP015.py:31:6: UP015 [*] Unnecessary open mode parameters
|
||||
32 | pass
|
||||
33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 | open(f("a", "b", "c"), "U")
|
||||
@@ -408,7 +408,7 @@ UP015.py:31:6: UP015 [*] Unnecessary open mode parameters
|
||||
33 33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
34 34 | pass
|
||||
|
||||
UP015.py:33:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:33:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
31 | with open(f("a", "b", "c"), "U") as f:
|
||||
32 | pass
|
||||
@@ -416,7 +416,7 @@ UP015.py:33:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
34 | pass
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
30 30 |
|
||||
@@ -428,7 +428,7 @@ UP015.py:33:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
35 35 |
|
||||
36 36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
|
||||
UP015.py:36:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:36:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
34 | pass
|
||||
35 |
|
||||
@@ -437,7 +437,7 @@ UP015.py:36:6: UP015 [*] Unnecessary open mode parameters
|
||||
37 | pass
|
||||
38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
@@ -449,7 +449,7 @@ UP015.py:36:6: UP015 [*] Unnecessary open mode parameters
|
||||
38 38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
39 39 | pass
|
||||
|
||||
UP015.py:36:30: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:36:30: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
34 | pass
|
||||
35 |
|
||||
@@ -458,7 +458,7 @@ UP015.py:36:30: UP015 [*] Unnecessary open mode parameters
|
||||
37 | pass
|
||||
38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
@@ -470,7 +470,7 @@ UP015.py:36:30: UP015 [*] Unnecessary open mode parameters
|
||||
38 38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
39 39 | pass
|
||||
|
||||
UP015.py:38:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:38:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
37 | pass
|
||||
@@ -478,7 +478,7 @@ UP015.py:38:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
| ^^^^^^^^^^^^^^^^^ UP015
|
||||
39 | pass
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
@@ -490,7 +490,7 @@ UP015.py:38:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
40 40 |
|
||||
41 41 | open("foo", mode="U")
|
||||
|
||||
UP015.py:38:31: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:38:31: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
37 | pass
|
||||
@@ -498,7 +498,7 @@ UP015.py:38:31: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
| ^^^^^^^^^^^^^^^^^ UP015
|
||||
39 | pass
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
@@ -510,7 +510,7 @@ UP015.py:38:31: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
40 40 |
|
||||
41 41 | open("foo", mode="U")
|
||||
|
||||
UP015.py:41:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:41:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
39 | pass
|
||||
40 |
|
||||
@@ -519,7 +519,7 @@ UP015.py:41:1: UP015 [*] Unnecessary open mode parameters
|
||||
42 | open(name="foo", mode="U")
|
||||
43 | open(mode="U", name="foo")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
38 38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
@@ -531,14 +531,14 @@ UP015.py:41:1: UP015 [*] Unnecessary open mode parameters
|
||||
43 43 | open(mode="U", name="foo")
|
||||
44 44 |
|
||||
|
||||
UP015.py:42:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:42:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
41 | open("foo", mode="U")
|
||||
42 | open(name="foo", mode="U")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
43 | open(mode="U", name="foo")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
39 39 | pass
|
||||
@@ -550,7 +550,7 @@ UP015.py:42:1: UP015 [*] Unnecessary open mode parameters
|
||||
44 44 |
|
||||
45 45 | with open("foo", mode="U") as f:
|
||||
|
||||
UP015.py:43:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:43:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
41 | open("foo", mode="U")
|
||||
42 | open(name="foo", mode="U")
|
||||
@@ -559,7 +559,7 @@ UP015.py:43:1: UP015 [*] Unnecessary open mode parameters
|
||||
44 |
|
||||
45 | with open("foo", mode="U") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
40 40 |
|
||||
@@ -571,7 +571,7 @@ UP015.py:43:1: UP015 [*] Unnecessary open mode parameters
|
||||
45 45 | with open("foo", mode="U") as f:
|
||||
46 46 | pass
|
||||
|
||||
UP015.py:45:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:45:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
43 | open(mode="U", name="foo")
|
||||
44 |
|
||||
@@ -580,7 +580,7 @@ UP015.py:45:6: UP015 [*] Unnecessary open mode parameters
|
||||
46 | pass
|
||||
47 | with open(name="foo", mode="U") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
42 42 | open(name="foo", mode="U")
|
||||
@@ -592,7 +592,7 @@ UP015.py:45:6: UP015 [*] Unnecessary open mode parameters
|
||||
47 47 | with open(name="foo", mode="U") as f:
|
||||
48 48 | pass
|
||||
|
||||
UP015.py:47:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:47:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
45 | with open("foo", mode="U") as f:
|
||||
46 | pass
|
||||
@@ -601,7 +601,7 @@ UP015.py:47:6: UP015 [*] Unnecessary open mode parameters
|
||||
48 | pass
|
||||
49 | with open(mode="U", name="foo") as f:
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
44 44 |
|
||||
@@ -613,7 +613,7 @@ UP015.py:47:6: UP015 [*] Unnecessary open mode parameters
|
||||
49 49 | with open(mode="U", name="foo") as f:
|
||||
50 50 | pass
|
||||
|
||||
UP015.py:49:6: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:49:6: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
47 | with open(name="foo", mode="U") as f:
|
||||
48 | pass
|
||||
@@ -621,7 +621,7 @@ UP015.py:49:6: UP015 [*] Unnecessary open mode parameters
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
50 | pass
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
46 46 | pass
|
||||
@@ -633,7 +633,7 @@ UP015.py:49:6: UP015 [*] Unnecessary open mode parameters
|
||||
51 51 |
|
||||
52 52 | open("foo", mode="Ub")
|
||||
|
||||
UP015.py:52:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:52:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
50 | pass
|
||||
51 |
|
||||
@@ -642,7 +642,7 @@ UP015.py:52:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
53 | open(name="foo", mode="Ub")
|
||||
54 | open(mode="Ub", name="foo")
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
49 49 | with open(mode="U", name="foo") as f:
|
||||
@@ -654,14 +654,14 @@ UP015.py:52:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
54 54 | open(mode="Ub", name="foo")
|
||||
55 55 |
|
||||
|
||||
UP015.py:53:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:53:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
52 | open("foo", mode="Ub")
|
||||
53 | open(name="foo", mode="Ub")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
54 | open(mode="Ub", name="foo")
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
50 50 | pass
|
||||
@@ -673,7 +673,7 @@ UP015.py:53:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
55 55 |
|
||||
56 56 | with open("foo", mode="Ub") as f:
|
||||
|
||||
UP015.py:54:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:54:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
52 | open("foo", mode="Ub")
|
||||
53 | open(name="foo", mode="Ub")
|
||||
@@ -682,7 +682,7 @@ UP015.py:54:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
55 |
|
||||
56 | with open("foo", mode="Ub") as f:
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
51 51 |
|
||||
@@ -694,7 +694,7 @@ UP015.py:54:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
56 56 | with open("foo", mode="Ub") as f:
|
||||
57 57 | pass
|
||||
|
||||
UP015.py:56:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:56:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
54 | open(mode="Ub", name="foo")
|
||||
55 |
|
||||
@@ -703,7 +703,7 @@ UP015.py:56:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
57 | pass
|
||||
58 | with open(name="foo", mode="Ub") as f:
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
53 53 | open(name="foo", mode="Ub")
|
||||
@@ -715,7 +715,7 @@ UP015.py:56:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
58 58 | with open(name="foo", mode="Ub") as f:
|
||||
59 59 | pass
|
||||
|
||||
UP015.py:58:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:58:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
56 | with open("foo", mode="Ub") as f:
|
||||
57 | pass
|
||||
@@ -724,7 +724,7 @@ UP015.py:58:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
59 | pass
|
||||
60 | with open(mode="Ub", name="foo") as f:
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
55 55 |
|
||||
@@ -736,7 +736,7 @@ UP015.py:58:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
60 60 | with open(mode="Ub", name="foo") as f:
|
||||
61 61 | pass
|
||||
|
||||
UP015.py:60:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:60:6: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
58 | with open(name="foo", mode="Ub") as f:
|
||||
59 | pass
|
||||
@@ -744,7 +744,7 @@ UP015.py:60:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
61 | pass
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
57 57 | pass
|
||||
@@ -756,7 +756,7 @@ UP015.py:60:6: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
62 62 |
|
||||
63 63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:63:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:63:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
61 | pass
|
||||
62 |
|
||||
@@ -765,7 +765,7 @@ UP015.py:63:1: UP015 [*] Unnecessary open mode parameters
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
60 60 | with open(mode="Ub", name="foo") as f:
|
||||
@@ -777,7 +777,7 @@ UP015.py:63:1: UP015 [*] Unnecessary open mode parameters
|
||||
65 65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:64:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:64:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
@@ -785,7 +785,7 @@ UP015.py:64:1: UP015 [*] Unnecessary open mode parameters
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | pass
|
||||
@@ -797,7 +797,7 @@ UP015.py:64:1: UP015 [*] Unnecessary open mode parameters
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 67 |
|
||||
|
||||
UP015.py:65:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:65:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
@@ -805,7 +805,7 @@ UP015.py:65:1: UP015 [*] Unnecessary open mode parameters
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
62 62 |
|
||||
@@ -817,7 +817,7 @@ UP015.py:65:1: UP015 [*] Unnecessary open mode parameters
|
||||
67 67 |
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:66:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:66:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
@@ -826,7 +826,7 @@ UP015.py:66:1: UP015 [*] Unnecessary open mode parameters
|
||||
67 |
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
63 63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
@@ -838,7 +838,7 @@ UP015.py:66:1: UP015 [*] Unnecessary open mode parameters
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
|
||||
UP015.py:68:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:68:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 |
|
||||
@@ -847,7 +847,7 @@ UP015.py:68:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
@@ -859,7 +859,7 @@ UP015.py:68:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
70 70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:69:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:69:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
@@ -867,7 +867,7 @@ UP015.py:69:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
@@ -879,7 +879,7 @@ UP015.py:69:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
71 71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
72 72 |
|
||||
|
||||
UP015.py:70:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:70:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
@@ -887,7 +887,7 @@ UP015.py:70:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
67 67 |
|
||||
@@ -899,7 +899,7 @@ UP015.py:70:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
72 72 |
|
||||
73 73 | import aiofiles
|
||||
|
||||
UP015.py:71:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
UP015.py:71:1: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
@@ -908,7 +908,7 @@ UP015.py:71:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
72 |
|
||||
73 | import aiofiles
|
||||
|
|
||||
= help: Replace with "rb"
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
@@ -920,7 +920,7 @@ UP015.py:71:1: UP015 [*] Unnecessary open mode parameters, use "rb"
|
||||
73 73 | import aiofiles
|
||||
74 74 |
|
||||
|
||||
UP015.py:75:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:75:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
73 | import aiofiles
|
||||
74 |
|
||||
@@ -929,7 +929,7 @@ UP015.py:75:1: UP015 [*] Unnecessary open mode parameters
|
||||
76 | aiofiles.open("foo", "r")
|
||||
77 | aiofiles.open("foo", mode="r")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
72 72 |
|
||||
@@ -941,14 +941,14 @@ UP015.py:75:1: UP015 [*] Unnecessary open mode parameters
|
||||
77 77 | aiofiles.open("foo", mode="r")
|
||||
78 78 |
|
||||
|
||||
UP015.py:76:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:76:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
75 | aiofiles.open("foo", "U")
|
||||
76 | aiofiles.open("foo", "r")
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^ UP015
|
||||
77 | aiofiles.open("foo", mode="r")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
73 73 | import aiofiles
|
||||
@@ -960,7 +960,7 @@ UP015.py:76:1: UP015 [*] Unnecessary open mode parameters
|
||||
78 78 |
|
||||
79 79 | open("foo", "r+")
|
||||
|
||||
UP015.py:77:1: UP015 [*] Unnecessary open mode parameters
|
||||
UP015.py:77:1: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
75 | aiofiles.open("foo", "U")
|
||||
76 | aiofiles.open("foo", "r")
|
||||
@@ -969,7 +969,7 @@ UP015.py:77:1: UP015 [*] Unnecessary open mode parameters
|
||||
78 |
|
||||
79 | open("foo", "r+")
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
74 74 |
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
snapshot_kind: text
|
||||
---
|
||||
UP015_1.py:3:5: UP015 [*] Unnecessary open mode parameters
|
||||
UP015_1.py:3:5: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
1 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
2 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
3 | x: 'open("foo", "r")'
|
||||
| ^^^^^^^^^^^^^^^^ UP015
|
||||
|
|
||||
= help: Remove open mode parameters
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP049_0.py:2:15: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
1 | # simple case, replace _T in signature and body
|
||||
2 | class Generic[_T]:
|
||||
| ^^ UP049
|
||||
3 | buf: list[_T]
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # simple case, replace _T in signature and body
|
||||
2 |-class Generic[_T]:
|
||||
3 |- buf: list[_T]
|
||||
2 |+class Generic[T]:
|
||||
3 |+ buf: list[T]
|
||||
4 4 |
|
||||
5 |- def append(self, t: _T):
|
||||
5 |+ def append(self, t: T):
|
||||
6 6 | self.buf.append(t)
|
||||
7 7 |
|
||||
8 8 |
|
||||
|
||||
UP049_0.py:10:12: UP049 [*] Generic function uses private type parameters
|
||||
|
|
||||
9 | # simple case, replace _T in signature and body
|
||||
10 | def second[_T](var: tuple[_T]) -> _T:
|
||||
| ^^ UP049
|
||||
11 | y: _T = var[1]
|
||||
12 | return y
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
7 7 |
|
||||
8 8 |
|
||||
9 9 | # simple case, replace _T in signature and body
|
||||
10 |-def second[_T](var: tuple[_T]) -> _T:
|
||||
11 |- y: _T = var[1]
|
||||
10 |+def second[T](var: tuple[T]) -> T:
|
||||
11 |+ y: T = var[1]
|
||||
12 12 | return y
|
||||
13 13 |
|
||||
14 14 |
|
||||
|
||||
UP049_0.py:17:5: UP049 [*] Generic function uses private type parameters
|
||||
|
|
||||
15 | # one diagnostic for each variable, comments are preserved
|
||||
16 | def many_generics[
|
||||
17 | _T, # first generic
|
||||
| ^^ UP049
|
||||
18 | _U, # second generic
|
||||
19 | ](args):
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 |
|
||||
15 15 | # one diagnostic for each variable, comments are preserved
|
||||
16 16 | def many_generics[
|
||||
17 |- _T, # first generic
|
||||
17 |+ T, # first generic
|
||||
18 18 | _U, # second generic
|
||||
19 19 | ](args):
|
||||
20 20 | return args
|
||||
|
||||
UP049_0.py:18:5: UP049 [*] Generic function uses private type parameters
|
||||
|
|
||||
16 | def many_generics[
|
||||
17 | _T, # first generic
|
||||
18 | _U, # second generic
|
||||
| ^^ UP049
|
||||
19 | ](args):
|
||||
20 | return args
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
15 15 | # one diagnostic for each variable, comments are preserved
|
||||
16 16 | def many_generics[
|
||||
17 17 | _T, # first generic
|
||||
18 |- _U, # second generic
|
||||
18 |+ U, # second generic
|
||||
19 19 | ](args):
|
||||
20 20 | return args
|
||||
21 21 |
|
||||
|
||||
UP049_0.py:27:7: UP049 [*] Generic function uses private type parameters
|
||||
|
|
||||
27 | def f[_T](v):
|
||||
| ^^ UP049
|
||||
28 | cast("_T", v)
|
||||
29 | cast("Literal['_T']")
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
24 24 | from typing import Literal, cast
|
||||
25 25 |
|
||||
26 26 |
|
||||
27 |-def f[_T](v):
|
||||
28 |- cast("_T", v)
|
||||
27 |+def f[T](v):
|
||||
28 |+ cast("T", v)
|
||||
29 29 | cast("Literal['_T']")
|
||||
30 |- cast("list[_T]", v)
|
||||
30 |+ cast("list[T]", v)
|
||||
@@ -0,0 +1,245 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP049_1.py:2:11: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
1 | # bound
|
||||
2 | class Foo[_T: str]:
|
||||
| ^^ UP049
|
||||
3 | var: _T
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # bound
|
||||
2 |-class Foo[_T: str]:
|
||||
3 |- var: _T
|
||||
2 |+class Foo[T: str]:
|
||||
3 |+ var: T
|
||||
4 4 |
|
||||
5 5 |
|
||||
6 6 | # constraint
|
||||
|
||||
UP049_1.py:7:11: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
6 | # constraint
|
||||
7 | class Foo[_T: (str, bytes)]:
|
||||
| ^^ UP049
|
||||
8 | var: _T
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 |
|
||||
6 6 | # constraint
|
||||
7 |-class Foo[_T: (str, bytes)]:
|
||||
8 |- var: _T
|
||||
7 |+class Foo[T: (str, bytes)]:
|
||||
8 |+ var: T
|
||||
9 9 |
|
||||
10 10 |
|
||||
11 11 | # python 3.13+ default
|
||||
|
||||
UP049_1.py:12:11: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
11 | # python 3.13+ default
|
||||
12 | class Foo[_T = int]:
|
||||
| ^^ UP049
|
||||
13 | var: _T
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
9 9 |
|
||||
10 10 |
|
||||
11 11 | # python 3.13+ default
|
||||
12 |-class Foo[_T = int]:
|
||||
13 |- var: _T
|
||||
12 |+class Foo[T = int]:
|
||||
13 |+ var: T
|
||||
14 14 |
|
||||
15 15 |
|
||||
16 16 | # tuple
|
||||
|
||||
UP049_1.py:17:12: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
16 | # tuple
|
||||
17 | class Foo[*_Ts]:
|
||||
| ^^^ UP049
|
||||
18 | var: tuple[*_Ts]
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 |
|
||||
15 15 |
|
||||
16 16 | # tuple
|
||||
17 |-class Foo[*_Ts]:
|
||||
18 |- var: tuple[*_Ts]
|
||||
17 |+class Foo[*Ts]:
|
||||
18 |+ var: tuple[*Ts]
|
||||
19 19 |
|
||||
20 20 |
|
||||
21 21 | # paramspec
|
||||
|
||||
UP049_1.py:22:11: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
21 | # paramspec
|
||||
22 | class C[**_P]:
|
||||
| ^^ UP049
|
||||
23 | var: _P
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
19 19 |
|
||||
20 20 |
|
||||
21 21 | # paramspec
|
||||
22 |-class C[**_P]:
|
||||
23 |- var: _P
|
||||
22 |+class C[**P]:
|
||||
23 |+ var: P
|
||||
24 24 |
|
||||
25 25 |
|
||||
26 26 | from typing import Callable
|
||||
|
||||
UP049_1.py:31:18: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 | # fixed
|
||||
31 | class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
| ^^ UP049
|
||||
32 | @staticmethod
|
||||
33 | def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 |
|
||||
29 29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 30 | # fixed
|
||||
31 |-class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
31 |+class Everything[T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
32 32 | @staticmethod
|
||||
33 |- def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
33 |+ def transform(t: T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, T] | None:
|
||||
34 34 | return None
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
UP049_1.py:31:22: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 | # fixed
|
||||
31 | class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
| ^^ UP049
|
||||
32 | @staticmethod
|
||||
33 | def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 |
|
||||
29 29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 30 | # fixed
|
||||
31 |-class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
31 |+class Everything[_T, U: str, _V: (int, float), *_W, **_X]:
|
||||
32 32 | @staticmethod
|
||||
33 |- def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
33 |+ def transform(t: _T, u: U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
34 34 | return None
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
UP049_1.py:31:31: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 | # fixed
|
||||
31 | class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
| ^^ UP049
|
||||
32 | @staticmethod
|
||||
33 | def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 |
|
||||
29 29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 30 | # fixed
|
||||
31 |-class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
31 |+class Everything[_T, _U: str, V: (int, float), *_W, **_X]:
|
||||
32 32 | @staticmethod
|
||||
33 |- def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
33 |+ def transform(t: _T, u: _U, v: V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
34 34 | return None
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
UP049_1.py:31:50: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 | # fixed
|
||||
31 | class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
| ^^ UP049
|
||||
32 | @staticmethod
|
||||
33 | def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 |
|
||||
29 29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 30 | # fixed
|
||||
31 |-class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
31 |+class Everything[_T, _U: str, _V: (int, float), *W, **_X]:
|
||||
32 32 | @staticmethod
|
||||
33 |- def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
33 |+ def transform(t: _T, u: _U, v: _V) -> tuple[*W] | Callable[_X, _T] | None:
|
||||
34 34 | return None
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
UP049_1.py:31:56: UP049 [*] Generic class uses private type parameters
|
||||
|
|
||||
29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 | # fixed
|
||||
31 | class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
| ^^ UP049
|
||||
32 | @staticmethod
|
||||
33 | def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 |
|
||||
29 29 | # each of these will get a separate diagnostic, but at least they'll all get
|
||||
30 30 | # fixed
|
||||
31 |-class Everything[_T, _U: str, _V: (int, float), *_W, **_X]:
|
||||
31 |+class Everything[_T, _U: str, _V: (int, float), *_W, **X]:
|
||||
32 32 | @staticmethod
|
||||
33 |- def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[_X, _T] | None:
|
||||
33 |+ def transform(t: _T, u: _U, v: _V) -> tuple[*_W] | Callable[X, _T] | None:
|
||||
34 34 | return None
|
||||
35 35 |
|
||||
36 36 |
|
||||
|
||||
UP049_1.py:39:9: UP049 Generic class uses private type parameters
|
||||
|
|
||||
37 | # this should not be fixed because the new name is a keyword, but we still
|
||||
38 | # offer a diagnostic
|
||||
39 | class F[_async]: ...
|
||||
| ^^^^^^ UP049
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
|
||||
UP049_1.py:47:25: UP049 Generic class uses private type parameters
|
||||
|
|
||||
45 | type X = int
|
||||
46 |
|
||||
47 | class ScopeConflict[_X]:
|
||||
| ^^ UP049
|
||||
48 | var: _X
|
||||
49 | x: X
|
||||
|
|
||||
= help: Remove the leading underscores
|
||||
@@ -0,0 +1,982 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP015.py:1:13: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
1 | open("foo", "U")
|
||||
| ^^^ UP015
|
||||
2 | open("foo", "Ur")
|
||||
3 | open("foo", "Ub")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
1 |-open("foo", "U")
|
||||
1 |+open("foo")
|
||||
2 2 | open("foo", "Ur")
|
||||
3 3 | open("foo", "Ub")
|
||||
4 4 | open("foo", "rUb")
|
||||
|
||||
UP015.py:2:13: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
1 | open("foo", "U")
|
||||
2 | open("foo", "Ur")
|
||||
| ^^^^ UP015
|
||||
3 | open("foo", "Ub")
|
||||
4 | open("foo", "rUb")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | open("foo", "U")
|
||||
2 |-open("foo", "Ur")
|
||||
2 |+open("foo")
|
||||
3 3 | open("foo", "Ub")
|
||||
4 4 | open("foo", "rUb")
|
||||
5 5 | open("foo", "r")
|
||||
|
||||
UP015.py:3:13: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
1 | open("foo", "U")
|
||||
2 | open("foo", "Ur")
|
||||
3 | open("foo", "Ub")
|
||||
| ^^^^ UP015
|
||||
4 | open("foo", "rUb")
|
||||
5 | open("foo", "r")
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | open("foo", "U")
|
||||
2 2 | open("foo", "Ur")
|
||||
3 |-open("foo", "Ub")
|
||||
3 |+open("foo", "rb")
|
||||
4 4 | open("foo", "rUb")
|
||||
5 5 | open("foo", "r")
|
||||
6 6 | open("foo", "rt")
|
||||
|
||||
UP015.py:4:13: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
2 | open("foo", "Ur")
|
||||
3 | open("foo", "Ub")
|
||||
4 | open("foo", "rUb")
|
||||
| ^^^^^ UP015
|
||||
5 | open("foo", "r")
|
||||
6 | open("foo", "rt")
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | open("foo", "U")
|
||||
2 2 | open("foo", "Ur")
|
||||
3 3 | open("foo", "Ub")
|
||||
4 |-open("foo", "rUb")
|
||||
4 |+open("foo", "rb")
|
||||
5 5 | open("foo", "r")
|
||||
6 6 | open("foo", "rt")
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
|
||||
UP015.py:5:13: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
3 | open("foo", "Ub")
|
||||
4 | open("foo", "rUb")
|
||||
5 | open("foo", "r")
|
||||
| ^^^ UP015
|
||||
6 | open("foo", "rt")
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 | open("foo", "Ur")
|
||||
3 3 | open("foo", "Ub")
|
||||
4 4 | open("foo", "rUb")
|
||||
5 |-open("foo", "r")
|
||||
5 |+open("foo")
|
||||
6 6 | open("foo", "rt")
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
8 8 | open("f", "wt")
|
||||
|
||||
UP015.py:6:13: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
4 | open("foo", "rUb")
|
||||
5 | open("foo", "r")
|
||||
6 | open("foo", "rt")
|
||||
| ^^^^ UP015
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
8 | open("f", "wt")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
3 3 | open("foo", "Ub")
|
||||
4 4 | open("foo", "rUb")
|
||||
5 5 | open("foo", "r")
|
||||
6 |-open("foo", "rt")
|
||||
6 |+open("foo")
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
8 8 | open("f", "wt")
|
||||
9 9 | open("f", "tw")
|
||||
|
||||
UP015.py:7:11: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
5 | open("foo", "r")
|
||||
6 | open("foo", "rt")
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
| ^^^ UP015
|
||||
8 | open("f", "wt")
|
||||
9 | open("f", "tw")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 | open("foo", "rUb")
|
||||
5 5 | open("foo", "r")
|
||||
6 6 | open("foo", "rt")
|
||||
7 |-open("f", "r", encoding="UTF-8")
|
||||
7 |+open("f", encoding="UTF-8")
|
||||
8 8 | open("f", "wt")
|
||||
9 9 | open("f", "tw")
|
||||
10 10 |
|
||||
|
||||
UP015.py:8:11: UP015 [*] Unnecessary modes, use `w`
|
||||
|
|
||||
6 | open("foo", "rt")
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
8 | open("f", "wt")
|
||||
| ^^^^ UP015
|
||||
9 | open("f", "tw")
|
||||
|
|
||||
= help: Replace with `w`
|
||||
|
||||
ℹ Safe fix
|
||||
5 5 | open("foo", "r")
|
||||
6 6 | open("foo", "rt")
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
8 |-open("f", "wt")
|
||||
8 |+open("f", "w")
|
||||
9 9 | open("f", "tw")
|
||||
10 10 |
|
||||
11 11 | with open("foo", "U") as f:
|
||||
|
||||
UP015.py:9:11: UP015 [*] Unnecessary modes, use `w`
|
||||
|
|
||||
7 | open("f", "r", encoding="UTF-8")
|
||||
8 | open("f", "wt")
|
||||
9 | open("f", "tw")
|
||||
| ^^^^ UP015
|
||||
10 |
|
||||
11 | with open("foo", "U") as f:
|
||||
|
|
||||
= help: Replace with `w`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 | open("foo", "rt")
|
||||
7 7 | open("f", "r", encoding="UTF-8")
|
||||
8 8 | open("f", "wt")
|
||||
9 |-open("f", "tw")
|
||||
9 |+open("f", "w")
|
||||
10 10 |
|
||||
11 11 | with open("foo", "U") as f:
|
||||
12 12 | pass
|
||||
|
||||
UP015.py:11:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
9 | open("f", "tw")
|
||||
10 |
|
||||
11 | with open("foo", "U") as f:
|
||||
| ^^^ UP015
|
||||
12 | pass
|
||||
13 | with open("foo", "Ur") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 | open("f", "wt")
|
||||
9 9 | open("f", "tw")
|
||||
10 10 |
|
||||
11 |-with open("foo", "U") as f:
|
||||
11 |+with open("foo") as f:
|
||||
12 12 | pass
|
||||
13 13 | with open("foo", "Ur") as f:
|
||||
14 14 | pass
|
||||
|
||||
UP015.py:13:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
11 | with open("foo", "U") as f:
|
||||
12 | pass
|
||||
13 | with open("foo", "Ur") as f:
|
||||
| ^^^^ UP015
|
||||
14 | pass
|
||||
15 | with open("foo", "Ub") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | with open("foo", "U") as f:
|
||||
12 12 | pass
|
||||
13 |-with open("foo", "Ur") as f:
|
||||
13 |+with open("foo") as f:
|
||||
14 14 | pass
|
||||
15 15 | with open("foo", "Ub") as f:
|
||||
16 16 | pass
|
||||
|
||||
UP015.py:15:18: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
13 | with open("foo", "Ur") as f:
|
||||
14 | pass
|
||||
15 | with open("foo", "Ub") as f:
|
||||
| ^^^^ UP015
|
||||
16 | pass
|
||||
17 | with open("foo", "rUb") as f:
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 | pass
|
||||
13 13 | with open("foo", "Ur") as f:
|
||||
14 14 | pass
|
||||
15 |-with open("foo", "Ub") as f:
|
||||
15 |+with open("foo", "rb") as f:
|
||||
16 16 | pass
|
||||
17 17 | with open("foo", "rUb") as f:
|
||||
18 18 | pass
|
||||
|
||||
UP015.py:17:18: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
15 | with open("foo", "Ub") as f:
|
||||
16 | pass
|
||||
17 | with open("foo", "rUb") as f:
|
||||
| ^^^^^ UP015
|
||||
18 | pass
|
||||
19 | with open("foo", "r") as f:
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 | pass
|
||||
15 15 | with open("foo", "Ub") as f:
|
||||
16 16 | pass
|
||||
17 |-with open("foo", "rUb") as f:
|
||||
17 |+with open("foo", "rb") as f:
|
||||
18 18 | pass
|
||||
19 19 | with open("foo", "r") as f:
|
||||
20 20 | pass
|
||||
|
||||
UP015.py:19:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
17 | with open("foo", "rUb") as f:
|
||||
18 | pass
|
||||
19 | with open("foo", "r") as f:
|
||||
| ^^^ UP015
|
||||
20 | pass
|
||||
21 | with open("foo", "rt") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 | pass
|
||||
17 17 | with open("foo", "rUb") as f:
|
||||
18 18 | pass
|
||||
19 |-with open("foo", "r") as f:
|
||||
19 |+with open("foo") as f:
|
||||
20 20 | pass
|
||||
21 21 | with open("foo", "rt") as f:
|
||||
22 22 | pass
|
||||
|
||||
UP015.py:21:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
19 | with open("foo", "r") as f:
|
||||
20 | pass
|
||||
21 | with open("foo", "rt") as f:
|
||||
| ^^^^ UP015
|
||||
22 | pass
|
||||
23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 | pass
|
||||
19 19 | with open("foo", "r") as f:
|
||||
20 20 | pass
|
||||
21 |-with open("foo", "rt") as f:
|
||||
21 |+with open("foo") as f:
|
||||
22 22 | pass
|
||||
23 23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
24 24 | pass
|
||||
|
||||
UP015.py:23:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
21 | with open("foo", "rt") as f:
|
||||
22 | pass
|
||||
23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
| ^^^ UP015
|
||||
24 | pass
|
||||
25 | with open("foo", "wt") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 | pass
|
||||
21 21 | with open("foo", "rt") as f:
|
||||
22 22 | pass
|
||||
23 |-with open("foo", "r", encoding="UTF-8") as f:
|
||||
23 |+with open("foo", encoding="UTF-8") as f:
|
||||
24 24 | pass
|
||||
25 25 | with open("foo", "wt") as f:
|
||||
26 26 | pass
|
||||
|
||||
UP015.py:25:18: UP015 [*] Unnecessary modes, use `w`
|
||||
|
|
||||
23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
24 | pass
|
||||
25 | with open("foo", "wt") as f:
|
||||
| ^^^^ UP015
|
||||
26 | pass
|
||||
|
|
||||
= help: Replace with `w`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 | pass
|
||||
23 23 | with open("foo", "r", encoding="UTF-8") as f:
|
||||
24 24 | pass
|
||||
25 |-with open("foo", "wt") as f:
|
||||
25 |+with open("foo", "w") as f:
|
||||
26 26 | pass
|
||||
27 27 |
|
||||
28 28 | open(f("a", "b", "c"), "U")
|
||||
|
||||
UP015.py:28:24: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
26 | pass
|
||||
27 |
|
||||
28 | open(f("a", "b", "c"), "U")
|
||||
| ^^^ UP015
|
||||
29 | open(f("a", "b", "c"), "Ub")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
25 25 | with open("foo", "wt") as f:
|
||||
26 26 | pass
|
||||
27 27 |
|
||||
28 |-open(f("a", "b", "c"), "U")
|
||||
28 |+open(f("a", "b", "c"))
|
||||
29 29 | open(f("a", "b", "c"), "Ub")
|
||||
30 30 |
|
||||
31 31 | with open(f("a", "b", "c"), "U") as f:
|
||||
|
||||
UP015.py:29:24: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
28 | open(f("a", "b", "c"), "U")
|
||||
29 | open(f("a", "b", "c"), "Ub")
|
||||
| ^^^^ UP015
|
||||
30 |
|
||||
31 | with open(f("a", "b", "c"), "U") as f:
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 | pass
|
||||
27 27 |
|
||||
28 28 | open(f("a", "b", "c"), "U")
|
||||
29 |-open(f("a", "b", "c"), "Ub")
|
||||
29 |+open(f("a", "b", "c"), "rb")
|
||||
30 30 |
|
||||
31 31 | with open(f("a", "b", "c"), "U") as f:
|
||||
32 32 | pass
|
||||
|
||||
UP015.py:31:29: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
29 | open(f("a", "b", "c"), "Ub")
|
||||
30 |
|
||||
31 | with open(f("a", "b", "c"), "U") as f:
|
||||
| ^^^ UP015
|
||||
32 | pass
|
||||
33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
28 28 | open(f("a", "b", "c"), "U")
|
||||
29 29 | open(f("a", "b", "c"), "Ub")
|
||||
30 30 |
|
||||
31 |-with open(f("a", "b", "c"), "U") as f:
|
||||
31 |+with open(f("a", "b", "c")) as f:
|
||||
32 32 | pass
|
||||
33 33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
34 34 | pass
|
||||
|
||||
UP015.py:33:29: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
31 | with open(f("a", "b", "c"), "U") as f:
|
||||
32 | pass
|
||||
33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
| ^^^^ UP015
|
||||
34 | pass
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
30 30 |
|
||||
31 31 | with open(f("a", "b", "c"), "U") as f:
|
||||
32 32 | pass
|
||||
33 |-with open(f("a", "b", "c"), "Ub") as f:
|
||||
33 |+with open(f("a", "b", "c"), "rb") as f:
|
||||
34 34 | pass
|
||||
35 35 |
|
||||
36 36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
|
||||
UP015.py:36:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
34 | pass
|
||||
35 |
|
||||
36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
| ^^^ UP015
|
||||
37 | pass
|
||||
38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
34 34 | pass
|
||||
35 35 |
|
||||
36 |-with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
36 |+with open("foo") as fa, open("bar", "U") as fb:
|
||||
37 37 | pass
|
||||
38 38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
39 39 | pass
|
||||
|
||||
UP015.py:36:42: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
34 | pass
|
||||
35 |
|
||||
36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
| ^^^ UP015
|
||||
37 | pass
|
||||
38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
33 33 | with open(f("a", "b", "c"), "Ub") as f:
|
||||
34 34 | pass
|
||||
35 35 |
|
||||
36 |-with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
36 |+with open("foo", "U") as fa, open("bar") as fb:
|
||||
37 37 | pass
|
||||
38 38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
39 39 | pass
|
||||
|
||||
UP015.py:38:18: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
37 | pass
|
||||
38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
| ^^^^ UP015
|
||||
39 | pass
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
36 36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
37 37 | pass
|
||||
38 |-with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
38 |+with open("foo", "rb") as fa, open("bar", "Ub") as fb:
|
||||
39 39 | pass
|
||||
40 40 |
|
||||
41 41 | open("foo", mode="U")
|
||||
|
||||
UP015.py:38:43: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
37 | pass
|
||||
38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
| ^^^^ UP015
|
||||
39 | pass
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 |
|
||||
36 36 | with open("foo", "U") as fa, open("bar", "U") as fb:
|
||||
37 37 | pass
|
||||
38 |-with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
38 |+with open("foo", "Ub") as fa, open("bar", "rb") as fb:
|
||||
39 39 | pass
|
||||
40 40 |
|
||||
41 41 | open("foo", mode="U")
|
||||
|
||||
UP015.py:41:18: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
39 | pass
|
||||
40 |
|
||||
41 | open("foo", mode="U")
|
||||
| ^^^ UP015
|
||||
42 | open(name="foo", mode="U")
|
||||
43 | open(mode="U", name="foo")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
38 38 | with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
|
||||
39 39 | pass
|
||||
40 40 |
|
||||
41 |-open("foo", mode="U")
|
||||
41 |+open("foo")
|
||||
42 42 | open(name="foo", mode="U")
|
||||
43 43 | open(mode="U", name="foo")
|
||||
44 44 |
|
||||
|
||||
UP015.py:42:23: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
41 | open("foo", mode="U")
|
||||
42 | open(name="foo", mode="U")
|
||||
| ^^^ UP015
|
||||
43 | open(mode="U", name="foo")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
39 39 | pass
|
||||
40 40 |
|
||||
41 41 | open("foo", mode="U")
|
||||
42 |-open(name="foo", mode="U")
|
||||
42 |+open(name="foo")
|
||||
43 43 | open(mode="U", name="foo")
|
||||
44 44 |
|
||||
45 45 | with open("foo", mode="U") as f:
|
||||
|
||||
UP015.py:43:11: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
41 | open("foo", mode="U")
|
||||
42 | open(name="foo", mode="U")
|
||||
43 | open(mode="U", name="foo")
|
||||
| ^^^ UP015
|
||||
44 |
|
||||
45 | with open("foo", mode="U") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
40 40 |
|
||||
41 41 | open("foo", mode="U")
|
||||
42 42 | open(name="foo", mode="U")
|
||||
43 |-open(mode="U", name="foo")
|
||||
43 |+open(name="foo")
|
||||
44 44 |
|
||||
45 45 | with open("foo", mode="U") as f:
|
||||
46 46 | pass
|
||||
|
||||
UP015.py:45:23: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
43 | open(mode="U", name="foo")
|
||||
44 |
|
||||
45 | with open("foo", mode="U") as f:
|
||||
| ^^^ UP015
|
||||
46 | pass
|
||||
47 | with open(name="foo", mode="U") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
42 42 | open(name="foo", mode="U")
|
||||
43 43 | open(mode="U", name="foo")
|
||||
44 44 |
|
||||
45 |-with open("foo", mode="U") as f:
|
||||
45 |+with open("foo") as f:
|
||||
46 46 | pass
|
||||
47 47 | with open(name="foo", mode="U") as f:
|
||||
48 48 | pass
|
||||
|
||||
UP015.py:47:28: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
45 | with open("foo", mode="U") as f:
|
||||
46 | pass
|
||||
47 | with open(name="foo", mode="U") as f:
|
||||
| ^^^ UP015
|
||||
48 | pass
|
||||
49 | with open(mode="U", name="foo") as f:
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
44 44 |
|
||||
45 45 | with open("foo", mode="U") as f:
|
||||
46 46 | pass
|
||||
47 |-with open(name="foo", mode="U") as f:
|
||||
47 |+with open(name="foo") as f:
|
||||
48 48 | pass
|
||||
49 49 | with open(mode="U", name="foo") as f:
|
||||
50 50 | pass
|
||||
|
||||
UP015.py:49:16: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
47 | with open(name="foo", mode="U") as f:
|
||||
48 | pass
|
||||
49 | with open(mode="U", name="foo") as f:
|
||||
| ^^^ UP015
|
||||
50 | pass
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
46 46 | pass
|
||||
47 47 | with open(name="foo", mode="U") as f:
|
||||
48 48 | pass
|
||||
49 |-with open(mode="U", name="foo") as f:
|
||||
49 |+with open(name="foo") as f:
|
||||
50 50 | pass
|
||||
51 51 |
|
||||
52 52 | open("foo", mode="Ub")
|
||||
|
||||
UP015.py:52:18: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
50 | pass
|
||||
51 |
|
||||
52 | open("foo", mode="Ub")
|
||||
| ^^^^ UP015
|
||||
53 | open(name="foo", mode="Ub")
|
||||
54 | open(mode="Ub", name="foo")
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
49 49 | with open(mode="U", name="foo") as f:
|
||||
50 50 | pass
|
||||
51 51 |
|
||||
52 |-open("foo", mode="Ub")
|
||||
52 |+open("foo", mode="rb")
|
||||
53 53 | open(name="foo", mode="Ub")
|
||||
54 54 | open(mode="Ub", name="foo")
|
||||
55 55 |
|
||||
|
||||
UP015.py:53:23: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
52 | open("foo", mode="Ub")
|
||||
53 | open(name="foo", mode="Ub")
|
||||
| ^^^^ UP015
|
||||
54 | open(mode="Ub", name="foo")
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
50 50 | pass
|
||||
51 51 |
|
||||
52 52 | open("foo", mode="Ub")
|
||||
53 |-open(name="foo", mode="Ub")
|
||||
53 |+open(name="foo", mode="rb")
|
||||
54 54 | open(mode="Ub", name="foo")
|
||||
55 55 |
|
||||
56 56 | with open("foo", mode="Ub") as f:
|
||||
|
||||
UP015.py:54:11: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
52 | open("foo", mode="Ub")
|
||||
53 | open(name="foo", mode="Ub")
|
||||
54 | open(mode="Ub", name="foo")
|
||||
| ^^^^ UP015
|
||||
55 |
|
||||
56 | with open("foo", mode="Ub") as f:
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
51 51 |
|
||||
52 52 | open("foo", mode="Ub")
|
||||
53 53 | open(name="foo", mode="Ub")
|
||||
54 |-open(mode="Ub", name="foo")
|
||||
54 |+open(mode="rb", name="foo")
|
||||
55 55 |
|
||||
56 56 | with open("foo", mode="Ub") as f:
|
||||
57 57 | pass
|
||||
|
||||
UP015.py:56:23: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
54 | open(mode="Ub", name="foo")
|
||||
55 |
|
||||
56 | with open("foo", mode="Ub") as f:
|
||||
| ^^^^ UP015
|
||||
57 | pass
|
||||
58 | with open(name="foo", mode="Ub") as f:
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
53 53 | open(name="foo", mode="Ub")
|
||||
54 54 | open(mode="Ub", name="foo")
|
||||
55 55 |
|
||||
56 |-with open("foo", mode="Ub") as f:
|
||||
56 |+with open("foo", mode="rb") as f:
|
||||
57 57 | pass
|
||||
58 58 | with open(name="foo", mode="Ub") as f:
|
||||
59 59 | pass
|
||||
|
||||
UP015.py:58:28: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
56 | with open("foo", mode="Ub") as f:
|
||||
57 | pass
|
||||
58 | with open(name="foo", mode="Ub") as f:
|
||||
| ^^^^ UP015
|
||||
59 | pass
|
||||
60 | with open(mode="Ub", name="foo") as f:
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
55 55 |
|
||||
56 56 | with open("foo", mode="Ub") as f:
|
||||
57 57 | pass
|
||||
58 |-with open(name="foo", mode="Ub") as f:
|
||||
58 |+with open(name="foo", mode="rb") as f:
|
||||
59 59 | pass
|
||||
60 60 | with open(mode="Ub", name="foo") as f:
|
||||
61 61 | pass
|
||||
|
||||
UP015.py:60:16: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
58 | with open(name="foo", mode="Ub") as f:
|
||||
59 | pass
|
||||
60 | with open(mode="Ub", name="foo") as f:
|
||||
| ^^^^ UP015
|
||||
61 | pass
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
57 57 | pass
|
||||
58 58 | with open(name="foo", mode="Ub") as f:
|
||||
59 59 | pass
|
||||
60 |-with open(mode="Ub", name="foo") as f:
|
||||
60 |+with open(mode="rb", name="foo") as f:
|
||||
61 61 | pass
|
||||
62 62 |
|
||||
63 63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:63:23: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
61 | pass
|
||||
62 |
|
||||
63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
| ^^^ UP015
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
60 60 | with open(mode="Ub", name="foo") as f:
|
||||
61 61 | pass
|
||||
62 62 |
|
||||
63 |-open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
63 |+open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:64:106: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
| ^^^ UP015
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
61 61 | pass
|
||||
62 62 |
|
||||
63 63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 |-open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
64 |+open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
65 65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 67 |
|
||||
|
||||
UP015.py:65:65: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
| ^^^ UP015
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
62 62 |
|
||||
63 63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 |-open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
65 |+open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 67 |
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:66:11: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
| ^^^ UP015
|
||||
67 |
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
63 63 | open(file="foo", mode='U', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
64 64 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
|
||||
65 65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 |-open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
66 |+open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 67 |
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
|
||||
UP015.py:68:23: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 |
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
| ^^^^ UP015
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
65 65 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 67 |
|
||||
68 |-open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
68 |+open(file="foo", mode="rb", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
||||
UP015.py:69:106: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
| ^^^^ UP015
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
66 66 | open(mode='U', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
67 67 |
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 |-open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
69 |+open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode="rb")
|
||||
70 70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
72 72 |
|
||||
|
||||
UP015.py:70:65: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
| ^^^^ UP015
|
||||
71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
67 67 |
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 |-open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
70 |+open(file="foo", buffering=-1, encoding=None, errors=None, mode="rb", newline=None, closefd=True, opener=None)
|
||||
71 71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
72 72 |
|
||||
73 73 | import aiofiles
|
||||
|
||||
UP015.py:71:11: UP015 [*] Unnecessary modes, use `rb`
|
||||
|
|
||||
69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 | open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
| ^^^^ UP015
|
||||
72 |
|
||||
73 | import aiofiles
|
||||
|
|
||||
= help: Replace with `rb`
|
||||
|
||||
ℹ Safe fix
|
||||
68 68 | open(file="foo", mode='Ub', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
69 69 | open(file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
|
||||
70 70 | open(file="foo", buffering=-1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
|
||||
71 |-open(mode='Ub', file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
71 |+open(mode="rb", file="foo", buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
|
||||
72 72 |
|
||||
73 73 | import aiofiles
|
||||
74 74 |
|
||||
|
||||
UP015.py:75:22: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
73 | import aiofiles
|
||||
74 |
|
||||
75 | aiofiles.open("foo", "U")
|
||||
| ^^^ UP015
|
||||
76 | aiofiles.open("foo", "r")
|
||||
77 | aiofiles.open("foo", mode="r")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
72 72 |
|
||||
73 73 | import aiofiles
|
||||
74 74 |
|
||||
75 |-aiofiles.open("foo", "U")
|
||||
75 |+aiofiles.open("foo")
|
||||
76 76 | aiofiles.open("foo", "r")
|
||||
77 77 | aiofiles.open("foo", mode="r")
|
||||
78 78 |
|
||||
|
||||
UP015.py:76:22: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
75 | aiofiles.open("foo", "U")
|
||||
76 | aiofiles.open("foo", "r")
|
||||
| ^^^ UP015
|
||||
77 | aiofiles.open("foo", mode="r")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
73 73 | import aiofiles
|
||||
74 74 |
|
||||
75 75 | aiofiles.open("foo", "U")
|
||||
76 |-aiofiles.open("foo", "r")
|
||||
76 |+aiofiles.open("foo")
|
||||
77 77 | aiofiles.open("foo", mode="r")
|
||||
78 78 |
|
||||
79 79 | open("foo", "r+")
|
||||
|
||||
UP015.py:77:27: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
75 | aiofiles.open("foo", "U")
|
||||
76 | aiofiles.open("foo", "r")
|
||||
77 | aiofiles.open("foo", mode="r")
|
||||
| ^^^ UP015
|
||||
78 |
|
||||
79 | open("foo", "r+")
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
74 74 |
|
||||
75 75 | aiofiles.open("foo", "U")
|
||||
76 76 | aiofiles.open("foo", "r")
|
||||
77 |-aiofiles.open("foo", mode="r")
|
||||
77 |+aiofiles.open("foo")
|
||||
78 78 |
|
||||
79 79 | open("foo", "r+")
|
||||
80 80 | open("foo", "rb")
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/pyupgrade/mod.rs
|
||||
---
|
||||
UP015_1.py:3:17: UP015 [*] Unnecessary mode argument
|
||||
|
|
||||
1 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
2 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
3 | x: 'open("foo", "r")'
|
||||
| ^^^ UP015
|
||||
|
|
||||
= help: Remove mode argument
|
||||
|
||||
ℹ Safe fix
|
||||
1 1 | # Not a valid type annotation but this test shouldn't result in a panic.
|
||||
2 2 | # Refer: https://github.com/astral-sh/ruff/issues/11736
|
||||
3 |-x: 'open("foo", "r")'
|
||||
3 |+x: 'open("foo")'
|
||||
4 4 |
|
||||
@@ -1,9 +1,11 @@
|
||||
use ruff_diagnostics::{Applicability, Edit, Fix};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, Expr};
|
||||
use ruff_python_codegen::Generator;
|
||||
use ruff_python_semantic::{BindingId, ResolvedReference, SemanticModel};
|
||||
use ruff_text_size::{Ranged, TextRange};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::settings::types::PythonVersion;
|
||||
|
||||
/// Format a code snippet to call `name.method()`.
|
||||
@@ -39,31 +41,45 @@ pub(super) fn generate_method_call(name: Name, method: &str, generator: Generato
|
||||
generator.stmt(&stmt.into())
|
||||
}
|
||||
|
||||
/// Format a code snippet comparing `name` to `None` (e.g., `name is None`).
|
||||
pub(super) fn generate_none_identity_comparison(
|
||||
name: Name,
|
||||
/// Returns a fix that replace `range` with
|
||||
/// a generated `a is None`/`a is not None` check.
|
||||
pub(super) fn replace_with_identity_check(
|
||||
left: &Expr,
|
||||
range: TextRange,
|
||||
negate: bool,
|
||||
generator: Generator,
|
||||
) -> String {
|
||||
// Construct `name`.
|
||||
let var = ast::ExprName {
|
||||
id: name,
|
||||
ctx: ast::ExprContext::Load,
|
||||
range: TextRange::default(),
|
||||
};
|
||||
// Construct `name is None` or `name is not None`.
|
||||
checker: &Checker,
|
||||
) -> Fix {
|
||||
let (semantic, generator) = (checker.semantic(), checker.generator());
|
||||
|
||||
let op = if negate {
|
||||
ast::CmpOp::IsNot
|
||||
} else {
|
||||
ast::CmpOp::Is
|
||||
};
|
||||
let compare = ast::ExprCompare {
|
||||
left: Box::new(var.into()),
|
||||
ops: Box::from([op]),
|
||||
comparators: Box::from([ast::Expr::NoneLiteral(ast::ExprNoneLiteral::default())]),
|
||||
|
||||
let new_expr = Expr::Compare(ast::ExprCompare {
|
||||
left: left.clone().into(),
|
||||
ops: [op].into(),
|
||||
comparators: [ast::ExprNoneLiteral::default().into()].into(),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
let new_content = generator.expr(&new_expr);
|
||||
let new_content = if semantic.current_expression_parent().is_some() {
|
||||
format!("({new_content})")
|
||||
} else {
|
||||
new_content
|
||||
};
|
||||
generator.expr(&compare.into())
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
let edit = Edit::range_replacement(new_content, range);
|
||||
|
||||
Fix::applicable_edit(edit, applicability)
|
||||
}
|
||||
|
||||
// Helpers for read-whole-file and write-whole-file
|
||||
|
||||
@@ -11,7 +11,7 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::types::PythonVersion;
|
||||
use crate::settings::types::{PreviewMode, PythonVersion};
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_messages, settings};
|
||||
|
||||
@@ -60,6 +60,24 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::TypeNoneComparison, Path::new("FURB169.py"))]
|
||||
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"preview__{}_{}",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("refurb").join(path).as_path(),
|
||||
&settings::LinterSettings {
|
||||
preview: PreviewMode::Enabled,
|
||||
..settings::LinterSettings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_messages!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_whole_file_python_39() -> Result<()> {
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{Expr, Stmt, StmtFor};
|
||||
use ruff_python_semantic::analyze::typing;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::helpers::parenthesize_loop_iter_if_necessary;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for code that updates a set with the contents of an iterable by
|
||||
/// using a `for` loop to call `.add()` or `.discard()` on each element
|
||||
@@ -34,6 +36,10 @@ use crate::checkers::ast::Checker;
|
||||
/// s.difference_update((1, 2, 3))
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// The fix will be marked as unsafe if applying the fix would delete any comments.
|
||||
/// Otherwise, it is marked as safe.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `set`](https://docs.python.org/3/library/stdtypes.html#set)
|
||||
#[derive(ViolationMetadata)]
|
||||
@@ -53,7 +59,7 @@ impl AlwaysFixableViolation for ForLoopSetMutations {
|
||||
}
|
||||
}
|
||||
|
||||
// FURB142
|
||||
/// FURB142
|
||||
pub(crate) fn for_loop_set_mutations(checker: &mut Checker, for_stmt: &StmtFor) {
|
||||
if !for_stmt.orelse.is_empty() {
|
||||
return;
|
||||
@@ -94,34 +100,41 @@ pub(crate) fn for_loop_set_mutations(checker: &mut Checker, for_stmt: &StmtFor)
|
||||
return;
|
||||
};
|
||||
|
||||
let locator = checker.locator();
|
||||
let content = match (for_stmt.target.as_ref(), arg) {
|
||||
(Expr::Name(for_target), Expr::Name(arg)) if for_target.id == arg.id => {
|
||||
format!(
|
||||
"{}.{batch_method_name}({})",
|
||||
set.id,
|
||||
checker.locator().slice(for_stmt.iter.as_ref())
|
||||
parenthesize_loop_iter_if_necessary(for_stmt, checker),
|
||||
)
|
||||
}
|
||||
(for_target, arg) => format!(
|
||||
"{}.{batch_method_name}({} for {} in {})",
|
||||
set.id,
|
||||
checker.locator().slice(arg),
|
||||
checker.locator().slice(for_target),
|
||||
checker.locator().slice(for_stmt.iter.as_ref())
|
||||
locator.slice(arg),
|
||||
locator.slice(for_target),
|
||||
parenthesize_loop_iter_if_necessary(for_stmt, checker),
|
||||
),
|
||||
};
|
||||
|
||||
checker.diagnostics.push(
|
||||
Diagnostic::new(
|
||||
ForLoopSetMutations {
|
||||
method_name,
|
||||
batch_method_name,
|
||||
},
|
||||
for_stmt.range,
|
||||
)
|
||||
.with_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
content,
|
||||
for_stmt.range,
|
||||
))),
|
||||
let applicability = if checker.comment_ranges().intersects(for_stmt.range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
let fix = Fix::applicable_edit(
|
||||
Edit::range_replacement(content, for_stmt.range),
|
||||
applicability,
|
||||
);
|
||||
|
||||
let diagnostic = Diagnostic::new(
|
||||
ForLoopSetMutations {
|
||||
method_name,
|
||||
batch_method_name,
|
||||
},
|
||||
for_stmt.range,
|
||||
);
|
||||
|
||||
checker.diagnostics.push(diagnostic.with_fix(fix));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::checkers::ast::Checker;
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Edit, Fix};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{Expr, ExprList, ExprName, ExprTuple, Stmt, StmtFor};
|
||||
@@ -6,6 +5,10 @@ use ruff_python_semantic::analyze::typing;
|
||||
use ruff_python_semantic::{Binding, ScopeId, SemanticModel, TypingOnlyBindingsStatus};
|
||||
use ruff_text_size::{Ranged, TextRange, TextSize};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
use super::helpers::parenthesize_loop_iter_if_necessary;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for the use of `IOBase.write` in a for loop.
|
||||
///
|
||||
@@ -51,6 +54,7 @@ impl AlwaysFixableViolation for ForLoopWrites {
|
||||
}
|
||||
}
|
||||
|
||||
/// FURB122
|
||||
pub(crate) fn for_loop_writes_binding(checker: &Checker, binding: &Binding) -> Option<Diagnostic> {
|
||||
if !binding.kind.is_loop_var() {
|
||||
return None;
|
||||
@@ -73,6 +77,7 @@ pub(crate) fn for_loop_writes_binding(checker: &Checker, binding: &Binding) -> O
|
||||
for_loop_writes(checker, for_stmt, binding.scope, &binding_names)
|
||||
}
|
||||
|
||||
/// FURB122
|
||||
pub(crate) fn for_loop_writes_stmt(checker: &mut Checker, for_stmt: &StmtFor) {
|
||||
// Loops with bindings are handled later.
|
||||
if !binding_names(&for_stmt.target).is_empty() {
|
||||
@@ -114,6 +119,7 @@ fn binding_names(for_target: &Expr) -> Vec<&ExprName> {
|
||||
names
|
||||
}
|
||||
|
||||
/// FURB122
|
||||
fn for_loop_writes(
|
||||
checker: &Checker,
|
||||
for_stmt: &StmtFor,
|
||||
@@ -155,30 +161,35 @@ fn for_loop_writes(
|
||||
return None;
|
||||
}
|
||||
|
||||
let locator = checker.locator();
|
||||
let content = match (for_stmt.target.as_ref(), write_arg) {
|
||||
(Expr::Name(for_target), Expr::Name(write_arg)) if for_target.id == write_arg.id => {
|
||||
format!(
|
||||
"{}.writelines({})",
|
||||
checker.locator().slice(io_object_name),
|
||||
checker.locator().slice(for_stmt.iter.as_ref()),
|
||||
locator.slice(io_object_name),
|
||||
parenthesize_loop_iter_if_necessary(for_stmt, checker),
|
||||
)
|
||||
}
|
||||
(for_target, write_arg) => {
|
||||
format!(
|
||||
"{}.writelines({} for {} in {})",
|
||||
checker.locator().slice(io_object_name),
|
||||
checker.locator().slice(write_arg),
|
||||
checker.locator().slice(for_target),
|
||||
checker.locator().slice(for_stmt.iter.as_ref()),
|
||||
locator.slice(io_object_name),
|
||||
locator.slice(write_arg),
|
||||
locator.slice(for_target),
|
||||
parenthesize_loop_iter_if_necessary(for_stmt, checker),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(for_stmt.range()) {
|
||||
let applicability = if checker.comment_ranges().intersects(for_stmt.range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
let fix = Fix::applicable_edit(
|
||||
Edit::range_replacement(content, for_stmt.range),
|
||||
applicability,
|
||||
);
|
||||
|
||||
let diagnostic = Diagnostic::new(
|
||||
ForLoopWrites {
|
||||
@@ -186,10 +197,6 @@ fn for_loop_writes(
|
||||
},
|
||||
for_stmt.range,
|
||||
);
|
||||
let fix = Fix::applicable_edit(
|
||||
Edit::range_replacement(content, for_stmt.range),
|
||||
applicability,
|
||||
);
|
||||
|
||||
Some(diagnostic.with_fix(fix))
|
||||
}
|
||||
|
||||
40
crates/ruff_linter/src/rules/refurb/rules/helpers.rs
Normal file
40
crates/ruff_linter/src/rules/refurb/rules/helpers.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ruff_python_ast::{self as ast, parenthesize::parenthesized_range};
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
|
||||
/// A helper function that extracts the `iter` from a [`ast::StmtFor`] node and,
|
||||
/// if the `iter` is an unparenthesized tuple, adds parentheses:
|
||||
///
|
||||
/// - `for x in z: ...` -> `"x"`
|
||||
/// - `for (x, y) in z: ...` -> `"(x, y)"`
|
||||
/// - `for [x, y] in z: ...` -> `"[x, y]"`
|
||||
/// - `for x, y in z: ...` -> `"(x, y)"` # <-- Parentheses added only for this example
|
||||
pub(super) fn parenthesize_loop_iter_if_necessary<'a>(
|
||||
for_stmt: &'a ast::StmtFor,
|
||||
checker: &'a Checker,
|
||||
) -> Cow<'a, str> {
|
||||
let locator = checker.locator();
|
||||
let iter = for_stmt.iter.as_ref();
|
||||
|
||||
let original_parenthesized_range = parenthesized_range(
|
||||
iter.into(),
|
||||
for_stmt.into(),
|
||||
checker.comment_ranges(),
|
||||
checker.source(),
|
||||
);
|
||||
|
||||
if let Some(range) = original_parenthesized_range {
|
||||
return Cow::Borrowed(locator.slice(range));
|
||||
}
|
||||
|
||||
let iter_in_source = locator.slice(iter);
|
||||
|
||||
match iter {
|
||||
ast::Expr::Tuple(tuple) if !tuple.parenthesized => {
|
||||
Cow::Owned(format!("({iter_in_source})"))
|
||||
}
|
||||
_ => Cow::Borrowed(iter_in_source),
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use ruff_diagnostics::{Applicability, Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_python_ast::{self as ast, CmpOp, Expr, Operator};
|
||||
|
||||
use ruff_diagnostics::{Diagnostic, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::{self as ast, Expr, Operator};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::TextRange;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::rules::refurb::helpers::replace_with_identity_check;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `isinstance` that check if an object is of type `None`.
|
||||
@@ -69,7 +68,7 @@ pub(crate) fn isinstance_type_none(checker: &mut Checker, call: &ast::ExprCall)
|
||||
return;
|
||||
}
|
||||
|
||||
let fix = replace_with_identity_check(expr, call.range, checker);
|
||||
let fix = replace_with_identity_check(expr, call.range, false, checker);
|
||||
let diagnostic = Diagnostic::new(IsinstanceTypeNone, call.range);
|
||||
|
||||
checker.diagnostics.push(diagnostic.with_fix(fix));
|
||||
@@ -138,31 +137,3 @@ fn is_none(expr: &Expr, semantic: &SemanticModel) -> bool {
|
||||
}
|
||||
inner(expr, false, semantic)
|
||||
}
|
||||
|
||||
fn replace_with_identity_check(expr: &Expr, range: TextRange, checker: &Checker) -> Fix {
|
||||
let (semantic, generator) = (checker.semantic(), checker.generator());
|
||||
|
||||
let new_expr = Expr::Compare(ast::ExprCompare {
|
||||
left: expr.clone().into(),
|
||||
ops: [CmpOp::Is].into(),
|
||||
comparators: [ast::ExprNoneLiteral::default().into()].into(),
|
||||
range: TextRange::default(),
|
||||
});
|
||||
|
||||
let new_content = generator.expr(&new_expr);
|
||||
let new_content = if semantic.current_expression_parent().is_some() {
|
||||
format!("({new_content})")
|
||||
} else {
|
||||
new_content
|
||||
};
|
||||
|
||||
let applicability = if checker.comment_ranges().intersects(range) {
|
||||
Applicability::Unsafe
|
||||
} else {
|
||||
Applicability::Safe
|
||||
};
|
||||
|
||||
let edit = Edit::range_replacement(new_content, range);
|
||||
|
||||
Fix::applicable_edit(edit, applicability)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ mod for_loop_writes;
|
||||
mod fstring_number_format;
|
||||
mod hardcoded_string_charset;
|
||||
mod hashlib_digest_hex;
|
||||
mod helpers;
|
||||
mod if_exp_instead_of_or_operator;
|
||||
mod if_expr_min_max;
|
||||
mod implicit_cwd;
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
|
||||
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::name::Name;
|
||||
use ruff_python_ast::{self as ast, CmpOp, Expr};
|
||||
use ruff_python_semantic::SemanticModel;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::edits::pad;
|
||||
use crate::rules::refurb::helpers::generate_none_identity_comparison;
|
||||
use crate::rules::refurb::helpers::replace_with_identity_check;
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for uses of `type` that compare the type of an object to the type of
|
||||
/// `None`.
|
||||
/// Checks for uses of `type` that compare the type of an object to the type of `None`.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// There is only ever one instance of `None`, so it is more efficient and
|
||||
/// readable to use the `is` operator to check if an object is `None`.
|
||||
///
|
||||
/// Only name expressions (e.g., `type(foo) == type(None)`) are reported.
|
||||
/// In [preview], the rule will also report other kinds of expressions.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// type(obj) is type(None)
|
||||
@@ -27,34 +26,32 @@ use crate::rules::refurb::helpers::generate_none_identity_comparison;
|
||||
/// obj is None
|
||||
/// ```
|
||||
///
|
||||
/// ## Fix safety
|
||||
/// If the fix might remove comments, it will be marked as unsafe.
|
||||
///
|
||||
/// ## References
|
||||
/// - [Python documentation: `isinstance`](https://docs.python.org/3/library/functions.html#isinstance)
|
||||
/// - [Python documentation: `None`](https://docs.python.org/3/library/constants.html#None)
|
||||
/// - [Python documentation: `type`](https://docs.python.org/3/library/functions.html#type)
|
||||
/// - [Python documentation: Identity comparisons](https://docs.python.org/3/reference/expressions.html#is-not)
|
||||
///
|
||||
/// [preview]: https://docs.astral.sh/ruff/preview/
|
||||
#[derive(ViolationMetadata)]
|
||||
pub(crate) struct TypeNoneComparison {
|
||||
object: Name,
|
||||
comparison: Comparison,
|
||||
replacement: IdentityCheck,
|
||||
}
|
||||
|
||||
impl Violation for TypeNoneComparison {
|
||||
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
|
||||
|
||||
impl AlwaysFixableViolation for TypeNoneComparison {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let TypeNoneComparison { object, .. } = self;
|
||||
format!("Compare the identities of `{object}` and `None` instead of their respective types")
|
||||
format!(
|
||||
"When checking against `None`, use `{}` instead of comparison with `type(None)`",
|
||||
self.replacement.op()
|
||||
)
|
||||
}
|
||||
|
||||
fn fix_title(&self) -> Option<String> {
|
||||
let TypeNoneComparison { object, comparison } = self;
|
||||
match comparison {
|
||||
Comparison::Is | Comparison::Eq => Some(format!("Replace with `{object} is None`")),
|
||||
Comparison::IsNot | Comparison::NotEq => {
|
||||
Some(format!("Replace with `{object} is not None`"))
|
||||
}
|
||||
}
|
||||
fn fix_title(&self) -> String {
|
||||
format!("Replace with `{} None`", self.replacement.op())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,16 +61,12 @@ pub(crate) fn type_none_comparison(checker: &mut Checker, compare: &ast::ExprCom
|
||||
return;
|
||||
};
|
||||
|
||||
// Ensure that the comparison is an identity or equality test.
|
||||
let comparison = match op {
|
||||
CmpOp::Is => Comparison::Is,
|
||||
CmpOp::IsNot => Comparison::IsNot,
|
||||
CmpOp::Eq => Comparison::Eq,
|
||||
CmpOp::NotEq => Comparison::NotEq,
|
||||
let replacement = match op {
|
||||
CmpOp::Is | CmpOp::Eq => IdentityCheck::Is,
|
||||
CmpOp::IsNot | CmpOp::NotEq => IdentityCheck::IsNot,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Get the objects whose types are being compared.
|
||||
let Some(left_arg) = type_call_arg(&compare.left, checker.semantic()) else {
|
||||
return;
|
||||
};
|
||||
@@ -81,48 +74,24 @@ pub(crate) fn type_none_comparison(checker: &mut Checker, compare: &ast::ExprCom
|
||||
return;
|
||||
};
|
||||
|
||||
// If one of the objects is `None`, get the other object; else, return.
|
||||
let other_arg = match (
|
||||
left_arg.is_none_literal_expr(),
|
||||
right_arg.is_none_literal_expr(),
|
||||
) {
|
||||
(true, false) => right_arg,
|
||||
(false, true) => left_arg,
|
||||
// If both are `None`, just pick one.
|
||||
(true, true) => left_arg,
|
||||
let other_arg = match (left_arg, right_arg) {
|
||||
(Expr::NoneLiteral(_), _) => right_arg,
|
||||
(_, Expr::NoneLiteral(_)) => left_arg,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
// Get the name of the other object (or `None` if both were `None`).
|
||||
let other_arg_name = match other_arg {
|
||||
Expr::Name(ast::ExprName { id, .. }) => id.clone(),
|
||||
Expr::NoneLiteral { .. } => Name::new_static("None"),
|
||||
_ => return,
|
||||
};
|
||||
if checker.settings.preview.is_disabled()
|
||||
&& !matches!(other_arg, Expr::Name(_) | Expr::NoneLiteral(_))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
TypeNoneComparison {
|
||||
object: other_arg_name.clone(),
|
||||
comparison,
|
||||
},
|
||||
compare.range(),
|
||||
);
|
||||
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
|
||||
pad(
|
||||
match comparison {
|
||||
Comparison::Is | Comparison::Eq => {
|
||||
generate_none_identity_comparison(other_arg_name, false, checker.generator())
|
||||
}
|
||||
Comparison::IsNot | Comparison::NotEq => {
|
||||
generate_none_identity_comparison(other_arg_name, true, checker.generator())
|
||||
}
|
||||
},
|
||||
compare.range(),
|
||||
checker.locator(),
|
||||
),
|
||||
compare.range(),
|
||||
)));
|
||||
checker.diagnostics.push(diagnostic);
|
||||
let diagnostic = Diagnostic::new(TypeNoneComparison { replacement }, compare.range);
|
||||
|
||||
let negate = replacement == IdentityCheck::IsNot;
|
||||
let fix = replace_with_identity_check(other_arg, compare.range, negate, checker);
|
||||
|
||||
checker.diagnostics.push(diagnostic.with_fix(fix));
|
||||
}
|
||||
|
||||
/// Returns the object passed to the function, if the expression is a call to
|
||||
@@ -143,9 +112,16 @@ fn type_call_arg<'a>(expr: &'a Expr, semantic: &'a SemanticModel) -> Option<&'a
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Comparison {
|
||||
enum IdentityCheck {
|
||||
Is,
|
||||
IsNot,
|
||||
Eq,
|
||||
NotEq,
|
||||
}
|
||||
|
||||
impl IdentityCheck {
|
||||
fn op(self) -> CmpOp {
|
||||
match self {
|
||||
Self::Is => CmpOp::Is,
|
||||
Self::IsNot => CmpOp::IsNot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,4 +232,78 @@ FURB122.py:75:9: FURB122 [*] Use of `f.write` in a for loop
|
||||
75 |+ f.writelines(() for [([([a[b]],)],), [], (c[d],)] in e)
|
||||
77 76 |
|
||||
78 77 |
|
||||
79 78 | # OK
|
||||
79 78 | def _():
|
||||
|
||||
FURB122.py:82:9: FURB122 [*] Use of `f.write` in a for loop
|
||||
|
|
||||
80 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
81 | with open("file", "w") as f:
|
||||
82 | / for char in "a", "b":
|
||||
83 | | f.write(char)
|
||||
| |_________________________^ FURB122
|
||||
84 |
|
||||
85 | def _():
|
||||
|
|
||||
= help: Replace with `f.writelines`
|
||||
|
||||
ℹ Safe fix
|
||||
79 79 | def _():
|
||||
80 80 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
81 81 | with open("file", "w") as f:
|
||||
82 |- for char in "a", "b":
|
||||
83 |- f.write(char)
|
||||
82 |+ f.writelines(("a", "b"))
|
||||
84 83 |
|
||||
85 84 | def _():
|
||||
86 85 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
|
||||
FURB122.py:88:9: FURB122 [*] Use of `f.write` in a for loop
|
||||
|
|
||||
86 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
87 | with open("file", "w") as f:
|
||||
88 | / for char in "a", "b":
|
||||
89 | | f.write(f"{char}")
|
||||
| |______________________________^ FURB122
|
||||
90 |
|
||||
91 | def _():
|
||||
|
|
||||
= help: Replace with `f.writelines`
|
||||
|
||||
ℹ Safe fix
|
||||
85 85 | def _():
|
||||
86 86 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
87 87 | with open("file", "w") as f:
|
||||
88 |- for char in "a", "b":
|
||||
89 |- f.write(f"{char}")
|
||||
88 |+ f.writelines(f"{char}" for char in ("a", "b"))
|
||||
90 89 |
|
||||
91 90 | def _():
|
||||
92 91 | with open("file", "w") as f:
|
||||
|
||||
FURB122.py:93:9: FURB122 [*] Use of `f.write` in a for loop
|
||||
|
|
||||
91 | def _():
|
||||
92 | with open("file", "w") as f:
|
||||
93 | / for char in (
|
||||
94 | | "a", # Comment
|
||||
95 | | "b"
|
||||
96 | | ):
|
||||
97 | | f.write(f"{char}")
|
||||
| |______________________________^ FURB122
|
||||
|
|
||||
= help: Replace with `f.writelines`
|
||||
|
||||
ℹ Unsafe fix
|
||||
90 90 |
|
||||
91 91 | def _():
|
||||
92 92 | with open("file", "w") as f:
|
||||
93 |- for char in (
|
||||
93 |+ f.writelines(f"{char}" for char in (
|
||||
94 94 | "a", # Comment
|
||||
95 95 | "b"
|
||||
96 |- ):
|
||||
97 |- f.write(f"{char}")
|
||||
96 |+ ))
|
||||
98 97 |
|
||||
99 98 |
|
||||
100 99 | # OK
|
||||
|
||||
@@ -193,7 +193,7 @@ FURB142.py:31:1: FURB142 [*] Use of `set.add()` in a for loop
|
||||
32 | | s.add(x + num)
|
||||
| |__________________^ FURB142
|
||||
33 |
|
||||
34 | # False negative
|
||||
34 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
|
|
||||
= help: Replace with `.update()`
|
||||
|
||||
@@ -205,5 +205,78 @@ FURB142.py:31:1: FURB142 [*] Use of `set.add()` in a for loop
|
||||
32 |- s.add(x + num)
|
||||
31 |+s.update(x + num for x in (1, 2, 3))
|
||||
33 32 |
|
||||
34 33 | # False negative
|
||||
35 34 |
|
||||
34 33 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
35 34 | for x in 1, 2, 3:
|
||||
|
||||
FURB142.py:35:1: FURB142 [*] Use of `set.add()` in a for loop
|
||||
|
|
||||
34 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
35 | / for x in 1, 2, 3:
|
||||
36 | | s.add(x)
|
||||
| |____________^ FURB142
|
||||
37 |
|
||||
38 | for x in 1, 2, 3:
|
||||
|
|
||||
= help: Replace with `.update()`
|
||||
|
||||
ℹ Safe fix
|
||||
32 32 | s.add(x + num)
|
||||
33 33 |
|
||||
34 34 | # https://github.com/astral-sh/ruff/issues/15936
|
||||
35 |-for x in 1, 2, 3:
|
||||
36 |- s.add(x)
|
||||
35 |+s.update((1, 2, 3))
|
||||
37 36 |
|
||||
38 37 | for x in 1, 2, 3:
|
||||
39 38 | s.add(f"{x}")
|
||||
|
||||
FURB142.py:38:1: FURB142 [*] Use of `set.add()` in a for loop
|
||||
|
|
||||
36 | s.add(x)
|
||||
37 |
|
||||
38 | / for x in 1, 2, 3:
|
||||
39 | | s.add(f"{x}")
|
||||
| |_________________^ FURB142
|
||||
40 |
|
||||
41 | for x in (
|
||||
|
|
||||
= help: Replace with `.update()`
|
||||
|
||||
ℹ Safe fix
|
||||
35 35 | for x in 1, 2, 3:
|
||||
36 36 | s.add(x)
|
||||
37 37 |
|
||||
38 |-for x in 1, 2, 3:
|
||||
39 |- s.add(f"{x}")
|
||||
38 |+s.update(f"{x}" for x in (1, 2, 3))
|
||||
40 39 |
|
||||
41 40 | for x in (
|
||||
42 41 | 1, # Comment
|
||||
|
||||
FURB142.py:41:1: FURB142 [*] Use of `set.add()` in a for loop
|
||||
|
|
||||
39 | s.add(f"{x}")
|
||||
40 |
|
||||
41 | / for x in (
|
||||
42 | | 1, # Comment
|
||||
43 | | 2, 3
|
||||
44 | | ):
|
||||
45 | | s.add(f"{x}")
|
||||
| |_________________^ FURB142
|
||||
|
|
||||
= help: Replace with `.update()`
|
||||
|
||||
ℹ Unsafe fix
|
||||
38 38 | for x in 1, 2, 3:
|
||||
39 39 | s.add(f"{x}")
|
||||
40 40 |
|
||||
41 |-for x in (
|
||||
41 |+s.update(f"{x}" for x in (
|
||||
42 42 | 1, # Comment
|
||||
43 43 | 2, 3
|
||||
44 |-):
|
||||
45 |- s.add(f"{x}")
|
||||
44 |+))
|
||||
46 45 |
|
||||
47 46 |
|
||||
48 47 | # False negative
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB169.py:5:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:5:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
3 | # Error.
|
||||
4 |
|
||||
@@ -10,7 +10,7 @@ FURB169.py:5:1: FURB169 [*] Compare the identities of `foo` and `None` instead o
|
||||
6 |
|
||||
7 | type(None) is type(foo)
|
||||
|
|
||||
= help: Replace with `foo is None`
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
@@ -22,7 +22,7 @@ FURB169.py:5:1: FURB169 [*] Compare the identities of `foo` and `None` instead o
|
||||
7 7 | type(None) is type(foo)
|
||||
8 8 |
|
||||
|
||||
FURB169.py:7:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:7:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
5 | type(foo) is type(None)
|
||||
6 |
|
||||
@@ -31,7 +31,7 @@ FURB169.py:7:1: FURB169 [*] Compare the identities of `foo` and `None` instead o
|
||||
8 |
|
||||
9 | type(None) is type(None)
|
||||
|
|
||||
= help: Replace with `foo is None`
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
@@ -43,7 +43,7 @@ FURB169.py:7:1: FURB169 [*] Compare the identities of `foo` and `None` instead o
|
||||
9 9 | type(None) is type(None)
|
||||
10 10 |
|
||||
|
||||
FURB169.py:9:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
||||
FURB169.py:9:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
7 | type(None) is type(foo)
|
||||
8 |
|
||||
@@ -52,7 +52,7 @@ FURB169.py:9:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
10 |
|
||||
11 | type(foo) is not type(None)
|
||||
|
|
||||
= help: Replace with `None is None`
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
@@ -64,7 +64,7 @@ FURB169.py:9:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
11 11 | type(foo) is not type(None)
|
||||
12 12 |
|
||||
|
||||
FURB169.py:11:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:11:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
9 | type(None) is type(None)
|
||||
10 |
|
||||
@@ -73,7 +73,7 @@ FURB169.py:11:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
12 |
|
||||
13 | type(None) is not type(foo)
|
||||
|
|
||||
= help: Replace with `foo is not None`
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 |
|
||||
@@ -85,7 +85,7 @@ FURB169.py:11:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
13 13 | type(None) is not type(foo)
|
||||
14 14 |
|
||||
|
||||
FURB169.py:13:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:13:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
11 | type(foo) is not type(None)
|
||||
12 |
|
||||
@@ -94,7 +94,7 @@ FURB169.py:13:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
14 |
|
||||
15 | type(None) is not type(None)
|
||||
|
|
||||
= help: Replace with `foo is not None`
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
@@ -106,7 +106,7 @@ FURB169.py:13:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
15 15 | type(None) is not type(None)
|
||||
16 16 |
|
||||
|
||||
FURB169.py:15:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
||||
FURB169.py:15:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
13 | type(None) is not type(foo)
|
||||
14 |
|
||||
@@ -115,7 +115,7 @@ FURB169.py:15:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
16 |
|
||||
17 | type(foo) == type(None)
|
||||
|
|
||||
= help: Replace with `None is not None`
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 |
|
||||
@@ -127,7 +127,7 @@ FURB169.py:15:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
17 17 | type(foo) == type(None)
|
||||
18 18 |
|
||||
|
||||
FURB169.py:17:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:17:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
15 | type(None) is not type(None)
|
||||
16 |
|
||||
@@ -136,7 +136,7 @@ FURB169.py:17:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
18 |
|
||||
19 | type(None) == type(foo)
|
||||
|
|
||||
= help: Replace with `foo is None`
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 |
|
||||
@@ -148,7 +148,7 @@ FURB169.py:17:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
19 19 | type(None) == type(foo)
|
||||
20 20 |
|
||||
|
||||
FURB169.py:19:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:19:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
17 | type(foo) == type(None)
|
||||
18 |
|
||||
@@ -157,7 +157,7 @@ FURB169.py:19:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
20 |
|
||||
21 | type(None) == type(None)
|
||||
|
|
||||
= help: Replace with `foo is None`
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 |
|
||||
@@ -169,7 +169,7 @@ FURB169.py:19:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
21 21 | type(None) == type(None)
|
||||
22 22 |
|
||||
|
||||
FURB169.py:21:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
||||
FURB169.py:21:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
19 | type(None) == type(foo)
|
||||
20 |
|
||||
@@ -178,7 +178,7 @@ FURB169.py:21:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
22 |
|
||||
23 | type(foo) != type(None)
|
||||
|
|
||||
= help: Replace with `None is None`
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 |
|
||||
@@ -190,7 +190,7 @@ FURB169.py:21:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
23 23 | type(foo) != type(None)
|
||||
24 24 |
|
||||
|
||||
FURB169.py:23:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:23:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
21 | type(None) == type(None)
|
||||
22 |
|
||||
@@ -199,7 +199,7 @@ FURB169.py:23:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
24 |
|
||||
25 | type(None) != type(foo)
|
||||
|
|
||||
= help: Replace with `foo is not None`
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 |
|
||||
@@ -211,7 +211,7 @@ FURB169.py:23:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
25 25 | type(None) != type(foo)
|
||||
26 26 |
|
||||
|
||||
FURB169.py:25:1: FURB169 [*] Compare the identities of `foo` and `None` instead of their respective types
|
||||
FURB169.py:25:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
23 | type(foo) != type(None)
|
||||
24 |
|
||||
@@ -220,7 +220,7 @@ FURB169.py:25:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
26 |
|
||||
27 | type(None) != type(None)
|
||||
|
|
||||
= help: Replace with `foo is not None`
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 |
|
||||
@@ -232,16 +232,16 @@ FURB169.py:25:1: FURB169 [*] Compare the identities of `foo` and `None` instead
|
||||
27 27 | type(None) != type(None)
|
||||
28 28 |
|
||||
|
||||
FURB169.py:27:1: FURB169 [*] Compare the identities of `None` and `None` instead of their respective types
|
||||
FURB169.py:27:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
25 | type(None) != type(foo)
|
||||
26 |
|
||||
27 | type(None) != type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
28 |
|
||||
29 | # Ok.
|
||||
29 | type(a.b) is type(None)
|
||||
|
|
||||
= help: Replace with `None is not None`
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
24 24 |
|
||||
@@ -250,5 +250,5 @@ FURB169.py:27:1: FURB169 [*] Compare the identities of `None` and `None` instead
|
||||
27 |-type(None) != type(None)
|
||||
27 |+None is not None
|
||||
28 28 |
|
||||
29 29 | # Ok.
|
||||
29 29 | type(a.b) is type(None)
|
||||
30 30 |
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
---
|
||||
source: crates/ruff_linter/src/rules/refurb/mod.rs
|
||||
---
|
||||
FURB169.py:5:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
3 | # Error.
|
||||
4 |
|
||||
5 | type(foo) is type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
6 |
|
||||
7 | type(None) is type(foo)
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
2 2 |
|
||||
3 3 | # Error.
|
||||
4 4 |
|
||||
5 |-type(foo) is type(None)
|
||||
5 |+foo is None
|
||||
6 6 |
|
||||
7 7 | type(None) is type(foo)
|
||||
8 8 |
|
||||
|
||||
FURB169.py:7:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
5 | type(foo) is type(None)
|
||||
6 |
|
||||
7 | type(None) is type(foo)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
8 |
|
||||
9 | type(None) is type(None)
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
4 4 |
|
||||
5 5 | type(foo) is type(None)
|
||||
6 6 |
|
||||
7 |-type(None) is type(foo)
|
||||
7 |+foo is None
|
||||
8 8 |
|
||||
9 9 | type(None) is type(None)
|
||||
10 10 |
|
||||
|
||||
FURB169.py:9:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
7 | type(None) is type(foo)
|
||||
8 |
|
||||
9 | type(None) is type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
10 |
|
||||
11 | type(foo) is not type(None)
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
6 6 |
|
||||
7 7 | type(None) is type(foo)
|
||||
8 8 |
|
||||
9 |-type(None) is type(None)
|
||||
9 |+None is None
|
||||
10 10 |
|
||||
11 11 | type(foo) is not type(None)
|
||||
12 12 |
|
||||
|
||||
FURB169.py:11:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
9 | type(None) is type(None)
|
||||
10 |
|
||||
11 | type(foo) is not type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
12 |
|
||||
13 | type(None) is not type(foo)
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
8 8 |
|
||||
9 9 | type(None) is type(None)
|
||||
10 10 |
|
||||
11 |-type(foo) is not type(None)
|
||||
11 |+foo is not None
|
||||
12 12 |
|
||||
13 13 | type(None) is not type(foo)
|
||||
14 14 |
|
||||
|
||||
FURB169.py:13:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
11 | type(foo) is not type(None)
|
||||
12 |
|
||||
13 | type(None) is not type(foo)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
14 |
|
||||
15 | type(None) is not type(None)
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
10 10 |
|
||||
11 11 | type(foo) is not type(None)
|
||||
12 12 |
|
||||
13 |-type(None) is not type(foo)
|
||||
13 |+foo is not None
|
||||
14 14 |
|
||||
15 15 | type(None) is not type(None)
|
||||
16 16 |
|
||||
|
||||
FURB169.py:15:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
13 | type(None) is not type(foo)
|
||||
14 |
|
||||
15 | type(None) is not type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
16 |
|
||||
17 | type(foo) == type(None)
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
12 12 |
|
||||
13 13 | type(None) is not type(foo)
|
||||
14 14 |
|
||||
15 |-type(None) is not type(None)
|
||||
15 |+None is not None
|
||||
16 16 |
|
||||
17 17 | type(foo) == type(None)
|
||||
18 18 |
|
||||
|
||||
FURB169.py:17:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
15 | type(None) is not type(None)
|
||||
16 |
|
||||
17 | type(foo) == type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
18 |
|
||||
19 | type(None) == type(foo)
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
14 14 |
|
||||
15 15 | type(None) is not type(None)
|
||||
16 16 |
|
||||
17 |-type(foo) == type(None)
|
||||
17 |+foo is None
|
||||
18 18 |
|
||||
19 19 | type(None) == type(foo)
|
||||
20 20 |
|
||||
|
||||
FURB169.py:19:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
17 | type(foo) == type(None)
|
||||
18 |
|
||||
19 | type(None) == type(foo)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
20 |
|
||||
21 | type(None) == type(None)
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
16 16 |
|
||||
17 17 | type(foo) == type(None)
|
||||
18 18 |
|
||||
19 |-type(None) == type(foo)
|
||||
19 |+foo is None
|
||||
20 20 |
|
||||
21 21 | type(None) == type(None)
|
||||
22 22 |
|
||||
|
||||
FURB169.py:21:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
19 | type(None) == type(foo)
|
||||
20 |
|
||||
21 | type(None) == type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
22 |
|
||||
23 | type(foo) != type(None)
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
18 18 |
|
||||
19 19 | type(None) == type(foo)
|
||||
20 20 |
|
||||
21 |-type(None) == type(None)
|
||||
21 |+None is None
|
||||
22 22 |
|
||||
23 23 | type(foo) != type(None)
|
||||
24 24 |
|
||||
|
||||
FURB169.py:23:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
21 | type(None) == type(None)
|
||||
22 |
|
||||
23 | type(foo) != type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
24 |
|
||||
25 | type(None) != type(foo)
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
20 20 |
|
||||
21 21 | type(None) == type(None)
|
||||
22 22 |
|
||||
23 |-type(foo) != type(None)
|
||||
23 |+foo is not None
|
||||
24 24 |
|
||||
25 25 | type(None) != type(foo)
|
||||
26 26 |
|
||||
|
||||
FURB169.py:25:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
23 | type(foo) != type(None)
|
||||
24 |
|
||||
25 | type(None) != type(foo)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
26 |
|
||||
27 | type(None) != type(None)
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
22 22 |
|
||||
23 23 | type(foo) != type(None)
|
||||
24 24 |
|
||||
25 |-type(None) != type(foo)
|
||||
25 |+foo is not None
|
||||
26 26 |
|
||||
27 27 | type(None) != type(None)
|
||||
28 28 |
|
||||
|
||||
FURB169.py:27:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
25 | type(None) != type(foo)
|
||||
26 |
|
||||
27 | type(None) != type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
28 |
|
||||
29 | type(a.b) is type(None)
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
24 24 |
|
||||
25 25 | type(None) != type(foo)
|
||||
26 26 |
|
||||
27 |-type(None) != type(None)
|
||||
27 |+None is not None
|
||||
28 28 |
|
||||
29 29 | type(a.b) is type(None)
|
||||
30 30 |
|
||||
|
||||
FURB169.py:29:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
27 | type(None) != type(None)
|
||||
28 |
|
||||
29 | type(a.b) is type(None)
|
||||
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB169
|
||||
30 |
|
||||
31 | type(
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
26 26 |
|
||||
27 27 | type(None) != type(None)
|
||||
28 28 |
|
||||
29 |-type(a.b) is type(None)
|
||||
29 |+a.b is None
|
||||
30 30 |
|
||||
31 31 | type(
|
||||
32 32 | a(
|
||||
|
||||
FURB169.py:31:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
29 | type(a.b) is type(None)
|
||||
30 |
|
||||
31 | / type(
|
||||
32 | | a(
|
||||
33 | | # Comment
|
||||
34 | | )
|
||||
35 | | ) != type(None)
|
||||
| |_______________^ FURB169
|
||||
36 |
|
||||
37 | type(
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Unsafe fix
|
||||
28 28 |
|
||||
29 29 | type(a.b) is type(None)
|
||||
30 30 |
|
||||
31 |-type(
|
||||
32 |- a(
|
||||
33 |- # Comment
|
||||
34 |- )
|
||||
35 |-) != type(None)
|
||||
31 |+a() is not None
|
||||
36 32 |
|
||||
37 33 | type(
|
||||
38 34 | a := 1
|
||||
|
||||
FURB169.py:37:1: FURB169 [*] When checking against `None`, use `is` instead of comparison with `type(None)`
|
||||
|
|
||||
35 | ) != type(None)
|
||||
36 |
|
||||
37 | / type(
|
||||
38 | | a := 1
|
||||
39 | | ) == type(None)
|
||||
| |_______________^ FURB169
|
||||
40 |
|
||||
41 | type(
|
||||
|
|
||||
= help: Replace with `is None`
|
||||
|
||||
ℹ Safe fix
|
||||
34 34 | )
|
||||
35 35 | ) != type(None)
|
||||
36 36 |
|
||||
37 |-type(
|
||||
38 |- a := 1
|
||||
39 |-) == type(None)
|
||||
37 |+(a := 1) is None
|
||||
40 38 |
|
||||
41 39 | type(
|
||||
42 40 | a for a in range(0)
|
||||
|
||||
FURB169.py:41:1: FURB169 [*] When checking against `None`, use `is not` instead of comparison with `type(None)`
|
||||
|
|
||||
39 | ) == type(None)
|
||||
40 |
|
||||
41 | / type(
|
||||
42 | | a for a in range(0)
|
||||
43 | | ) is not type(None)
|
||||
| |___________________^ FURB169
|
||||
|
|
||||
= help: Replace with `is not None`
|
||||
|
||||
ℹ Safe fix
|
||||
38 38 | a := 1
|
||||
39 39 | ) == type(None)
|
||||
40 40 |
|
||||
41 |-type(
|
||||
42 |- a for a in range(0)
|
||||
43 |-) is not type(None)
|
||||
41 |+(a for a in range(0)) is not None
|
||||
44 42 |
|
||||
45 43 |
|
||||
46 44 | # Ok.
|
||||
@@ -30,6 +30,7 @@ mod tests {
|
||||
#[test_case(Rule::ZipInsteadOfPairwise, Path::new("RUF007.py"))]
|
||||
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008.py"))]
|
||||
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008_attrs.py"))]
|
||||
#[test_case(Rule::MutableDataclassDefault, Path::new("RUF008_deferred.py"))]
|
||||
#[test_case(Rule::FunctionCallInDataclassDefaultArgument, Path::new("RUF009.py"))]
|
||||
#[test_case(
|
||||
Rule::FunctionCallInDataclassDefaultArgument,
|
||||
@@ -39,8 +40,13 @@ mod tests {
|
||||
Rule::FunctionCallInDataclassDefaultArgument,
|
||||
Path::new("RUF009_attrs_auto_attribs.py")
|
||||
)]
|
||||
#[test_case(
|
||||
Rule::FunctionCallInDataclassDefaultArgument,
|
||||
Path::new("RUF009_deferred.py")
|
||||
)]
|
||||
#[test_case(Rule::ExplicitFStringTypeConversion, Path::new("RUF010.py"))]
|
||||
#[test_case(Rule::MutableClassDefault, Path::new("RUF012.py"))]
|
||||
#[test_case(Rule::MutableClassDefault, Path::new("RUF012_deferred.py"))]
|
||||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_0.py"))]
|
||||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_1.py"))]
|
||||
#[test_case(Rule::ImplicitOptional, Path::new("RUF013_2.py"))]
|
||||
|
||||
@@ -75,8 +75,9 @@ impl Violation for FunctionCallInDataclassDefaultArgument {
|
||||
|
||||
/// RUF009
|
||||
pub(crate) fn function_call_in_dataclass_default(
|
||||
checker: &mut Checker,
|
||||
checker: &Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
@@ -152,7 +153,7 @@ pub(crate) fn function_call_in_dataclass_default(
|
||||
};
|
||||
let diagnostic = Diagnostic::new(kind, expr.range());
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,11 @@ impl Violation for MutableClassDefault {
|
||||
}
|
||||
|
||||
/// RUF012
|
||||
pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
pub(crate) fn mutable_class_default(
|
||||
checker: &Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
for statement in &class_def.body {
|
||||
match statement {
|
||||
Stmt::AnnAssign(ast::StmtAnnAssign {
|
||||
@@ -75,9 +79,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(MutableClassDefault, value.range()));
|
||||
diagnostics.push(Diagnostic::new(MutableClassDefault, value.range()));
|
||||
}
|
||||
}
|
||||
Stmt::Assign(ast::StmtAssign { value, targets, .. }) => {
|
||||
@@ -89,9 +91,7 @@ pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::Stmt
|
||||
return;
|
||||
}
|
||||
|
||||
checker
|
||||
.diagnostics
|
||||
.push(Diagnostic::new(MutableClassDefault, value.range()));
|
||||
diagnostics.push(Diagnostic::new(MutableClassDefault, value.range()));
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
||||
@@ -65,7 +65,11 @@ impl Violation for MutableDataclassDefault {
|
||||
}
|
||||
|
||||
/// RUF008
|
||||
pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::StmtClassDef) {
|
||||
pub(crate) fn mutable_dataclass_default(
|
||||
checker: &Checker,
|
||||
class_def: &ast::StmtClassDef,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
) {
|
||||
let semantic = checker.semantic();
|
||||
|
||||
if dataclass_kind(class_def, semantic).is_none() {
|
||||
@@ -88,7 +92,7 @@ pub(crate) fn mutable_dataclass_default(checker: &mut Checker, class_def: &ast::
|
||||
{
|
||||
let diagnostic = Diagnostic::new(MutableDataclassDefault, value.range());
|
||||
|
||||
checker.diagnostics.push(diagnostic);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
|
||||
use ruff_macros::{derive_message_formats, ViolationMetadata};
|
||||
use ruff_python_ast::helpers::is_dunder;
|
||||
use ruff_python_semantic::{Binding, BindingId, ScopeId};
|
||||
use ruff_python_stdlib::{
|
||||
builtins::is_python_builtin, identifiers::is_identifier, keyword::is_keyword,
|
||||
};
|
||||
use ruff_python_stdlib::identifiers::is_identifier;
|
||||
use ruff_text_size::Ranged;
|
||||
|
||||
use crate::{checkers::ast::Checker, renamer::Renamer};
|
||||
use crate::{
|
||||
checkers::ast::Checker,
|
||||
renamer::{Renamer, ShadowedKind},
|
||||
};
|
||||
|
||||
/// ## What it does
|
||||
/// Checks for "dummy variables" (variables that are named as if to indicate they are unused)
|
||||
@@ -164,53 +165,50 @@ pub(crate) fn used_dummy_variable(
|
||||
return None;
|
||||
}
|
||||
|
||||
let shadowed_kind = try_shadowed_kind(name, checker, binding.scope);
|
||||
// If the name doesn't start with an underscore, we don't consider it for a fix
|
||||
if !name.starts_with('_') {
|
||||
return Some(Diagnostic::new(
|
||||
UsedDummyVariable {
|
||||
name: name.to_string(),
|
||||
shadowed_kind: None,
|
||||
},
|
||||
binding.range(),
|
||||
));
|
||||
}
|
||||
|
||||
// Trim the leading underscores for further checks
|
||||
let trimmed_name = name.trim_start_matches('_');
|
||||
|
||||
let shadowed_kind = ShadowedKind::new(trimmed_name, checker, binding.scope);
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UsedDummyVariable {
|
||||
name: name.to_string(),
|
||||
shadowed_kind,
|
||||
shadowed_kind: Some(shadowed_kind),
|
||||
},
|
||||
binding.range(),
|
||||
);
|
||||
|
||||
// If fix available
|
||||
if let Some(shadowed_kind) = shadowed_kind {
|
||||
// Get the possible fix based on the scope
|
||||
if let Some(fix) = get_possible_fix(name, shadowed_kind, binding.scope, checker) {
|
||||
diagnostic.try_set_fix(|| {
|
||||
Renamer::rename(name, &fix, scope, semantic, checker.stylist())
|
||||
.map(|(edit, rest)| Fix::unsafe_edits(edit, rest))
|
||||
});
|
||||
}
|
||||
// Get the possible fix based on the scope
|
||||
if let Some(new_name) =
|
||||
get_possible_new_name(trimmed_name, shadowed_kind, binding.scope, checker)
|
||||
{
|
||||
diagnostic.try_set_fix(|| {
|
||||
Renamer::rename(name, &new_name, scope, semantic, checker.stylist())
|
||||
.map(|(edit, rest)| Fix::unsafe_edits(edit, rest))
|
||||
});
|
||||
}
|
||||
|
||||
Some(diagnostic)
|
||||
}
|
||||
|
||||
/// Enumeration of various ways in which a binding can shadow other variables
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
enum ShadowedKind {
|
||||
/// The variable shadows a global, nonlocal or local symbol
|
||||
Some,
|
||||
/// The variable shadows a builtin symbol
|
||||
BuiltIn,
|
||||
/// The variable shadows a keyword
|
||||
Keyword,
|
||||
/// The variable does not shadow any other symbols
|
||||
None,
|
||||
}
|
||||
|
||||
/// Suggests a potential alternative name to resolve a shadowing conflict.
|
||||
fn get_possible_fix(
|
||||
name: &str,
|
||||
fn get_possible_new_name(
|
||||
trimmed_name: &str,
|
||||
kind: ShadowedKind,
|
||||
scope_id: ScopeId,
|
||||
checker: &Checker,
|
||||
) -> Option<String> {
|
||||
// Remove leading underscores for processing
|
||||
let trimmed_name = name.trim_start_matches('_');
|
||||
|
||||
// Construct the potential fix name based on ShadowedKind
|
||||
let fix_name = match kind {
|
||||
ShadowedKind::Some | ShadowedKind::BuiltIn | ShadowedKind::Keyword => {
|
||||
@@ -235,37 +233,3 @@ fn get_possible_fix(
|
||||
// Check if the fix name is a valid identifier
|
||||
is_identifier(&fix_name).then_some(fix_name)
|
||||
}
|
||||
|
||||
/// Determines the kind of shadowing or conflict for a given variable name.
|
||||
fn try_shadowed_kind(name: &str, checker: &Checker, scope_id: ScopeId) -> Option<ShadowedKind> {
|
||||
// If the name starts with an underscore, we don't consider it
|
||||
if !name.starts_with('_') {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Trim the leading underscores for further checks
|
||||
let trimmed_name = name.trim_start_matches('_');
|
||||
|
||||
// Check the kind in order of precedence
|
||||
if is_keyword(trimmed_name) {
|
||||
return Some(ShadowedKind::Keyword);
|
||||
}
|
||||
|
||||
if is_python_builtin(
|
||||
trimmed_name,
|
||||
checker.settings.target_version.minor(),
|
||||
checker.source_type.is_ipynb(),
|
||||
) {
|
||||
return Some(ShadowedKind::BuiltIn);
|
||||
}
|
||||
|
||||
if !checker
|
||||
.semantic()
|
||||
.is_available_in_scope(trimmed_name, scope_id)
|
||||
{
|
||||
return Some(ShadowedKind::Some);
|
||||
}
|
||||
|
||||
// Default to no shadowing
|
||||
Some(ShadowedKind::None)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user