Compare commits

...

71 Commits

Author SHA1 Message Date
Charlie Marsh
4cfa350112 Bump version to 0.0.249 (#3063) 2023-02-20 13:11:29 -05:00
Charlie Marsh
41f163fc8d Avoid assert() to assert statement conversion in expressions (#3062) 2023-02-20 17:49:22 +00:00
Charlie Marsh
d21dd994e6 Increase expected size of FormatElement (#3049) 2023-02-20 12:47:35 -05:00
Josh Karpel
6f5a6b8c8b Do not autofix E731 in class bodies (#3050) 2023-02-20 12:38:42 -05:00
Jeong YunWon
35606d7b05 clean up to fix nightly clippy warnings and dedents (#3057) 2023-02-20 09:33:47 -05:00
Matthew Lloyd
3ad257cfea Add PDM to "Who's Using Ruff?" (#3048) 2023-02-20 03:58:22 +00:00
Charlie Marsh
b39f960cd1 Relax constraints on pep8-naming module validation (#3043) 2023-02-19 17:34:23 -05:00
Charlie Marsh
c297d46899 Remove unused AsFormat trait for Option<T> (#3041)
We should re-add this, but it's currently unused and doesn't compile under 1.66.0.

See: #3039.
2023-02-19 20:19:35 +00:00
Jonathan Plasse
d6a100028c Update docs and pre-commit after #3006 (#3038) 2023-02-19 14:23:01 -05:00
Jonathan Plasse
35d4e03f2a Fix ruff_dev regex workspace dependency (#3037) 2023-02-19 18:02:08 +00:00
Charlie Marsh
41e77bb01d Add some additional users to "Who's Using Ruff?" (#3035) 2023-02-19 16:30:01 +00:00
Charlie Marsh
2ff3dd5fbe Bump version to 0.0.248 (#3034) 2023-02-19 16:21:30 +00:00
Charlie Marsh
0f0e7a521a Avoid false-positives for break in with (#3032) 2023-02-19 11:17:04 -05:00
Jonathan Plasse
b75663be6d Add missing rust-version in crates (#3009) 2023-02-19 15:07:17 +00:00
Tomer Chachamu
4d3d04ee61 [PLE0101] error when __init__ returns a value (#3007) 2023-02-19 14:54:43 +00:00
Manuel Jacob
87422ba362 Add configuration option for C408 to allow dict calls with keyword arguments. (#2977)
When creating a dict with string keys, some prefer to call dict instead of writing a dict literal.
For example: `dict(a=1, b=2, c=3)` instead of `{"a": 1, "b": 2, "c": 3}`.
2023-02-19 14:47:03 +00:00
Jeremy Goh
c1d2976fff [docs] Add docs for flake8-implicit-str-concat rules (#3028) 2023-02-19 14:38:59 +00:00
Jeremy Goh
13281cd9ca [docs] Add some docs for flake8-simplify (#3027) 2023-02-19 14:26:56 +00:00
Jonathan Plasse
e53652779d Avoid raising B027 violations in .pyi files (#3016) 2023-02-19 14:21:33 +00:00
Jonathan Plasse
db4c611c6f Fix broken links and markdown style (#3017) 2023-02-19 08:46:49 -05:00
Charlie Marsh
c25be31eb1 Fix documentation-link detection in generate_mkdocs.py (#3030) 2023-02-19 12:20:44 +00:00
Micha Reiser
a7c533634d chore: Remove default_members from Cargo.toml (#3006)
This PR removes the `default_members` from the workspace configuration. 

## Why

I'm not familiar with the motivation for why the `default_members` setting was added initially, and I do not object to keeping it. I'll explain my motivation for removing it below. 

My main reason for removing the `default_members` override is that new contributors may not know that `cargo test`, `cargo build`, and other commands only run on a subset of crates. They may then be surprised that their PRs are failing in CI, but everything works locally. 

My guess why `default_members` was added is to speed up the development workflow. That's fair, but I question the value because `ruff` is the heaviest crate to build.
2023-02-19 07:18:47 -05:00
Simon Brugman
cfa6883431 docs(readme): add Diffusers (#3029) 2023-02-19 07:10:02 -05:00
Nyakku Shigure
216aa929af Remove duplicate underline in B007 autofix message (#3021) 2023-02-18 19:38:20 -05:00
Simon Brugman
9e45424ed6 [pycodestyle] autofix useless semicolons (#3001) 2023-02-17 18:52:42 -05:00
Charlie Marsh
db7f16e276 Support positional messages in assertion rewrites (#3002) 2023-02-17 23:44:13 +00:00
Charlie Marsh
a10a500a26 Ignore namedtuple methods in flake8-self (#2998) 2023-02-17 17:16:25 -05:00
Charlie Marsh
b9fef7cef7 Unlink flake8-bugbear in summary (#2997) 2023-02-17 15:58:33 -05:00
Charlie Marsh
34294ccc00 Deduplicate user list (#2996) 2023-02-17 20:07:42 +00:00
Simon Brugman
a934d01bdb [flake8-tidy-imports] extend autofix of relative imports (#2990)
This extends the autofix for TID252 to work with for relative imports without `module` (i.e. `from .. import`). Tested with `matplotlib` and `bokeh`.
(Previously it would panic on unwrap of the module) 

Note that pandas has [replaced](6057d7a93e) `absolufy-imports` with `ruff` now!
2023-02-17 19:35:28 +00:00
Simon Brugman
0dd590f137 Fix for F541 unescape f-string (#2971) 2023-02-17 14:27:01 -05:00
Charlie Marsh
909a5c3253 Avoid zero-indexed column for IOError (#2995) 2023-02-17 14:14:28 -05:00
Charlie Marsh
5c987874c4 Enforce D403 on methods (#2992) 2023-02-17 18:05:48 +00:00
Nyakku Shigure
0cfe4f9c69 Remove a whitespace in B004 message (#2991) 2023-02-17 12:37:08 -05:00
Charlie Marsh
6a369e4a30 Remove via from sentence in README (#2987) 2023-02-17 13:49:09 +00:00
Charlie Marsh
6f97e2c457 Split list of users into top-level and dedicated section (#2986) 2023-02-17 13:36:32 +00:00
Charlie Marsh
bebd412469 Adjust header depth in docs (#2985) 2023-02-17 13:19:55 +00:00
Charlie Marsh
cd1f57b713 Move FAQ into MkDocs (#2984) 2023-02-17 13:15:53 +00:00
Charlie Marsh
a0912deb2b Move editor integrations into MkDocs (#2983) 2023-02-17 13:12:20 +00:00
Charlie Marsh
50ee14a418 Fix references to specific settings in README.md (#2982) 2023-02-17 13:07:37 +00:00
Martin Fischer
f5adbbebc5 Fix table of contents enumeration 2023-02-17 07:55:50 -05:00
Martin Fischer
c88e05dc1b Merge Reference README section into Configuration section 2023-02-17 07:55:50 -05:00
Martin Fischer
d658bfc024 Remove options from README 2023-02-17 07:55:50 -05:00
Martin Fischer
b0d72c47b4 refactor: Move Top-level heading into ruff_dev 2023-02-17 07:55:50 -05:00
Martin Fischer
8195873cdf Remove rule tables from README 2023-02-17 07:55:50 -05:00
Martin Fischer
bf8108469f Remove auto-generated table of contents 2023-02-17 07:55:50 -05:00
Martin Fischer
a2277cfeba refactor: Move fix symbol legend into ruff_dev 2023-02-17 07:55:50 -05:00
Charlie Marsh
180541a924 Unify comment terminology with that of rome_formatter (#2979) 2023-02-17 03:02:25 +00:00
Simon Brugman
34664a0ca0 [numpy] numpy-legacy-random (#2960)
The new `Generator` in NumPy uses bits provided by [PCG64](https://numpy.org/doc/stable/reference/random/bit_generators/pcg64.html#numpy.random.PCG64) which has better statistical properties than the legacy [MT19937](https://numpy.org/doc/stable/reference/random/bit_generators/mt19937.html#numpy.random.MT19937) used in [RandomState](https://numpy.org/doc/stable/reference/random/legacy.html#numpy.random.RandomState). Global random functions can also be problematic with parallel processing.

This rule is probably quite useful for data scientists (perhaps in combination with `nbqa`)

References:
- [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)
- [Using PyTorch + NumPy? You're making a mistake.](https://tanelp.github.io/posts/a-bug-that-plagues-thousands-of-open-source-ml-projects/)
2023-02-17 02:06:30 +00:00
Charlie Marsh
e081455b06 Add support for file-scoped noqa directives (#2978)
# Summary

This allows users to do things like:

```py
# ruff: noqa: F401
```

...to ignore all `F401` directives in a file. It's equivalent to `per-file-ignores`, but allows users to specify the behavior inline.

Note that Flake8 does _not_ support this, so we _don't_ respect `# flake8: noqa: F401`. (Flake8 treats that as equivalent to `# flake8: noqa`, so ignores _all_ errors in the file. I think all of [these usages](https://cs.github.com/?scopeName=All+repos&scope=&q=%22%23+flake8%3A+noqa%3A+%22) are probably mistakes!)

A couple notes on the details:

- If a user has `# ruff: noqa: F401` in the file, but also `# noqa: F401` on a line that would legitimately trigger an `F401` violation, we _do_ mark that as "unused" for `RUF100` purposes. This may be the wrong choice. The `noqa` is legitimately unused, but it's also not "wrong". It's just redundant.
- If a user has `# ruff: noqa: F401`, and runs `--add-noqa`, we _won't_ add `# noqa: F401` to any lines (which seems like the obvious right choice to me).

Closes #1054 (which has some extra pieces that I'll carve out into a separate issue).

Closes #2446.
2023-02-17 01:59:01 +00:00
Artem Mukhin
4f18fa6733 Add test case for '\u' prefix in B005 (#2976)
Based on #2958.
2023-02-16 19:45:29 -05:00
Charlie Marsh
6088a36cd3 Use line_suffix for end-of-line comments (#2975) 2023-02-16 18:37:40 -05:00
Charlie Marsh
66a162fa40 Handle non-from __future__ imports (#2974)
These are uncommon, but currently panic.

Closes #2967.
2023-02-16 22:56:03 +00:00
Mike Taves
e6722f92ed Add Rust Trove classifier (#2973) 2023-02-16 17:38:36 -05:00
Charlie Marsh
750c28868f Enable jemalloc on FreeBSD and NetBSD (#2965) 2023-02-16 15:21:34 -05:00
Charlie Marsh
5157f584ab Improve pow operator spacing (#2970)
Ensure that we add spaces to expressions like `foo.bar() ** 2`.
2023-02-16 15:17:32 -05:00
Charlie Marsh
1c01ec21cb Regenerate expected Black snapshots (#2968) 2023-02-16 19:39:17 +00:00
Manuel Jacob
879512742f Skip .pytype directory by default. (#2966)
Pytype stores .pyi files in .pytype that ruff shouldn’t check or touch.
2023-02-16 14:38:08 -05:00
Florian Best
a919041dda feat(isort): Implement isort.force_to_top (#2877) 2023-02-16 19:01:59 +00:00
Charlie Marsh
059601d968 Avoid trying to fix implicit returns with control flow (#2962) 2023-02-16 13:42:46 -05:00
Charlie Marsh
2ec1701543 Remove link in asyncio.create_task (#2963) 2023-02-16 17:50:56 +00:00
Charlie Marsh
370c3a5daf Remove mdcat dependency (#2959) 2023-02-16 12:09:37 -05:00
Charlie Marsh
fdcb78fd8c Avoid jemallocator on BSD (#2957) 2023-02-16 11:48:51 -05:00
Simon Brugman
2a744d24e5 docs: flake8-self remove unnecessary backticks (#2951) 2023-02-16 08:25:34 -05:00
Simon Brugman
cc30738148 Implement flake8-module-naming (#2855)
- Implement N999 (following flake8-module-naming) in pep8_naming
- Refactor pep8_naming: split rules.rs into file per rule
- Documentation for majority of the violations

Closes https://github.com/charliermarsh/ruff/issues/2734
2023-02-16 04:20:33 +00:00
Edgar R. M
147c6ff1db Exclude crates/ruff_python_formatter/resources from pre-commit check (#2947) 2023-02-15 22:56:42 -05:00
Charlie Marsh
036380e6a8 Fix add-required-import with multi-line offsets (#2946) 2023-02-16 03:24:55 +00:00
Charlie Marsh
b6587e51ee Use an enum to represent composition kind (#2945) 2023-02-15 22:14:00 -05:00
Simon Brugman
1bc37110d4 [flake8-pytest-style] autofix for composite-assertion (PT018) (#2732) 2023-02-16 00:36:07 +00:00
Lunarmagpie
28acdb76cf Add support for ensure_future for RUF006 (#2943) 2023-02-15 23:18:11 +00:00
Martin Fischer
7b09972c97 Merge convert-loop-to-any & convert-loop-to-all to reimplemented-builtin 2023-02-15 16:24:31 -05:00
234 changed files with 8546 additions and 6373 deletions

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

3533
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -4,3 +4,4 @@ extend-exclude = ["snapshots", "black"]
[default.extend-words]
trivias = "trivias"
hel = "hel"
whos = "whos"

View File

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

View File

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

View File

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

View 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]):
...

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,4 +4,5 @@ import attrs
from ..protocol import commands, definitions, responses
from ..server import example
from .. import server
from . import logger, models

View 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

View File

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

View File

@@ -0,0 +1,4 @@
"""a
b"""
# b
import os

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

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

View File

@@ -4,3 +4,5 @@ from __future__ import absolute_import
from collections import namedtuple
from __future__ import print_function
import __future__

View File

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

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

View File

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

View File

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

View File

@@ -0,0 +1,8 @@
# ruff: noqa: F401
import os
import foo
def f():
x = 1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
expression: diagnostics
---
[]

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
---
source: src/rules/isort/mod.rs
source: crates/ruff/src/rules/isort/mod.rs
expression: diagnostics
---
- kind:

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,9 +15,10 @@ expression: diagnostics
- import os
- import sys
- ""
- import ruff
- ""
- import leading_prefix
- ""
- import ruff
- from . import leading_prefix
- ""
location:

View File

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

View File

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

View File

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

View File

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

View 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