Compare commits

...

29 Commits

Author SHA1 Message Date
Zanie Blue
70cf8a94d5 Disable CRL checks during Windows test CI 2025-02-05 15:24:44 -06:00
Andrew Gallant
d47088c8f8 [red-knot] fix unresolvable import range (#15976)
This causes the diagnostic to highlight the actual unresovable import
instead of the entire `from ... import ...` statement.

While we're here, we expand the test coverage to cover all of the
possible ways that an `import` or a `from ... import` can fail.

Some considerations:

* The first commit in this PR adds a regression test for the current
behavior.
* This creates a new `mdtest/diagnostics` directory. Are folks cool
with this? I guess the idea is to put tests more devoted to diagnostics
than semantics in this directory. (Although I'm guessing there will
be some overlap.)

Fixes #15866
2025-02-05 14:01:58 -05:00
David Peter
1f0ad675d3 [red-knot] Initial set of descriptor protocol tests (#15972)
## Summary

This is a first step towards creating a test suite for
[descriptors](https://docs.python.org/3/howto/descriptor.html). It does
not (yet) aim to be exhaustive.

relevant ticket: #15966 

## Test Plan

Compared desired behavior with the runtime behavior and the behavior of
existing type checkers.

---------

Co-authored-by: Mike Perlov <mishamsk@gmail.com>
2025-02-05 19:47:43 +01:00
Andrew Gallant
a84b27e679 red_knot_test: add support for diagnostic snapshotting
This ties together everything from the previous commits.
Some interesting bits here are how the snapshot is generated
(where we include relevant info to make it easier to review
the snapshots) and also a tweak to how inline assertions are
processed.

This commit also includes some example snapshots just to get
a sense of what they look like. Follow-up work should add
more of these I think.
2025-02-05 13:02:54 -05:00
Andrew Gallant
8d4679b3ae red_knot_test: update README with section on diagnostic snapshotting
I split this out into a separate commit and put it here
so that reviewers can get a conceptual model of what the
code is doing before seeing the code. (Hopefully that helps.)
2025-02-05 13:02:54 -05:00
Andrew Gallant
b40a7cce15 red_knot_test: add snapshot path
This makes it possible for callers to set where snapshots
should be stored. In general, I think we expect this to
always be set, since otherwise snapshots will end up in
`red_knot_test`, which is where the tests are actually run.
But that's overall counter-intuitive. This permits us to
store snapshots from mdtests alongside the mdtests themselves.
2025-02-05 13:02:54 -05:00
Andrew Gallant
54b3849dfb ruff_db: add more dyn Diagnostic impls
I found it useful to have the `&dyn Diagnostic` trait impl
specifically. I added `Arc<dyn Diagnostic>` for completeness.

(I do kind of wonder if we should be preferring `Arc<dyn ...>`
over something like `Box<dyn ...>` more generally, especially
for things with immutable APIs. It would make cloning cheap.)
2025-02-05 13:02:54 -05:00
Andrew Gallant
ffd94e9ace red_knot_test: generate names for unnamed files using more local reasoning
This change was done to reduce snapshot churn. Previously,
if one added a new section to an Markdown test suite, then
the snapshots of all sections with unnamed files below it would
necessarily change because of the unnamed file count being
global to the test suite.

Instead, we track counts based on section. While adding new
unnamed files within a section will still change unnamed
files below it, I believe this will be less "churn" because
the snapshot will need to change anyway. Some churn is still
possible, e.g., if code blocks are re-ordered. But I think this
is an acceptable trade-off.
2025-02-05 13:02:54 -05:00
Alex Waygood
c816542704 [red-knot] Fix some instance-attribute TODOs around ModuleType (#15974) 2025-02-05 15:33:37 +00:00
Zanie Blue
3f958a9d4c Use a larger runner for the cargo build (msrv) job (#15973) 2025-02-05 09:03:55 -06:00
Alex Waygood
2ebb5e8d4b [red-knot] Make Symbol::or_fall_back_to() lazy (#15943) 2025-02-05 14:51:02 +00:00
Dylan
c69b19fe1d [flake8-comprehensions] Handle trailing comma in fixes for unnecessary-generator-list/set (C400,C401) (#15929)
The unsafe fixes for the rules [unnecessary-generator-list
(C400)](https://docs.astral.sh/ruff/rules/unnecessary-generator-list/#unnecessary-generator-list-c400)
and [unnecessary-generator-set
(C401)](https://docs.astral.sh/ruff/rules/unnecessary-generator-set/#unnecessary-generator-set-c401)
used to introduce syntax errors if the argument to `list` or `set` had a
trailing comma, because the fix would retain the comma after
transforming the function call to a comprehension.

This PR accounts for the trailing comma when replacing the end of the
call with a `]` or `}`.

Closes #15852
2025-02-05 07:38:03 -06:00
Brent Westbrook
076d35fb93 [minor] Mention UP049 in UP046 and UP047, add See also section to UP040 (#15956)
## Summary

Minor docs follow-up to #15862 to mention UP049 in the UP046 and UP047
`See also` sections. I wanted to mention it in UP040 too but realized it
didn't have a `See also` section, so I also added that, adapted from the
other two rules.

## Test Plan

cargo test
2025-02-05 08:34:47 -05:00
Dylan
16f2a93fca [ruff] Analyze deferred annotations before enforcing mutable-(data)class-default and function-call-in-dataclass-default-argument (RUF008,RUF009,RUF012) (#15921) 2025-02-05 06:44:19 -06:00
David Peter
eb08345fd5 [red-knot] Extend instance/class attribute tests (#15959)
## Summary

In preparation for creating some (sub) issues for
https://github.com/astral-sh/ruff/issues/14164, I'm trying to document
the current behavior (and a bug) a bit better.
2025-02-05 12:45:00 +01:00
Alex Waygood
7ca778f492 [refurb] Minor nits regarding for-loop-writes and for-loop-set-mutations (#15958) 2025-02-05 10:21:36 +00:00
Vasco Schiavo
827a076a2f [pylint] Fix PL1730: min/max auto-fix and suggestion (#15930)
## Summary

The PR addresses the issue #15887 

For two objects `a` and `b`, we ensure that the auto-fix and the
suggestion is of the form `a = min(a, b)` (or `a = max(a, b)`). This is
because we want to be consistent with the python implementation of the
methods: `min` and `max`. See the above issue for more details.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2025-02-05 09:29:10 +00:00
InSync
4855e0b288 [refurb] Handle unparenthesized tuples correctly (FURB122, FURB142) (#15953)
## Summary

Resolves #15936.

The fixes will now attempt to preserve the original iterable's format
and quote it if necessary. For `FURB142`, comments within the fix range
will make it unsafe as well.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-02-05 10:16:54 +01:00
InSync
44ddd98d7e [pyupgrade] Better messages and diagnostic range (UP015) (#15872)
## Summary

Resolves #15863.

In preview, diagnostic ranges will now be limited to that of the
argument. Rule documentation, variable names, error messages and fix
titles have all been modified to use "argument" consistently.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-02-05 09:44:26 +01:00
InSync
82cb8675dd [pep8-naming] Ignore @override methods (N803) (#15954)
## Summary

Resolves #15925.

`N803` now checks for functions instead of parameters. In preview mode,
if a method is decorated with `@override` and the current scope is that
of a class, it will be ignored.

## Test Plan

`cargo nextest run` and `cargo insta test`.
2025-02-05 09:35:57 +01:00
InSync
5852217198 [refurb] Also report non-name expressions (FURB169) (#15905)
## Summary

Follow-up to #15779.

Prior to this change, non-name expressions are not reported at all:

```python
type(a.b) is type(None)  # no error
```

This change enhances the rule so that such cases are also reported in
preview. Additionally:

* The fix will now be marked as unsafe if there are any comments within
its range.
* Error messages are slightly modified.

## Test Plan

`cargo nextest run` and `cargo insta test`.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-05 08:46:37 +01:00
Dylan
700e969c56 Config error only when flake8-import-conventions alias conflicts with isort.required-imports bound name (#15918)
Previously an error was emitted any time the configuration required both
an import of a module and an alias for that module. However, required
imports could themselves contain an alias, which may or may not agree
with the required alias.

To wit: requiring `import pandas as pd` does not conflict with the
`flake8-import-conventions.alias` config `{"pandas":"pd"}`.

This PR refines the check before throwing an error.

Closes #15911
2025-02-04 17:05:35 -06:00
InSync
4c15d7a559 Fix a typo in non_pep695_generic_class.rs (#15946)
(Accidentally introduced in #15904.)
2025-02-04 22:16:18 +00:00
Mike Perlov
e15419396c [red-knot] Fix Stack overflow in Type::bool (#15843)
## Summary

This PR adds `Type::call_bound` method for calls that should follow
descriptor protocol calling convention. The PR is intentionally shallow
in scope and only fixes #15672

Couple of obvious things that weren't done:

* Switch to `call_bound` everywhere it should be used
* Address the fact, that red_knot resolves `__bool__ = bool` as a Union,
which includes `Type::Dynamic` and hence fails to infer that the
truthiness is always false for such a class (I've added a todo comment
in mdtests)
* Doesn't try to invent a new type for descriptors, although I have a
gut feeling it may be more convenient in the end, instead of doing
method lookup each time like I did in `call_bound`

## Test Plan

* extended mdtests with 2 examples from the issue
* cargo neatest run
2025-02-04 12:40:07 -08:00
Douglas Creager
444b055cec [red-knot] Use ternary decision diagrams (TDDs) for visibility constraints (#15861)
We now use ternary decision diagrams (TDDs) to represent visibility
constraints. A TDD is just like a BDD ([_binary_ decision
diagram](https://en.wikipedia.org/wiki/Binary_decision_diagram)), but
with "ambiguous" as an additional allowed value. Unlike the previous
representation, TDDs are strongly normalizing, so equivalent ternary
formulas are represented by exactly the same graph node, and can be
compared for equality in constant time.

We currently have a slight 1-3% performance regression with this in
place, according to local testing. However, we also have a _5× increase_
in performance for pathological cases, since we can now remove the
recursion limit when we evaluate visibility constraints.

As follow-on work, we are now closer to being able to remove the
`simplify_visibility_constraint` calls in the semantic index builder. In
the vast majority of cases, we now see (for instance) that the
visibility constraint after an `if` statement, for bindings of symbols
that weren't rebound in any branch, simplifies back to `true`. But there
are still some cases we generate constraints that are cyclic. With
fixed-point cycle support in salsa, or with some careful analysis of the
still-failing cases, we might be able to remove those.
2025-02-04 14:32:11 -05:00
Brent Westbrook
6bb32355ef [pyupgrade] Rename private type parameters in PEP 695 generics (UP049) (#15862)
## Summary

This is a new rule to implement the renaming of PEP 695 type parameters
with leading underscores after they have (presumably) been converted
from standalone type variables by either UP046 or UP047. Part of #15642.

I'm not 100% sure the fix is always safe, but I haven't come up with any
counterexamples yet. `Renamer` seems pretty precise, so I don't think
the usual issues with comments apply.

I initially tried writing this as a rule that receives a `Stmt` rather
than a `Binding`, but in that case the
`checker.semantic().current_scope()` was the global scope, rather than
the scope of the type parameters as I needed. Most of the other rules
using `Renamer` also used `Binding`s, but it does have the downside of
offering separate diagnostics for each parameter to rename.

## Test Plan

New snapshot tests for UP049 alone and the combination of UP046, UP049,
and PYI018.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-04 13:22:57 -05:00
Alex Waygood
cb71393332 Simplify the StringFlags trait (#15944) 2025-02-04 18:14:28 +00:00
Alex Waygood
64e64d2681 [flake8-pyi] Make PYI019 autofixable for .py files in preview mode as well as stubs (#15889) 2025-02-04 16:41:22 +00:00
Alexander Nordin
9d83e76a3b Docs (linter.md): clarify that Python files are always searched for in subdirectories (#15882)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
2025-02-04 15:36:16 +00:00
112 changed files with 5505 additions and 945 deletions

View File

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

@@ -2518,6 +2518,7 @@ dependencies = [
"anyhow",
"camino",
"colored 3.0.0",
"insta",
"memchr",
"red_knot_python_semantic",
"red_knot_vendored",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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(&copy).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,
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

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

View 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__: ...

View File

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

View File

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

View File

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

View File

@@ -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]]'= []

View 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

View 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

View File

@@ -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' = []

View 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

View File

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

View File

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

View File

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

View File

@@ -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(
&parameter.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);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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