Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cfa350112 | ||
|
|
41f163fc8d | ||
|
|
d21dd994e6 | ||
|
|
6f5a6b8c8b | ||
|
|
35606d7b05 | ||
|
|
3ad257cfea | ||
|
|
b39f960cd1 | ||
|
|
c297d46899 | ||
|
|
d6a100028c | ||
|
|
35d4e03f2a | ||
|
|
41e77bb01d | ||
|
|
2ff3dd5fbe | ||
|
|
0f0e7a521a | ||
|
|
b75663be6d | ||
|
|
4d3d04ee61 | ||
|
|
87422ba362 | ||
|
|
c1d2976fff | ||
|
|
13281cd9ca | ||
|
|
e53652779d | ||
|
|
db4c611c6f | ||
|
|
c25be31eb1 | ||
|
|
a7c533634d | ||
|
|
cfa6883431 | ||
|
|
216aa929af | ||
|
|
9e45424ed6 | ||
|
|
db7f16e276 | ||
|
|
a10a500a26 | ||
|
|
b9fef7cef7 | ||
|
|
34294ccc00 | ||
|
|
a934d01bdb | ||
|
|
0dd590f137 | ||
|
|
909a5c3253 | ||
|
|
5c987874c4 | ||
|
|
0cfe4f9c69 | ||
|
|
6a369e4a30 | ||
|
|
6f97e2c457 | ||
|
|
bebd412469 | ||
|
|
cd1f57b713 | ||
|
|
a0912deb2b | ||
|
|
50ee14a418 | ||
|
|
f5adbbebc5 | ||
|
|
c88e05dc1b | ||
|
|
d658bfc024 | ||
|
|
b0d72c47b4 | ||
|
|
8195873cdf | ||
|
|
bf8108469f | ||
|
|
a2277cfeba | ||
|
|
180541a924 | ||
|
|
34664a0ca0 | ||
|
|
e081455b06 | ||
|
|
4f18fa6733 | ||
|
|
6088a36cd3 | ||
|
|
66a162fa40 | ||
|
|
e6722f92ed | ||
|
|
750c28868f | ||
|
|
5157f584ab | ||
|
|
1c01ec21cb | ||
|
|
879512742f | ||
|
|
a919041dda | ||
|
|
059601d968 | ||
|
|
2ec1701543 | ||
|
|
370c3a5daf | ||
|
|
fdcb78fd8c | ||
|
|
2a744d24e5 | ||
|
|
cc30738148 | ||
|
|
147c6ff1db | ||
|
|
036380e6a8 | ||
|
|
b6587e51ee | ||
|
|
1bc37110d4 | ||
|
|
28acdb76cf | ||
|
|
7b09972c97 |
@@ -1,3 +1,4 @@
|
||||
fail_fast: true
|
||||
repos:
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.10.1
|
||||
@@ -12,6 +13,7 @@ repos:
|
||||
- --disable
|
||||
- MD013 # line-length
|
||||
- MD033 # no-inline-html
|
||||
- MD041 # first-line-h1
|
||||
- --
|
||||
|
||||
- repo: local
|
||||
@@ -28,11 +30,15 @@ repos:
|
||||
pass_filenames: false
|
||||
- id: ruff
|
||||
name: ruff
|
||||
entry: cargo run -- --no-cache --fix
|
||||
entry: cargo run -p ruff_cli -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix
|
||||
language: rust
|
||||
types_or: [python, pyi]
|
||||
require_serial: true
|
||||
exclude: ^crates/ruff/resources
|
||||
exclude: |
|
||||
(?x)^(
|
||||
crates/ruff/resources/.*|
|
||||
crates/ruff_python_formatter/resources/.*
|
||||
)$
|
||||
- id: dev-generate-all
|
||||
name: dev-generate-all
|
||||
entry: cargo dev generate-all
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
- [Our Pledge](#our-pledge)
|
||||
- [Our Standards](#our-standards)
|
||||
- [Enforcement Responsibilities](#enforcement-responsibilities)
|
||||
- [Scope](#scope)
|
||||
- [Enforcement](#enforcement)
|
||||
- [Enforcement Guidelines](#enforcement-guidelines)
|
||||
- [1. Correction](#1-correction)
|
||||
- [2. Warning](#2-warning)
|
||||
- [3. Temporary Ban](#3-temporary-ban)
|
||||
- [4. Permanent Ban](#4-permanent-ban)
|
||||
- [Attribution](#attribution)
|
||||
* [Our Pledge](#our-pledge)
|
||||
* [Our Standards](#our-standards)
|
||||
* [Enforcement Responsibilities](#enforcement-responsibilities)
|
||||
* [Scope](#scope)
|
||||
* [Enforcement](#enforcement)
|
||||
* [Enforcement Guidelines](#enforcement-guidelines)
|
||||
* [1. Correction](#1-correction)
|
||||
* [2. Warning](#2-warning)
|
||||
* [3. Temporary Ban](#3-temporary-ban)
|
||||
* [4. Permanent Ban](#4-permanent-ban)
|
||||
* [Attribution](#attribution)
|
||||
|
||||
## Our Pledge
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
|
||||
|
||||
- [The Basics](#the-basics)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Development](#development)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
|
||||
- [Rule naming convention](#rule-naming-convention)
|
||||
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
|
||||
- [MkDocs](#mkdocs)
|
||||
- [Release Process](#release-process)
|
||||
- [Benchmarks](#benchmarks)
|
||||
* [The Basics](#the-basics)
|
||||
* [Prerequisites](#prerequisites)
|
||||
* [Development](#development)
|
||||
* [Project Structure](#project-structure)
|
||||
* [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
|
||||
* [Rule naming convention](#rule-naming-convention)
|
||||
* [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
|
||||
* [MkDocs](#mkdocs)
|
||||
* [Release Process](#release-process)
|
||||
* [Benchmarks](#benchmarks)
|
||||
|
||||
## The Basics
|
||||
|
||||
@@ -52,7 +52,7 @@ cargo install cargo-insta
|
||||
After cloning the repository, run Ruff locally with:
|
||||
|
||||
```shell
|
||||
cargo run check /path/to/file.py --no-cache
|
||||
cargo run -p ruff_cli -- check /path/to/file.py --no-cache
|
||||
```
|
||||
|
||||
Prior to opening a pull request, ensure that your code has been auto-formatted,
|
||||
@@ -94,12 +94,12 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra
|
||||
|
||||
At time of writing, the repository includes the following crates:
|
||||
|
||||
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
|
||||
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
|
||||
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`).
|
||||
- `crates/ruff_macros`: library crate containing macros used by Ruff.
|
||||
- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of standard library modules by versionb).
|
||||
- `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration.
|
||||
* `crates/ruff`: library crate containing all lint rules and the core logic for running them.
|
||||
* `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
|
||||
* `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`).
|
||||
* `crates/ruff_macros`: library crate containing macros used by Ruff.
|
||||
* `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of standard library modules by versionb).
|
||||
* `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration.
|
||||
|
||||
### Example: Adding a new lint rule
|
||||
|
||||
@@ -135,7 +135,7 @@ contain a variety of violations and non-violations designed to evaluate and demo
|
||||
of your lint rule.
|
||||
|
||||
Run `cargo dev generate-all` to generate the code for your new fixture. Then run Ruff
|
||||
locally with (e.g.) `cargo run check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
|
||||
locally with (e.g.) `cargo run -p ruff_cli -- check crates/ruff/resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
|
||||
|
||||
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
|
||||
`test_case` macro in the relevant `crates/ruff/src/[linter]/mod.rs` file. Then, run `cargo test --all`.
|
||||
@@ -146,7 +146,7 @@ Finally, regenerate the documentation and generated code with `cargo dev generat
|
||||
|
||||
#### Rule naming convention
|
||||
|
||||
The rule name should make sense when read as "allow *rule-name*" or "allow *rule-name* items".
|
||||
The rule name should make sense when read as "allow _rule-name_" or "allow _rule-name_ items".
|
||||
|
||||
This implies that rule names:
|
||||
|
||||
@@ -186,14 +186,19 @@ Finally, regenerate the documentation and generated code with `cargo dev generat
|
||||
To preview any changes to the documentation locally:
|
||||
|
||||
1. Install MkDocs and Material for MkDocs with:
|
||||
|
||||
```shell
|
||||
pip install -r docs/requirements.txt
|
||||
```
|
||||
|
||||
2. Generate the MkDocs site with:
|
||||
|
||||
```shell
|
||||
python scripts/generate_mkdocs.py
|
||||
```
|
||||
|
||||
3. Run the development server with:
|
||||
|
||||
```shell
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
801
Cargo.lock
generated
801
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,9 @@
|
||||
[workspace]
|
||||
members = ["crates/*"]
|
||||
default-members = ["crates/ruff", "crates/ruff_cli"]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
|
||||
[workspace.dependencies]
|
||||
anyhow = { version = "1.0.66" }
|
||||
|
||||
@@ -4,3 +4,4 @@ extend-exclude = ["snapshots", "black"]
|
||||
[default.extend-words]
|
||||
trivias = "trivias"
|
||||
hel = "hel"
|
||||
whos = "whos"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.247"
|
||||
edition = "2021"
|
||||
version = "0.0.249"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.247"
|
||||
version = "0.0.249"
|
||||
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
edition = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
documentation = "https://github.com/charliermarsh/ruff"
|
||||
homepage = "https://github.com/charliermarsh/ruff"
|
||||
repository = "https://github.com/charliermarsh/ruff"
|
||||
|
||||
@@ -20,6 +20,8 @@ s.rstrip(r"\n\t ") # warning
|
||||
s.strip("a") # no warning
|
||||
s.strip("あ") # no warning
|
||||
s.strip("ああ") # warning
|
||||
s.strip("\ufeff") # no warning
|
||||
s.strip("\u0074\u0065\u0073\u0074") # warning
|
||||
|
||||
from somewhere import other_type, strip
|
||||
|
||||
|
||||
101
crates/ruff/resources/test/fixtures/flake8_bugbear/B027.pyi
vendored
Normal file
101
crates/ruff/resources/test/fixtures/flake8_bugbear/B027.pyi
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
"""
|
||||
Should emit:
|
||||
B027 - on lines 13, 16, 19, 23
|
||||
"""
|
||||
import abc
|
||||
from abc import ABC
|
||||
from abc import abstractmethod, abstractproperty
|
||||
from abc import abstractmethod as notabstract
|
||||
from abc import abstractproperty as notabstract_property
|
||||
|
||||
|
||||
class AbstractClass(ABC):
|
||||
def empty_1(self): # error
|
||||
...
|
||||
|
||||
def empty_2(self): # error
|
||||
pass
|
||||
|
||||
def empty_3(self): # error
|
||||
"""docstring"""
|
||||
...
|
||||
|
||||
def empty_4(self): # error
|
||||
"""multiple ellipsis/pass"""
|
||||
...
|
||||
pass
|
||||
...
|
||||
pass
|
||||
|
||||
@notabstract
|
||||
def abstract_0(self):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def abstract_1(self):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def abstract_2(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def abstract_3(self):
|
||||
...
|
||||
|
||||
@abc.abstractproperty
|
||||
def abstract_4(self):
|
||||
...
|
||||
|
||||
@abstractproperty
|
||||
def abstract_5(self):
|
||||
...
|
||||
|
||||
@notabstract_property
|
||||
def abstract_6(self):
|
||||
...
|
||||
|
||||
def body_1(self):
|
||||
print("foo")
|
||||
...
|
||||
|
||||
def body_2(self):
|
||||
self.body_1()
|
||||
|
||||
|
||||
class NonAbstractClass:
|
||||
def empty_1(self): # safe
|
||||
...
|
||||
|
||||
def empty_2(self): # safe
|
||||
pass
|
||||
|
||||
|
||||
# ignore @overload, fixes issue #304
|
||||
# ignore overload with other imports, fixes #308
|
||||
import typing
|
||||
import typing as t
|
||||
import typing as anything
|
||||
from typing import Union, overload
|
||||
|
||||
|
||||
class AbstractClass(ABC):
|
||||
@overload
|
||||
def empty_1(self, foo: str):
|
||||
...
|
||||
|
||||
@typing.overload
|
||||
def empty_1(self, foo: int):
|
||||
...
|
||||
|
||||
@t.overload
|
||||
def empty_1(self, foo: list):
|
||||
...
|
||||
|
||||
@anything.overload
|
||||
def empty_1(self, foo: float):
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
def empty_1(self, foo: Union[str, int, list, float]):
|
||||
...
|
||||
@@ -17,6 +17,12 @@ class Test(unittest.TestCase):
|
||||
self.assertTrue(**{"expr": expr, "msg": msg}) # Error, unfixable
|
||||
self.assertTrue(msg=msg, expr=expr, unexpected_arg=False) # Error, unfixable
|
||||
self.assertTrue(msg=msg) # Error, unfixable
|
||||
(
|
||||
self.assertIsNotNone(value) # Error, unfixable
|
||||
if expect_condition
|
||||
else self.assertIsNone(value) # Error, unfixable
|
||||
)
|
||||
return self.assertEqual(True, False) # Error, unfixable
|
||||
|
||||
def test_assert_false(self):
|
||||
self.assertFalse(True) # Error
|
||||
|
||||
@@ -6,13 +6,24 @@ def test_ok():
|
||||
assert something or something_else
|
||||
assert something or something_else and something_third
|
||||
assert not (something and something_else)
|
||||
|
||||
assert something, "something message"
|
||||
assert something or something_else and something_third, "another message"
|
||||
|
||||
def test_error():
|
||||
assert something and something_else
|
||||
assert something and something_else and something_third
|
||||
assert something and not something_else
|
||||
assert something and (something_else or something_third)
|
||||
assert not something and something_else
|
||||
assert not (something or something_else)
|
||||
assert not (something or something_else or something_third)
|
||||
|
||||
# recursive case
|
||||
assert not (a or not (b or c))
|
||||
assert not (a or not (b and c)) # note that we only reduce once here
|
||||
|
||||
# detected, but no autofix for messages
|
||||
assert something and something_else, "error message"
|
||||
assert not (something or something_else and something_third), "with message"
|
||||
# detected, but no autofix for mixed conditions (e.g. `a or b and c`)
|
||||
assert not (something or something_else and something_third)
|
||||
|
||||
@@ -251,3 +251,11 @@ def noreturn_pytest_xfail_2():
|
||||
if x > 0:
|
||||
return 1
|
||||
py_xfail("oof")
|
||||
|
||||
|
||||
def nested(values):
|
||||
if not values:
|
||||
return False
|
||||
|
||||
for value in values:
|
||||
print(value)
|
||||
|
||||
@@ -56,12 +56,6 @@ class Foo(metaclass=BazMeta):
|
||||
|
||||
foo = Foo()
|
||||
|
||||
print(foo.public_thing)
|
||||
print(foo.public_func())
|
||||
print(foo.__dict__)
|
||||
print(foo.__str__())
|
||||
print(foo().__class__)
|
||||
|
||||
print(foo._private_thing) # SLF001
|
||||
print(foo.__really_private_thing) # SLF001
|
||||
print(foo._private_func()) # SLF001
|
||||
@@ -69,3 +63,10 @@ print(foo.__really_private_func(1)) # SLF001
|
||||
print(foo.bar._private) # SLF001
|
||||
print(foo()._private_thing) # SLF001
|
||||
print(foo()._private_thing__) # SLF001
|
||||
|
||||
print(foo.public_thing)
|
||||
print(foo.public_func())
|
||||
print(foo.__dict__)
|
||||
print(foo.__str__())
|
||||
print(foo().__class__)
|
||||
print(foo._asdict())
|
||||
|
||||
@@ -4,4 +4,5 @@ import attrs
|
||||
|
||||
from ..protocol import commands, definitions, responses
|
||||
from ..server import example
|
||||
from .. import server
|
||||
from . import logger, models
|
||||
|
||||
23
crates/ruff/resources/test/fixtures/isort/force_to_top.py
vendored
Normal file
23
crates/ruff/resources/test/fixtures/isort/force_to_top.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
import lib6
|
||||
import lib2
|
||||
import lib5
|
||||
import lib1
|
||||
import lib3
|
||||
import lib4
|
||||
|
||||
import foo
|
||||
import z
|
||||
from foo import bar
|
||||
from lib1 import foo
|
||||
from lib2 import foo
|
||||
from lib1.lib2 import foo
|
||||
from foo.lib1.bar import baz
|
||||
from lib4 import lib1
|
||||
from lib5 import lib2
|
||||
from lib4 import lib2
|
||||
from lib5 import lib1
|
||||
|
||||
import lib3.lib4
|
||||
import lib3.lib4.lib5
|
||||
from lib3.lib4 import foo
|
||||
from lib3.lib4.lib5 import foo
|
||||
@@ -5,3 +5,4 @@ line-length = 88
|
||||
lines-after-imports = 3
|
||||
lines-between-types = 2
|
||||
known-local-folder = ["ruff"]
|
||||
force-to-top = ["lib1", "lib3", "lib5", "lib3.lib4", "z"]
|
||||
|
||||
4
crates/ruff/resources/test/fixtures/isort/required_imports/multiline_docstring.py
vendored
Normal file
4
crates/ruff/resources/test/fixtures/isort/required_imports/multiline_docstring.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
"""a
|
||||
b"""
|
||||
# b
|
||||
import os
|
||||
62
crates/ruff/resources/test/fixtures/numpy/NPY002.py
vendored
Normal file
62
crates/ruff/resources/test/fixtures/numpy/NPY002.py
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
# Do this (new version)
|
||||
from numpy.random import default_rng
|
||||
rng = default_rng()
|
||||
vals = rng.standard_normal(10)
|
||||
more_vals = rng.standard_normal(10)
|
||||
numbers = rng.integers(high, size=5)
|
||||
|
||||
# instead of this (legacy version)
|
||||
from numpy import random
|
||||
vals = random.standard_normal(10)
|
||||
more_vals = random.standard_normal(10)
|
||||
numbers = random.integers(high, size=5)
|
||||
|
||||
import numpy
|
||||
numpy.random.seed()
|
||||
numpy.random.get_state()
|
||||
numpy.random.set_state()
|
||||
numpy.random.rand()
|
||||
numpy.random.randn()
|
||||
numpy.random.randint()
|
||||
numpy.random.random_integers()
|
||||
numpy.random.random_sample()
|
||||
numpy.random.choice()
|
||||
numpy.random.bytes()
|
||||
numpy.random.shuffle()
|
||||
numpy.random.permutation()
|
||||
numpy.random.beta()
|
||||
numpy.random.binomial()
|
||||
numpy.random.chisquare()
|
||||
numpy.random.dirichlet()
|
||||
numpy.random.exponential()
|
||||
numpy.random.f()
|
||||
numpy.random.gamma()
|
||||
numpy.random.geometric()
|
||||
numpy.random.get_state()
|
||||
numpy.random.gumbel()
|
||||
numpy.random.hypergeometric()
|
||||
numpy.random.laplace()
|
||||
numpy.random.logistic()
|
||||
numpy.random.lognormal()
|
||||
numpy.random.logseries()
|
||||
numpy.random.multinomial()
|
||||
numpy.random.multivariate_normal()
|
||||
numpy.random.negative_binomial()
|
||||
numpy.random.noncentral_chisquare()
|
||||
numpy.random.noncentral_f()
|
||||
numpy.random.normal()
|
||||
numpy.random.pareto()
|
||||
numpy.random.poisson()
|
||||
numpy.random.power()
|
||||
numpy.random.rayleigh()
|
||||
numpy.random.standard_cauchy()
|
||||
numpy.random.standard_exponential()
|
||||
numpy.random.standard_gamma()
|
||||
numpy.random.standard_normal()
|
||||
numpy.random.standard_t()
|
||||
numpy.random.triangular()
|
||||
numpy.random.uniform()
|
||||
numpy.random.vonmises()
|
||||
numpy.random.wald()
|
||||
numpy.random.weibull()
|
||||
numpy.random.zipf()
|
||||
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/MODULE/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/MODULE/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/MODULE/file.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/MODULE/file.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/flake9/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/flake9/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod with spaces/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod with spaces/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod with spaces/file.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod with spaces/file.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod with spaces/file2.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod with spaces/file2.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod-with-dashes/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/mod-with-dashes/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/no_module/test.txt
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/no_module/test.txt
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/0001_initial.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/0001_initial.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__init__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__main__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__main__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__setup__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/__setup__.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/file-with-dashes.py
vendored
Normal file
0
crates/ruff/resources/test/fixtures/pep8_naming/N999/module/valid_name/file-with-dashes.py
vendored
Normal file
@@ -9,6 +9,9 @@ while False:
|
||||
f = lambda: (yield 1)
|
||||
#: E731
|
||||
f = lambda: (yield from g())
|
||||
#: E731
|
||||
class F:
|
||||
f = lambda x: 2 * x
|
||||
|
||||
f = object()
|
||||
f.method = lambda: "Method"
|
||||
|
||||
@@ -4,3 +4,5 @@ from __future__ import absolute_import
|
||||
from collections import namedtuple
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import __future__
|
||||
|
||||
@@ -33,3 +33,11 @@ f"{f'{v:0.2f}'}"
|
||||
# Errors
|
||||
f"{v:{f'0.2f'}}"
|
||||
f"{f''}"
|
||||
f"{{test}}"
|
||||
f'{{ 40 }}'
|
||||
f"{{a {{x}}"
|
||||
f"{{{{x}}}}"
|
||||
|
||||
# To be fixed
|
||||
# Error: f-string: single '}' is not allowed at line 41 column 8
|
||||
# f"\{{x}}"
|
||||
|
||||
41
crates/ruff/resources/test/fixtures/pylint/return_in_init.py
vendored
Normal file
41
crates/ruff/resources/test/fixtures/pylint/return_in_init.py
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
def a():
|
||||
return
|
||||
|
||||
def __init__():
|
||||
return
|
||||
|
||||
class A:
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
|
||||
class B:
|
||||
def __init__(self):
|
||||
return 3
|
||||
|
||||
def gen(self):
|
||||
return 5
|
||||
|
||||
class MyClass:
|
||||
|
||||
def __init__(self):
|
||||
return 1
|
||||
|
||||
class MyClass2:
|
||||
"""dummy class"""
|
||||
|
||||
def __init__(self):
|
||||
return
|
||||
|
||||
|
||||
class MyClass3:
|
||||
"""dummy class"""
|
||||
|
||||
def __init__(self):
|
||||
return None
|
||||
|
||||
class MyClass5:
|
||||
"""dummy class"""
|
||||
|
||||
def __init__(self):
|
||||
self.callable = lambda: (yield None)
|
||||
@@ -113,3 +113,14 @@ def test_break_in_if_orelse():
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def test_break_in_with():
|
||||
"""no false positive for break in with"""
|
||||
for name in ["demo"]:
|
||||
with open(__file__) as f:
|
||||
if name in f.read():
|
||||
break
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -6,6 +6,11 @@ def f():
|
||||
asyncio.create_task(coordinator.ws_connect()) # Error
|
||||
|
||||
|
||||
# Error
|
||||
def f():
|
||||
asyncio.ensure_future(coordinator.ws_connect()) # Error
|
||||
|
||||
|
||||
# OK
|
||||
def f():
|
||||
background_tasks = set()
|
||||
@@ -22,6 +27,22 @@ def f():
|
||||
task.add_done_callback(background_tasks.discard)
|
||||
|
||||
|
||||
# OK
|
||||
def f():
|
||||
background_tasks = set()
|
||||
|
||||
for i in range(10):
|
||||
task = asyncio.ensure_future(some_coro(param=i))
|
||||
|
||||
# Add task to the set. This creates a strong reference.
|
||||
background_tasks.add(task)
|
||||
|
||||
# To prevent keeping references to finished tasks forever,
|
||||
# make each task remove its own reference from the set after
|
||||
# completion:
|
||||
task.add_done_callback(background_tasks.discard)
|
||||
|
||||
|
||||
# OK
|
||||
def f():
|
||||
ctx.task = asyncio.create_task(make_request())
|
||||
|
||||
8
crates/ruff/resources/test/fixtures/ruff/ruff_targeted_noqa.py
vendored
Normal file
8
crates/ruff/resources/test/fixtures/ruff/ruff_targeted_noqa.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# ruff: noqa: F401
|
||||
|
||||
import os
|
||||
import foo
|
||||
|
||||
|
||||
def f():
|
||||
x = 1
|
||||
@@ -8,14 +8,14 @@ behaviors.
|
||||
Running from the repo root should pick up and enforce the appropriate settings for each package:
|
||||
|
||||
```console
|
||||
∴ cargo run resources/test/project/
|
||||
resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/
|
||||
crates/ruff/resources/test/project/examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
crates/ruff/resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
crates/ruff/resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
crates/ruff/resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
crates/ruff/resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
crates/ruff/resources/test/project/project/file.py:1:8: F401 `os` imported but unused
|
||||
crates/ruff/resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
Found 7 errors.
|
||||
7 potentially fixable with the --fix option.
|
||||
```
|
||||
@@ -23,7 +23,7 @@ Found 7 errors.
|
||||
Running from the project directory itself should exhibit the same behavior:
|
||||
|
||||
```console
|
||||
∴ (cd resources/test/project/ && cargo run .)
|
||||
∴ (cd crates/ruff/resources/test/project/ && cargo run -p ruff_cli -- check .)
|
||||
examples/.dotfiles/script.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
@@ -39,7 +39,7 @@ Running from the sub-package directory should exhibit the same behavior, but omi
|
||||
files:
|
||||
|
||||
```console
|
||||
∴ (cd resources/test/project/examples/docs && cargo run .)
|
||||
∴ (cd crates/ruff/resources/test/project/examples/docs && cargo run -p ruff_cli -- check .)
|
||||
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
Found 2 errors.
|
||||
@@ -50,16 +50,16 @@ Found 2 errors.
|
||||
file paths from the current working directory:
|
||||
|
||||
```console
|
||||
∴ (cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/)
|
||||
resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
|
||||
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
|
||||
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
|
||||
∴ (cargo run -p ruff_cli -- check --config=crates/ruff/resources/test/project/pyproject.toml crates/ruff/resources/test/project/)
|
||||
crates/ruff/resources/test/project/examples/.dotfiles/script.py:1:8: F401 `numpy` imported but unused
|
||||
crates/ruff/resources/test/project/examples/.dotfiles/script.py:2:17: F401 `app.app_file` imported but unused
|
||||
crates/ruff/resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
|
||||
crates/ruff/resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
crates/ruff/resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
|
||||
crates/ruff/resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
|
||||
crates/ruff/resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
|
||||
crates/ruff/resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
|
||||
crates/ruff/resources/test/project/project/file.py:1:8: F401 `os` imported but unused
|
||||
Found 9 errors.
|
||||
9 potentially fixable with the --fix option.
|
||||
```
|
||||
@@ -68,7 +68,7 @@ Running from a parent directory should "ignore" the `exclude` (hence, `concepts/
|
||||
included in the output):
|
||||
|
||||
```console
|
||||
∴ (cd resources/test/project/examples && cargo run -- --config=docs/ruff.toml .)
|
||||
∴ (cd crates/ruff/resources/test/project/examples && cargo run -p ruff_cli -- check --config=docs/ruff.toml .)
|
||||
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
@@ -80,8 +80,8 @@ Found 4 errors.
|
||||
Passing an excluded directory directly should report errors in the contained files:
|
||||
|
||||
```console
|
||||
∴ cargo run resources/test/project/examples/excluded/
|
||||
resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
|
||||
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/examples/excluded/
|
||||
crates/ruff/resources/test/project/examples/excluded/script.py:1:8: F401 `os` imported but unused
|
||||
Found 1 error.
|
||||
1 potentially fixable with the --fix option.
|
||||
```
|
||||
@@ -89,8 +89,8 @@ Found 1 error.
|
||||
Unless we `--force-exclude`:
|
||||
|
||||
```console
|
||||
∴ cargo run resources/test/project/examples/excluded/ --force-exclude
|
||||
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/examples/excluded/ --force-exclude
|
||||
warning: No Python files found under the given path(s)
|
||||
∴ cargo run resources/test/project/examples/excluded/script.py --force-exclude
|
||||
∴ cargo run -p ruff_cli -- check crates/ruff/resources/test/project/examples/excluded/script.py --force-exclude
|
||||
warning: No Python files found under the given path(s)
|
||||
```
|
||||
|
||||
@@ -304,13 +304,14 @@ pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::In));
|
||||
}
|
||||
Tok::Is => {
|
||||
if let Some((_, _, end)) =
|
||||
let op = if let Some((_, _, end)) =
|
||||
tok_iter.next_if(|(_, tok, _)| matches!(tok, Tok::Not))
|
||||
{
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::IsNot));
|
||||
LocatedCmpop::new(start, end, Cmpop::IsNot)
|
||||
} else {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::Is));
|
||||
}
|
||||
LocatedCmpop::new(start, end, Cmpop::Is)
|
||||
};
|
||||
ops.push(op);
|
||||
}
|
||||
Tok::NotEqual => {
|
||||
ops.push(LocatedCmpop::new(start, end, Cmpop::NotEq));
|
||||
|
||||
@@ -32,6 +32,7 @@ use crate::ast::visitor::{walk_excepthandler, Visitor};
|
||||
use crate::ast::{branch_detection, cast, helpers, operations, typing, visitor};
|
||||
use crate::docstrings::definition::{Definition, DefinitionKind, Docstring, Documentable};
|
||||
use crate::registry::{Diagnostic, Rule};
|
||||
use crate::resolver::is_interface_definition_path;
|
||||
use crate::rules::{
|
||||
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
|
||||
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
|
||||
@@ -58,6 +59,7 @@ pub struct Checker<'a> {
|
||||
pub(crate) path: &'a Path,
|
||||
module_path: Option<Vec<String>>,
|
||||
package: Option<&'a Path>,
|
||||
is_interface_definition: bool,
|
||||
autofix: flags::Autofix,
|
||||
noqa: flags::Noqa,
|
||||
pub(crate) settings: &'a Settings,
|
||||
@@ -126,6 +128,7 @@ impl<'a> Checker<'a> {
|
||||
style: &'a Stylist,
|
||||
indexer: &'a Indexer,
|
||||
) -> Checker<'a> {
|
||||
let is_interface_definition = is_interface_definition_path(path);
|
||||
Checker {
|
||||
settings,
|
||||
noqa_line_for,
|
||||
@@ -134,6 +137,7 @@ impl<'a> Checker<'a> {
|
||||
path,
|
||||
package,
|
||||
module_path,
|
||||
is_interface_definition,
|
||||
locator,
|
||||
stylist: style,
|
||||
indexer,
|
||||
@@ -173,7 +177,7 @@ impl<'a> Checker<'a> {
|
||||
in_type_checking_block: false,
|
||||
seen_import_boundary: false,
|
||||
futures_allowed: true,
|
||||
annotations_future_enabled: path.extension().map_or(false, |ext| ext == "pyi"),
|
||||
annotations_future_enabled: is_interface_definition,
|
||||
except_handlers: vec![],
|
||||
// Check-specific state.
|
||||
flake8_bugbear_seen: vec![],
|
||||
@@ -235,51 +239,51 @@ impl<'a> Checker<'a> {
|
||||
'b: 'a,
|
||||
{
|
||||
let call_path = collect_call_path(value);
|
||||
if let Some(head) = call_path.first() {
|
||||
if let Some(binding) = self.find_binding(head) {
|
||||
match &binding.kind {
|
||||
BindingKind::Importation(.., name)
|
||||
| BindingKind::SubmoduleImportation(name, ..) => {
|
||||
return if name.starts_with('.') {
|
||||
if let Some(module) = &self.module_path {
|
||||
let mut source_path = from_relative_import(module, name);
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut source_path: CallPath = name.split('.').collect();
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
};
|
||||
let Some(head) = call_path.first() else {
|
||||
return None;
|
||||
};
|
||||
let Some(binding) = self.find_binding(head) else {
|
||||
return None;
|
||||
};
|
||||
match &binding.kind {
|
||||
BindingKind::Importation(.., name) | BindingKind::SubmoduleImportation(name, ..) => {
|
||||
if name.starts_with('.') {
|
||||
if let Some(module) = &self.module_path {
|
||||
let mut source_path = from_relative_import(module, name);
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
BindingKind::FromImportation(.., name) => {
|
||||
return if name.starts_with('.') {
|
||||
if let Some(module) = &self.module_path {
|
||||
let mut source_path = from_relative_import(module, name);
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut source_path: CallPath = name.split('.').collect();
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
};
|
||||
}
|
||||
BindingKind::Builtin => {
|
||||
let mut source_path: CallPath = smallvec![];
|
||||
source_path.push("");
|
||||
source_path.extend(call_path);
|
||||
return Some(source_path);
|
||||
}
|
||||
_ => {}
|
||||
} else {
|
||||
let mut source_path: CallPath = name.split('.').collect();
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
}
|
||||
}
|
||||
BindingKind::FromImportation(.., name) => {
|
||||
if name.starts_with('.') {
|
||||
if let Some(module) = &self.module_path {
|
||||
let mut source_path = from_relative_import(module, name);
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
let mut source_path: CallPath = name.split('.').collect();
|
||||
source_path.extend(call_path.into_iter().skip(1));
|
||||
Some(source_path)
|
||||
}
|
||||
}
|
||||
BindingKind::Builtin => {
|
||||
let mut source_path: CallPath = smallvec![];
|
||||
source_path.push("");
|
||||
source_path.extend(call_path);
|
||||
Some(source_path)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Return `true` if a `Rule` is disabled by a `noqa` directive.
|
||||
@@ -775,6 +779,9 @@ where
|
||||
if self.settings.rules.enabled(&Rule::ReturnOutsideFunction) {
|
||||
pyflakes::rules::return_outside_function(self, stmt);
|
||||
}
|
||||
if self.settings.rules.enabled(&Rule::ReturnInInit) {
|
||||
pylint::rules::return_in_init(self, stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::ClassDef {
|
||||
name,
|
||||
@@ -835,18 +842,20 @@ where
|
||||
flake8_bugbear::rules::useless_expression(self, body);
|
||||
}
|
||||
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(&Rule::AbstractBaseClassWithoutAbstractMethod)
|
||||
|| self
|
||||
if !self.is_interface_definition {
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(&Rule::EmptyMethodWithoutAbstractDecorator)
|
||||
{
|
||||
flake8_bugbear::rules::abstract_base_class(
|
||||
self, stmt, name, bases, keywords, body,
|
||||
);
|
||||
.enabled(&Rule::AbstractBaseClassWithoutAbstractMethod)
|
||||
|| self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(&Rule::EmptyMethodWithoutAbstractDecorator)
|
||||
{
|
||||
flake8_bugbear::rules::abstract_base_class(
|
||||
self, stmt, name, bases, keywords, body,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
@@ -900,7 +909,38 @@ where
|
||||
}
|
||||
|
||||
for alias in names {
|
||||
if alias.node.name.contains('.') && alias.node.asname.is_none() {
|
||||
if alias.node.name == "__future__" {
|
||||
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
|
||||
self.add_binding(
|
||||
name,
|
||||
Binding {
|
||||
kind: BindingKind::FutureImportation,
|
||||
runtime_usage: None,
|
||||
// Always mark `__future__` imports as used.
|
||||
synthetic_usage: Some((
|
||||
self.scopes[*(self
|
||||
.scope_stack
|
||||
.last()
|
||||
.expect("No current scope found"))]
|
||||
.id,
|
||||
Range::from_located(alias),
|
||||
)),
|
||||
typing_usage: None,
|
||||
range: Range::from_located(alias),
|
||||
source: Some(self.current_stmt().clone()),
|
||||
context: self.execution_context(),
|
||||
},
|
||||
);
|
||||
|
||||
if self.settings.rules.enabled(&Rule::LateFutureImport)
|
||||
&& !self.futures_allowed
|
||||
{
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::LateFutureImport,
|
||||
Range::from_located(stmt),
|
||||
));
|
||||
}
|
||||
} else if alias.node.name.contains('.') && alias.node.asname.is_none() {
|
||||
// Given `import foo.bar`, `name` would be "foo", and `full_name` would be
|
||||
// "foo.bar".
|
||||
let name = alias.node.name.split('.').next().unwrap();
|
||||
@@ -918,10 +958,6 @@ where
|
||||
},
|
||||
);
|
||||
} else {
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
self.check_builtin_shadowing(asname, stmt, false);
|
||||
}
|
||||
|
||||
// Given `import foo`, `name` and `full_name` would both be `foo`.
|
||||
// Given `import foo as bar`, `name` would be `bar` and `full_name` would
|
||||
// be `foo`.
|
||||
@@ -957,6 +993,10 @@ where
|
||||
context: self.execution_context(),
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(asname) = &alias.node.asname {
|
||||
self.check_builtin_shadowing(asname, stmt, false);
|
||||
}
|
||||
}
|
||||
|
||||
// flake8-debugger
|
||||
@@ -1572,11 +1612,12 @@ where
|
||||
}
|
||||
}
|
||||
if self.settings.rules.enabled(&Rule::CompositeAssertion) {
|
||||
if let Some(diagnostic) =
|
||||
flake8_pytest_style::rules::composite_condition(stmt, test)
|
||||
{
|
||||
self.diagnostics.push(diagnostic);
|
||||
}
|
||||
flake8_pytest_style::rules::composite_condition(
|
||||
self,
|
||||
stmt,
|
||||
test,
|
||||
msg.as_deref(),
|
||||
);
|
||||
}
|
||||
}
|
||||
StmtKind::With { items, body, .. } => {
|
||||
@@ -1643,9 +1684,7 @@ where
|
||||
pylint::rules::useless_else_on_loop(self, stmt, body, orelse);
|
||||
}
|
||||
if matches!(stmt.node, StmtKind::For { .. }) {
|
||||
if self.settings.rules.enabled(&Rule::ConvertLoopToAny)
|
||||
|| self.settings.rules.enabled(&Rule::ConvertLoopToAll)
|
||||
{
|
||||
if self.settings.rules.enabled(&Rule::ReimplementedBuiltin) {
|
||||
flake8_simplify::rules::convert_for_loop_to_any_all(
|
||||
self,
|
||||
stmt,
|
||||
@@ -1739,8 +1778,8 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
|
||||
if self.path.extension().map_or(false, |ext| ext == "pyi") {
|
||||
if self.is_interface_definition {
|
||||
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
|
||||
flake8_pyi::rules::prefix_type_params(self, value, targets);
|
||||
}
|
||||
}
|
||||
@@ -2526,7 +2565,12 @@ where
|
||||
.enabled(&Rule::UnnecessaryCollectionCall)
|
||||
{
|
||||
flake8_comprehensions::rules::unnecessary_collection_call(
|
||||
self, expr, func, args, keywords,
|
||||
self,
|
||||
expr,
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
&self.settings.flake8_comprehensions,
|
||||
);
|
||||
}
|
||||
if self
|
||||
@@ -2811,6 +2855,11 @@ where
|
||||
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
|
||||
}
|
||||
|
||||
// numpy
|
||||
if self.settings.rules.enabled(&Rule::NumpyLegacyRandom) {
|
||||
numpy::rules::numpy_legacy_random(self, func);
|
||||
}
|
||||
|
||||
// flake8-logging-format
|
||||
if self.settings.rules.enabled(&Rule::LoggingStringFormat)
|
||||
|| self.settings.rules.enabled(&Rule::LoggingPercentFormat)
|
||||
@@ -3174,13 +3223,13 @@ where
|
||||
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators);
|
||||
}
|
||||
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(&Rule::UnrecognizedPlatformCheck)
|
||||
|| self.settings.rules.enabled(&Rule::UnrecognizedPlatformName)
|
||||
{
|
||||
if self.path.extension().map_or(false, |ext| ext == "pyi") {
|
||||
if self.is_interface_definition {
|
||||
if self
|
||||
.settings
|
||||
.rules
|
||||
.enabled(&Rule::UnrecognizedPlatformCheck)
|
||||
|| self.settings.rules.enabled(&Rule::UnrecognizedPlatformName)
|
||||
{
|
||||
flake8_pyi::rules::unrecognized_platform(
|
||||
self,
|
||||
expr,
|
||||
@@ -4120,146 +4169,147 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
|
||||
fn handle_node_load(&mut self, expr: &Expr) {
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
let scope_id = self.current_scope().id;
|
||||
let ExprKind::Name { id, .. } = &expr.node else {
|
||||
return;
|
||||
};
|
||||
let scope_id = self.current_scope().id;
|
||||
|
||||
let mut first_iter = true;
|
||||
let mut in_generator = false;
|
||||
let mut import_starred = false;
|
||||
let mut first_iter = true;
|
||||
let mut in_generator = false;
|
||||
let mut import_starred = false;
|
||||
|
||||
for scope_index in self.scope_stack.iter().rev() {
|
||||
let scope = &self.scopes[*scope_index];
|
||||
|
||||
if matches!(scope.kind, ScopeKind::Class(_)) {
|
||||
if id == "__class__" {
|
||||
return;
|
||||
} else if !first_iter && !in_generator {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(index) = scope.bindings.get(&id.as_str()) {
|
||||
// Mark the binding as used.
|
||||
let context = self.execution_context();
|
||||
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
|
||||
|
||||
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
|
||||
&& !self.in_deferred_string_type_definition
|
||||
&& !self.in_deferred_type_definition
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the name of the sub-importation is the same as an alias of another
|
||||
// importation and the alias is used, that sub-importation should be
|
||||
// marked as used too.
|
||||
//
|
||||
// This handles code like:
|
||||
// import pyarrow as pa
|
||||
// import pyarrow.csv
|
||||
// print(pa.csv.read_csv("test.csv"))
|
||||
match &self.bindings[*index].kind {
|
||||
BindingKind::Importation(name, full_name)
|
||||
| BindingKind::SubmoduleImportation(name, full_name) => {
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
if let Some(index) = scope.bindings.get(full_name) {
|
||||
self.bindings[*index].mark_used(
|
||||
scope_id,
|
||||
Range::from_located(expr),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
BindingKind::FromImportation(name, full_name) => {
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
if let Some(index) = scope.bindings.get(full_name.as_str()) {
|
||||
self.bindings[*index].mark_used(
|
||||
scope_id,
|
||||
Range::from_located(expr),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
for scope_index in self.scope_stack.iter().rev() {
|
||||
let scope = &self.scopes[*scope_index];
|
||||
|
||||
if matches!(scope.kind, ScopeKind::Class(_)) {
|
||||
if id == "__class__" {
|
||||
return;
|
||||
} else if !first_iter && !in_generator {
|
||||
continue;
|
||||
}
|
||||
|
||||
first_iter = false;
|
||||
in_generator = matches!(scope.kind, ScopeKind::Generator);
|
||||
import_starred = import_starred || scope.import_starred;
|
||||
}
|
||||
|
||||
if import_starred {
|
||||
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
|
||||
let mut from_list = vec![];
|
||||
for scope_index in self.scope_stack.iter().rev() {
|
||||
let scope = &self.scopes[*scope_index];
|
||||
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
|
||||
if let BindingKind::StarImportation(level, module) = &binding.kind {
|
||||
from_list.push(helpers::format_import_from(
|
||||
level.as_ref(),
|
||||
module.as_deref(),
|
||||
));
|
||||
if let Some(index) = scope.bindings.get(&id.as_str()) {
|
||||
// Mark the binding as used.
|
||||
let context = self.execution_context();
|
||||
self.bindings[*index].mark_used(scope_id, Range::from_located(expr), context);
|
||||
|
||||
if matches!(self.bindings[*index].kind, BindingKind::Annotation)
|
||||
&& !self.in_deferred_string_type_definition
|
||||
&& !self.in_deferred_type_definition
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the name of the sub-importation is the same as an alias of another
|
||||
// importation and the alias is used, that sub-importation should be
|
||||
// marked as used too.
|
||||
//
|
||||
// This handles code like:
|
||||
// import pyarrow as pa
|
||||
// import pyarrow.csv
|
||||
// print(pa.csv.read_csv("test.csv"))
|
||||
match &self.bindings[*index].kind {
|
||||
BindingKind::Importation(name, full_name)
|
||||
| BindingKind::SubmoduleImportation(name, full_name) => {
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
if let Some(index) = scope.bindings.get(full_name) {
|
||||
self.bindings[*index].mark_used(
|
||||
scope_id,
|
||||
Range::from_located(expr),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
from_list.sort();
|
||||
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::ImportStarUsage {
|
||||
name: id.to_string(),
|
||||
sources: from_list,
|
||||
},
|
||||
Range::from_located(expr),
|
||||
));
|
||||
BindingKind::FromImportation(name, full_name) => {
|
||||
let has_alias = full_name
|
||||
.split('.')
|
||||
.last()
|
||||
.map(|segment| &segment != name)
|
||||
.unwrap_or_default();
|
||||
if has_alias {
|
||||
// Mark the sub-importation as used.
|
||||
if let Some(index) = scope.bindings.get(full_name.as_str()) {
|
||||
self.bindings[*index].mark_used(
|
||||
scope_id,
|
||||
Range::from_located(expr),
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(&Rule::UndefinedName) {
|
||||
// Allow __path__.
|
||||
if self.path.ends_with("__init__.py") && id == "__path__" {
|
||||
return;
|
||||
}
|
||||
first_iter = false;
|
||||
in_generator = matches!(scope.kind, ScopeKind::Generator);
|
||||
import_starred = import_starred || scope.import_starred;
|
||||
}
|
||||
|
||||
// Allow "__module__" and "__qualname__" in class scopes.
|
||||
if (id == "__module__" || id == "__qualname__")
|
||||
&& matches!(self.current_scope().kind, ScopeKind::Class(..))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid flagging if NameError is handled.
|
||||
if let Some(handler_names) = self.except_handlers.last() {
|
||||
if handler_names
|
||||
.iter()
|
||||
.any(|call_path| call_path.as_slice() == ["NameError"])
|
||||
{
|
||||
return;
|
||||
if import_starred {
|
||||
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
|
||||
let mut from_list = vec![];
|
||||
for scope_index in self.scope_stack.iter().rev() {
|
||||
let scope = &self.scopes[*scope_index];
|
||||
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
|
||||
if let BindingKind::StarImportation(level, module) = &binding.kind {
|
||||
from_list.push(helpers::format_import_from(
|
||||
level.as_ref(),
|
||||
module.as_deref(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
from_list.sort();
|
||||
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName { name: id.clone() },
|
||||
pyflakes::rules::ImportStarUsage {
|
||||
name: id.to_string(),
|
||||
sources: from_list,
|
||||
},
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if self.settings.rules.enabled(&Rule::UndefinedName) {
|
||||
// Allow __path__.
|
||||
if self.path.ends_with("__init__.py") && id == "__path__" {
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow "__module__" and "__qualname__" in class scopes.
|
||||
if (id == "__module__" || id == "__qualname__")
|
||||
&& matches!(self.current_scope().kind, ScopeKind::Class(..))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid flagging if NameError is handled.
|
||||
if let Some(handler_names) = self.except_handlers.last() {
|
||||
if handler_names
|
||||
.iter()
|
||||
.any(|call_path| call_path.as_slice() == ["NameError"])
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName { name: id.clone() },
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4457,26 +4507,29 @@ impl<'a> Checker<'a> {
|
||||
where
|
||||
'b: 'a,
|
||||
{
|
||||
if let ExprKind::Name { id, .. } = &expr.node {
|
||||
if operations::on_conditional_branch(
|
||||
&mut self.parents.iter().rev().map(std::convert::Into::into),
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scope =
|
||||
&mut self.scopes[*(self.scope_stack.last().expect("No current scope found"))];
|
||||
if scope.bindings.remove(&id.as_str()).is_none()
|
||||
&& self.settings.rules.enabled(&Rule::UndefinedName)
|
||||
{
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName {
|
||||
name: id.to_string(),
|
||||
},
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
let ExprKind::Name { id, .. } = &expr.node else {
|
||||
return;
|
||||
};
|
||||
if operations::on_conditional_branch(
|
||||
&mut self.parents.iter().rev().map(std::convert::Into::into),
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scope = &mut self.scopes[*(self.scope_stack.last().expect("No current scope found"))];
|
||||
if scope.bindings.remove(&id.as_str()).is_some() {
|
||||
return;
|
||||
}
|
||||
if !self.settings.rules.enabled(&Rule::UndefinedName) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.diagnostics.push(Diagnostic::new(
|
||||
pyflakes::rules::UndefinedName {
|
||||
name: id.to_string(),
|
||||
},
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
|
||||
fn visit_docstring<'b>(&mut self, python_ast: &'b Suite) -> bool
|
||||
|
||||
@@ -2,6 +2,7 @@ use std::path::Path;
|
||||
|
||||
use crate::registry::{Diagnostic, Rule};
|
||||
use crate::rules::flake8_no_pep420::rules::implicit_namespace_package;
|
||||
use crate::rules::pep8_naming::rules::invalid_module_name;
|
||||
use crate::settings::Settings;
|
||||
|
||||
pub fn check_file_path(
|
||||
@@ -20,5 +21,12 @@ pub fn check_file_path(
|
||||
}
|
||||
}
|
||||
|
||||
// pep8-naming
|
||||
if settings.rules.enabled(&Rule::InvalidModuleName) {
|
||||
if let Some(diagnostic) = invalid_module_name(path, package) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
diagnostics
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! `NoQA` enforcement and validation.
|
||||
|
||||
use log::warn;
|
||||
use nohash_hasher::IntMap;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
@@ -7,7 +8,7 @@ use crate::ast::types::Range;
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::fix::Fix;
|
||||
use crate::noqa;
|
||||
use crate::noqa::{is_file_exempt, Directive};
|
||||
use crate::noqa::{extract_file_exemption, Directive, Exemption};
|
||||
use crate::registry::{Diagnostic, DiagnosticKind, Rule};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
|
||||
@@ -21,17 +22,38 @@ pub fn check_noqa(
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) {
|
||||
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
|
||||
let mut ignored = vec![];
|
||||
|
||||
let enforce_noqa = settings.rules.enabled(&Rule::UnusedNOQA);
|
||||
|
||||
// Whether the file is exempted from all checks.
|
||||
let mut file_exempted = false;
|
||||
|
||||
// Codes that are globally exempted (within the current file).
|
||||
let mut file_exemptions: Vec<NoqaCode> = vec![];
|
||||
|
||||
// Map from line number to `noqa` directive on that line, along with any codes
|
||||
// that were matched by the directive.
|
||||
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
|
||||
|
||||
// Indices of diagnostics that were ignored by a `noqa` directive.
|
||||
let mut ignored_diagnostics = vec![];
|
||||
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
for lineno in commented_lines {
|
||||
// If we hit an exemption for the entire file, bail.
|
||||
if is_file_exempt(lines[lineno - 1]) {
|
||||
diagnostics.drain(..);
|
||||
return;
|
||||
match extract_file_exemption(lines[lineno - 1]) {
|
||||
Exemption::All => {
|
||||
file_exempted = true;
|
||||
}
|
||||
Exemption::Codes(codes) => {
|
||||
file_exemptions.extend(codes.into_iter().filter_map(|code| {
|
||||
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
|
||||
Some(rule.noqa_code())
|
||||
} else {
|
||||
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
|
||||
None
|
||||
}
|
||||
}));
|
||||
}
|
||||
Exemption::None => {}
|
||||
}
|
||||
|
||||
if enforce_noqa {
|
||||
@@ -47,6 +69,20 @@ pub fn check_noqa(
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the file is exempted, ignore all diagnostics.
|
||||
if file_exempted {
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the diagnostic is ignored by a global exemption, ignore it.
|
||||
if !file_exemptions.is_empty() {
|
||||
if file_exemptions.contains(&diagnostic.kind.rule().noqa_code()) {
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Is the violation ignored by a `noqa` directive on the parent line?
|
||||
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
|
||||
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
|
||||
@@ -57,13 +93,13 @@ pub fn check_noqa(
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored.push(index);
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
(Directive::Codes(.., codes), matches) => {
|
||||
if noqa::includes(diagnostic.kind.rule(), codes) {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored.push(index);
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -84,12 +120,14 @@ pub fn check_noqa(
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored.push(index);
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
(Directive::Codes(.., codes), matches) => {
|
||||
if noqa::includes(diagnostic.kind.rule(), codes) {
|
||||
matches.push(diagnostic.kind.rule().noqa_code());
|
||||
ignored.push(index);
|
||||
ignored_diagnostics.push(index);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
(Directive::None, ..) => {}
|
||||
@@ -203,8 +241,8 @@ pub fn check_noqa(
|
||||
}
|
||||
}
|
||||
|
||||
ignored.sort_unstable();
|
||||
for index in ignored.iter().rev() {
|
||||
ignored_diagnostics.sort_unstable();
|
||||
for index in ignored_diagnostics.iter().rev() {
|
||||
diagnostics.swap_remove(*index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ pub fn check_tokens(
|
||||
// E701, E702, E703
|
||||
if enforce_compound_statements {
|
||||
diagnostics.extend(
|
||||
pycodestyle::rules::compound_statements(tokens)
|
||||
pycodestyle::rules::compound_statements(tokens, settings, autofix)
|
||||
.into_iter()
|
||||
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
|
||||
);
|
||||
|
||||
@@ -123,6 +123,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
|
||||
// pylint
|
||||
(Pylint, "E0100") => Rule::YieldInInit,
|
||||
(Pylint, "E0101") => Rule::ReturnInInit,
|
||||
(Pylint, "E0604") => Rule::InvalidAllObject,
|
||||
(Pylint, "E0605") => Rule::InvalidAllFormat,
|
||||
(Pylint, "E1307") => Rule::BadStringFormatType,
|
||||
@@ -270,8 +271,8 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(Flake8Simplify, "107") => Rule::ReturnInTryExceptFinally,
|
||||
(Flake8Simplify, "108") => Rule::UseTernaryOperator,
|
||||
(Flake8Simplify, "109") => Rule::CompareWithTuple,
|
||||
(Flake8Simplify, "110") => Rule::ConvertLoopToAny,
|
||||
(Flake8Simplify, "111") => Rule::ConvertLoopToAll,
|
||||
(Flake8Simplify, "110") => Rule::ReimplementedBuiltin,
|
||||
// (Flake8Simplify, "111") => Rule::ReimplementedBuiltin,
|
||||
(Flake8Simplify, "112") => Rule::UseCapitalEnvironmentVariables,
|
||||
(Flake8Simplify, "114") => Rule::IfWithSameArms,
|
||||
(Flake8Simplify, "115") => Rule::OpenFileWithContextHandler,
|
||||
@@ -391,6 +392,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
(PEP8Naming, "816") => Rule::MixedCaseVariableInGlobalScope,
|
||||
(PEP8Naming, "817") => Rule::CamelcaseImportedAsAcronym,
|
||||
(PEP8Naming, "818") => Rule::ErrorSuffixOnExceptionName,
|
||||
(PEP8Naming, "999") => Rule::InvalidModuleName,
|
||||
|
||||
// isort
|
||||
(Isort, "001") => Rule::UnsortedImports,
|
||||
@@ -589,6 +591,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
|
||||
|
||||
// numpy
|
||||
(Numpy, "001") => Rule::NumpyDeprecatedTypeAlias,
|
||||
(Numpy, "002") => Rule::NumpyLegacyRandom,
|
||||
|
||||
// ruff
|
||||
(Ruff, "001") => Rule::AmbiguousUnicodeCharacterString,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
//!
|
||||
//! [Ruff]: https://github.com/charliermarsh/ruff
|
||||
|
||||
pub use ast::types::Range;
|
||||
use cfg_if::cfg_if;
|
||||
pub use rule_selector::RuleSelector;
|
||||
pub use rules::pycodestyle::rules::IOError;
|
||||
|
||||
@@ -9,10 +9,10 @@ use crate::directives;
|
||||
use crate::linter::{check_path, LinterResult};
|
||||
use crate::registry::Rule;
|
||||
use crate::rules::{
|
||||
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_errmsg,
|
||||
flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes,
|
||||
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
|
||||
pycodestyle, pydocstyle, pylint, pyupgrade,
|
||||
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
|
||||
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style,
|
||||
flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
|
||||
isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, pyupgrade,
|
||||
};
|
||||
use crate::rustpython_helpers::tokenize;
|
||||
use crate::settings::configuration::Configuration;
|
||||
@@ -139,9 +139,11 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
|
||||
flake8_bandit: Some(flake8_bandit::settings::Settings::default().into()),
|
||||
flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()),
|
||||
flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()),
|
||||
flake8_comprehensions: Some(flake8_comprehensions::settings::Settings::default().into()),
|
||||
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
|
||||
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
|
||||
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
|
||||
flake8_self: Some(flake8_self::settings::Settings::default().into()),
|
||||
flake8_implicit_str_concat: Some(
|
||||
flake8_implicit_str_concat::settings::Settings::default().into(),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use log::warn;
|
||||
use nohash_hasher::IntMap;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
@@ -11,6 +12,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::codes::NoqaCode;
|
||||
use crate::registry::{Diagnostic, Rule};
|
||||
use crate::rule_redirects::get_redirect_target;
|
||||
use crate::source_code::{LineEnding, Locator};
|
||||
@@ -23,16 +25,47 @@ static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
});
|
||||
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Exemption<'a> {
|
||||
None,
|
||||
All,
|
||||
Codes(Vec<&'a str>),
|
||||
}
|
||||
|
||||
/// Return `true` if a file is exempt from checking based on the contents of the
|
||||
/// given line.
|
||||
pub fn is_file_exempt(line: &str) -> bool {
|
||||
pub fn extract_file_exemption(line: &str) -> Exemption {
|
||||
let line = line.trim_start();
|
||||
line.starts_with("# flake8: noqa")
|
||||
|
||||
if line.starts_with("# flake8: noqa")
|
||||
|| line.starts_with("# flake8: NOQA")
|
||||
|| line.starts_with("# flake8: NoQA")
|
||||
|| line.starts_with("# ruff: noqa")
|
||||
|| line.starts_with("# ruff: NOQA")
|
||||
|| line.starts_with("# ruff: NoQA")
|
||||
{
|
||||
return Exemption::All;
|
||||
}
|
||||
|
||||
if let Some(remainder) = line
|
||||
.strip_prefix("# ruff: noqa")
|
||||
.or_else(|| line.strip_prefix("# ruff: NOQA"))
|
||||
.or_else(|| line.strip_prefix("# ruff: NoQA"))
|
||||
{
|
||||
if remainder.is_empty() {
|
||||
return Exemption::All;
|
||||
} else if let Some(codes) = remainder.strip_prefix(':') {
|
||||
let codes: Vec<&str> = SPLIT_COMMA_REGEX
|
||||
.split(codes.trim())
|
||||
.map(str::trim)
|
||||
.filter(|code| !code.is_empty())
|
||||
.collect();
|
||||
if codes.is_empty() {
|
||||
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
|
||||
}
|
||||
return Exemption::Codes(codes);
|
||||
}
|
||||
warn!("Unexpected suffix on `noqa` directive: \"{line}\"");
|
||||
}
|
||||
|
||||
Exemption::None
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -48,16 +81,22 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
|
||||
Some(caps) => match caps.name("spaces") {
|
||||
Some(spaces) => match caps.name("noqa") {
|
||||
Some(noqa) => match caps.name("codes") {
|
||||
Some(codes) => Directive::Codes(
|
||||
spaces.as_str().chars().count(),
|
||||
noqa.start(),
|
||||
noqa.end(),
|
||||
SPLIT_COMMA_REGEX
|
||||
.split(codes.as_str())
|
||||
Some(codes) => {
|
||||
let codes: Vec<&str> = SPLIT_COMMA_REGEX
|
||||
.split(codes.as_str().trim())
|
||||
.map(str::trim)
|
||||
.filter(|code| !code.is_empty())
|
||||
.collect(),
|
||||
),
|
||||
.collect();
|
||||
if codes.is_empty() {
|
||||
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
|
||||
}
|
||||
Directive::Codes(
|
||||
spaces.as_str().chars().count(),
|
||||
noqa.start(),
|
||||
noqa.end(),
|
||||
codes,
|
||||
)
|
||||
}
|
||||
None => {
|
||||
Directive::All(spaces.as_str().chars().count(), noqa.start(), noqa.end())
|
||||
}
|
||||
@@ -124,68 +163,93 @@ fn add_noqa_inner(
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
line_ending: &LineEnding,
|
||||
) -> (usize, String) {
|
||||
// Map of line number to set of (non-ignored) diagnostic codes that are triggered on that line.
|
||||
let mut matches_by_line: FxHashMap<usize, FxHashSet<&Rule>> = FxHashMap::default();
|
||||
|
||||
// Whether the file is exempted from all checks.
|
||||
let mut file_exempted = false;
|
||||
|
||||
// Codes that are globally exempted (within the current file).
|
||||
let mut file_exemptions: Vec<NoqaCode> = vec![];
|
||||
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
for (lineno, line) in lines.iter().enumerate() {
|
||||
// If we hit an exemption for the entire file, bail.
|
||||
if is_file_exempt(line) {
|
||||
return (0, contents.to_string());
|
||||
for lineno in commented_lines {
|
||||
match extract_file_exemption(lines[lineno - 1]) {
|
||||
Exemption::All => {
|
||||
file_exempted = true;
|
||||
}
|
||||
Exemption::Codes(codes) => {
|
||||
file_exemptions.extend(codes.into_iter().filter_map(|code| {
|
||||
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
|
||||
Some(rule.noqa_code())
|
||||
} else {
|
||||
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
|
||||
None
|
||||
}
|
||||
}));
|
||||
}
|
||||
Exemption::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark any non-ignored diagnostics.
|
||||
for diagnostic in diagnostics {
|
||||
// If the file is exempted, don't add any noqa directives.
|
||||
if file_exempted {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Grab the noqa (logical) line number for the current (physical) line.
|
||||
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
|
||||
|
||||
let mut codes: FxHashSet<&Rule> = FxHashSet::default();
|
||||
for diagnostic in diagnostics {
|
||||
if diagnostic.location.row() == lineno + 1 {
|
||||
// Is the violation ignored by a `noqa` directive on the parent line?
|
||||
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
|
||||
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
match extract_noqa_directive(lines[noqa_lineno - 1]) {
|
||||
Directive::All(..) => {
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(.., codes) => {
|
||||
if includes(diagnostic.kind.rule(), &codes) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Directive::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is the diagnostic ignored by a `noqa` directive on the same line?
|
||||
let diagnostic_lineno = diagnostic.location.row();
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(&diagnostic_lineno)
|
||||
.unwrap_or(&diagnostic_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
match extract_noqa_directive(lines[noqa_lineno - 1]) {
|
||||
Directive::All(..) => {
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(.., codes) => {
|
||||
if includes(diagnostic.kind.rule(), &codes) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Directive::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
// The diagnostic is not ignored by any `noqa` directive; add it to the list.
|
||||
codes.insert(diagnostic.kind.rule());
|
||||
// If the diagnostic is ignored by a global exemption, don't add a noqa directive.
|
||||
if !file_exemptions.is_empty() {
|
||||
if file_exemptions.contains(&diagnostic.kind.rule().noqa_code()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if !codes.is_empty() {
|
||||
matches_by_line
|
||||
.entry(noqa_lineno)
|
||||
.or_default()
|
||||
.extend(codes);
|
||||
// Is the violation ignored by a `noqa` directive on the parent line?
|
||||
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
|
||||
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
match extract_noqa_directive(lines[noqa_lineno - 1]) {
|
||||
Directive::All(..) => {
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(.., codes) => {
|
||||
if includes(diagnostic.kind.rule(), &codes) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Directive::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Is the diagnostic ignored by a `noqa` directive on the same line?
|
||||
let diagnostic_lineno = diagnostic.location.row();
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(&diagnostic_lineno)
|
||||
.unwrap_or(&diagnostic_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
match extract_noqa_directive(lines[noqa_lineno - 1]) {
|
||||
Directive::All(..) => {
|
||||
continue;
|
||||
}
|
||||
Directive::Codes(.., codes) => {
|
||||
if includes(diagnostic.kind.rule(), &codes) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
Directive::None => {}
|
||||
}
|
||||
}
|
||||
|
||||
// The diagnostic is not ignored by any `noqa` directive; add it to the list.
|
||||
let lineno = diagnostic.location.row() - 1;
|
||||
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
|
||||
matches_by_line
|
||||
.entry(noqa_lineno)
|
||||
.or_default()
|
||||
.insert(diagnostic.kind.rule());
|
||||
}
|
||||
|
||||
let mut count: usize = 0;
|
||||
@@ -258,7 +322,7 @@ fn push_codes<I: Display>(str: &mut String, codes: impl Iterator<Item = I>) {
|
||||
if !first {
|
||||
str.push_str(", ");
|
||||
}
|
||||
let _ = write!(str, "{}", code);
|
||||
let _ = write!(str, "{code}");
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ ruff_macros::register_rules!(
|
||||
rules::pylint::rules::UsedPriorGlobalDeclaration,
|
||||
rules::pylint::rules::AwaitOutsideAsync,
|
||||
rules::pylint::rules::PropertyWithParameters,
|
||||
rules::pylint::rules::ReturnInInit,
|
||||
rules::pylint::rules::ConsiderUsingFromImport,
|
||||
rules::pylint::rules::ComparisonOfConstant,
|
||||
rules::pylint::rules::ConsiderMergingIsinstance,
|
||||
@@ -259,8 +260,7 @@ ruff_macros::register_rules!(
|
||||
rules::flake8_simplify::rules::ReturnInTryExceptFinally,
|
||||
rules::flake8_simplify::rules::UseTernaryOperator,
|
||||
rules::flake8_simplify::rules::CompareWithTuple,
|
||||
rules::flake8_simplify::rules::ConvertLoopToAny,
|
||||
rules::flake8_simplify::rules::ConvertLoopToAll,
|
||||
rules::flake8_simplify::rules::ReimplementedBuiltin,
|
||||
rules::flake8_simplify::rules::UseCapitalEnvironmentVariables,
|
||||
rules::flake8_simplify::rules::IfWithSameArms,
|
||||
rules::flake8_simplify::rules::OpenFileWithContextHandler,
|
||||
@@ -377,6 +377,7 @@ ruff_macros::register_rules!(
|
||||
rules::pep8_naming::rules::MixedCaseVariableInGlobalScope,
|
||||
rules::pep8_naming::rules::CamelcaseImportedAsAcronym,
|
||||
rules::pep8_naming::rules::ErrorSuffixOnExceptionName,
|
||||
rules::pep8_naming::rules::InvalidModuleName,
|
||||
// isort
|
||||
rules::isort::rules::UnsortedImports,
|
||||
rules::isort::rules::MissingRequiredImport,
|
||||
@@ -552,6 +553,7 @@ ruff_macros::register_rules!(
|
||||
rules::flake8_self::rules::PrivateMemberAccess,
|
||||
// numpy
|
||||
rules::numpy::rules::NumpyDeprecatedTypeAlias,
|
||||
rules::numpy::rules::NumpyLegacyRandom,
|
||||
// ruff
|
||||
rules::ruff::rules::AmbiguousUnicodeCharacterString,
|
||||
rules::ruff::rules::AmbiguousUnicodeCharacterDocstring,
|
||||
@@ -797,7 +799,7 @@ impl Rule {
|
||||
| Rule::TrailingCommaProhibited => &LintSource::Tokens,
|
||||
Rule::IOError => &LintSource::Io,
|
||||
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
|
||||
Rule::ImplicitNamespacePackage => &LintSource::Filesystem,
|
||||
Rule::ImplicitNamespacePackage | Rule::InvalidModuleName => &LintSource::Filesystem,
|
||||
#[cfg(feature = "logical_lines")]
|
||||
Rule::IndentationWithInvalidMultiple
|
||||
| Rule::IndentationWithInvalidMultipleComment
|
||||
|
||||
@@ -203,6 +203,11 @@ fn is_python_path(path: &Path) -> bool {
|
||||
.map_or(false, |ext| ext == "py" || ext == "pyi")
|
||||
}
|
||||
|
||||
/// Return `true` if the `Path` appears to be that of a Python interface definition file (`.pyi`).
|
||||
pub fn is_interface_definition_path(path: &Path) -> bool {
|
||||
path.extension().map_or(false, |ext| ext == "pyi")
|
||||
}
|
||||
|
||||
/// Return `true` if the `Entry` appears to be that of a Python file.
|
||||
pub fn is_python_entry(entry: &DirEntry) -> bool {
|
||||
is_python_path(entry.path())
|
||||
|
||||
@@ -15,6 +15,9 @@ pub(crate) fn get_redirect(code: &str) -> Option<(&'static str, &'static str)> {
|
||||
|
||||
static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
|
||||
HashMap::from_iter([
|
||||
// The following are here because we don't yet have the many-to-one mapping enabled.
|
||||
("SIM111", "SIM110"),
|
||||
// The following are deprecated.
|
||||
("C", "C4"),
|
||||
("C9", "C90"),
|
||||
("T", "T10"),
|
||||
|
||||
@@ -40,6 +40,7 @@ mod tests {
|
||||
#[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"); "B025")]
|
||||
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"); "B026")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"); "B027")]
|
||||
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"); "B027_pyi")]
|
||||
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"); "B904")]
|
||||
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"); "B905")]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
|
||||
@@ -13,7 +13,7 @@ impl Violation for UnreliableCallableCheck {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
" Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use \
|
||||
"Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use \
|
||||
`callable(x)` for consistent results."
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
//! method()
|
||||
//! ```
|
||||
|
||||
use std::iter;
|
||||
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustpython_parser::ast::{Expr, ExprKind, Stmt};
|
||||
@@ -75,7 +73,7 @@ impl Violation for UnusedLoopControlVariable {
|
||||
if matches!(certainty, Certainty::Certain) && rename.is_some() {
|
||||
Some(|UnusedLoopControlVariable { name, rename, .. }| {
|
||||
let rename = rename.as_ref().unwrap();
|
||||
format!("Rename unused `{name}` to `_{rename}`")
|
||||
format!("Rename unused `{name}` to `{rename}`")
|
||||
})
|
||||
} else {
|
||||
None
|
||||
@@ -174,27 +172,19 @@ pub fn unused_loop_control_variable(
|
||||
if matches!(certainty, Certainty::Certain) && checker.patch(diagnostic.kind.rule()) {
|
||||
// Find the `BindingKind::LoopVar` corresponding to the name.
|
||||
let scope = checker.current_scope();
|
||||
if let Some(binding) = iter::once(scope.bindings.get(name))
|
||||
.flatten()
|
||||
.chain(
|
||||
iter::once(scope.rebounds.get(name))
|
||||
.flatten()
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
let binding = scope
|
||||
.bindings
|
||||
.get(name)
|
||||
.into_iter()
|
||||
.chain(scope.rebounds.get(name).into_iter().flatten())
|
||||
.find_map(|index| {
|
||||
let binding = &checker.bindings[*index];
|
||||
if let Some(source) = &binding.source {
|
||||
if source == &RefEquality(stmt) {
|
||||
Some(binding)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
{
|
||||
binding
|
||||
.source
|
||||
.as_ref()
|
||||
.and_then(|source| (source == &RefEquality(stmt)).then_some(binding))
|
||||
});
|
||||
if let Some(binding) = binding {
|
||||
if matches!(binding.kind, BindingKind::LoopVar) {
|
||||
if !binding.used() {
|
||||
diagnostic.amend(Fix::replacement(
|
||||
|
||||
@@ -72,4 +72,14 @@ expression: diagnostics
|
||||
column: 13
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
StripWithMultiCharacters: ~
|
||||
location:
|
||||
row: 24
|
||||
column: 0
|
||||
end_location:
|
||||
row: 24
|
||||
column: 35
|
||||
fix: ~
|
||||
parent: ~
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
[]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Rules from [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/).
|
||||
mod fixes;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -9,9 +10,10 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::assert_yaml_snapshot;
|
||||
use crate::registry::Rule;
|
||||
use crate::settings::Settings;
|
||||
use crate::test::test_path;
|
||||
use crate::{assert_yaml_snapshot, settings};
|
||||
|
||||
#[test_case(Rule::UnnecessaryGeneratorList, Path::new("C400.py"); "C400")]
|
||||
#[test_case(Rule::UnnecessaryGeneratorSet, Path::new("C401.py"); "C401")]
|
||||
@@ -34,7 +36,27 @@ mod tests {
|
||||
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_comprehensions").join(path).as_path(),
|
||||
&settings::Settings::for_rule(rule_code),
|
||||
&Settings::for_rule(rule_code),
|
||||
)?;
|
||||
assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Rule::UnnecessaryCollectionCall, Path::new("C408.py"); "C408")]
|
||||
fn allow_dict_calls_with_keyword_arguments(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!(
|
||||
"{}_{}_allow_dict_calls_with_keyword_arguments",
|
||||
rule_code.noqa_code(),
|
||||
path.to_string_lossy()
|
||||
);
|
||||
let diagnostics = test_path(
|
||||
Path::new("flake8_comprehensions").join(path).as_path(),
|
||||
&Settings {
|
||||
flake8_comprehensions: super::settings::Settings {
|
||||
allow_dict_calls_with_keyword_arguments: true,
|
||||
},
|
||||
..Settings::for_rule(rule_code)
|
||||
},
|
||||
)?;
|
||||
assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::ast::types::Range;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Diagnostic;
|
||||
use crate::rules::flake8_comprehensions::fixes;
|
||||
use crate::rules::flake8_comprehensions::settings::Settings;
|
||||
use crate::violation::AlwaysAutofixableViolation;
|
||||
|
||||
define_violation!(
|
||||
@@ -33,6 +34,7 @@ pub fn unnecessary_collection_call(
|
||||
func: &Expr,
|
||||
args: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
settings: &Settings,
|
||||
) {
|
||||
if !args.is_empty() {
|
||||
return;
|
||||
@@ -41,7 +43,11 @@ pub fn unnecessary_collection_call(
|
||||
return;
|
||||
};
|
||||
match id {
|
||||
"dict" if keywords.is_empty() || keywords.iter().all(|kw| kw.node.arg.is_some()) => {
|
||||
"dict"
|
||||
if keywords.is_empty()
|
||||
|| (!settings.allow_dict_calls_with_keyword_arguments
|
||||
&& keywords.iter().all(|kw| kw.node.arg.is_some())) =>
|
||||
{
|
||||
// `dict()` or `dict(a=1)` (as opposed to `dict(**a)`)
|
||||
}
|
||||
"list" | "tuple" => {
|
||||
|
||||
48
crates/ruff/src/rules/flake8_comprehensions/settings.rs
Normal file
48
crates/ruff/src/rules/flake8_comprehensions/settings.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
//! Settings for the `flake8-comprehensions` plugin.
|
||||
|
||||
use ruff_macros::ConfigurationOptions;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, JsonSchema,
|
||||
)]
|
||||
#[serde(
|
||||
deny_unknown_fields,
|
||||
rename_all = "kebab-case",
|
||||
rename = "Flake8ComprehensionsOptions"
|
||||
)]
|
||||
pub struct Options {
|
||||
#[option(
|
||||
default = "false",
|
||||
value_type = "bool",
|
||||
example = "allow-dict-calls-with-keyword-arguments = true"
|
||||
)]
|
||||
/// Allow `dict` calls that make use of keyword arguments (e.g., `dict(a=1, b=2)`).
|
||||
pub allow_dict_calls_with_keyword_arguments: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Hash)]
|
||||
pub struct Settings {
|
||||
pub allow_dict_calls_with_keyword_arguments: bool,
|
||||
}
|
||||
|
||||
impl From<Options> for Settings {
|
||||
fn from(options: Options) -> Self {
|
||||
Self {
|
||||
allow_dict_calls_with_keyword_arguments: options
|
||||
.allow_dict_calls_with_keyword_arguments
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Settings> for Options {
|
||||
fn from(settings: Settings) -> Self {
|
||||
Self {
|
||||
allow_dict_calls_with_keyword_arguments: Some(
|
||||
settings.allow_dict_calls_with_keyword_arguments,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_comprehensions/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
UnnecessaryCollectionCall:
|
||||
obj_type: tuple
|
||||
location:
|
||||
row: 1
|
||||
column: 4
|
||||
end_location:
|
||||
row: 1
|
||||
column: 11
|
||||
fix:
|
||||
content:
|
||||
- ()
|
||||
location:
|
||||
row: 1
|
||||
column: 4
|
||||
end_location:
|
||||
row: 1
|
||||
column: 11
|
||||
parent: ~
|
||||
- kind:
|
||||
UnnecessaryCollectionCall:
|
||||
obj_type: list
|
||||
location:
|
||||
row: 2
|
||||
column: 4
|
||||
end_location:
|
||||
row: 2
|
||||
column: 10
|
||||
fix:
|
||||
content:
|
||||
- "[]"
|
||||
location:
|
||||
row: 2
|
||||
column: 4
|
||||
end_location:
|
||||
row: 2
|
||||
column: 10
|
||||
parent: ~
|
||||
- kind:
|
||||
UnnecessaryCollectionCall:
|
||||
obj_type: dict
|
||||
location:
|
||||
row: 3
|
||||
column: 5
|
||||
end_location:
|
||||
row: 3
|
||||
column: 11
|
||||
fix:
|
||||
content:
|
||||
- "{}"
|
||||
location:
|
||||
row: 3
|
||||
column: 5
|
||||
end_location:
|
||||
row: 3
|
||||
column: 11
|
||||
parent: ~
|
||||
|
||||
@@ -9,6 +9,27 @@ use crate::rules::flake8_implicit_str_concat::settings::Settings;
|
||||
use crate::violation::Violation;
|
||||
|
||||
define_violation!(
|
||||
/// ## What it does
|
||||
/// Checks for implicitly concatenated strings on a single line.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// While it is valid Python syntax to concatenate multiple string or byte
|
||||
/// literals implicitly (via whitespace delimiters), it is unnecessary and
|
||||
/// negatively affects code readability.
|
||||
///
|
||||
/// In some cases, the implicit concatenation may also be unintentional, as
|
||||
/// autoformatters are capable of introducing single-line implicit
|
||||
/// concatenations when collapsing long lines.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// z = "The quick " "brown fox."
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// z = "The quick brown fox."
|
||||
/// ```
|
||||
pub struct SingleLineImplicitStringConcatenation;
|
||||
);
|
||||
impl Violation for SingleLineImplicitStringConcatenation {
|
||||
@@ -19,6 +40,39 @@ impl Violation for SingleLineImplicitStringConcatenation {
|
||||
}
|
||||
|
||||
define_violation!(
|
||||
/// ## What it does
|
||||
/// Checks for implicitly concatenated strings that span multiple lines.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// For string literals that wrap across multiple lines, PEP 8 recommends
|
||||
/// the use of implicit string concatenation within parentheses instead of
|
||||
/// using a backslash for line continuation, as the former is more readable
|
||||
/// than the latter.
|
||||
///
|
||||
/// By default, this rule will only trigger if the string literal is
|
||||
/// concatenated via a backslash. To disallow implicit string concatenation
|
||||
/// altogether, set the `flake8-implicit-str-concat.allow-multiline` option
|
||||
/// to `false`.
|
||||
///
|
||||
/// ## Options
|
||||
/// * `flake8-implicit-str-concat.allow-multiline`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// z = "The quick brown fox jumps over the lazy "\
|
||||
/// "dog."
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// "dog."
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// * [PEP 8](https://peps.python.org/pep-0008/#maximum-line-length)
|
||||
pub struct MultiLineImplicitStringConcatenation;
|
||||
);
|
||||
impl Violation for MultiLineImplicitStringConcatenation {
|
||||
@@ -29,6 +83,30 @@ impl Violation for MultiLineImplicitStringConcatenation {
|
||||
}
|
||||
|
||||
define_violation!(
|
||||
/// ## What it does
|
||||
/// Checks for string literals that are explicitly concatenated (using the
|
||||
/// `+` operator).
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// For string literals that wrap across multiple lines, implicit string
|
||||
/// concatenation within parentheses is preferred over explicit
|
||||
/// concatenation using the `+` operator, as the former is more readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// + "dog"
|
||||
/// )
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// z = (
|
||||
/// "The quick brown fox jumps over the lazy "
|
||||
/// "dog"
|
||||
/// )
|
||||
/// ```
|
||||
pub struct ExplicitStringConcatenation;
|
||||
);
|
||||
impl Violation for ExplicitStringConcatenation {
|
||||
|
||||
@@ -1,27 +1,69 @@
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
use rustpython_parser::ast::{
|
||||
Boolop, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, Stmt, StmtKind, Unaryop,
|
||||
};
|
||||
|
||||
use super::helpers::is_falsy_constant;
|
||||
use super::unittest_assert::UnittestAssert;
|
||||
use crate::ast::helpers::unparse_stmt;
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
|
||||
use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt};
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::visitor;
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::fix::Fix;
|
||||
use crate::registry::Diagnostic;
|
||||
use crate::violation::{AlwaysAutofixableViolation, Violation};
|
||||
use crate::source_code::Stylist;
|
||||
use crate::violation::{AutofixKind, Availability, Violation};
|
||||
|
||||
use super::helpers::is_falsy_constant;
|
||||
use super::unittest_assert::UnittestAssert;
|
||||
|
||||
define_violation!(
|
||||
pub struct CompositeAssertion;
|
||||
/// ## What it does
|
||||
/// Checks for assertions that combine multiple independent conditions.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// Composite assertion statements are harder debug upon failure, as the
|
||||
/// failure message will not indicate which condition failed.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// def test_foo():
|
||||
/// assert something and something_else
|
||||
///
|
||||
/// def test_bar():
|
||||
/// assert not (something or something_else)
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// def test_foo():
|
||||
/// assert something
|
||||
/// assert something_else
|
||||
///
|
||||
/// def test_bar():
|
||||
/// assert not something
|
||||
/// assert not something_else
|
||||
/// ```
|
||||
pub struct CompositeAssertion {
|
||||
pub fixable: bool,
|
||||
}
|
||||
);
|
||||
impl Violation for CompositeAssertion {
|
||||
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
format!("Assertion should be broken down into multiple parts")
|
||||
}
|
||||
|
||||
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
|
||||
let CompositeAssertion { fixable } = self;
|
||||
if *fixable {
|
||||
Some(|_| format!("Break down assertion into multiple parts"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
define_violation!(
|
||||
@@ -34,7 +76,7 @@ impl Violation for AssertInExcept {
|
||||
fn message(&self) -> String {
|
||||
let AssertInExcept { name } = self;
|
||||
format!(
|
||||
"Found assertion on exception `{name}` in except block, use `pytest.raises()` instead"
|
||||
"Found assertion on exception `{name}` in `except` block, use `pytest.raises()` instead"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -52,18 +94,23 @@ impl Violation for AssertAlwaysFalse {
|
||||
define_violation!(
|
||||
pub struct UnittestAssertion {
|
||||
pub assertion: String,
|
||||
pub fixable: bool,
|
||||
}
|
||||
);
|
||||
impl AlwaysAutofixableViolation for UnittestAssertion {
|
||||
impl Violation for UnittestAssertion {
|
||||
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
|
||||
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let UnittestAssertion { assertion } = self;
|
||||
let UnittestAssertion { assertion, .. } = self;
|
||||
format!("Use a regular `assert` instead of unittest-style `{assertion}`")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let UnittestAssertion { assertion } = self;
|
||||
format!("Replace `{assertion}(...)` with `assert ...`")
|
||||
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
|
||||
self.fixable
|
||||
.then_some(|UnittestAssertion { assertion, .. }| {
|
||||
format!("Replace `{assertion}(...)` with `assert ...`")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,22 +166,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if the test expression is a composite condition.
|
||||
/// For example, `a and b` or `not (a or b)`. The latter is equivalent
|
||||
/// to `not a and not b` by De Morgan's laws.
|
||||
const fn is_composite_condition(test: &Expr) -> bool {
|
||||
match &test.node {
|
||||
ExprKind::BoolOp {
|
||||
op: Boolop::And, ..
|
||||
} => true,
|
||||
ExprKind::UnaryOp {
|
||||
op: Unaryop::Not,
|
||||
operand,
|
||||
} => matches!(&operand.node, ExprKind::BoolOp { op: Boolop::Or, .. }),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn check_assert_in_except(name: &str, body: &[Stmt]) -> Vec<Diagnostic> {
|
||||
// Walk body to find assert statements that reference the exception name
|
||||
let mut visitor = ExceptionHandlerVisitor::new(name);
|
||||
@@ -155,13 +186,18 @@ pub fn unittest_assertion(
|
||||
match &func.node {
|
||||
ExprKind::Attribute { attr, .. } => {
|
||||
if let Ok(unittest_assert) = UnittestAssert::try_from(attr.as_str()) {
|
||||
// We're converting an expression to a statement, so avoid applying the fix if
|
||||
// the assertion is part of a larger expression.
|
||||
let fixable = checker.current_expr_parent().is_none()
|
||||
&& matches!(checker.current_stmt().node, StmtKind::Expr { .. });
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
UnittestAssertion {
|
||||
assertion: unittest_assert.to_string(),
|
||||
fixable,
|
||||
},
|
||||
Range::from_located(func),
|
||||
);
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if fixable && checker.patch(diagnostic.kind.rule()) {
|
||||
if let Ok(stmt) = unittest_assert.generate_assert(args, keywords) {
|
||||
diagnostic.amend(Fix::replacement(
|
||||
unparse_stmt(&stmt, checker.stylist),
|
||||
@@ -180,11 +216,11 @@ pub fn unittest_assertion(
|
||||
}
|
||||
|
||||
/// PT015
|
||||
pub fn assert_falsy(assert_stmt: &Stmt, test_expr: &Expr) -> Option<Diagnostic> {
|
||||
if is_falsy_constant(test_expr) {
|
||||
pub fn assert_falsy(stmt: &Stmt, test: &Expr) -> Option<Diagnostic> {
|
||||
if is_falsy_constant(test) {
|
||||
Some(Diagnostic::new(
|
||||
AssertAlwaysFalse,
|
||||
Range::from_located(assert_stmt),
|
||||
Range::from_located(stmt),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
@@ -207,14 +243,130 @@ pub fn assert_in_exception_handler(handlers: &[Excepthandler]) -> Vec<Diagnostic
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// PT018
|
||||
pub fn composite_condition(assert_stmt: &Stmt, test_expr: &Expr) -> Option<Diagnostic> {
|
||||
if is_composite_condition(test_expr) {
|
||||
Some(Diagnostic::new(
|
||||
CompositeAssertion,
|
||||
Range::from_located(assert_stmt),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
enum CompositionKind {
|
||||
// E.g., `a or b or c`.
|
||||
None,
|
||||
// E.g., `a and b` or `not (a or b)`.
|
||||
Simple,
|
||||
// E.g., `not (a and b or c)`.
|
||||
Mixed,
|
||||
}
|
||||
|
||||
/// Check if the test expression is a composite condition, and whether it can
|
||||
/// be split into multiple independent conditions.
|
||||
///
|
||||
/// For example, `a and b` or `not (a or b)`. The latter is equivalent to
|
||||
/// `not a and not b` by De Morgan's laws.
|
||||
fn is_composite_condition(test: &Expr) -> CompositionKind {
|
||||
match &test.node {
|
||||
ExprKind::BoolOp {
|
||||
op: Boolop::And, ..
|
||||
} => {
|
||||
return CompositionKind::Simple;
|
||||
}
|
||||
ExprKind::UnaryOp {
|
||||
op: Unaryop::Not,
|
||||
operand,
|
||||
} => {
|
||||
if let ExprKind::BoolOp {
|
||||
op: Boolop::Or,
|
||||
values,
|
||||
} = &operand.node
|
||||
{
|
||||
// Only split cases without mixed `and` and `or`.
|
||||
return if values.iter().all(|expr| {
|
||||
!matches!(
|
||||
expr.node,
|
||||
ExprKind::BoolOp {
|
||||
op: Boolop::And,
|
||||
..
|
||||
}
|
||||
)
|
||||
}) {
|
||||
CompositionKind::Simple
|
||||
} else {
|
||||
CompositionKind::Mixed
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
CompositionKind::None
|
||||
}
|
||||
|
||||
/// Negate a condition, i.e., `a` => `not a` and `not a` => `a`.
|
||||
fn negate(f: Expr) -> Expr {
|
||||
match f.node {
|
||||
ExprKind::UnaryOp {
|
||||
op: Unaryop::Not,
|
||||
operand,
|
||||
} => *operand,
|
||||
_ => create_expr(ExprKind::UnaryOp {
|
||||
op: Unaryop::Not,
|
||||
operand: Box::new(f),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace composite condition `assert a == "hello" and b == "world"` with two statements
|
||||
/// `assert a == "hello"` and `assert b == "world"`.
|
||||
fn fix_composite_condition(stylist: &Stylist, stmt: &Stmt, test: &Expr) -> Fix {
|
||||
let mut conditions: Vec<Expr> = vec![];
|
||||
match &test.node {
|
||||
ExprKind::BoolOp {
|
||||
op: Boolop::And,
|
||||
values,
|
||||
} => {
|
||||
// Compound, so split.
|
||||
conditions.extend(values.clone());
|
||||
}
|
||||
ExprKind::UnaryOp {
|
||||
op: Unaryop::Not,
|
||||
operand,
|
||||
} => {
|
||||
match &operand.node {
|
||||
ExprKind::BoolOp {
|
||||
op: Boolop::Or,
|
||||
values,
|
||||
} => {
|
||||
// Split via `not (a or b)` equals `not a and not b`.
|
||||
conditions.extend(values.iter().map(|f| negate(f.clone())));
|
||||
}
|
||||
_ => {
|
||||
// Do not split.
|
||||
conditions.push(*operand.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
// For each condition, create an `assert condition` statement.
|
||||
let mut content: Vec<String> = Vec::with_capacity(conditions.len());
|
||||
for condition in conditions {
|
||||
content.push(unparse_stmt(
|
||||
&create_stmt(StmtKind::Assert {
|
||||
test: Box::new(condition.clone()),
|
||||
msg: None,
|
||||
}),
|
||||
stylist,
|
||||
));
|
||||
}
|
||||
|
||||
let content = content.join(stylist.line_ending().as_str());
|
||||
Fix::replacement(content, stmt.location, stmt.end_location.unwrap())
|
||||
}
|
||||
|
||||
/// PT018
|
||||
pub fn composite_condition(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option<&Expr>) {
|
||||
let composite = is_composite_condition(test);
|
||||
if matches!(composite, CompositionKind::Simple | CompositionKind::Mixed) {
|
||||
let fixable = matches!(composite, CompositionKind::Simple) && msg.is_none();
|
||||
let mut diagnostic =
|
||||
Diagnostic::new(CompositeAssertion { fixable }, Range::from_located(stmt));
|
||||
if fixable && checker.patch(diagnostic.kind.rule()) {
|
||||
diagnostic.amend(fix_composite_condition(checker.stylist, stmt, test));
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,70 +158,44 @@ fn compare(left: &Expr, cmpop: Cmpop, right: &Expr) -> Expr {
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Arguments<'a> {
|
||||
positional: Vec<&'a str>,
|
||||
keyword: Vec<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> Arguments<'a> {
|
||||
pub fn new(positional: Vec<&'a str>, keyword: Vec<&'a str>) -> Self {
|
||||
Self {
|
||||
positional,
|
||||
keyword,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains(&self, arg: &str) -> bool {
|
||||
self.positional.contains(&arg) || self.keyword.contains(&arg)
|
||||
}
|
||||
}
|
||||
|
||||
impl UnittestAssert {
|
||||
pub fn arguments(&self) -> Arguments {
|
||||
fn arg_spec(&self) -> &[&str] {
|
||||
match self {
|
||||
UnittestAssert::AlmostEqual => {
|
||||
Arguments::new(vec!["first", "second"], vec!["places", "msg", "delta"])
|
||||
}
|
||||
UnittestAssert::AlmostEquals => {
|
||||
Arguments::new(vec!["first", "second"], vec!["places", "msg", "delta"])
|
||||
}
|
||||
UnittestAssert::CountEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::DictContainsSubset => {
|
||||
Arguments::new(vec!["subset", "dictionary"], vec!["msg"])
|
||||
}
|
||||
UnittestAssert::DictEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::Equal => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::Equals => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::False => Arguments::new(vec!["expr"], vec!["msg"]),
|
||||
UnittestAssert::Greater => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::GreaterEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::In => Arguments::new(vec!["member", "container"], vec!["msg"]),
|
||||
UnittestAssert::Is => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::IsInstance => Arguments::new(vec!["obj", "cls"], vec!["msg"]),
|
||||
UnittestAssert::IsNone => Arguments::new(vec!["expr"], vec!["msg"]),
|
||||
UnittestAssert::IsNot => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::IsNotNone => Arguments::new(vec!["expr"], vec!["msg"]),
|
||||
UnittestAssert::Less => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::LessEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::ListEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::MultiLineEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::NotAlmostEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::NotAlmostEquals => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::NotEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::NotEquals => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::NotIn => Arguments::new(vec!["member", "container"], vec!["msg"]),
|
||||
UnittestAssert::NotIsInstance => Arguments::new(vec!["obj", "cls"], vec!["msg"]),
|
||||
UnittestAssert::NotRegex => Arguments::new(vec!["text", "regex"], vec!["msg"]),
|
||||
UnittestAssert::NotRegexpMatches => Arguments::new(vec!["text", "regex"], vec!["msg"]),
|
||||
UnittestAssert::Regex => Arguments::new(vec!["text", "regex"], vec!["msg"]),
|
||||
UnittestAssert::RegexpMatches => Arguments::new(vec!["text", "regex"], vec!["msg"]),
|
||||
UnittestAssert::SequenceEqual => {
|
||||
Arguments::new(vec!["first", "second"], vec!["msg", "seq_type"])
|
||||
}
|
||||
UnittestAssert::SetEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::True => Arguments::new(vec!["expr"], vec!["msg"]),
|
||||
UnittestAssert::TupleEqual => Arguments::new(vec!["first", "second"], vec!["msg"]),
|
||||
UnittestAssert::Underscore => Arguments::new(vec!["expr"], vec!["msg"]),
|
||||
UnittestAssert::AlmostEqual => &["first", "second", "places", "msg", "delta"],
|
||||
UnittestAssert::AlmostEquals => &["first", "second", "places", "msg", "delta"],
|
||||
UnittestAssert::CountEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::DictContainsSubset => &["subset", "dictionary", "msg"],
|
||||
UnittestAssert::DictEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::Equal => &["first", "second", "msg"],
|
||||
UnittestAssert::Equals => &["first", "second", "msg"],
|
||||
UnittestAssert::False => &["expr", "msg"],
|
||||
UnittestAssert::Greater => &["first", "second", "msg"],
|
||||
UnittestAssert::GreaterEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::In => &["member", "container", "msg"],
|
||||
UnittestAssert::Is => &["first", "second", "msg"],
|
||||
UnittestAssert::IsInstance => &["obj", "cls", "msg"],
|
||||
UnittestAssert::IsNone => &["expr", "msg"],
|
||||
UnittestAssert::IsNot => &["first", "second", "msg"],
|
||||
UnittestAssert::IsNotNone => &["expr", "msg"],
|
||||
UnittestAssert::Less => &["first", "second", "msg"],
|
||||
UnittestAssert::LessEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::ListEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::MultiLineEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::NotAlmostEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::NotAlmostEquals => &["first", "second", "msg"],
|
||||
UnittestAssert::NotEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::NotEquals => &["first", "second", "msg"],
|
||||
UnittestAssert::NotIn => &["member", "container", "msg"],
|
||||
UnittestAssert::NotIsInstance => &["obj", "cls", "msg"],
|
||||
UnittestAssert::NotRegex => &["text", "regex", "msg"],
|
||||
UnittestAssert::NotRegexpMatches => &["text", "regex", "msg"],
|
||||
UnittestAssert::Regex => &["text", "regex", "msg"],
|
||||
UnittestAssert::RegexpMatches => &["text", "regex", "msg"],
|
||||
UnittestAssert::SequenceEqual => &["first", "second", "msg", "seq_type"],
|
||||
UnittestAssert::SetEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::True => &["expr", "msg"],
|
||||
UnittestAssert::TupleEqual => &["first", "second", "msg"],
|
||||
UnittestAssert::Underscore => &["expr", "msg"],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,29 +205,56 @@ impl UnittestAssert {
|
||||
args: &'a [Expr],
|
||||
keywords: &'a [Keyword],
|
||||
) -> Result<FxHashMap<&'a str, &'a Expr>> {
|
||||
// If we have variable-length arguments, abort.
|
||||
if args
|
||||
.iter()
|
||||
.any(|arg| matches!(arg.node, ExprKind::Starred { .. }))
|
||||
|| keywords.iter().any(|kw| kw.node.arg.is_none())
|
||||
{
|
||||
bail!("Contains variable-length arguments. Cannot autofix.".to_string());
|
||||
bail!("Variable-length arguments are not supported");
|
||||
}
|
||||
|
||||
let arg_spec = self.arg_spec();
|
||||
|
||||
// If any of the keyword arguments are not in the argument spec, abort.
|
||||
if keywords.iter().any(|kw| {
|
||||
kw.node
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |kwarg_name| !arg_spec.contains(&kwarg_name.as_str()))
|
||||
}) {
|
||||
bail!("Unknown keyword argument");
|
||||
}
|
||||
|
||||
// Generate a map from argument name to value.
|
||||
let mut args_map: FxHashMap<&str, &Expr> = FxHashMap::with_capacity_and_hasher(
|
||||
args.len() + keywords.len(),
|
||||
BuildHasherDefault::default(),
|
||||
);
|
||||
let arguments = self.arguments();
|
||||
for (arg, value) in arguments.positional.iter().zip(args.iter()) {
|
||||
args_map.insert(arg, value);
|
||||
|
||||
// Process positional arguments.
|
||||
for (arg_name, value) in arg_spec.iter().zip(args.iter()) {
|
||||
args_map.insert(arg_name, value);
|
||||
}
|
||||
for kw in keywords {
|
||||
let arg = kw.node.arg.as_ref().unwrap();
|
||||
if !arguments.contains((*arg).as_str()) {
|
||||
bail!("Unexpected keyword argument `{arg}`");
|
||||
|
||||
// Process keyword arguments.
|
||||
for arg_name in arg_spec.iter().skip(args.len()) {
|
||||
if let Some(value) = keywords.iter().find_map(|keyword| {
|
||||
if keyword
|
||||
.node
|
||||
.arg
|
||||
.as_ref()
|
||||
.map_or(false, |kwarg_name| kwarg_name == arg_name)
|
||||
{
|
||||
Some(&keyword.node.value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
args_map.insert(arg_name, value);
|
||||
}
|
||||
args_map.insert(kw.node.arg.as_ref().unwrap().as_str(), &kw.node.value);
|
||||
}
|
||||
|
||||
Ok(args_map)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
source: src/rules/flake8_pytest_style/mod.rs
|
||||
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 11
|
||||
column: 8
|
||||
@@ -24,6 +25,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 12
|
||||
column: 8
|
||||
@@ -43,6 +45,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 13
|
||||
column: 8
|
||||
@@ -51,7 +54,7 @@ expression: diagnostics
|
||||
column: 23
|
||||
fix:
|
||||
content:
|
||||
- assert expr
|
||||
- "assert expr, msg"
|
||||
location:
|
||||
row: 13
|
||||
column: 8
|
||||
@@ -62,6 +65,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 14
|
||||
column: 8
|
||||
@@ -81,6 +85,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 15
|
||||
column: 8
|
||||
@@ -100,6 +105,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 16
|
||||
column: 8
|
||||
@@ -111,6 +117,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 17
|
||||
column: 8
|
||||
@@ -122,6 +129,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 18
|
||||
column: 8
|
||||
@@ -133,6 +141,7 @@ expression: diagnostics
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertTrue
|
||||
fixable: true
|
||||
location:
|
||||
row: 19
|
||||
column: 8
|
||||
@@ -141,365 +150,420 @@ expression: diagnostics
|
||||
column: 23
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIsNotNone
|
||||
fixable: false
|
||||
location:
|
||||
row: 21
|
||||
column: 12
|
||||
end_location:
|
||||
row: 21
|
||||
column: 32
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIsNone
|
||||
fixable: false
|
||||
location:
|
||||
row: 23
|
||||
column: 17
|
||||
end_location:
|
||||
row: 23
|
||||
column: 34
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertEqual
|
||||
fixable: false
|
||||
location:
|
||||
row: 25
|
||||
column: 15
|
||||
end_location:
|
||||
row: 25
|
||||
column: 31
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertFalse
|
||||
fixable: true
|
||||
location:
|
||||
row: 22
|
||||
row: 28
|
||||
column: 8
|
||||
end_location:
|
||||
row: 22
|
||||
row: 28
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- assert not True
|
||||
location:
|
||||
row: 22
|
||||
row: 28
|
||||
column: 8
|
||||
end_location:
|
||||
row: 22
|
||||
row: 28
|
||||
column: 30
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertEqual
|
||||
fixable: true
|
||||
location:
|
||||
row: 25
|
||||
row: 31
|
||||
column: 8
|
||||
end_location:
|
||||
row: 25
|
||||
row: 31
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- assert 1 == 2
|
||||
location:
|
||||
row: 25
|
||||
row: 31
|
||||
column: 8
|
||||
end_location:
|
||||
row: 25
|
||||
row: 31
|
||||
column: 30
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertNotEqual
|
||||
fixable: true
|
||||
location:
|
||||
row: 28
|
||||
row: 34
|
||||
column: 8
|
||||
end_location:
|
||||
row: 28
|
||||
row: 34
|
||||
column: 27
|
||||
fix:
|
||||
content:
|
||||
- assert 1 != 1
|
||||
location:
|
||||
row: 28
|
||||
row: 34
|
||||
column: 8
|
||||
end_location:
|
||||
row: 28
|
||||
row: 34
|
||||
column: 33
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertGreater
|
||||
fixable: true
|
||||
location:
|
||||
row: 31
|
||||
row: 37
|
||||
column: 8
|
||||
end_location:
|
||||
row: 31
|
||||
row: 37
|
||||
column: 26
|
||||
fix:
|
||||
content:
|
||||
- assert 1 > 2
|
||||
location:
|
||||
row: 31
|
||||
row: 37
|
||||
column: 8
|
||||
end_location:
|
||||
row: 31
|
||||
row: 37
|
||||
column: 32
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertGreaterEqual
|
||||
fixable: true
|
||||
location:
|
||||
row: 34
|
||||
row: 40
|
||||
column: 8
|
||||
end_location:
|
||||
row: 34
|
||||
row: 40
|
||||
column: 31
|
||||
fix:
|
||||
content:
|
||||
- assert 1 >= 2
|
||||
location:
|
||||
row: 34
|
||||
row: 40
|
||||
column: 8
|
||||
end_location:
|
||||
row: 34
|
||||
row: 40
|
||||
column: 37
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertLess
|
||||
fixable: true
|
||||
location:
|
||||
row: 37
|
||||
row: 43
|
||||
column: 8
|
||||
end_location:
|
||||
row: 37
|
||||
row: 43
|
||||
column: 23
|
||||
fix:
|
||||
content:
|
||||
- assert 2 < 1
|
||||
location:
|
||||
row: 37
|
||||
row: 43
|
||||
column: 8
|
||||
end_location:
|
||||
row: 37
|
||||
row: 43
|
||||
column: 29
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertLessEqual
|
||||
fixable: true
|
||||
location:
|
||||
row: 40
|
||||
row: 46
|
||||
column: 8
|
||||
end_location:
|
||||
row: 40
|
||||
row: 46
|
||||
column: 28
|
||||
fix:
|
||||
content:
|
||||
- assert 1 <= 2
|
||||
location:
|
||||
row: 40
|
||||
row: 46
|
||||
column: 8
|
||||
end_location:
|
||||
row: 40
|
||||
row: 46
|
||||
column: 34
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIn
|
||||
fixable: true
|
||||
location:
|
||||
row: 43
|
||||
row: 49
|
||||
column: 8
|
||||
end_location:
|
||||
row: 43
|
||||
row: 49
|
||||
column: 21
|
||||
fix:
|
||||
content:
|
||||
- "assert 1 in [2, 3]"
|
||||
location:
|
||||
row: 43
|
||||
row: 49
|
||||
column: 8
|
||||
end_location:
|
||||
row: 43
|
||||
row: 49
|
||||
column: 32
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertNotIn
|
||||
fixable: true
|
||||
location:
|
||||
row: 46
|
||||
row: 52
|
||||
column: 8
|
||||
end_location:
|
||||
row: 46
|
||||
row: 52
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- "assert 2 not in [2, 3]"
|
||||
location:
|
||||
row: 46
|
||||
row: 52
|
||||
column: 8
|
||||
end_location:
|
||||
row: 46
|
||||
row: 52
|
||||
column: 35
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIsNone
|
||||
fixable: true
|
||||
location:
|
||||
row: 49
|
||||
row: 55
|
||||
column: 8
|
||||
end_location:
|
||||
row: 49
|
||||
row: 55
|
||||
column: 25
|
||||
fix:
|
||||
content:
|
||||
- assert 0 is None
|
||||
location:
|
||||
row: 49
|
||||
row: 55
|
||||
column: 8
|
||||
end_location:
|
||||
row: 49
|
||||
row: 55
|
||||
column: 28
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIsNotNone
|
||||
fixable: true
|
||||
location:
|
||||
row: 52
|
||||
row: 58
|
||||
column: 8
|
||||
end_location:
|
||||
row: 52
|
||||
row: 58
|
||||
column: 28
|
||||
fix:
|
||||
content:
|
||||
- assert 0 is not None
|
||||
location:
|
||||
row: 52
|
||||
row: 58
|
||||
column: 8
|
||||
end_location:
|
||||
row: 52
|
||||
row: 58
|
||||
column: 31
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIs
|
||||
fixable: true
|
||||
location:
|
||||
row: 55
|
||||
row: 61
|
||||
column: 8
|
||||
end_location:
|
||||
row: 55
|
||||
row: 61
|
||||
column: 21
|
||||
fix:
|
||||
content:
|
||||
- "assert [] is []"
|
||||
location:
|
||||
row: 55
|
||||
row: 61
|
||||
column: 8
|
||||
end_location:
|
||||
row: 55
|
||||
row: 61
|
||||
column: 29
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIsNot
|
||||
fixable: true
|
||||
location:
|
||||
row: 58
|
||||
row: 64
|
||||
column: 8
|
||||
end_location:
|
||||
row: 58
|
||||
row: 64
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- assert 1 is not 1
|
||||
location:
|
||||
row: 58
|
||||
row: 64
|
||||
column: 8
|
||||
end_location:
|
||||
row: 58
|
||||
row: 64
|
||||
column: 30
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertIsInstance
|
||||
fixable: true
|
||||
location:
|
||||
row: 61
|
||||
row: 67
|
||||
column: 8
|
||||
end_location:
|
||||
row: 61
|
||||
row: 67
|
||||
column: 29
|
||||
fix:
|
||||
content:
|
||||
- "assert isinstance(1, str)"
|
||||
location:
|
||||
row: 61
|
||||
row: 67
|
||||
column: 8
|
||||
end_location:
|
||||
row: 61
|
||||
row: 67
|
||||
column: 37
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertNotIsInstance
|
||||
fixable: true
|
||||
location:
|
||||
row: 64
|
||||
row: 70
|
||||
column: 8
|
||||
end_location:
|
||||
row: 64
|
||||
row: 70
|
||||
column: 32
|
||||
fix:
|
||||
content:
|
||||
- "assert not isinstance(1, int)"
|
||||
location:
|
||||
row: 64
|
||||
row: 70
|
||||
column: 8
|
||||
end_location:
|
||||
row: 64
|
||||
row: 70
|
||||
column: 40
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertRegex
|
||||
fixable: true
|
||||
location:
|
||||
row: 67
|
||||
row: 73
|
||||
column: 8
|
||||
end_location:
|
||||
row: 67
|
||||
row: 73
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- "assert re.search(\"def\", \"abc\")"
|
||||
location:
|
||||
row: 67
|
||||
row: 73
|
||||
column: 8
|
||||
end_location:
|
||||
row: 67
|
||||
row: 73
|
||||
column: 39
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertNotRegex
|
||||
fixable: true
|
||||
location:
|
||||
row: 70
|
||||
row: 76
|
||||
column: 8
|
||||
end_location:
|
||||
row: 70
|
||||
row: 76
|
||||
column: 27
|
||||
fix:
|
||||
content:
|
||||
- "assert not re.search(\"abc\", \"abc\")"
|
||||
location:
|
||||
row: 70
|
||||
row: 76
|
||||
column: 8
|
||||
end_location:
|
||||
row: 70
|
||||
row: 76
|
||||
column: 42
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertRegexpMatches
|
||||
fixable: true
|
||||
location:
|
||||
row: 73
|
||||
row: 79
|
||||
column: 8
|
||||
end_location:
|
||||
row: 73
|
||||
row: 79
|
||||
column: 32
|
||||
fix:
|
||||
content:
|
||||
- "assert re.search(\"def\", \"abc\")"
|
||||
location:
|
||||
row: 73
|
||||
row: 79
|
||||
column: 8
|
||||
end_location:
|
||||
row: 73
|
||||
row: 79
|
||||
column: 47
|
||||
parent: ~
|
||||
- kind:
|
||||
UnittestAssertion:
|
||||
assertion: assertNotRegex
|
||||
fixable: true
|
||||
location:
|
||||
row: 76
|
||||
row: 82
|
||||
column: 8
|
||||
end_location:
|
||||
row: 76
|
||||
row: 82
|
||||
column: 27
|
||||
fix:
|
||||
content:
|
||||
- "assert not re.search(\"abc\", \"abc\")"
|
||||
location:
|
||||
row: 76
|
||||
row: 82
|
||||
column: 8
|
||||
end_location:
|
||||
row: 76
|
||||
row: 82
|
||||
column: 42
|
||||
parent: ~
|
||||
|
||||
|
||||
@@ -1,74 +1,219 @@
|
||||
---
|
||||
source: src/rules/flake8_pytest_style/mod.rs
|
||||
source: crates/ruff/src/rules/flake8_pytest_style/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 12
|
||||
row: 13
|
||||
column: 4
|
||||
end_location:
|
||||
row: 12
|
||||
row: 13
|
||||
column: 39
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- assert something
|
||||
- assert something_else
|
||||
location:
|
||||
row: 13
|
||||
column: 4
|
||||
end_location:
|
||||
row: 13
|
||||
column: 39
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 13
|
||||
row: 14
|
||||
column: 4
|
||||
end_location:
|
||||
row: 13
|
||||
row: 14
|
||||
column: 59
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- assert something
|
||||
- assert something_else
|
||||
- assert something_third
|
||||
location:
|
||||
row: 14
|
||||
column: 4
|
||||
end_location:
|
||||
row: 14
|
||||
column: 59
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 14
|
||||
row: 15
|
||||
column: 4
|
||||
end_location:
|
||||
row: 14
|
||||
row: 15
|
||||
column: 43
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- assert something
|
||||
- assert not something_else
|
||||
location:
|
||||
row: 15
|
||||
column: 4
|
||||
end_location:
|
||||
row: 15
|
||||
column: 43
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 15
|
||||
row: 16
|
||||
column: 4
|
||||
end_location:
|
||||
row: 15
|
||||
row: 16
|
||||
column: 60
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- assert something
|
||||
- assert something_else or something_third
|
||||
location:
|
||||
row: 16
|
||||
column: 4
|
||||
end_location:
|
||||
row: 16
|
||||
column: 60
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 16
|
||||
row: 17
|
||||
column: 4
|
||||
end_location:
|
||||
row: 16
|
||||
row: 17
|
||||
column: 43
|
||||
fix:
|
||||
content:
|
||||
- assert not something
|
||||
- assert something_else
|
||||
location:
|
||||
row: 17
|
||||
column: 4
|
||||
end_location:
|
||||
row: 17
|
||||
column: 43
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 18
|
||||
column: 4
|
||||
end_location:
|
||||
row: 18
|
||||
column: 44
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- assert not something
|
||||
- assert not something_else
|
||||
location:
|
||||
row: 18
|
||||
column: 4
|
||||
end_location:
|
||||
row: 18
|
||||
column: 44
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 17
|
||||
row: 19
|
||||
column: 4
|
||||
end_location:
|
||||
row: 17
|
||||
row: 19
|
||||
column: 63
|
||||
fix:
|
||||
content:
|
||||
- assert not something
|
||||
- assert not something_else
|
||||
- assert not something_third
|
||||
location:
|
||||
row: 19
|
||||
column: 4
|
||||
end_location:
|
||||
row: 19
|
||||
column: 63
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 22
|
||||
column: 4
|
||||
end_location:
|
||||
row: 22
|
||||
column: 34
|
||||
fix:
|
||||
content:
|
||||
- assert not a
|
||||
- assert b or c
|
||||
location:
|
||||
row: 22
|
||||
column: 4
|
||||
end_location:
|
||||
row: 22
|
||||
column: 34
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion:
|
||||
fixable: true
|
||||
location:
|
||||
row: 23
|
||||
column: 4
|
||||
end_location:
|
||||
row: 23
|
||||
column: 35
|
||||
fix:
|
||||
content:
|
||||
- assert not a
|
||||
- assert b and c
|
||||
location:
|
||||
row: 23
|
||||
column: 4
|
||||
end_location:
|
||||
row: 23
|
||||
column: 35
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion:
|
||||
fixable: false
|
||||
location:
|
||||
row: 26
|
||||
column: 4
|
||||
end_location:
|
||||
row: 26
|
||||
column: 56
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion: ~
|
||||
CompositeAssertion:
|
||||
fixable: false
|
||||
location:
|
||||
row: 18
|
||||
row: 27
|
||||
column: 4
|
||||
end_location:
|
||||
row: 18
|
||||
row: 27
|
||||
column: 80
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
CompositeAssertion:
|
||||
fixable: false
|
||||
location:
|
||||
row: 29
|
||||
column: 4
|
||||
end_location:
|
||||
row: 29
|
||||
column: 64
|
||||
fix: ~
|
||||
parent: ~
|
||||
|
||||
@@ -66,7 +66,7 @@ pub struct Options {
|
||||
"#
|
||||
)]
|
||||
/// Whether to avoid using single quotes if a string contains single quotes,
|
||||
/// or vice-versa with double quotes, as per [PEP8](https://peps.python.org/pep-0008/#string-quotes).
|
||||
/// or vice-versa with double quotes, as per [PEP 8](https://peps.python.org/pep-0008/#string-quotes).
|
||||
/// This minimizes the need to escape quotation marks within strings.
|
||||
pub avoid_escape: Option<bool>,
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
use itertools::Itertools;
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||
|
||||
use super::branch::Branch;
|
||||
use super::helpers::result_exists;
|
||||
use super::visitor::{ReturnVisitor, Stack};
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
|
||||
use crate::ast::helpers::elif_else_range;
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::visitor::Visitor;
|
||||
@@ -14,6 +12,10 @@ use crate::fix::Fix;
|
||||
use crate::registry::{Diagnostic, Rule};
|
||||
use crate::violation::{AlwaysAutofixableViolation, Violation};
|
||||
|
||||
use super::branch::Branch;
|
||||
use super::helpers::result_exists;
|
||||
use super::visitor::{ReturnVisitor, Stack};
|
||||
|
||||
define_violation!(
|
||||
pub struct UnnecessaryReturnNone;
|
||||
);
|
||||
@@ -196,29 +198,45 @@ fn is_noreturn_func(checker: &Checker, func: &Expr) -> bool {
|
||||
}
|
||||
|
||||
/// RET503
|
||||
fn implicit_return(checker: &mut Checker, last_stmt: &Stmt) {
|
||||
match &last_stmt.node {
|
||||
fn implicit_return(checker: &mut Checker, stmt: &Stmt) {
|
||||
match &stmt.node {
|
||||
StmtKind::If { body, orelse, .. } => {
|
||||
if body.is_empty() || orelse.is_empty() {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
ImplicitReturn,
|
||||
Range::from_located(last_stmt),
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(last_stmt) = body.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
}
|
||||
if let Some(last_stmt) = orelse.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
} else {
|
||||
let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from_located(stmt));
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indent) = indentation(checker.locator, stmt) {
|
||||
let mut content = String::new();
|
||||
content.push_str(checker.stylist.line_ending().as_str());
|
||||
content.push_str(indent);
|
||||
content.push_str("return None");
|
||||
diagnostic.amend(Fix::insertion(content, stmt.end_location.unwrap()));
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
StmtKind::For { body, orelse, .. } | StmtKind::AsyncFor { body, orelse, .. } => {
|
||||
StmtKind::For { orelse, .. }
|
||||
| StmtKind::AsyncFor { orelse, .. }
|
||||
| StmtKind::While { orelse, .. } => {
|
||||
if let Some(last_stmt) = orelse.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
} else if let Some(last_stmt) = body.last() {
|
||||
implicit_return(checker, last_stmt);
|
||||
} else {
|
||||
let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from_located(stmt));
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indent) = indentation(checker.locator, stmt) {
|
||||
let mut content = String::new();
|
||||
content.push_str(checker.stylist.line_ending().as_str());
|
||||
content.push_str(indent);
|
||||
content.push_str("return None");
|
||||
diagnostic.amend(Fix::insertion(content, stmt.end_location.unwrap()));
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
StmtKind::With { body, .. } | StmtKind::AsyncWith { body, .. } => {
|
||||
@@ -234,10 +252,7 @@ fn implicit_return(checker: &mut Checker, last_stmt: &Stmt) {
|
||||
..
|
||||
}
|
||||
) => {}
|
||||
StmtKind::Return { .. }
|
||||
| StmtKind::While { .. }
|
||||
| StmtKind::Raise { .. }
|
||||
| StmtKind::Try { .. } => {}
|
||||
StmtKind::Return { .. } | StmtKind::Raise { .. } | StmtKind::Try { .. } => {}
|
||||
StmtKind::Expr { value, .. }
|
||||
if matches!(
|
||||
&value.node,
|
||||
@@ -245,17 +260,14 @@ fn implicit_return(checker: &mut Checker, last_stmt: &Stmt) {
|
||||
if is_noreturn_func(checker, func)
|
||||
) => {}
|
||||
_ => {
|
||||
let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from_located(last_stmt));
|
||||
let mut diagnostic = Diagnostic::new(ImplicitReturn, Range::from_located(stmt));
|
||||
if checker.patch(diagnostic.kind.rule()) {
|
||||
if let Some(indent) = indentation(checker.locator, last_stmt) {
|
||||
if let Some(indent) = indentation(checker.locator, stmt) {
|
||||
let mut content = String::new();
|
||||
content.push_str(checker.stylist.line_ending().as_str());
|
||||
content.push_str(indent);
|
||||
content.push_str("return None");
|
||||
content.push_str(checker.stylist.line_ending().as_str());
|
||||
diagnostic.amend(Fix::insertion(
|
||||
content,
|
||||
Location::new(last_stmt.end_location.unwrap().row() + 1, 0),
|
||||
));
|
||||
diagnostic.amend(Fix::insertion(content, stmt.end_location.unwrap()));
|
||||
}
|
||||
}
|
||||
checker.diagnostics.push(diagnostic);
|
||||
|
||||
@@ -10,7 +10,16 @@ expression: diagnostics
|
||||
end_location:
|
||||
row: 19
|
||||
column: 16
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 19
|
||||
column: 16
|
||||
end_location:
|
||||
row: 19
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
@@ -22,14 +31,14 @@ expression: diagnostics
|
||||
column: 15
|
||||
fix:
|
||||
content:
|
||||
- " return None"
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 26
|
||||
column: 0
|
||||
row: 25
|
||||
column: 15
|
||||
end_location:
|
||||
row: 26
|
||||
column: 0
|
||||
row: 25
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
@@ -41,24 +50,33 @@ expression: diagnostics
|
||||
column: 11
|
||||
fix:
|
||||
content:
|
||||
- " return None"
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 35
|
||||
column: 0
|
||||
row: 34
|
||||
column: 11
|
||||
end_location:
|
||||
row: 35
|
||||
column: 0
|
||||
row: 34
|
||||
column: 11
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 40
|
||||
column: 8
|
||||
row: 39
|
||||
column: 4
|
||||
end_location:
|
||||
row: 41
|
||||
column: 20
|
||||
fix: ~
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 41
|
||||
column: 20
|
||||
end_location:
|
||||
row: 41
|
||||
column: 20
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
@@ -70,14 +88,14 @@ expression: diagnostics
|
||||
column: 15
|
||||
fix:
|
||||
content:
|
||||
- " return None"
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 51
|
||||
column: 0
|
||||
row: 50
|
||||
column: 15
|
||||
end_location:
|
||||
row: 51
|
||||
column: 0
|
||||
row: 50
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
@@ -89,14 +107,14 @@ expression: diagnostics
|
||||
column: 22
|
||||
fix:
|
||||
content:
|
||||
- " return None"
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 58
|
||||
column: 0
|
||||
row: 57
|
||||
column: 22
|
||||
end_location:
|
||||
row: 58
|
||||
column: 0
|
||||
row: 57
|
||||
column: 22
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
@@ -108,13 +126,127 @@ expression: diagnostics
|
||||
column: 21
|
||||
fix:
|
||||
content:
|
||||
- " return None"
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 65
|
||||
column: 0
|
||||
row: 64
|
||||
column: 21
|
||||
end_location:
|
||||
row: 65
|
||||
column: 0
|
||||
row: 64
|
||||
column: 21
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 80
|
||||
column: 4
|
||||
end_location:
|
||||
row: 83
|
||||
column: 14
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 83
|
||||
column: 14
|
||||
end_location:
|
||||
row: 83
|
||||
column: 14
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 111
|
||||
column: 4
|
||||
end_location:
|
||||
row: 114
|
||||
column: 16
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 114
|
||||
column: 16
|
||||
end_location:
|
||||
row: 114
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 118
|
||||
column: 4
|
||||
end_location:
|
||||
row: 124
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 124
|
||||
column: 19
|
||||
end_location:
|
||||
row: 124
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 128
|
||||
column: 4
|
||||
end_location:
|
||||
row: 131
|
||||
column: 16
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 131
|
||||
column: 16
|
||||
end_location:
|
||||
row: 131
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 135
|
||||
column: 4
|
||||
end_location:
|
||||
row: 141
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 141
|
||||
column: 19
|
||||
end_location:
|
||||
row: 141
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ImplicitReturn: ~
|
||||
location:
|
||||
row: 260
|
||||
column: 4
|
||||
end_location:
|
||||
row: 261
|
||||
column: 20
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- " return None"
|
||||
location:
|
||||
row: 261
|
||||
column: 20
|
||||
end_location:
|
||||
row: 261
|
||||
column: 20
|
||||
parent: ~
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Rules from [flake8-self](https://pypi.org/project/flake8-self/).
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -22,6 +22,9 @@ define_violation!(
|
||||
/// versions, that it will have the same type, or that it will have the same
|
||||
/// behavior. Instead, use the class's public interface.
|
||||
///
|
||||
/// ## Options
|
||||
/// * `flake8-self.ignore-names`
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// class Class:
|
||||
@@ -44,7 +47,6 @@ define_violation!(
|
||||
///
|
||||
/// ## References
|
||||
/// * [_What is the meaning of single or double underscores before an object name?_](https://stackoverflow.com/questions/1301346/what-is-the-meaning-of-single-and-double-underscore-before-an-object-name)
|
||||
/// ```
|
||||
pub struct PrivateMemberAccess {
|
||||
pub access: String,
|
||||
}
|
||||
@@ -63,6 +65,10 @@ pub fn private_member_access(checker: &mut Checker, expr: &Expr) {
|
||||
if (attr.starts_with("__") && !attr.ends_with("__"))
|
||||
|| (attr.starts_with('_') && !attr.starts_with("__"))
|
||||
{
|
||||
if checker.settings.flake8_self.ignore_names.contains(attr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let ExprKind::Call { func, .. } = &value.node {
|
||||
// Ignore `super()` calls.
|
||||
let call_path = collect_call_path(func);
|
||||
|
||||
60
crates/ruff/src/rules/flake8_self/settings.rs
Normal file
60
crates/ruff/src/rules/flake8_self/settings.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Settings for the `flake8-self` plugin.
|
||||
|
||||
use ruff_macros::ConfigurationOptions;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// By default, ignore the `namedtuple` methods and attributes, which are underscore-prefixed to
|
||||
// prevent conflicts with field names.
|
||||
const IGNORE_NAMES: [&str; 5] = ["_make", "_asdict", "_replace", "_fields", "_field_defaults"];
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
|
||||
)]
|
||||
#[serde(
|
||||
deny_unknown_fields,
|
||||
rename_all = "kebab-case",
|
||||
rename = "Flake8SelfOptions"
|
||||
)]
|
||||
pub struct Options {
|
||||
#[option(
|
||||
default = r#"["_make", "_asdict", "_replace", "_fields", "_field_defaults"]"#,
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
ignore-names = ["_new"]
|
||||
"#
|
||||
)]
|
||||
/// A list of names to ignore when considering `flake8-self` violations.
|
||||
pub ignore_names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Hash)]
|
||||
pub struct Settings {
|
||||
pub ignore_names: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ignore_names: IGNORE_NAMES.map(String::from).to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Options> for Settings {
|
||||
fn from(options: Options) -> Self {
|
||||
Self {
|
||||
ignore_names: options
|
||||
.ignore_names
|
||||
.unwrap_or_else(|| IGNORE_NAMES.map(String::from).to_vec()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Settings> for Options {
|
||||
fn from(settings: Settings) -> Self {
|
||||
Self {
|
||||
ignore_names: Some(settings.ignore_names),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,10 +50,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: _private_thing
|
||||
location:
|
||||
row: 65
|
||||
row: 59
|
||||
column: 6
|
||||
end_location:
|
||||
row: 65
|
||||
row: 59
|
||||
column: 24
|
||||
fix: ~
|
||||
parent: ~
|
||||
@@ -61,10 +61,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: __really_private_thing
|
||||
location:
|
||||
row: 66
|
||||
row: 60
|
||||
column: 6
|
||||
end_location:
|
||||
row: 66
|
||||
row: 60
|
||||
column: 32
|
||||
fix: ~
|
||||
parent: ~
|
||||
@@ -72,10 +72,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: _private_func
|
||||
location:
|
||||
row: 67
|
||||
row: 61
|
||||
column: 6
|
||||
end_location:
|
||||
row: 67
|
||||
row: 61
|
||||
column: 23
|
||||
fix: ~
|
||||
parent: ~
|
||||
@@ -83,10 +83,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: __really_private_func
|
||||
location:
|
||||
row: 68
|
||||
row: 62
|
||||
column: 6
|
||||
end_location:
|
||||
row: 68
|
||||
row: 62
|
||||
column: 31
|
||||
fix: ~
|
||||
parent: ~
|
||||
@@ -94,10 +94,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: _private
|
||||
location:
|
||||
row: 69
|
||||
row: 63
|
||||
column: 6
|
||||
end_location:
|
||||
row: 69
|
||||
row: 63
|
||||
column: 22
|
||||
fix: ~
|
||||
parent: ~
|
||||
@@ -105,10 +105,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: _private_thing
|
||||
location:
|
||||
row: 70
|
||||
row: 64
|
||||
column: 6
|
||||
end_location:
|
||||
row: 70
|
||||
row: 64
|
||||
column: 26
|
||||
fix: ~
|
||||
parent: ~
|
||||
@@ -116,10 +116,10 @@ expression: diagnostics
|
||||
PrivateMemberAccess:
|
||||
access: _private_thing__
|
||||
location:
|
||||
row: 71
|
||||
row: 65
|
||||
column: 6
|
||||
end_location:
|
||||
row: 71
|
||||
row: 65
|
||||
column: 28
|
||||
fix: ~
|
||||
parent: ~
|
||||
|
||||
@@ -19,8 +19,8 @@ mod tests {
|
||||
#[test_case(Rule::ReturnInTryExceptFinally, Path::new("SIM107.py"); "SIM107")]
|
||||
#[test_case(Rule::UseTernaryOperator, Path::new("SIM108.py"); "SIM108")]
|
||||
#[test_case(Rule::CompareWithTuple, Path::new("SIM109.py"); "SIM109")]
|
||||
#[test_case(Rule::ConvertLoopToAny, Path::new("SIM110.py"); "SIM110")]
|
||||
#[test_case(Rule::ConvertLoopToAll, Path::new("SIM111.py"); "SIM111")]
|
||||
#[test_case(Rule::ReimplementedBuiltin, Path::new("SIM110.py"); "SIM110")]
|
||||
#[test_case(Rule::ReimplementedBuiltin, Path::new("SIM111.py"); "SIM111")]
|
||||
#[test_case(Rule::UseCapitalEnvironmentVariables, Path::new("SIM112.py"); "SIM112")]
|
||||
#[test_case(Rule::OpenFileWithContextHandler, Path::new("SIM115.py"); "SIM115")]
|
||||
#[test_case(Rule::MultipleWithStatements, Path::new("SIM117.py"); "SIM117")]
|
||||
|
||||
@@ -13,6 +13,32 @@ use crate::registry::Diagnostic;
|
||||
use crate::violation::AlwaysAutofixableViolation;
|
||||
|
||||
define_violation!(
|
||||
/// ## What it does
|
||||
/// Checks for multiple `isinstance` calls on the same target.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// To check if an object is an instance of any one of multiple types
|
||||
/// or classes, it is unnecessary to use multiple `isinstance` calls, as
|
||||
/// the second argument of the `isinstance` built-in function accepts a
|
||||
/// tuple of types and classes.
|
||||
///
|
||||
/// Using a single `isinstance` call implements the same behavior with more
|
||||
/// concise code and clearer intent.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// if isinstance(obj, int) or isinstance(obj, float):
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// if isinstance(obj, (int, float)):
|
||||
/// pass
|
||||
///
|
||||
/// ## References
|
||||
/// * [Python: "isinstance"](https://docs.python.org/3/library/functions.html#isinstance)
|
||||
/// ```
|
||||
pub struct DuplicateIsinstanceCall {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
@@ -12,6 +12,33 @@ use crate::violation::{AutofixKind, Availability, Violation};
|
||||
use super::fix_with;
|
||||
|
||||
define_violation!(
|
||||
/// ## What it does
|
||||
/// Checks for the unnecessary nesting of multiple consecutive context
|
||||
/// managers.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// In Python 3, a single `with` block can include multiple context
|
||||
/// managers.
|
||||
///
|
||||
/// Combining multiple context managers into a single `with` statement
|
||||
/// will minimize the indentation depth of the code, making it more
|
||||
/// readable.
|
||||
///
|
||||
/// ## Example
|
||||
/// ```python
|
||||
/// with A() as a:
|
||||
/// with B() as b:
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// with A() as a, B() as b:
|
||||
/// pass
|
||||
/// ```
|
||||
///
|
||||
/// ## References
|
||||
/// * [Python: "The with statement"](https://docs.python.org/3/reference/compound_stmts.html#the-with-statement)
|
||||
pub struct MultipleWithStatements {
|
||||
pub fixable: bool,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ pub use ast_bool_op::{
|
||||
AAndNotA, AOrNotA, AndFalse, CompareWithTuple, DuplicateIsinstanceCall, OrTrue,
|
||||
};
|
||||
pub use ast_expr::{use_capital_environment_variables, UseCapitalEnvironmentVariables};
|
||||
pub use ast_for::{convert_for_loop_to_any_all, ConvertLoopToAll, ConvertLoopToAny};
|
||||
pub use ast_if::{
|
||||
if_with_same_arms, nested_if_statements, return_bool_condition_directly,
|
||||
use_dict_get_with_default, use_ternary_operator, CollapsibleIf, DictGetWithDefault,
|
||||
@@ -22,13 +21,13 @@ pub use key_in_dict::{key_in_dict_compare, key_in_dict_for, KeyInDict};
|
||||
pub use open_file_with_context_handler::{
|
||||
open_file_with_context_handler, OpenFileWithContextHandler,
|
||||
};
|
||||
pub use reimplemented_builtin::{convert_for_loop_to_any_all, ReimplementedBuiltin};
|
||||
pub use return_in_try_except_finally::{return_in_try_except_finally, ReturnInTryExceptFinally};
|
||||
pub use use_contextlib_suppress::{use_contextlib_suppress, UseContextlibSuppress};
|
||||
pub use yoda_conditions::{yoda_conditions, YodaConditions};
|
||||
|
||||
mod ast_bool_op;
|
||||
mod ast_expr;
|
||||
mod ast_for;
|
||||
mod ast_if;
|
||||
mod ast_ifexp;
|
||||
mod ast_unary_op;
|
||||
@@ -37,6 +36,7 @@ mod fix_if;
|
||||
mod fix_with;
|
||||
mod key_in_dict;
|
||||
mod open_file_with_context_handler;
|
||||
mod reimplemented_builtin;
|
||||
mod return_in_try_except_finally;
|
||||
mod use_contextlib_suppress;
|
||||
mod yoda_conditions;
|
||||
|
||||
@@ -12,38 +12,20 @@ use crate::source_code::Stylist;
|
||||
use crate::violation::AlwaysAutofixableViolation;
|
||||
|
||||
define_violation!(
|
||||
pub struct ConvertLoopToAny {
|
||||
pub any: String,
|
||||
pub struct ReimplementedBuiltin {
|
||||
pub repl: String,
|
||||
}
|
||||
);
|
||||
impl AlwaysAutofixableViolation for ConvertLoopToAny {
|
||||
impl AlwaysAutofixableViolation for ReimplementedBuiltin {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let ConvertLoopToAny { any } = self;
|
||||
format!("Use `{any}` instead of `for` loop")
|
||||
let ReimplementedBuiltin { repl } = self;
|
||||
format!("Use `{repl}` instead of `for` loop")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let ConvertLoopToAny { any } = self;
|
||||
format!("Replace with `{any}`")
|
||||
}
|
||||
}
|
||||
|
||||
define_violation!(
|
||||
pub struct ConvertLoopToAll {
|
||||
pub all: String,
|
||||
}
|
||||
);
|
||||
impl AlwaysAutofixableViolation for ConvertLoopToAll {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let ConvertLoopToAll { all } = self;
|
||||
format!("Use `{all}` instead of `for` loop")
|
||||
}
|
||||
|
||||
fn autofix_title(&self) -> String {
|
||||
let ConvertLoopToAll { all } = self;
|
||||
format!("Replace with `{all}`")
|
||||
let ReimplementedBuiltin { repl } = self;
|
||||
format!("Replace with `{repl}`")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,7 +201,7 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling:
|
||||
.or_else(|| sibling.and_then(|sibling| return_values_for_siblings(stmt, sibling)))
|
||||
{
|
||||
if loop_info.return_value && !loop_info.next_return_value {
|
||||
if checker.settings.rules.enabled(&Rule::ConvertLoopToAny) {
|
||||
if checker.settings.rules.enabled(&Rule::ReimplementedBuiltin) {
|
||||
let contents = return_stmt(
|
||||
"any",
|
||||
loop_info.test,
|
||||
@@ -234,8 +216,8 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling:
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ConvertLoopToAny {
|
||||
any: contents.clone(),
|
||||
ReimplementedBuiltin {
|
||||
repl: contents.clone(),
|
||||
},
|
||||
Range::from_located(stmt),
|
||||
);
|
||||
@@ -251,7 +233,7 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling:
|
||||
}
|
||||
|
||||
if !loop_info.return_value && loop_info.next_return_value {
|
||||
if checker.settings.rules.enabled(&Rule::ConvertLoopToAll) {
|
||||
if checker.settings.rules.enabled(&Rule::ReimplementedBuiltin) {
|
||||
// Invert the condition.
|
||||
let test = {
|
||||
if let ExprKind::UnaryOp {
|
||||
@@ -311,8 +293,8 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling:
|
||||
}
|
||||
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
ConvertLoopToAll {
|
||||
all: contents.clone(),
|
||||
ReimplementedBuiltin {
|
||||
repl: contents.clone(),
|
||||
},
|
||||
Range::from_located(stmt),
|
||||
);
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
source: src/rules/flake8_simplify/mod.rs
|
||||
source: crates/ruff/src/rules/flake8_simplify/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
ConvertLoopToAny:
|
||||
any: return any(check(x) for x in iterable)
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 3
|
||||
column: 4
|
||||
@@ -22,8 +22,46 @@ expression: diagnostics
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAny:
|
||||
any: return any(check(x) for x in iterable)
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 25
|
||||
column: 4
|
||||
end_location:
|
||||
row: 27
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 25
|
||||
column: 4
|
||||
end_location:
|
||||
row: 28
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(x.is_empty() for x in iterable)
|
||||
location:
|
||||
row: 33
|
||||
column: 4
|
||||
end_location:
|
||||
row: 35
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x.is_empty() for x in iterable)
|
||||
location:
|
||||
row: 33
|
||||
column: 4
|
||||
end_location:
|
||||
row: 36
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 55
|
||||
column: 4
|
||||
@@ -41,8 +79,27 @@ expression: diagnostics
|
||||
column: 20
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAny:
|
||||
any: return any(check(x) for x in iterable)
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 64
|
||||
column: 4
|
||||
end_location:
|
||||
row: 68
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 64
|
||||
column: 4
|
||||
end_location:
|
||||
row: 68
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 73
|
||||
column: 4
|
||||
@@ -60,8 +117,27 @@ expression: diagnostics
|
||||
column: 20
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAny:
|
||||
any: return any(check(x) for x in iterable)
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 83
|
||||
column: 4
|
||||
end_location:
|
||||
row: 87
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 83
|
||||
column: 4
|
||||
end_location:
|
||||
row: 87
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 124
|
||||
column: 4
|
||||
@@ -71,8 +147,19 @@ expression: diagnostics
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAny:
|
||||
any: return any(check(x) for x in iterable)
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 134
|
||||
column: 4
|
||||
end_location:
|
||||
row: 136
|
||||
column: 24
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 144
|
||||
column: 4
|
||||
@@ -89,4 +176,23 @@ expression: diagnostics
|
||||
row: 147
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 154
|
||||
column: 4
|
||||
end_location:
|
||||
row: 156
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 154
|
||||
column: 4
|
||||
end_location:
|
||||
row: 157
|
||||
column: 15
|
||||
parent: ~
|
||||
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_simplify/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 3
|
||||
column: 4
|
||||
end_location:
|
||||
row: 5
|
||||
column: 23
|
||||
fix:
|
||||
content:
|
||||
- return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 3
|
||||
column: 4
|
||||
end_location:
|
||||
row: 6
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 25
|
||||
column: 4
|
||||
end_location:
|
||||
row: 27
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 25
|
||||
column: 4
|
||||
end_location:
|
||||
row: 28
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(x.is_empty() for x in iterable)
|
||||
location:
|
||||
row: 33
|
||||
column: 4
|
||||
end_location:
|
||||
row: 35
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x.is_empty() for x in iterable)
|
||||
location:
|
||||
row: 33
|
||||
column: 4
|
||||
end_location:
|
||||
row: 36
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 55
|
||||
column: 4
|
||||
end_location:
|
||||
row: 59
|
||||
column: 20
|
||||
fix:
|
||||
content:
|
||||
- return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 55
|
||||
column: 4
|
||||
end_location:
|
||||
row: 59
|
||||
column: 20
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 64
|
||||
column: 4
|
||||
end_location:
|
||||
row: 68
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 64
|
||||
column: 4
|
||||
end_location:
|
||||
row: 68
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 73
|
||||
column: 4
|
||||
end_location:
|
||||
row: 77
|
||||
column: 20
|
||||
fix:
|
||||
content:
|
||||
- return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 73
|
||||
column: 4
|
||||
end_location:
|
||||
row: 77
|
||||
column: 20
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 83
|
||||
column: 4
|
||||
end_location:
|
||||
row: 87
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 83
|
||||
column: 4
|
||||
end_location:
|
||||
row: 87
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 124
|
||||
column: 4
|
||||
end_location:
|
||||
row: 126
|
||||
column: 23
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 134
|
||||
column: 4
|
||||
end_location:
|
||||
row: 136
|
||||
column: 24
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 144
|
||||
column: 4
|
||||
end_location:
|
||||
row: 146
|
||||
column: 23
|
||||
fix:
|
||||
content:
|
||||
- return any(check(x) for x in iterable)
|
||||
location:
|
||||
row: 144
|
||||
column: 4
|
||||
end_location:
|
||||
row: 147
|
||||
column: 16
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 154
|
||||
column: 4
|
||||
end_location:
|
||||
row: 156
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 154
|
||||
column: 4
|
||||
end_location:
|
||||
row: 157
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(x in y for x in iterable)
|
||||
location:
|
||||
row: 162
|
||||
column: 4
|
||||
end_location:
|
||||
row: 164
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x in y for x in iterable)
|
||||
location:
|
||||
row: 162
|
||||
column: 4
|
||||
end_location:
|
||||
row: 165
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ReimplementedBuiltin:
|
||||
repl: return all(x <= y for x in iterable)
|
||||
location:
|
||||
row: 170
|
||||
column: 4
|
||||
end_location:
|
||||
row: 172
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x <= y for x in iterable)
|
||||
location:
|
||||
row: 170
|
||||
column: 4
|
||||
end_location:
|
||||
row: 173
|
||||
column: 15
|
||||
parent: ~
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/flake8_simplify/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 25
|
||||
column: 4
|
||||
end_location:
|
||||
row: 27
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 25
|
||||
column: 4
|
||||
end_location:
|
||||
row: 28
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(x.is_empty() for x in iterable)
|
||||
location:
|
||||
row: 33
|
||||
column: 4
|
||||
end_location:
|
||||
row: 35
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x.is_empty() for x in iterable)
|
||||
location:
|
||||
row: 33
|
||||
column: 4
|
||||
end_location:
|
||||
row: 36
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 64
|
||||
column: 4
|
||||
end_location:
|
||||
row: 68
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 64
|
||||
column: 4
|
||||
end_location:
|
||||
row: 68
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 83
|
||||
column: 4
|
||||
end_location:
|
||||
row: 87
|
||||
column: 19
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 83
|
||||
column: 4
|
||||
end_location:
|
||||
row: 87
|
||||
column: 19
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 134
|
||||
column: 4
|
||||
end_location:
|
||||
row: 136
|
||||
column: 24
|
||||
fix: ~
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 154
|
||||
column: 4
|
||||
end_location:
|
||||
row: 156
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(not check(x) for x in iterable)
|
||||
location:
|
||||
row: 154
|
||||
column: 4
|
||||
end_location:
|
||||
row: 157
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(x in y for x in iterable)
|
||||
location:
|
||||
row: 162
|
||||
column: 4
|
||||
end_location:
|
||||
row: 164
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x in y for x in iterable)
|
||||
location:
|
||||
row: 162
|
||||
column: 4
|
||||
end_location:
|
||||
row: 165
|
||||
column: 15
|
||||
parent: ~
|
||||
- kind:
|
||||
ConvertLoopToAll:
|
||||
all: return all(x <= y for x in iterable)
|
||||
location:
|
||||
row: 170
|
||||
column: 4
|
||||
end_location:
|
||||
row: 172
|
||||
column: 24
|
||||
fix:
|
||||
content:
|
||||
- return all(x <= y for x in iterable)
|
||||
location:
|
||||
row: 170
|
||||
column: 4
|
||||
end_location:
|
||||
row: 173
|
||||
column: 15
|
||||
parent: ~
|
||||
|
||||
@@ -3,7 +3,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
use ruff_python::string::is_lower_with_underscore;
|
||||
use ruff_python::identifiers::is_module_name;
|
||||
|
||||
use crate::ast::helpers::{create_stmt, from_relative_import, unparse_stmt};
|
||||
use crate::ast::types::Range;
|
||||
@@ -92,34 +92,51 @@ fn fix_banned_relative_import(
|
||||
module_path: Option<&Vec<String>>,
|
||||
stylist: &Stylist,
|
||||
) -> Option<Fix> {
|
||||
// Only fix is the module path is known
|
||||
// Only fix is the module path is known.
|
||||
if let Some(mut parts) = module_path.cloned() {
|
||||
// Remove relative level from module path
|
||||
// Remove relative level from module path.
|
||||
for _ in 0..*level? {
|
||||
parts.pop();
|
||||
}
|
||||
|
||||
let call_path = from_relative_import(&parts, module.unwrap());
|
||||
let module_name = call_path.as_slice();
|
||||
|
||||
// Require import to be a valid PEP 8 module:
|
||||
// https://python.org/dev/peps/pep-0008/#package-and-module-names
|
||||
if module_name.iter().any(|f| !is_lower_with_underscore(f)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let content = match &stmt.node {
|
||||
StmtKind::ImportFrom { names, .. } => unparse_stmt(
|
||||
&create_stmt(StmtKind::ImportFrom {
|
||||
module: Some(module_name.join(".")),
|
||||
names: names.clone(),
|
||||
level: Some(0),
|
||||
}),
|
||||
stylist,
|
||||
),
|
||||
_ => return None,
|
||||
let module_name = if let Some(module) = module {
|
||||
let call_path = from_relative_import(&parts, module);
|
||||
// Require import to be a valid PEP 8 module:
|
||||
// https://python.org/dev/peps/pep-0008/#package-and-module-names
|
||||
if !call_path.iter().all(|part| is_module_name(part)) {
|
||||
return None;
|
||||
}
|
||||
call_path.as_slice().join(".")
|
||||
} else if parts.len() > 1 {
|
||||
let module = parts.last().unwrap();
|
||||
let call_path = from_relative_import(&parts, module);
|
||||
// Require import to be a valid PEP 8 module:
|
||||
// https://python.org/dev/peps/pep-0008/#package-and-module-names
|
||||
if !call_path.iter().all(|part| is_module_name(part)) {
|
||||
return None;
|
||||
}
|
||||
call_path.as_slice().join(".")
|
||||
} else {
|
||||
// Require import to be a valid PEP 8 module:
|
||||
// https://python.org/dev/peps/pep-0008/#package-and-module-names
|
||||
if !parts.iter().all(|part| is_module_name(part)) {
|
||||
return None;
|
||||
}
|
||||
parts.join(".")
|
||||
};
|
||||
|
||||
let StmtKind::ImportFrom { names, .. } = &stmt.node else {
|
||||
unreachable!("Expected StmtKind::ImportFrom");
|
||||
};
|
||||
let content = unparse_stmt(
|
||||
&create_stmt(StmtKind::ImportFrom {
|
||||
module: Some(module_name),
|
||||
names: names.clone(),
|
||||
level: Some(0),
|
||||
}),
|
||||
stylist,
|
||||
);
|
||||
|
||||
Some(Fix::replacement(
|
||||
content,
|
||||
stmt.location,
|
||||
|
||||
@@ -78,4 +78,23 @@ expression: diagnostics
|
||||
row: 6
|
||||
column: 28
|
||||
parent: ~
|
||||
- kind:
|
||||
RelativeImports:
|
||||
strictness: parents
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
end_location:
|
||||
row: 7
|
||||
column: 21
|
||||
fix:
|
||||
content:
|
||||
- from my_package.sublib.sublib import server
|
||||
location:
|
||||
row: 7
|
||||
column: 0
|
||||
end_location:
|
||||
row: 7
|
||||
column: 21
|
||||
parent: ~
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ pub fn find_splice_location(body: &[Stmt], locator: &Locator) -> Location {
|
||||
|
||||
// Find the first token that isn't a comment or whitespace.
|
||||
let contents = locator.skip(splice);
|
||||
for (.., tok, end) in lexer::make_tokenizer(contents).flatten() {
|
||||
for (.., tok, end) in lexer::make_tokenizer_located(contents, splice).flatten() {
|
||||
if matches!(tok, Tok::Comment(..) | Tok::Newline) {
|
||||
splice = end;
|
||||
} else {
|
||||
|
||||
@@ -120,6 +120,7 @@ pub fn format_imports(
|
||||
force_single_line: bool,
|
||||
force_sort_within_sections: bool,
|
||||
force_wrap_aliases: bool,
|
||||
force_to_top: &BTreeSet<String>,
|
||||
known_first_party: &BTreeSet<String>,
|
||||
known_third_party: &BTreeSet<String>,
|
||||
known_local_folder: &BTreeSet<String>,
|
||||
@@ -155,6 +156,7 @@ pub fn format_imports(
|
||||
force_single_line,
|
||||
force_sort_within_sections,
|
||||
force_wrap_aliases,
|
||||
force_to_top,
|
||||
known_first_party,
|
||||
known_third_party,
|
||||
known_local_folder,
|
||||
@@ -214,6 +216,7 @@ fn format_import_block(
|
||||
force_single_line: bool,
|
||||
force_sort_within_sections: bool,
|
||||
force_wrap_aliases: bool,
|
||||
force_to_top: &BTreeSet<String>,
|
||||
known_first_party: &BTreeSet<String>,
|
||||
known_third_party: &BTreeSet<String>,
|
||||
known_local_folder: &BTreeSet<String>,
|
||||
@@ -252,6 +255,7 @@ fn format_import_block(
|
||||
classes,
|
||||
constants,
|
||||
variables,
|
||||
force_to_top,
|
||||
);
|
||||
|
||||
if force_single_line {
|
||||
@@ -267,7 +271,7 @@ fn format_import_block(
|
||||
.collect::<Vec<EitherImport>>();
|
||||
if force_sort_within_sections {
|
||||
imports.sort_by(|import1, import2| {
|
||||
cmp_either_import(import1, import2, relative_imports_order)
|
||||
cmp_either_import(import1, import2, relative_imports_order, force_to_top)
|
||||
});
|
||||
};
|
||||
imports
|
||||
@@ -347,6 +351,7 @@ mod tests {
|
||||
#[test_case(Path::new("fit_line_length.py"))]
|
||||
#[test_case(Path::new("fit_line_length_comment.py"))]
|
||||
#[test_case(Path::new("force_sort_within_sections.py"))]
|
||||
#[test_case(Path::new("force_to_top.py"))]
|
||||
#[test_case(Path::new("force_wrap_aliases.py"))]
|
||||
#[test_case(Path::new("import_from_after_import.py"))]
|
||||
#[test_case(Path::new("inline_comments.py"))]
|
||||
@@ -387,12 +392,6 @@ mod tests {
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&Settings {
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
isort: super::settings::Settings {
|
||||
known_local_folder: vec!["ruff".to_string()]
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
..Settings::for_rule(Rule::UnsortedImports)
|
||||
},
|
||||
)?;
|
||||
@@ -418,6 +417,48 @@ mod tests {
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
#[test_case(Path::new("separate_local_folder_imports.py"))]
|
||||
fn known_local_folder(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("known_local_folder_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&Settings {
|
||||
isort: super::settings::Settings {
|
||||
known_local_folder: BTreeSet::from(["ruff".to_string()]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
..Settings::for_rule(Rule::UnsortedImports)
|
||||
},
|
||||
)?;
|
||||
assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("force_to_top.py"))]
|
||||
fn force_to_top(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("force_to_top_{}", path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("isort").join(path).as_path(),
|
||||
&Settings {
|
||||
isort: super::settings::Settings {
|
||||
force_to_top: BTreeSet::from([
|
||||
"z".to_string(),
|
||||
"lib1".to_string(),
|
||||
"lib3".to_string(),
|
||||
"lib5".to_string(),
|
||||
"lib3.lib4".to_string(),
|
||||
]),
|
||||
..super::settings::Settings::default()
|
||||
},
|
||||
src: vec![test_resource_path("fixtures/isort")],
|
||||
..Settings::for_rule(Rule::UnsortedImports)
|
||||
},
|
||||
)?;
|
||||
assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_case(Path::new("combine_as_imports.py"))]
|
||||
fn combine_as_imports(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("combine_as_imports_{}", path.to_string_lossy());
|
||||
@@ -620,6 +661,7 @@ mod tests {
|
||||
|
||||
#[test_case(Path::new("docstring.py"))]
|
||||
#[test_case(Path::new("docstring_only.py"))]
|
||||
#[test_case(Path::new("multiline_docstring.py"))]
|
||||
#[test_case(Path::new("empty.py"))]
|
||||
fn required_import(path: &Path) -> Result<()> {
|
||||
let snapshot = format!("required_import_{}", path.to_string_lossy());
|
||||
|
||||
@@ -15,6 +15,7 @@ pub fn order_imports<'a>(
|
||||
classes: &'a BTreeSet<String>,
|
||||
constants: &'a BTreeSet<String>,
|
||||
variables: &'a BTreeSet<String>,
|
||||
force_to_top: &'a BTreeSet<String>,
|
||||
) -> OrderedImportBlock<'a> {
|
||||
let mut ordered = OrderedImportBlock::default();
|
||||
|
||||
@@ -23,7 +24,7 @@ pub fn order_imports<'a>(
|
||||
block
|
||||
.import
|
||||
.into_iter()
|
||||
.sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2)),
|
||||
.sorted_by(|(alias1, _), (alias2, _)| cmp_modules(alias1, alias2, force_to_top)),
|
||||
);
|
||||
|
||||
// Sort `StmtKind::ImportFrom`.
|
||||
@@ -101,6 +102,7 @@ pub fn order_imports<'a>(
|
||||
classes,
|
||||
constants,
|
||||
variables,
|
||||
force_to_top,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<(AliasData, CommentSet)>>(),
|
||||
@@ -108,21 +110,26 @@ pub fn order_imports<'a>(
|
||||
})
|
||||
.sorted_by(
|
||||
|(import_from1, _, _, aliases1), (import_from2, _, _, aliases2)| {
|
||||
cmp_import_from(import_from1, import_from2, relative_imports_order).then_with(
|
||||
|| match (aliases1.first(), aliases2.first()) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(Some((alias1, _)), Some((alias2, _))) => cmp_members(
|
||||
alias1,
|
||||
alias2,
|
||||
order_by_type,
|
||||
classes,
|
||||
constants,
|
||||
variables,
|
||||
),
|
||||
},
|
||||
cmp_import_from(
|
||||
import_from1,
|
||||
import_from2,
|
||||
relative_imports_order,
|
||||
force_to_top,
|
||||
)
|
||||
.then_with(|| match (aliases1.first(), aliases2.first()) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(Some((alias1, _)), Some((alias2, _))) => cmp_members(
|
||||
alias1,
|
||||
alias2,
|
||||
order_by_type,
|
||||
classes,
|
||||
constants,
|
||||
variables,
|
||||
force_to_top,
|
||||
),
|
||||
})
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -125,6 +125,7 @@ pub fn organize_imports(
|
||||
settings.isort.force_single_line,
|
||||
settings.isort.force_sort_within_sections,
|
||||
settings.isort.force_wrap_aliases,
|
||||
&settings.isort.force_to_top,
|
||||
&settings.isort.known_first_party,
|
||||
&settings.isort.known_third_party,
|
||||
&settings.isort.known_local_folder,
|
||||
|
||||
@@ -118,6 +118,15 @@ pub struct Options {
|
||||
/// imports (like `from itertools import groupby`). Instead, sort the
|
||||
/// imports by module, independent of import style.
|
||||
pub force_sort_within_sections: Option<bool>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "list[str]",
|
||||
example = r#"
|
||||
force-to-top = ["src"]
|
||||
"#
|
||||
)]
|
||||
/// Force specific imports to the top of their appropriate section.
|
||||
pub force_to_top: Option<Vec<String>>,
|
||||
#[option(
|
||||
default = r#"[]"#,
|
||||
value_type = "list[str]",
|
||||
@@ -265,6 +274,7 @@ pub struct Settings {
|
||||
pub force_single_line: bool,
|
||||
pub force_sort_within_sections: bool,
|
||||
pub force_wrap_aliases: bool,
|
||||
pub force_to_top: BTreeSet<String>,
|
||||
pub known_first_party: BTreeSet<String>,
|
||||
pub known_third_party: BTreeSet<String>,
|
||||
pub known_local_folder: BTreeSet<String>,
|
||||
@@ -290,6 +300,7 @@ impl Default for Settings {
|
||||
force_single_line: false,
|
||||
force_sort_within_sections: false,
|
||||
force_wrap_aliases: false,
|
||||
force_to_top: BTreeSet::new(),
|
||||
known_first_party: BTreeSet::new(),
|
||||
known_third_party: BTreeSet::new(),
|
||||
known_local_folder: BTreeSet::new(),
|
||||
@@ -319,6 +330,7 @@ impl From<Options> for Settings {
|
||||
force_single_line: options.force_single_line.unwrap_or(false),
|
||||
force_sort_within_sections: options.force_sort_within_sections.unwrap_or(false),
|
||||
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
|
||||
force_to_top: BTreeSet::from_iter(options.force_to_top.unwrap_or_default()),
|
||||
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
|
||||
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
|
||||
known_local_folder: BTreeSet::from_iter(options.known_local_folder.unwrap_or_default()),
|
||||
@@ -348,6 +360,7 @@ impl From<Settings> for Options {
|
||||
force_single_line: Some(settings.force_single_line),
|
||||
force_sort_within_sections: Some(settings.force_sort_within_sections),
|
||||
force_wrap_aliases: Some(settings.force_wrap_aliases),
|
||||
force_to_top: Some(settings.force_to_top.into_iter().collect()),
|
||||
known_first_party: Some(settings.known_first_party.into_iter().collect()),
|
||||
known_third_party: Some(settings.known_third_party.into_iter().collect()),
|
||||
known_local_folder: Some(settings.known_local_folder.into_iter().collect()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
source: src/rules/isort/mod.rs
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
UnsortedImports: ~
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 24
|
||||
column: 0
|
||||
fix:
|
||||
content:
|
||||
- import foo
|
||||
- import lib1
|
||||
- import lib2
|
||||
- import lib3
|
||||
- import lib3.lib4
|
||||
- import lib3.lib4.lib5
|
||||
- import lib4
|
||||
- import lib5
|
||||
- import lib6
|
||||
- import z
|
||||
- from foo import bar
|
||||
- from foo.lib1.bar import baz
|
||||
- from lib1 import foo
|
||||
- from lib1.lib2 import foo
|
||||
- from lib2 import foo
|
||||
- from lib3.lib4 import foo
|
||||
- from lib3.lib4.lib5 import foo
|
||||
- "from lib4 import lib1, lib2"
|
||||
- "from lib5 import lib1, lib2"
|
||||
- ""
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 24
|
||||
column: 0
|
||||
parent: ~
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
UnsortedImports: ~
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 24
|
||||
column: 0
|
||||
fix:
|
||||
content:
|
||||
- import lib1
|
||||
- import lib3
|
||||
- import lib3.lib4
|
||||
- import lib5
|
||||
- import z
|
||||
- import foo
|
||||
- import lib2
|
||||
- import lib3.lib4.lib5
|
||||
- import lib4
|
||||
- import lib6
|
||||
- from lib1 import foo
|
||||
- from lib3.lib4 import foo
|
||||
- "from lib5 import lib1, lib2"
|
||||
- from foo import bar
|
||||
- from foo.lib1.bar import baz
|
||||
- from lib1.lib2 import foo
|
||||
- from lib2 import foo
|
||||
- from lib3.lib4.lib5 import foo
|
||||
- "from lib4 import lib1, lib2"
|
||||
- ""
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 24
|
||||
column: 0
|
||||
parent: ~
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
UnsortedImports: ~
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 0
|
||||
fix:
|
||||
content:
|
||||
- import os
|
||||
- import sys
|
||||
- ""
|
||||
- import leading_prefix
|
||||
- ""
|
||||
- import ruff
|
||||
- from . import leading_prefix
|
||||
- ""
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 6
|
||||
column: 0
|
||||
parent: ~
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
source: crates/ruff/src/rules/isort/mod.rs
|
||||
expression: diagnostics
|
||||
---
|
||||
- kind:
|
||||
MissingRequiredImport: from __future__ import annotations
|
||||
location:
|
||||
row: 1
|
||||
column: 0
|
||||
end_location:
|
||||
row: 1
|
||||
column: 0
|
||||
fix:
|
||||
content:
|
||||
- ""
|
||||
- from __future__ import annotations
|
||||
location:
|
||||
row: 3
|
||||
column: 3
|
||||
end_location:
|
||||
row: 3
|
||||
column: 3
|
||||
parent: ~
|
||||
|
||||
@@ -15,9 +15,10 @@ expression: diagnostics
|
||||
- import os
|
||||
- import sys
|
||||
- ""
|
||||
- import ruff
|
||||
- ""
|
||||
- import leading_prefix
|
||||
- ""
|
||||
- import ruff
|
||||
- from . import leading_prefix
|
||||
- ""
|
||||
location:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use crate::rules::isort::types::Importable;
|
||||
use ruff_python::string;
|
||||
|
||||
use super::settings::RelativeImportsOrder;
|
||||
@@ -43,15 +44,28 @@ fn prefix(
|
||||
}
|
||||
|
||||
/// Compare two top-level modules.
|
||||
pub fn cmp_modules(alias1: &AliasData, alias2: &AliasData) -> Ordering {
|
||||
natord::compare_ignore_case(alias1.name, alias2.name)
|
||||
.then_with(|| natord::compare(alias1.name, alias2.name))
|
||||
.then_with(|| match (alias1.asname, alias2.asname) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(Some(asname1), Some(asname2)) => natord::compare(asname1, asname2),
|
||||
})
|
||||
pub fn cmp_modules(
|
||||
alias1: &AliasData,
|
||||
alias2: &AliasData,
|
||||
force_to_top: &BTreeSet<String>,
|
||||
) -> Ordering {
|
||||
(match (
|
||||
force_to_top.contains(alias1.name),
|
||||
force_to_top.contains(alias2.name),
|
||||
) {
|
||||
(true, true) => Ordering::Equal,
|
||||
(false, false) => Ordering::Equal,
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
})
|
||||
.then_with(|| natord::compare_ignore_case(alias1.name, alias2.name))
|
||||
.then_with(|| natord::compare(alias1.name, alias2.name))
|
||||
.then_with(|| match (alias1.asname, alias2.asname) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(Some(asname1), Some(asname2)) => natord::compare(asname1, asname2),
|
||||
})
|
||||
}
|
||||
|
||||
/// Compare two member imports within `StmtKind::ImportFrom` blocks.
|
||||
@@ -62,6 +76,7 @@ pub fn cmp_members(
|
||||
classes: &BTreeSet<String>,
|
||||
constants: &BTreeSet<String>,
|
||||
variables: &BTreeSet<String>,
|
||||
force_to_top: &BTreeSet<String>,
|
||||
) -> Ordering {
|
||||
match (alias1.name == "*", alias2.name == "*") {
|
||||
(true, false) => Ordering::Less,
|
||||
@@ -70,9 +85,9 @@ pub fn cmp_members(
|
||||
if order_by_type {
|
||||
prefix(alias1.name, classes, constants, variables)
|
||||
.cmp(&prefix(alias2.name, classes, constants, variables))
|
||||
.then_with(|| cmp_modules(alias1, alias2))
|
||||
.then_with(|| cmp_modules(alias1, alias2, force_to_top))
|
||||
} else {
|
||||
cmp_modules(alias1, alias2)
|
||||
cmp_modules(alias1, alias2, force_to_top)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,12 +115,24 @@ pub fn cmp_import_from(
|
||||
import_from1: &ImportFromData,
|
||||
import_from2: &ImportFromData,
|
||||
relative_imports_order: RelativeImportsOrder,
|
||||
force_to_top: &BTreeSet<String>,
|
||||
) -> Ordering {
|
||||
cmp_levels(
|
||||
import_from1.level,
|
||||
import_from2.level,
|
||||
relative_imports_order,
|
||||
)
|
||||
.then_with(|| {
|
||||
match (
|
||||
force_to_top.contains(&import_from1.module_name()),
|
||||
force_to_top.contains(&import_from2.module_name()),
|
||||
) {
|
||||
(true, true) => Ordering::Equal,
|
||||
(false, false) => Ordering::Equal,
|
||||
(true, false) => Ordering::Less,
|
||||
(false, true) => Ordering::Greater,
|
||||
}
|
||||
})
|
||||
.then_with(|| match (&import_from1.module, import_from2.module) {
|
||||
(None, None) => Ordering::Equal,
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
@@ -121,17 +148,21 @@ pub fn cmp_either_import(
|
||||
a: &EitherImport,
|
||||
b: &EitherImport,
|
||||
relative_imports_order: RelativeImportsOrder,
|
||||
force_to_top: &BTreeSet<String>,
|
||||
) -> Ordering {
|
||||
match (a, b) {
|
||||
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2),
|
||||
(Import((alias1, _)), Import((alias2, _))) => cmp_modules(alias1, alias2, force_to_top),
|
||||
(ImportFrom((import_from, ..)), Import((alias, _))) => {
|
||||
natord::compare_ignore_case(import_from.module.unwrap_or_default(), alias.name)
|
||||
}
|
||||
(Import((alias, _)), ImportFrom((import_from, ..))) => {
|
||||
natord::compare_ignore_case(alias.name, import_from.module.unwrap_or_default())
|
||||
}
|
||||
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => {
|
||||
cmp_import_from(import_from1, import_from2, relative_imports_order)
|
||||
}
|
||||
(ImportFrom((import_from1, ..)), ImportFrom((import_from2, ..))) => cmp_import_from(
|
||||
import_from1,
|
||||
import_from2,
|
||||
relative_imports_order,
|
||||
force_to_top,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use rustpython_parser::ast::{
|
||||
use super::helpers;
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::resolver::is_interface_definition_path;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -39,7 +40,7 @@ impl<'a> ImportTracker<'a> {
|
||||
Self {
|
||||
locator,
|
||||
directives,
|
||||
pyi: path.extension().map_or(false, |ext| ext == "pyi"),
|
||||
pyi: is_interface_definition_path(path),
|
||||
blocks: vec![Block::default()],
|
||||
split_index: 0,
|
||||
nested: false,
|
||||
|
||||
@@ -14,6 +14,7 @@ mod tests {
|
||||
use crate::{assert_yaml_snapshot, settings};
|
||||
|
||||
#[test_case(Rule::NumpyDeprecatedTypeAlias, Path::new("NPY001.py"); "NPY001")]
|
||||
#[test_case(Rule::NumpyLegacyRandom, Path::new("NPY002.py"); "NPY002")]
|
||||
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
pub use deprecated_type_alias::{deprecated_type_alias, NumpyDeprecatedTypeAlias};
|
||||
pub use numpy_legacy_random::{numpy_legacy_random, NumpyLegacyRandom};
|
||||
|
||||
mod deprecated_type_alias;
|
||||
mod numpy_legacy_random;
|
||||
|
||||
128
crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs
Normal file
128
crates/ruff/src/rules/numpy/rules/numpy_legacy_random.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
use ruff_macros::{define_violation, derive_message_formats};
|
||||
use rustpython_parser::ast::Expr;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::Diagnostic;
|
||||
use crate::violation::Violation;
|
||||
|
||||
define_violation!(
|
||||
/// ## What it does
|
||||
/// Checks for the use of legacy `np.random` function calls.
|
||||
///
|
||||
/// ## Why is this bad?
|
||||
/// According to the NumPy documentation's [Legacy Random Generation]:
|
||||
///
|
||||
/// > The `RandomState` provides access to legacy generators... This class
|
||||
/// > should only be used if it is essential to have randoms that are
|
||||
/// > identical to what would have been produced by previous versions of
|
||||
/// > NumPy.
|
||||
///
|
||||
/// The members exposed directly on the `random` module are convenience
|
||||
/// functions that alias to methods on a global singleton `RandomState`
|
||||
/// instance. NumPy recommends using a dedicated `Generator` instance
|
||||
/// rather than the random variate generation methods exposed directly on
|
||||
/// the `random` module, as the new `Generator` is both faster and has
|
||||
/// better statistical properties.
|
||||
///
|
||||
/// See the documentation on [Random Sampling] and [NEP 19] for further
|
||||
/// details.
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```python
|
||||
/// import numpy as np
|
||||
///
|
||||
/// np.random.seed(1337)
|
||||
/// np.random.normal()
|
||||
/// ```
|
||||
///
|
||||
/// Use instead:
|
||||
/// ```python
|
||||
/// rng = np.random.default_rng(1337)
|
||||
/// rng.normal()
|
||||
/// ```
|
||||
///
|
||||
/// [Legacy Random Generation]: https://numpy.org/doc/stable/reference/random/legacy.html#legacy
|
||||
/// [Random Sampling]: https://numpy.org/doc/stable/reference/random/index.html#random-quick-start
|
||||
/// [NEP 19]: https://numpy.org/neps/nep-0019-rng-policy.html
|
||||
pub struct NumpyLegacyRandom {
|
||||
pub method_name: String,
|
||||
}
|
||||
);
|
||||
impl Violation for NumpyLegacyRandom {
|
||||
#[derive_message_formats]
|
||||
fn message(&self) -> String {
|
||||
let NumpyLegacyRandom { method_name } = self;
|
||||
format!("Replace legacy `np.random.{method_name}` call with `np.random.Generator`")
|
||||
}
|
||||
}
|
||||
|
||||
/// NPY002
|
||||
pub fn numpy_legacy_random(checker: &mut Checker, expr: &Expr) {
|
||||
if let Some(method_name) = checker.resolve_call_path(expr).and_then(|call_path| {
|
||||
// seeding state
|
||||
if call_path.as_slice() == ["numpy", "random", "seed"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "get_state"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "set_state"]
|
||||
// simple random data
|
||||
|| call_path.as_slice() == ["numpy", "random", "rand"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "randn"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "randint"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "random_integers"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "random_sample"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "choice"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "bytes"]
|
||||
// permutations
|
||||
|| call_path.as_slice() == ["numpy", "random", "shuffle"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "permutation"]
|
||||
// distributions
|
||||
|| call_path.as_slice() == ["numpy", "random", "beta"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "binomial"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "chisquare"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "dirichlet"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "exponential"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "f"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "gamma"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "geometric"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "get_state"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "gumbel"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "hypergeometric"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "laplace"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "logistic"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "lognormal"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "logseries"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "multinomial"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "multivariate_normal"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "negative_binomial"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "noncentral_chisquare"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "noncentral_f"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "normal"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "pareto"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "poisson"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "power"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "rayleigh"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "standard_cauchy"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "standard_exponential"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "standard_gamma"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "standard_normal"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "standard_t"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "triangular"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "uniform"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "vonmises"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "wald"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "weibull"]
|
||||
|| call_path.as_slice() == ["numpy", "random", "zipf"]
|
||||
{
|
||||
Some(call_path[2])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
NumpyLegacyRandom {
|
||||
method_name: method_name.to_string(),
|
||||
},
|
||||
Range::from_located(expr),
|
||||
));
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user