Compare commits

...

46 Commits

Author SHA1 Message Date
Charlie Marsh
ea9acda732 Bump version to 0.0.143 2022-11-28 15:42:25 -05:00
Anders Kaseorg
6c8021e970 Fix clippy::manual_let_else (pedantic) (#939) 2022-11-28 09:52:59 -05:00
Charlie Marsh
61b6ad46ea Allow @override methods to be undocumented (#941) 2022-11-28 09:52:12 -05:00
Anders Kaseorg
041d8108e6 Don’t require files with --explain or --generate-shell-completion (#937) 2022-11-28 00:40:20 -05:00
Charlie Marsh
e2c4a098de Bump version to 0.0.142 2022-11-28 00:19:27 -05:00
Charlie Marsh
e865f58426 Add all plugin options to README reference (#936) 2022-11-28 00:19:14 -05:00
messense
23b4e16b1d Add shell completions support (#935) 2022-11-27 23:59:36 -05:00
Charlie Marsh
ae2ac905dc Document all top-level configuration options (#934) 2022-11-27 23:50:24 -05:00
Charlie Marsh
55619b321a Run cargo fmt 2022-11-27 22:58:42 -05:00
Harutaka Kawamura
6f31b002f8 Do not enforce line length limit for comments ending with a URL (#920) 2022-11-27 22:36:17 -05:00
Charlie Marsh
1a79965aa0 Allow varargs and kwargs to be prefixed with stars (#933) 2022-11-27 22:08:27 -05:00
Charlie Marsh
16da183f8e Add some user testimonials (#932) 2022-11-27 21:55:01 -05:00
Charlie Marsh
3f689917cb Use alternative TOML format for per-file-ignores in README (#931) 2022-11-27 21:38:43 -05:00
Charlie Marsh
a4a215e8a3 Add Homebrew installation to README (#930) 2022-11-27 21:37:34 -05:00
Charlie Marsh
aa1c884910 Tweak Flake8 parity in README 2022-11-27 21:34:47 -05:00
Oliver Margetts
7fb55c6d99 F50x implementation (#919) 2022-11-27 21:30:55 -05:00
Charlie Marsh
04ea523ad8 Track aliased import-from members (#929) 2022-11-27 17:27:27 -05:00
Charlie Marsh
9897f81cf3 Bump version to 0.0.141 2022-11-26 16:33:08 -05:00
Charlie Marsh
1a2559b001 Avoid flagging redundant open modes when open is rebound (#918) 2022-11-26 16:24:41 -05:00
Denis Gavrilyuk
721a1e9443 Add flake8-debugger (#909) 2022-11-26 16:21:03 -05:00
Charlie Marsh
f38bba18ee Fix clippy warnings 2022-11-26 15:56:33 -05:00
Charlie Marsh
14cf36f922 Bump version to 0.0.140 2022-11-26 15:05:46 -05:00
Charlie Marsh
d28e026525 Preserve existing noqa codes in --add-noqa (#913) 2022-11-26 14:42:19 -05:00
Jonathan Plasse
d19a8aa54d Auto-generate CheckCodePrefix::fixables() (#916) 2022-11-26 14:10:30 -05:00
Charlie Marsh
f299940452 Respect noqa comments in U009 (#917) 2022-11-26 14:03:18 -05:00
Charlie Marsh
e1ab7163ac Respect f-string locations in B023 check (#914) 2022-11-26 10:31:23 -05:00
Jonathan Plasse
9edc479c6c Fix F821 false positive (#911) 2022-11-26 10:12:07 -05:00
Charlie Marsh
560558b814 Bump version to 0.0.139 2022-11-25 18:38:26 -05:00
Andri Bergsson
bef601b994 Add keyword argument handling for redundant open modes. (#906) 2022-11-25 18:38:05 -05:00
Charlie Marsh
7445d00b88 Implement B023 (function uses loop variable) (#907) 2022-11-25 18:29:54 -05:00
Charlie Marsh
7c78d4e103 Use docstring comment for CheckCode 2022-11-25 13:15:43 -05:00
Oliver Margetts
8b14f1b8cc Implement F522-F525 (#899) 2022-11-25 13:14:31 -05:00
Xuan (Sean) Hu
5a6b51e623 Minor changes in README. (#903) 2022-11-25 09:49:16 -05:00
Charlie Marsh
0b60242fb7 Bump version to 0.0.138 2022-11-25 00:05:41 -05:00
Charlie Marsh
65b77feeb8 Bump LibCST version 2022-11-25 00:05:03 -05:00
Charlie Marsh
04b9c0a31d Fix cargo clippy 2022-11-24 23:40:43 -05:00
Harutaka Kawamura
49dc8231be Fix typo (#902) 2022-11-24 23:38:45 -05:00
Charlie Marsh
92ca114882 Move some main.rs subcommands to a new module (#901) 2022-11-24 22:43:43 -05:00
Charlie Marsh
553bc7443a Remove UserConfiguration struct (#900) 2022-11-24 22:39:07 -05:00
CelebrateVC
a3af6c1ea5 Implement GlobSet optimization for file path exclusions (#883) 2022-11-24 22:31:55 -05:00
Charlie Marsh
b50016fe89 Regenerate README.md 2022-11-24 18:10:07 -05:00
Oliver Margetts
2cf2805848 Implement F521 (#898) 2022-11-24 18:09:36 -05:00
Harutaka Kawamura
33fbef7700 Implement B904 (#892) 2022-11-24 09:49:57 -05:00
Charlie Marsh
68668a584b Bump version to 0.0.137 2022-11-23 20:28:45 -05:00
Charlie Marsh
6cd8655d29 Treat withitem variables as bindings (#897) 2022-11-23 20:28:37 -05:00
Charlie Marsh
72a9bd3cfb Revert "Upload wheels back to GitHub Releases (#884)"
This reverts commit bd08fc359d.
2022-11-23 20:27:33 -05:00
124 changed files with 5459 additions and 980 deletions

View File

@@ -287,14 +287,10 @@ jobs:
with:
name: wheels
- uses: actions/setup-python@v4
- name: Publish to PyPI
- name: Publish to PyPi
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.RUFF_TOKEN }}
run: |
pip install --upgrade twine
twine upload --skip-existing *
- name: Publish to GitHub Releases
uses: softprops/action-gh-release@v1
with:
files: *

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.136
rev: v0.0.143
hooks:
- id: ruff

56
Cargo.lock generated
View File

@@ -290,6 +290,36 @@ dependencies = [
"termcolor",
]
[[package]]
name = "clap_complete"
version = "4.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b3c9eae0de7bf8e3f904a5e40612b21fb2e2e566456d177809a48b892d24da"
dependencies = [
"clap 4.0.22",
]
[[package]]
name = "clap_complete_command"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4160b4a4f72ef58bd766bad27c09e6ef1cc9d82a22f6a0f55d152985a4a48e31"
dependencies = [
"clap 4.0.22",
"clap_complete",
"clap_complete_fig",
]
[[package]]
name = "clap_complete_fig"
version = "4.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46b30e010e669cd021e5004f3be26cff6b7c08d2a8a0d65b48d43a8cc0efd6c3"
dependencies = [
"clap 4.0.22",
"clap_complete",
]
[[package]]
name = "clap_derive"
version = "4.0.21"
@@ -670,7 +700,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.136-dev.0"
version = "0.0.143-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -769,10 +799,17 @@ dependencies = [
]
[[package]]
name = "glob"
version = "0.3.0"
name = "globset"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
dependencies = [
"aho-corasick",
"bstr 0.2.17",
"fnv",
"log",
"regex",
]
[[package]]
name = "half"
@@ -1029,7 +1066,7 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
dependencies = [
"chic",
"itertools",
@@ -1044,7 +1081,7 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
dependencies = [
"quote",
"syn",
@@ -1768,7 +1805,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.136"
version = "0.0.143"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1779,6 +1816,7 @@ dependencies = [
"cachedir",
"chrono",
"clap 4.0.22",
"clap_complete_command",
"clearscreen",
"colored",
"common-path",
@@ -1787,7 +1825,7 @@ dependencies = [
"fern",
"filetime",
"getrandom 0.2.8",
"glob",
"globset",
"insta",
"itertools",
"libcst",
@@ -1818,7 +1856,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.136"
version = "0.0.143"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.136"
version = "0.0.143"
edition = "2021"
rust-version = "1.65.0"
@@ -22,14 +22,15 @@ bitflags = { version = "1.3.2" }
cachedir = { version = "0.3.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
clap = { version = "4.0.1", features = ["derive"] }
clap_complete_command = "0.4.0"
colored = { version = "2.0.0" }
common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = { version = "0.3.0" }
globset = {version = "0.4.9" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
nohash-hasher = { version = "0.2.0" }
notify = { version = "4.0.17" }

50
LICENSE
View File

@@ -242,6 +242,31 @@ are:
SOFTWARE.
"""
- flake8-debugger, licensed as follows:
"""
MIT License
Copyright (c) 2016 Joseph Kahn
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-tidy-imports, licensed as follows:
"""
MIT License
@@ -443,3 +468,28 @@ are:
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""
- RustPython, licensed as follows:
"""
MIT License
Copyright (c) 2020 RustPython Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

729
README.md
View File

@@ -20,10 +20,9 @@ An extremely fast Python linter, written in Rust.
- 🤝 Python 3.10 compatibility
- 🛠️ `pyproject.toml` support
- 📦 Built-in caching, to avoid re-analyzing unchanged files
- 🔧 `--fix` support, for automatic error correction (e.g., automatically remove unused imports)
- 👀 `--watch` support, for continuous file monitoring
- 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
- ⚖️ [Near-parity](#how-does-ruff-compare-to-flake8) with the built-in Flake8 rule set
- 🔌 Native re-implementations of popular Flake8 plugins, like [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/) ([`pydocstyle`](https://pypi.org/project/pydocstyle/))
- 🔌 Native re-implementations of popular Flake8 plugins, like [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
functionality behind a single, common interface. Ruff can be used to replace Flake8 (plus a variety
@@ -32,7 +31,7 @@ of plugins), [`isort`](https://pypi.org/project/isort/), [`pydocstyle`](https://
and [`autoflake`](https://pypi.org/project/autoflake/) all while executing tens or hundreds of times
faster than any individual tool.
Ruff is actively developed and used in major open-source projects like:
Ruff is extremely actively developed and used in major open-source projects like:
- [FastAPI](https://github.com/tiangolo/fastapi)
- [Bokeh](https://github.com/bokeh/bokeh)
@@ -43,6 +42,26 @@ Ruff is actively developed and used in major open-source projects like:
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
## Testimonials
[**Sebastián Ramírez**](https://twitter.com/tiangolo/status/1591912354882764802), creator
of [FastAPI](https://github.com/tiangolo/fastapi):
> Ruff is so fast that sometimes I add an intentional bug in the code just to confirm it's actually
> running and checking the code.
[**Bryan Van de Ven**](https://github.com/bokeh/bokeh/pull/12605), co-creator
of [Bokeh](https://github.com/bokeh/bokeh/), original author
of [Conda](https://docs.conda.io/en/latest/):
> Ruff is ~150-200x faster than flake8 on my machine, scanning the whole repo takes ~0.2s instead of
> ~20s. This is an enormous quality of life improvement for local dev. It's fast enough that I added
> it as an actual commit hook, which is terrific.
[**Tim Abbott**](https://github.com/charliermarsh/ruff/issues/465#issuecomment-1317400028), lead developer of [Zulip](https://github.com/zulip/zulip):
> This is just ridiculously fast... `ruff` is amazing.
## Table of Contents
1. [Installation and Usage](#installation-and-usage)
@@ -56,15 +75,16 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
1. [pep8-naming (N)](#pep8-naming)
1. [flake8-bandit (S)](#flake8-bandit)
1. [flake8-comprehensions (C)](#flake8-comprehensions)
1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap)
1. [flake8-bugbear (B)](#flake8-bugbear)
1. [flake8-builtins (A)](#flake8-builtins)
1. [flake8-debugger (T)](#flake8-debugger)
1. [flake8-tidy-imports (I25)](#flake8-tidy-imports)
1. [flake8-print (T)](#flake8-print)
1. [flake8-quotes (Q)](#flake8-quotes)
1. [flake8-annotations (ANN)](#flake8-annotations)
1. [flake8-2020 (YTT)](#flake8-2020)
1. [flake8-blind-except (BLE)](#flake8-blind-except)
1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap)
1. [mccabe (C90)](#mccabe)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules)
1. [Meta rules (M)](#meta-rules)
@@ -73,6 +93,7 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
1. [Development](#development)
1. [Releases](#releases)
1. [Benchmarks](#benchmarks)
1. [Reference](#reference)
1. [License](#license)
1. [Contributing](#contributing)
@@ -80,12 +101,18 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
### Installation
Available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
```shell
pip install ruff
```
If you're a **macOS Homebrew** or a **Linuxbrew** user, you can also install `ruff` via Homebrew:
```shell
brew install ruff
```
### Usage
To run Ruff, try any of the following:
@@ -107,7 +134,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.136
rev: v0.0.143
hooks:
- id: ruff
```
@@ -117,8 +144,10 @@ _Note: prior to `v0.0.86`, `ruff-pre-commit` used `lint` (rather than `ruff`) as
## Configuration
Ruff is configurable both via `pyproject.toml` and the command line. If left unspecified, the
default configuration is equivalent to:
Ruff is configurable both via `pyproject.toml` and the command line. For a full list of configurable
options, see the [API reference](#reference).
If left unspecified, the default configuration is equivalent to:
```toml
[tool.ruff]
@@ -179,8 +208,10 @@ ignore = ["E501"]
fix = true
unfixable = ["F401"]
# Ignore `E402` (import violations in any `__init__.py` file, and in `path/to/file.py`.
per-file-ignores = {"__init__.py" = ["E402"], "path/to/file.py" = ["E402"]}
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
[tool.ruff.per-file-ignores]
"__init__.py" = ["E402"]
"path/to/file.py" = ["E402"]
```
Plugin configurations should be expressed as subsections, e.g.:
@@ -194,7 +225,9 @@ select = ["E", "F", "Q"]
docstring-quotes = "double"
```
Alternatively, common configuration settings can be provided via the command-line:
For a full list of configurable options, see the [API reference](#reference).
Some common configuration settings can be provided via the command-line:
```shell
ruff path/to/code/ --select F401 --select F403
@@ -271,21 +304,10 @@ Options:
Print version information
```
### Excluding files
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
`foo_*.py` ).
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py`
(to exclude any Python files in `directory`). Note that these paths are relative to the
project root (e.g., the directory containing your `pyproject.toml`).
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
either on the command-line or in your `project.toml` file.
either on the command-line or in your `project.toml` file.
To ignore an error in-line, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
@@ -350,6 +372,20 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | |
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | |
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | |
| F501 | PercentFormatInvalidFormat | '...' % ... has invalid format string: ... | |
| F502 | PercentFormatExpectedMapping | '...' % ... expected mapping but got sequence | |
| F503 | PercentFormatExpectedSequence | '...' % ... expected sequence but got mapping | |
| F504 | PercentFormatExtraNamedArguments | '...' % ... has unused named argument(s): ... | |
| F505 | PercentFormatMissingArgument | '...' % ... is missing argument(s) for placeholder(s): ... | |
| F506 | PercentFormatMixedPositionalAndNamed | '...' % ... has mixed positional and named placeholders | |
| F507 | PercentFormatPositionalCountMismatch | '...' % ... has 4 placeholder(s) but 2 substitution(s) | |
| F508 | PercentFormatStarRequiresSequence | '...' % ... `*` specifier requires sequence | |
| F509 | PercentFormatUnsupportedFormatCharacter | '...' % ... has unsupported format character 'c' | |
| F521 | StringDotFormatInvalidFormat | '...'.format(...) has invalid format string: ... | |
| F522 | StringDotFormatExtraNamedArguments | '...'.format(...) has unused named argument(s): ... | |
| F523 | StringDotFormatExtraPositionalArguments | '...'.format(...) has unused arguments at position(s): ... | |
| F524 | StringDotFormatMissingArguments | '...'.format(...) is missing argument(s) for placeholder(s): ... | |
| F525 | StringDotFormatMixingAutomatic | '...'.format(...) mixes automatic and manual numbering | |
| F541 | FStringMissingPlaceholders | f-string without any placeholders | |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
@@ -533,6 +569,14 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | 🛠 |
| C417 | UnnecessaryMap | Unnecessary `map` usage (rewrite using a `(list\|set\|dict)` comprehension) | |
### flake8-debugger
For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/4.1.2/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| T100 | Debugger | Import for `...` found | |
### flake8-boolean-trap
For more, see [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/0.1.0/) on PyPI.
@@ -570,10 +614,12 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B020 | LoopVariableOverridesIterator | Loop control variable `...` overrides iterable it iterates | |
| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | |
| B022 | UselessContextlibSuppress | No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant | |
| B023 | FunctionUsesLoopVariable | Function definition does not bind loop variable `...` | |
| B024 | AbstractBaseClassWithoutAbstractMethod | `...` is an abstract base class, but it has no abstract methods | |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
| B027 | EmptyMethodWithoutAbstractDecorator | `...` is an empty method in an abstract base class, but has no abstract decorator | |
| B904 | RaiseWithoutFromInsideExcept | Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling | |
### flake8-builtins
@@ -802,28 +848,28 @@ stylistic lint rules that are obviated by autoformatting.
(Coming from Flake8? Try [`flake8-to-ruff`](https://pypi.org/project/flake8-to-ruff/) to
automatically convert your existing configuration.)
Ruff can be used as a (near) drop-in replacement for Flake8 when used (1) without or with a small
number of plugins, (2) alongside Black, and (3) on Python 3 code.
Ruff can be used as a drop-in replacement for Flake8 when used (1) without or with a small number of
plugins, (2) alongside Black, and (3) on Python 3 code.
Under those conditions Ruff is missing 14 rules related to string `.format` calls, 1 rule related
to docstring parsing, and 1 rule related to redefined variables.
Under those conditions, Ruff implements every rule in Flake8, with the exception of `F811`.
Ruff re-implements some of the most popular Flake8 plugins and related code quality tools natively,
including:
Ruff also re-implements some of the most popular Flake8 plugins and related code quality tools
natively, including:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`yesqa`](https://github.com/asottile/yesqa)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-debugger`](https://pypi.org/project/flake8-debugger/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
@@ -832,12 +878,12 @@ including:
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
Beyond rule-set parity, Ruff suffers from the following limitations vis-à-vis Flake8:
Beyond the rule set, Ruff suffers from the following limitations vis-à-vis Flake8:
1. Ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
pattern matching and parenthesized context managers.
2. Flake8 has a plugin architecture and supports writing custom lint rules. (To date, popular Flake8
plugins have been re-implemented within Ruff directly.)
2. Flake8 has a plugin architecture and supports writing custom lint rules. (Instead, popular Flake8
plugins are re-implemented in Rust as part of Ruff itself.)
### Which tools does Ruff replace?
@@ -845,16 +891,17 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-debugger`](https://pypi.org/project/flake8-debugger/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
@@ -1139,6 +1186,614 @@ Summary
389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'
```
## Reference
### Options
#### [`dummy_variable_rgx`](#dummy_variable_rgx)
A regular expression used to identify "dummy" variables, or those which should be ignored when evaluating
(e.g.) unused-variable checks.
**Default value**: `"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"` (matches `_`, `__`, and `_var`, but not `_var_`)
**Type**: `Regex`
**Example usage**:
```toml
[tool.ruff]
# Only ignore variables named "_".
dummy_variable_rgx = "^_$"
```
---
#### [`exclude`](#exclude)
A list of file patterns to exclude from linting.
Exclusions are based on globs, and can be either:
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
`foo_*.py` ).
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py`
(to exclude any Python files in `directory`). Note that these paths are relative to the
project root (e.g., the directory containing your `pyproject.toml`).
Note that you'll typically want to use [`extend_exclude`](#extend_exclude) to modify the excluded
paths.
**Default value**: `[".bzr", ".direnv", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", ".pants.d", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv"]`
**Type**: `Vec<FilePattern>`
**Example usage**:
```toml
[tool.ruff]
exclude = [".venv"]
````
---
#### [`extend_exclude`](#extend_exclude)
A list of file patterns to omit from linting, in addition to those specified by `exclude`.
**Default value**: `[]`
**Type**: `Vec<FilePattern>`
**Example usage**:
```toml
[tool.ruff]
# In addition to the standard set of exclusions, omit all tests, plus a specific file.
extend-exclude = ["tests", "src/bad.py"]
````
---
#### [`ignore`](#ignore)
A list of check code prefixes to ignore. Prefixes can specify exact checks (like `F841`), entire
categories (like `F`), or anything in between.
When breaking ties between enabled and disabled checks (via `select` and `ignore`, respectively),
more specific prefixes override less specific prefixes.
**Default value**: `[]`
**Type**: `Vec<CheckCodePrefix>`
**Example usage**:
```toml
[tool.ruff]
# Skip unused variable checks (`F841`).
ignore = ["F841"]
```
---
#### [`extend_ignore`](#extend_ignore)
A list of check code prefixes to ignore, in addition to those specified by `ignore`.
**Default value**: `[]`
**Type**: `Vec<CheckCodePrefix>`
**Example usage**:
```toml
[tool.ruff]
# Skip unused variable checks (`F841`).
extend-ignore = ["F841"]
```
---
#### [`select`](#select)
A list of check code prefixes to enable. Prefixes can specify exact checks (like `F841`), entire
categories (like `F`), or anything in between.
When breaking ties between enabled and disabled checks (via `select` and `ignore`, respectively),
more specific prefixes override less specific prefixes.
**Default value**: `["E", "F"]`
**Type**: `Vec<CheckCodePrefix>`
**Example usage**:
```toml
[tool.ruff]
# On top of the defaults (`E`, `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
select = ["E", "F", "B", "Q"]
```
---
#### [`extend_select`](#extend_select)
A list of check code prefixes to enable, in addition to those specified by `select`.
**Default value**: `[]`
**Type**: `Vec<CheckCodePrefix>`
**Example usage**:
```toml
[tool.ruff]
# On top of the default `select` (`E`, `F`), enable flake8-bugbear (`B`) and flake8-quotes (`Q`).
extend-select = ["B", "Q"]
```
---
#### [`fix`](#fix)
Enable autofix behavior by-default when running `ruff` (overridden by the `--fix` and `--no-fix`
command-line flags).
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
fix = true
```
---
#### [`fixable`](#fixable)
A list of check code prefixes to consider autofix-able.
**Default value**: `["A", "ANN", "B", "BLE", "C", "D", "E", "F", "FBT", "I", "M", "N", "Q", "RUF", "S", "T", "U", "W", "YTT"]`
**Type**: `Vec<CheckCodePrefix>`
**Example usage**:
```toml
[tool.ruff]
# Only allow autofix behavior for `E` and `F` checks.
fixable = ["E", "F"]
```
---
#### [`unfixable`](#unfixable)
A list of check code prefixes to consider un-autofix-able.
**Default value**: `[]`
**Type**: `Vec<CheckCodePrefix>`
**Example usage**:
```toml
[tool.ruff]
# Disable autofix for unused imports (`F401`).
unfixable = ["F401"]
```
---
#### [`line_length`](#line_length)
The line length to use when enforcing long-lines violations (like E501).
**Default value**: `88`
**Type**: `usize`
**Example usage**:
```toml
[tool.ruff]
# Allow lines to be as long as 120 characters.
line-length = 120
```
---
#### [`per_file_ignores`](#per_file_ignores)
A list of mappings from file pattern to check code prefixes to exclude, when considering any
matching files.
**Default value**: `{}`
**Type**: `HashMap<String, Vec<CheckCodePrefix>>`
**Example usage**:
```toml
[tool.ruff]
# Ignore `E402` (import violations) in all `__init__.py` files, and in `path/to/file.py`.
[tool.ruff.per-file-ignores]
"__init__.py" = ["E402"]
"path/to/file.py" = ["E402"]
```
---
#### [`show_source`](#show_source)
Whether to show source code snippets when reporting lint error violations (overridden by the
`--show-source` command-line flag).
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
# By default, always show source code snippets.
show_source = true
```
---
#### [`src`](#src)
The source code paths to consider, e.g., when resolving first- vs. third-party imports.
**Default value**: `["."]`
**Type**: `Vec<PathBuf>`
**Example usage**:
```toml
[tool.ruff]
# Allow imports relative to the "src" and "test" directories.
src = ["src", "test"]
```
---
#### [`target_version`](#target_version)
The Python version to target, e.g., when considering automatic code upgrades, like rewriting type
annotations. Note that the target version will _not_ be inferred from the _current_ Python version,
and instead must be specified explicitly (as seen below).
**Default value**: `"py310"`
**Type**: `PythonVersion`
**Example usage**:
```toml
[tool.ruff]
# Always generate Python 3.7-compatible code.
target-version = "py37"
```
### `flake8-annotations`
#### [`mypy_init_return`](#mypy_init_return)
Whether to allow the omission of a return type hint for `__init__` if at least one argument is
annotated.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-annotations]
mypy_init_return = true
```
---
#### [`suppress_dummy_args`](#suppress_dummy_args)
Whether to suppress `ANN000`-level errors for arguments matching the "dummy" variable regex (like
`_`).
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-annotations]
suppress_dummy_args = true
```
---
#### [`suppress_none_returning`](#suppress_none_returning)
Whether to suppress `ANN200`-level errors for functions that meet either of the following criteria:
- Contain no `return` statement.
- Explicit `return` statement(s) all return `None` (explicitly or implicitly).
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-annotations]
suppress_none_returning = true
```
---
#### [`allow_star_arg_any`](#allow_star_arg_any)
Whether to suppress `ANN401` for dynamically typed `*args` and `**kwargs` arguments.
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-annotations]
allow_star_arg_any = true
```
### `flake8-bugbear`
#### [`extend_immutable_calls`](#extend_immutable_calls)
Additional callable functions to consider "immutable" when evaluating, e.g., no-mutable-default-argument
checks (`B006`).
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.flake8-bugbear]
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
```
### `flake8-quotes`
#### [`inline_quotes`](#inline_quotes)
Quote style to prefer for inline strings (either "single" (`'`) or "double" (`"`)).
**Default value**: `"double"`
**Type**: `Quote`
**Example usage**:
```toml
[tool.ruff.flake8-quotes]
inline-quotes = "single"
```
---
#### [`multiline_quotes`](#multiline_quotes)
Quote style to prefer for multiline strings (either "single" (`'`) or "double" (`"`)).
**Default value**: `"double"`
**Type**: `Quote`
**Example usage**:
```toml
[tool.ruff.flake8-quotes]
multiline-quotes = "single"
```
---
#### [`docstring_quotes`](#docstring_quotes)
Quote style to prefer for docstrings (either "single" (`'`) or "double" (`"`)).
**Default value**: `"double"`
**Type**: `Quote`
**Example usage**:
```toml
[tool.ruff.flake8-quotes]
docstring-quotes = "single"
```
---
#### [`avoid_escape`](#avoid_escape)
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). This minimizes the
need to escape quotation marks within strings.
**Default value**: `true`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-quotes]
# Don't bother trying to avoid escapes.
avoid-escape = false
```
### `flake8-tidy-imports`
#### [`ban_relative_imports`](#ban_relative_imports)
Whether to ban all relative imports (`"all"`), or only those imports that extend into the parent
module and beyond (`"parents"`).
**Default value**: `"parents"`
**Type**: `Strictness`
**Example usage**:
```toml
[tool.ruff.flake8-tidy-imports]
# Disallow all relative imports.
ban-relative-imports = "all"
```
### `isort`
#### [`known_first_party`](known_first_party)
A list of modules to consider first-party, regardless of whether they can be identified as such
via introspection of the local filesystem.
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.isort]
known-first-party = ["src"]
```
---
#### [`known_third_party`](known_third_party)
A list of modules to consider third-party, regardless of whether they can be identified as such
via introspection of the local filesystem.
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.isort]
known-third-party = ["fastapi"]
```
---
#### [`extra_standard_library`](extra_standard_library)
A list of modules to consider standard-library, in addition to those known to Ruff in advance.
**Default value**: `[]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.isort]
extra-standard-library = ["path"]
```
### `mccabe`
#### [`max_complexity`](#max_complexity)
The maximum McCabe complexity to allow before triggering `C901` errors.
**Default value**: `10`
**Type**: `usize`
**Example usage**:
```toml
[tool.ruff.flake8-tidy-imports]
# Flag errors (`C901`) whenever the complexity level exceeds 5.
max-complexity = 5
```
### `pep8-naming`
#### [`ignore_names`](#ignore_names)
A list of names to ignore when considering `pep8-naming` violations.
**Default value**: `["setUp", "tearDown", "setUpClass", "tearDownClass", "setUpModule", "tearDownModule", "asyncSetUp", "asyncTearDown", "setUpTestData", "failureException", "longMessage", "maxDiff"]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.pep8-naming]
ignore-names = ["callMethod"]
```
---
#### [`classmethod_decorators`](#classmethod_decorators)
A list of decorators that, when applied to a method, indicate that the method should be treated as
a class method. For example, Ruff will expect that any method decorated by a decorator in this list
takes a `cls` argument as its first argument.
**Default value**: `["classmethod"]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.pep8-naming]
# Allow Pydantic's `@validator` decorator to trigger class method treatment.
classmethod-decorators = ["classmethod", "pydantic.validator"]
```
---
#### [`staticmethod_decorators`](#staticmethod_decorators)
A list of decorators that, when applied to a method, indicate that the method should be treated as
a static method. For example, Ruff will expect that any method decorated by a decorator in this list
has no `self` or `cls` argument.
**Default value**: `["staticmethod"]`
**Type**: `Vec<String>`
**Example usage**:
```toml
[tool.ruff.pep8-naming]
# Allow a shorthand alias, `@stcmthd`, to trigger static method treatment.
staticmethod-decorators = ["staticmethod", "stcmthd"]
```
## License
MIT

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.136"
version = "0.0.143"
dependencies = [
"anyhow",
"clap",
@@ -1265,7 +1265,7 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
dependencies = [
"chic",
"itertools",
@@ -1280,7 +1280,7 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
dependencies = [
"quote",
"syn",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.136"
version = "0.0.143"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.136-dev.0"
version = "0.0.143-dev.0"
edition = "2021"
[lib]

View File

@@ -10,6 +10,7 @@ pub enum Plugin {
Flake8Bugbear,
Flake8Builtins,
Flake8Comprehensions,
Flake8Debugger,
Flake8Docstrings,
Flake8TidyImports,
Flake8Print,
@@ -30,6 +31,7 @@ impl FromStr for Plugin {
"flake8-bugbear" => Ok(Plugin::Flake8Bugbear),
"flake8-builtins" => Ok(Plugin::Flake8Builtins),
"flake8-comprehensions" => Ok(Plugin::Flake8Comprehensions),
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"flake8-print" => Ok(Plugin::Flake8Print),
@@ -51,9 +53,10 @@ impl Plugin {
Plugin::Flake8Bugbear => CheckCodePrefix::B,
Plugin::Flake8Builtins => CheckCodePrefix::A,
Plugin::Flake8Comprehensions => CheckCodePrefix::C4,
Plugin::Flake8Debugger => CheckCodePrefix::T1,
Plugin::Flake8Docstrings => CheckCodePrefix::D,
Plugin::Flake8TidyImports => CheckCodePrefix::I25,
Plugin::Flake8Print => CheckCodePrefix::T,
Plugin::Flake8Print => CheckCodePrefix::T2,
Plugin::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Annotations => CheckCodePrefix::ANN,
Plugin::Flake8BlindExcept => CheckCodePrefix::BLE,
@@ -69,6 +72,7 @@ impl Plugin {
Plugin::Flake8Bugbear => vec![CheckCodePrefix::B],
Plugin::Flake8Builtins => vec![CheckCodePrefix::A],
Plugin::Flake8Comprehensions => vec![CheckCodePrefix::C4],
Plugin::Flake8Debugger => vec![CheckCodePrefix::T1],
Plugin::Flake8Docstrings => {
// Use the user-provided docstring.
for key in ["docstring-convention", "docstring_convention"] {
@@ -86,7 +90,7 @@ impl Plugin {
DocstringConvention::PEP8.select()
}
Plugin::Flake8TidyImports => vec![CheckCodePrefix::I25],
Plugin::Flake8Print => vec![CheckCodePrefix::T],
Plugin::Flake8Print => vec![CheckCodePrefix::T2],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Annotations => vec![CheckCodePrefix::ANN],
Plugin::Flake8BlindExcept => vec![CheckCodePrefix::BLE],
@@ -364,6 +368,7 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Bugbear,
Plugin::Flake8Builtins,
Plugin::Flake8Comprehensions,
Plugin::Flake8Debugger,
Plugin::Flake8Docstrings,
Plugin::Flake8TidyImports,
Plugin::Flake8Print,

82
resources/test/fixtures/B023.py vendored Normal file
View File

@@ -0,0 +1,82 @@
"""
Should emit:
B023 - on lines 12, 13, 16, 28, 29, 30, 31, 40, 42, 50, 51, 52, 53, 61, 68.
"""
functions = []
z = 0
for x in range(3):
y = x + 1
# Subject to late-binding problems
functions.append(lambda: x)
functions.append(lambda: y) # not just the loop var
def f_bad_1():
return x
# Actually OK
functions.append(lambda x: x * 2)
functions.append(lambda x=x: x)
functions.append(lambda: z) # OK because not assigned in the loop
def f_ok_1(x):
return x * 2
def check_inside_functions_too():
ls = [lambda: x for x in range(2)]
st = {lambda: x for x in range(2)}
gn = (lambda: x for x in range(2))
dt = {x: lambda: x for x in range(2)}
async def pointless_async_iterable():
yield 1
async def container_for_problems():
async for x in pointless_async_iterable():
functions.append(lambda: x)
[lambda: x async for x in pointless_async_iterable()]
a = 10
b = 0
while True:
a = a_ = a - 1
b += 1
functions.append(lambda: a)
functions.append(lambda: a_)
functions.append(lambda: b)
functions.append(lambda: c) # not a name error because of late binding!
c: bool = a > 3
if not c:
break
# Nested loops should not duplicate reports
for j in range(2):
for k in range(3):
lambda: j * k
for j, k, l in [(1, 2, 3)]:
def f():
j = None # OK because it's an assignment
[l for k in range(2)] # error for l, not for k
assert a and functions
a.attribute = 1 # modifying an attribute doesn't make it a loop variable
functions[0] = lambda: None # same for an element
for var in range(2):
def explicit_capture(captured=var):
return captured
for i in range(3):
lambda: f"{i}"

55
resources/test/fixtures/B904.py vendored Normal file
View File

@@ -0,0 +1,55 @@
"""
Should emit:
B904 - on lines 10, 11 and 16
"""
try:
raise ValueError
except ValueError:
if "abc":
raise TypeError
raise UserWarning
except AssertionError:
raise # Bare `raise` should not be an error
except Exception as err:
assert err
raise Exception("No cause here...")
except BaseException as base_err:
# Might use this instead of bare raise with the `.with_traceback()` method
raise base_err
finally:
raise Exception("Nothing to chain from, so no warning here")
try:
raise ValueError
except ValueError:
# should not emit, since we are not raising something
def proxy():
raise NameError
try:
from preferred_library import Thing
except ImportError:
try:
from fallback_library import Thing
except ImportError:
class Thing:
def __getattr__(self, name):
# same as the case above, should not emit.
raise AttributeError
try:
from preferred_library import Thing
except ImportError:
try:
from fallback_library import Thing
except ImportError:
def context_switch():
try:
raise ValueError
except ValueError:
raise

View File

@@ -3,6 +3,7 @@ from functools import wraps
import os
from .expected import Expectation
from typing import overload
from typing_extensions import override
expectation = Expectation()
@@ -42,9 +43,13 @@ class class_:
"D418: Function/ Method decorated with @overload"
" shouldn't contain a docstring")
@override
def overridden_method(a):
return str(a)
@property
def foo(self):
"""The foo of the thing, which isn't in imperitive mood."""
"""The foo of the thing, which isn't in imperative mood."""
return "hello"
@expect('D102: Missing docstring in public method')

View File

@@ -49,3 +49,6 @@ sit amet consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labor
sit amet consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
""", # noqa: E501
}
# OK
# A very long URL: https://loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong.url.com

13
resources/test/fixtures/F502.py vendored Normal file
View File

@@ -0,0 +1,13 @@
dog = {"bob": "bob"}
"%(bob)s" % dog
"%(bob)s" % {"bob": "bob"}
"%(bob)s" % {**{"bob": "bob"}}
"%(bob)s" % ["bob"] # F202
"%(bob)s" % ("bob",) # F202
"%(bob)s" % {"bob"} # F202
"%(bob)s" % [*["bob"]] # F202
"%(bob)s" % {"bob": "bob" for _ in range(1)}
"%(bob)s" % ["bob" for _ in range(1)] # F202
"%(bob)s" % ("bob" for _ in range(1)) # F202
"%(bob)s" % {"bob" for _ in range(1)} # F202

26
resources/test/fixtures/F503.py vendored Normal file
View File

@@ -0,0 +1,26 @@
dog = {"bob": "bob"}
# Single placeholder always fine
"%s" % dog
"%s" % {"bob": "bob"}
"%s" % {**{"bob": "bob"}}
"%s" % ["bob"]
"%s" % ("bob",)
"%s" % {"bob"}
"%s" % [*["bob"]]
"%s" % {"bob": "bob" for _ in range(1)}
"%s" % ["bob" for _ in range(1)]
"%s" % ("bob" for _ in range(1))
"%s" % {"bob" for _ in range(1)}
# Multiple placeholders
"%s %s" % dog
"%s %s" % {"bob": "bob"} # F503
"%s %s" % {**{"bob": "bob"}} # F503
"%s %s" % ["bob"]
"%s %s" % ("bob",)
"%s %s" % {"bob"}
"%s %s" % [*["bob"]]
"%s %s" % {"bob": "bob" for _ in range(1)} # F503
"%s %s" % ["bob" for _ in range(1)]
"%s %s" % ("bob" for _ in range(1))
"%s %s" % {"bob" for _ in range(1)}

6
resources/test/fixtures/F504.py vendored Normal file
View File

@@ -0,0 +1,6 @@
# Ruff has no way of knowing if the following are F505s
a = "wrong"
"%(a)s %(c)s" % {a: "?", "b": "!"} # F504 ("b" not used)
hidden = {"a": "!"}
"%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat)

25
resources/test/fixtures/F50x.py vendored Normal file
View File

@@ -0,0 +1,25 @@
'%(foo)' % {'foo': 'bar'} # F501
'%s %(foo)s' % {'foo': 'bar'} # F506
'%(foo)s %s' % {'foo': 'bar'} # F506
'%j' % (1,) # F509
'%s %s' % (1,) # F507
'%s %s' % (1, 2, 3) # F507
'%(bar)s' % {} # F505
'%(bar)s' % {'bar': 1, 'baz': 2} # F504
'%(bar)s' % (1, 2, 3) # F502
'%s %s' % {'k': 'v'} # F503
'%(bar)*s' % {'bar': 'baz'} # F506, F508
# ok: single %s with mapping
'%s' % {'foo': 'bar', 'baz': 'womp'}
# ok: %% should not count towards placeholder count
'%% %s %% %s' % (1, 2)
# ok: * consumes one positional argument
'%.*f' % (2, 1.1234)
'%*.*f' % (5, 2, 3.1234)
# ok *args and **kwargs
a = []
'%s %s' % [*a]
'%s %s' % (*a,)
k = {}
'%(k)s' % {**k}

21
resources/test/fixtures/F521.py vendored Normal file
View File

@@ -0,0 +1,21 @@
"{".format(1)
"}".format(1)
"{foo[}".format(foo=1)
# too much string recursion (placeholder-in-placeholder)
"{:{:{}}}".format(1, 2, 3)
# ruff picks these issues up, but flake8 doesn't
"{foo[]}".format(foo={"": 1})
"{foo..}".format(foo=1)
"{foo..bar}".format(foo=1)
# The following are all "good" uses of .format
"{.__class__}".format("")
"{foo[bar]}".format(foo={"bar": "barv"})
"{[bar]}".format({"bar": "barv"})
"{:{}} {}".format(1, 15, 2)
"{:2}".format(1)
"{foo}-{}".format(1, foo=2)
a = ()
"{}".format(*a)
k = {}
"{foo}".format(**k)

4
resources/test/fixtures/F522.py vendored Normal file
View File

@@ -0,0 +1,4 @@
"{}".format(1, bar=2) # F522
"{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522

12
resources/test/fixtures/F523.py vendored Normal file
View File

@@ -0,0 +1,12 @@
# With indexes
"{0}".format(1, 2) # F523
"{1}".format(1, 2, 3) # F523
"{1:{0}}".format(1, 2) # No issues
"{1:{0}}".format(1, 2, 3) # F523
"{0}{2}".format(1, 2) # F523, # F524
# With no indexes
"{}".format(1, 2) # F523
"{}".format(1, 2, 3) # F523
"{:{}}".format(1, 2) # No issues
"{:{}}".format(1, 2, 3) # F523

6
resources/test/fixtures/F524.py vendored Normal file
View File

@@ -0,0 +1,6 @@
"{} {}".format(1) # F524
"{2}".format() # F524
"{bar}".format() # F524
"{0} {bar}".format(1) # F524
"{0} {bar}".format() # F524
"{bar} {0}".format() # F524

2
resources/test/fixtures/F525.py vendored Normal file
View File

@@ -0,0 +1,2 @@
"{} {1}".format(1, 2) # F525
"{0} {}".format(1, 2) # F523, F525

View File

@@ -12,3 +12,13 @@ x: dict["key", "value"]
# OK
x: dict[str, str]
# OK
def unimportant(name):
pass
def dang(dict, set, list):
unimportant(name=dict["name"])
unimportant(name=set["name"])
unimportant(name=list["name"])

View File

@@ -57,3 +57,11 @@ def f5():
def f6():
annotations = []
assert len([annotations for annotations in annotations])
def f7():
def connect():
return None, None
with connect() as (connection, cursor):
cursor.execute("SELECT * FROM users")

14
resources/test/fixtures/T100.py vendored Normal file
View File

@@ -0,0 +1,14 @@
breakpoint()
import pdb
from builtins import breakpoint
from pdb import set_trace as st
from celery.contrib.rdb import set_trace
from celery.contrib import rdb
import celery.contrib.rdb
breakpoint()
st()
set_trace()

View File

@@ -1,3 +1,3 @@
# coding=utf8
print('Hello world')
print("Hello world")

3
resources/test/fixtures/U009_4.py vendored Normal file
View File

@@ -0,0 +1,3 @@
# coding=utf8 # noqa: U009
print("Hello world")

View File

@@ -36,3 +36,45 @@ with open("foo", "U") as fa, open("bar", "U") as fb:
pass
with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
pass
open("foo", mode="U")
open(name="foo", mode="U")
open(mode="U", name="foo")
with open("foo", mode="U") as f:
pass
with open(name="foo", mode="U") as f:
pass
with open(mode="U", name="foo") as f:
pass
open("foo", mode="Ub")
open(name="foo", mode="Ub")
open(mode="Ub", name="foo")
with open("foo", mode="Ub") as f:
pass
with open(name="foo", mode="Ub") as f:
pass
with open(mode="Ub", name="foo") as f:
pass
open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
open(file="foo", buffering=- 1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
open(mode='Ub', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open = 1
open("foo", "U")
open("foo", "Ur")
open("foo", "Ub")
open("foo", "rUb")
open("foo", "r")
open("foo", "rt")
open("f", "r", encoding="UTF-8")
open("f", "wt")

View File

@@ -44,7 +44,7 @@ expectation.expected.add((
@expect("D407: Missing dashed underline after section ('Returns')",
arg_count=3)
@expect("D413: Missing blank line after last section ('Raises')", arg_count=3)
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
def fetch_bigtable_rows(big_table, keys, other_silly_variable=None, **kwargs):
"""Fetches rows from a Bigtable.
Retrieves rows pertaining to the given keys from the Table instance
@@ -57,6 +57,7 @@ def fetch_bigtable_rows(big_table, keys, other_silly_variable=None):
to fetch.
other_silly_variable: Another optional variable, that has a much
longer name than the other args, and which does nothing.
**kwargs: More keyword arguments.
Returns:
A dict mapping keys to the corresponding table row data

View File

@@ -73,7 +73,7 @@ expectation.expected.add((
"(found 'A')", arg_count=3)
@expect("D413: Missing blank line after last section ('Examples')",
arg_count=3)
def foo(var1, var2, long_var_name='hi'):
def foo(var1, var2, long_var_name='hi', **kwargs):
r"""A one-line summary that does not use variable names.
Several sentences providing an extended description. Refer to
@@ -91,6 +91,8 @@ def foo(var1, var2, long_var_name='hi'):
detail, e.g. ``(N,) ndarray`` or ``array_like``.
long_var_name : {'hi', 'ho'}, optional
Choices in brackets, default first when optional.
**kwargs : int
More keyword arguments.
Returns
-------

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.136"
version = "0.0.143"
edition = "2021"
[dependencies]
@@ -8,7 +8,7 @@ anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }
codegen = { version = "0.2.0" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
ruff = { path = ".." }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "f885db8c61514f069979861f6b3bd83292086231" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "f885db8c61514f069979861f6b3bd83292086231" }

View File

@@ -117,7 +117,8 @@ pub fn main(cli: &Cli) -> Result<()> {
// Construct the output contents.
let mut output = String::new();
output.push_str("//! File automatically generated by examples/generate_check_code_prefix.rs.");
output
.push_str("//! File automatically generated by `examples/generate_check_code_prefix.rs`.");
output.push('\n');
output.push('\n');
output.push_str("use serde::{{Serialize, Deserialize}};");
@@ -129,6 +130,21 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push('\n');
output.push('\n');
output.push_str(&scope.to_string());
output.push('\n');
output.push('\n');
// Add the list of output categories (not generated).
output.push_str("pub const CATEGORIES: &[CheckCodePrefix] = &[");
output.push('\n');
for prefix in prefix_to_codes.keys() {
if prefix.chars().all(char::is_alphabetic) {
output.push_str(&format!("CheckCodePrefix::{prefix},"));
output.push('\n');
}
}
output.push_str("];");
output.push('\n');
output.push('\n');
// Write the output to `src/checks_gen.rs` (or stdout).
if cli.dry_run {

View File

@@ -1,7 +1,9 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, Stmt, StmtKind};
use rustpython_ast::{
Arguments, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Location, Stmt, StmtKind,
};
use crate::ast::types::Range;
use crate::SourceCodeLocator;
@@ -213,6 +215,27 @@ pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<Vec<&str>> {
handler_names
}
/// Return the set of all bound argument names.
pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
let mut arg_names: FxHashSet<&'a str> = FxHashSet::default();
for arg in &arguments.posonlyargs {
arg_names.insert(arg.node.arg.as_str());
}
for arg in &arguments.args {
arg_names.insert(arg.node.arg.as_str());
}
if let Some(arg) = &arguments.vararg {
arg_names.insert(arg.node.arg.as_str());
}
for arg in &arguments.kwonlyargs {
arg_names.insert(arg.node.arg.as_str());
}
if let Some(arg) = &arguments.kwarg {
arg_names.insert(arg.node.arg.as_str());
}
arg_names
}
/// Returns `true` if a call is an argumented `super` invocation.
pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
// Check: is this a `super` call?

View File

@@ -1,7 +1,7 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use rustc_hash::FxHashMap;
use rustpython_ast::{Expr, Keyword};
use rustpython_ast::{Expr, Keyword, Stmt};
use rustpython_parser::ast::{Located, Location};
fn id() -> usize {
@@ -9,6 +9,12 @@ fn id() -> usize {
COUNTER.fetch_add(1, Ordering::Relaxed)
}
#[derive(Clone)]
pub enum Node<'a> {
Stmt(&'a Stmt),
Expr(&'a Expr),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct Range {
pub location: Location,

View File

@@ -19,8 +19,8 @@ pub trait Visitor<'a> {
fn visit_constant(&mut self, constant: &'a Constant) {
walk_constant(self, constant);
}
fn visit_expr_context(&mut self, expr_content: &'a ExprContext) {
walk_expr_context(self, expr_content);
fn visit_expr_context(&mut self, expr_context: &'a ExprContext) {
walk_expr_context(self, expr_context);
}
fn visit_boolop(&mut self, boolop: &'a Boolop) {
walk_boolop(self, boolop);

View File

@@ -19,7 +19,7 @@ pub fn leading_space(line: &str) -> String {
}
/// Extract the leading indentation from a line.
pub fn indentation<'a, T>(checker: &'a Checker, located: &Located<T>) -> String {
pub fn indentation<T>(checker: &Checker, located: &Located<T>) -> String {
let range = Range::from_located(located);
checker
.locator

View File

@@ -6,6 +6,7 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Withitem;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
@@ -18,10 +19,10 @@ use crate::ast::helpers::{
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, ImportKind, Range, Scope,
ScopeKind,
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, ImportKind, Node, Range,
Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::visitor::{walk_excepthandler, walk_withitem, Visitor};
use crate::ast::{helpers, operations, visitor};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
@@ -32,11 +33,13 @@ use crate::python::typing::SubscriptKind;
use crate::settings::types::PythonVersion;
use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
use crate::vendored::cformat::{CFormatError, CFormatErrorType};
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_print,
flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade, rules,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger,
flake8_print, flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pyupgrade, rules,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -77,10 +80,13 @@ pub struct Checker<'a> {
in_deferred_string_annotation: bool,
in_literal: bool,
in_subscript: bool,
in_withitem: bool,
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
except_handlers: Vec<Vec<Vec<&'a str>>>,
// Check-specific state.
pub(crate) seen_b023: Vec<&'a Expr>,
}
impl<'a> Checker<'a> {
@@ -120,10 +126,13 @@ impl<'a> Checker<'a> {
in_deferred_string_annotation: false,
in_literal: false,
in_subscript: false,
in_withitem: false,
seen_import_boundary: false,
futures_allowed: true,
annotations_future_enabled: false,
except_handlers: vec![],
// Check-specific state.
seen_b023: vec![],
}
}
@@ -170,6 +179,15 @@ impl<'a> Checker<'a> {
|| (typing::in_extensions(target)
&& match_call_path(call_path, "typing_extensions", target, &self.from_imports))
}
/// Return `true` if `member` is bound as a builtin.
pub fn is_builtin(&self, member: &str) -> bool {
self.current_scopes()
.find_map(|scope| scope.values.get(member))
.map_or(false, |binding| {
matches!(binding.kind, BindingKind::Builtin)
})
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
@@ -619,6 +637,15 @@ where
);
}
// flake8-debugger
if self.settings.enabled.contains(&CheckCode::T100) {
if let Some(check) =
flake8_debugger::checks::debugger_import(stmt, None, &alias.node.name)
{
self.add_check(check);
}
}
if let Some(asname) = &alias.node.asname {
for alias in names {
if let Some(asname) = &alias.node.asname {
@@ -687,12 +714,7 @@ where
self.from_imports
.entry(module)
.or_insert_with(FxHashSet::default)
.extend(
names
.iter()
.filter(|alias| alias.node.asname.is_none())
.map(|alias| alias.node.name.as_str()),
);
.extend(names.iter().map(|alias| alias.node.name.as_str()));
}
for alias in names {
if let Some(asname) = &alias.node.asname {
@@ -848,6 +870,17 @@ where
}
}
// flake8-debugger
if self.settings.enabled.contains(&CheckCode::T100) {
if let Some(check) = flake8_debugger::checks::debugger_import(
stmt,
module.as_ref().map(String::as_str),
&alias.node.name,
) {
self.add_check(check);
}
}
if let Some(asname) = &alias.node.asname {
if self.settings.enabled.contains(&CheckCode::N811) {
if let Some(check) =
@@ -948,8 +981,16 @@ where
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
}
}
StmtKind::While { .. } => {
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Stmt(stmt));
}
}
StmtKind::For {
target, body, iter, ..
}
| StmtKind::AsyncFor {
target, body, iter, ..
} => {
if self.settings.enabled.contains(&CheckCode::B007) {
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
@@ -957,6 +998,9 @@ where
if self.settings.enabled.contains(&CheckCode::B020) {
flake8_bugbear::plugins::loop_variable_overrides_iterator(self, target, iter);
}
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Stmt(stmt));
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
@@ -1240,6 +1284,81 @@ where
args,
keywords,
} => {
// pyflakes
if self.settings.enabled.contains(&CheckCode::F521)
|| self.settings.enabled.contains(&CheckCode::F522)
|| self.settings.enabled.contains(&CheckCode::F523)
|| self.settings.enabled.contains(&CheckCode::F524)
|| self.settings.enabled.contains(&CheckCode::F525)
{
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &value.node
{
if attr == "format" {
// "...".format(...) call
let location = Range::from_located(expr);
match pyflakes::format::FormatSummary::try_from(value.as_ref()) {
Err(e) => {
if self.settings.enabled.contains(&CheckCode::F521) {
self.add_check(Check::new(
CheckKind::StringDotFormatInvalidFormat(
e.to_string(),
),
location,
));
}
}
Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F522) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_named_arguments(
&summary, keywords, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F523) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_positional_arguments(
&summary, args, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F524) {
if let Some(check) =
pyflakes::checks::string_dot_format_missing_argument(
&summary, args, keywords, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F525) {
if let Some(check) =
pyflakes::checks::string_dot_format_mixing_automatic(
&summary, location,
)
{
self.add_check(check);
}
}
}
}
}
}
}
}
// pyupgrade
if self.settings.enabled.contains(&CheckCode::U005) {
pyupgrade::plugins::deprecated_unittest_alias(self, func);
}
@@ -1532,6 +1651,18 @@ where
}
}
// flake8-debugger
if self.settings.enabled.contains(&CheckCode::T100) {
if let Some(check) = flake8_debugger::checks::debugger_call(
expr,
func,
&self.from_imports,
&self.import_aliases,
) {
self.add_check(check);
}
}
// Ruff
if self.settings.enabled.contains(&CheckCode::RUF101) {
rules::plugins::convert_exit_to_sys_exit(self, func);
@@ -1586,6 +1717,116 @@ where
pyflakes::plugins::invalid_print_syntax(self, left);
}
}
ExprKind::BinOp {
left,
op: Operator::Mod,
right,
} => {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &left.node
{
if self.settings.enabled.contains(&CheckCode::F501)
|| self.settings.enabled.contains(&CheckCode::F502)
|| self.settings.enabled.contains(&CheckCode::F503)
|| self.settings.enabled.contains(&CheckCode::F504)
|| self.settings.enabled.contains(&CheckCode::F505)
|| self.settings.enabled.contains(&CheckCode::F506)
|| self.settings.enabled.contains(&CheckCode::F507)
|| self.settings.enabled.contains(&CheckCode::F508)
|| self.settings.enabled.contains(&CheckCode::F509)
{
let location = Range::from_located(expr);
match pyflakes::cformat::CFormatSummary::try_from(value.as_ref()) {
Err(CFormatError {
typ: CFormatErrorType::UnsupportedFormatChar(c),
..
}) => {
if self.settings.enabled.contains(&CheckCode::F509) {
self.add_check(Check::new(
CheckKind::PercentFormatUnsupportedFormatCharacter(c),
location,
));
}
}
Err(e) => {
if self.settings.enabled.contains(&CheckCode::F501) {
self.add_check(Check::new(
CheckKind::PercentFormatInvalidFormat(e.to_string()),
location,
));
}
}
Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F502) {
if let Some(check) =
pyflakes::checks::percent_format_expected_mapping(
&summary, right, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F503) {
if let Some(check) =
pyflakes::checks::percent_format_expected_sequence(
&summary, right, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F504) {
if let Some(check) =
pyflakes::checks::percent_format_extra_named_arguments(
&summary, right, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F505) {
if let Some(check) =
pyflakes::checks::percent_format_missing_arguments(
&summary, right, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F506) {
if let Some(check) =
pyflakes::checks::percent_format_mixed_positional_and_named(
&summary, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F507) {
if let Some(check) =
pyflakes::checks::percent_format_positional_count_mismatch(
&summary, right, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F508) {
if let Some(check) =
pyflakes::checks::percent_format_star_requires_sequence(
&summary, right, location,
)
{
self.add_check(check);
}
}
}
}
}
}
}
ExprKind::UnaryOp { op, operand } => {
let check_not_in = self.settings.enabled.contains(&CheckCode::E713);
let check_not_is = self.settings.enabled.contains(&CheckCode::E714);
@@ -1732,9 +1973,15 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Expr(expr));
}
self.push_scope(Scope::new(ScopeKind::Generator));
}
ExprKind::GeneratorExp { .. } | ExprKind::DictComp { .. } => {
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Expr(expr));
}
self.push_scope(Scope::new(ScopeKind::Generator));
}
_ => {}
@@ -1865,6 +2112,7 @@ where
value,
&self.from_imports,
&self.import_aliases,
|member| self.is_builtin(member),
) {
Some(subscript) => {
match subscript {
@@ -1926,13 +2174,20 @@ where
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, name, .. } => {
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
self.add_check(Check::new(
CheckKind::DoNotUseBareExcept,
Range::from_located(excepthandler),
));
}
if self.settings.enabled.contains(&CheckCode::B904) {
{
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
}
}
match name {
Some(name) => {
if self.settings.enabled.contains(&CheckCode::E741) {
@@ -2080,6 +2335,13 @@ where
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
}
fn visit_withitem(&mut self, withitem: &'b Withitem) {
let prev_in_withitem = self.in_withitem;
self.in_withitem = true;
walk_withitem(self, withitem);
self.in_withitem = prev_in_withitem;
}
}
fn try_mark_used(scope: &mut Scope, scope_id: usize, id: &str, expr: &Expr) -> bool {
@@ -2386,7 +2648,7 @@ impl<'a> Checker<'a> {
return;
}
if operations::is_unpacking_assignment(parent) {
if self.in_withitem || operations::is_unpacking_assignment(parent) {
self.add_binding(
id,
Binding {

View File

@@ -16,13 +16,16 @@ use crate::settings::Settings;
static CODING_COMMENT_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*utf-?8").expect("Invalid regex"));
static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://\S+$").expect("Invalid regex"));
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length > limit {
let mut chunks = line.split_whitespace();
if let (Some(first), Some(_)) = (chunks.next(), chunks.next()) {
// Do not enforce the line length for commented lines with a single word
!(first == "#" && chunks.next().is_none())
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
!(first == "#" && chunks.last().map_or(false, |c| URL_REGEX.is_match(c)))
} else {
// Single word / no printable chars - no way to make the line shorter
false
@@ -38,6 +41,7 @@ pub fn check_lines(
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: bool,
ignore_noqa: bool,
) {
let enforce_unnecessary_coding_comment = settings.enabled.contains(&CheckCode::U009);
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
@@ -53,6 +57,30 @@ pub fn check_lines(
assert!(check.location.row() >= 1);
}
macro_rules! add_if {
($check:expr, $noqa:expr) => {{
match $noqa {
(Directive::All(..), matches) => {
matches.push($check.kind.code().as_ref());
if ignore_noqa {
line_checks.push($check);
}
}
(Directive::Codes(.., codes), matches) => {
if codes.contains(&$check.kind.code().as_ref()) {
matches.push($check.kind.code().as_ref());
if ignore_noqa {
line_checks.push($check);
}
} else {
line_checks.push($check);
}
}
(Directive::None, ..) => line_checks.push($check),
}
}};
}
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// Grab the noqa (logical) line number for the current (physical) line.
@@ -65,21 +93,24 @@ pub fn check_lines(
if lineno < 2 {
// PEP3120 makes utf-8 the default encoding.
if CODING_COMMENT_REGEX.is_match(line) {
let line_length = line.len();
let mut check = Check::new(
CheckKind::PEP3120UnnecessaryCodingComment,
Range {
location: Location::new(lineno + 1, 0),
end_location: Location::new(lineno + 1, line_length + 1),
end_location: Location::new(lineno + 2, 0),
},
);
if autofix && settings.fixable.contains(check.kind.code()) {
check.amend(Fix::deletion(
Location::new(lineno + 1, 0),
Location::new(lineno + 1, line_length + 1),
Location::new(lineno + 2, 0),
));
}
line_checks.push(check);
let noqa = noqa_directives.entry(noqa_lineno).or_insert_with(|| {
(noqa::extract_noqa_directive(lines[noqa_lineno]), vec![])
});
add_if!(check, noqa);
}
}
}
@@ -109,7 +140,7 @@ pub fn check_lines(
ignored.push(index);
}
}
(Directive::None, _) => {}
(Directive::None, ..) => {}
}
}
@@ -117,10 +148,6 @@ pub fn check_lines(
if enforce_line_too_long {
let line_length = line.chars().count();
if should_enforce_line_length(line, line_length, settings.line_length) {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::LineTooLong(line_length, settings.line_length),
Range {
@@ -129,35 +156,19 @@ pub fn check_lines(
},
);
match noqa {
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(.., codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
add_if!(check, noqa);
}
}
}
// Enforce newlines at end of files.
// Enforce newlines at end of files (W292).
if settings.enabled.contains(&CheckCode::W292) && !contents.ends_with('\n') {
// Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't
// want to raise W292 anyway).
if let Some(line) = lines.last() {
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
let check = Check::new(
CheckKind::NoNewLineAtEndOfFile,
Range {
@@ -166,23 +177,16 @@ pub fn check_lines(
},
);
match noqa {
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
}
(Directive::Codes(.., codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
} else {
line_checks.push(check);
}
}
(Directive::None, _) => line_checks.push(check),
}
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
add_if!(check, noqa);
}
}
// Enforce that the noqa directive was actually used.
// Enforce that the noqa directive was actually used (M001).
if enforce_noqa {
for (row, (directive, matches)) in noqa_directives {
match directive {
@@ -245,9 +249,11 @@ pub fn check_lines(
}
}
ignored.sort_unstable();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
if !ignore_noqa {
ignored.sort_unstable();
for index in ignored.iter().rev() {
checks.swap_remove(*index);
}
}
checks.extend(line_checks);
}
@@ -275,6 +281,7 @@ mod tests {
..Settings::for_rule(CheckCode::E501)
},
true,
false,
);
checks
};

View File

@@ -7,6 +7,7 @@ use strum_macros::{AsRefStr, EnumIter, EnumString};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::flake8_debugger::types::DebuggerUsingType;
use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness;
use crate::pyupgrade::types::Primitive;
@@ -52,6 +53,20 @@ pub enum CheckCode {
F405,
F406,
F407,
F501,
F502,
F503,
F504,
F505,
F506,
F507,
F508,
F509,
F521,
F522,
F523,
F524,
F525,
F541,
F601,
F602,
@@ -99,10 +114,12 @@ pub enum CheckCode {
B020,
B021,
B022,
B023,
B024,
B025,
B026,
B027,
B904,
// flake8-blind-except
BLE001,
// flake8-comprehensions
@@ -122,6 +139,8 @@ pub enum CheckCode {
C415,
C416,
C417,
// flake8-debugger
T100,
// mccabe
C901,
// flake8-tidy-imports
@@ -265,6 +284,7 @@ pub enum CheckCategory {
PEP8Naming,
Flake8Bandit,
Flake8Comprehensions,
Flake8Debugger,
Flake8BooleanTrap,
Flake8Bugbear,
Flake8Builtins,
@@ -290,6 +310,7 @@ impl CheckCategory {
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Bugbear => "flake8-bugbear",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Debugger => "flake8-debugger",
CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
@@ -319,6 +340,9 @@ impl CheckCategory {
CheckCategory::Flake8Comprehensions => {
Some("https://pypi.org/project/flake8-comprehensions/3.10.1/")
}
CheckCategory::Flake8Debugger => {
Some("https://pypi.org/project/flake8-debugger/4.1.2/")
}
CheckCategory::Flake8TidyImports => {
Some("https://pypi.org/project/flake8-tidy-imports/4.8.0/")
}
@@ -400,8 +424,22 @@ pub enum CheckKind {
LateFutureImport,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
PercentFormatExpectedMapping,
PercentFormatExpectedSequence,
PercentFormatExtraNamedArguments(Vec<String>),
PercentFormatInvalidFormat(String),
PercentFormatMissingArgument(Vec<String>),
PercentFormatMixedPositionalAndNamed,
PercentFormatPositionalCountMismatch(usize, usize),
PercentFormatStarRequiresSequence,
PercentFormatUnsupportedFormatCharacter(char),
RaiseNotImplemented,
ReturnOutsideFunction,
StringDotFormatExtraNamedArguments(Vec<String>),
StringDotFormatExtraPositionalArguments(Vec<String>),
StringDotFormatInvalidFormat(String),
StringDotFormatMissingArguments(Vec<String>),
StringDotFormatMixingAutomatic,
TwoStarredExpressions,
UndefinedExport(String),
UndefinedLocal(String),
@@ -416,31 +454,33 @@ pub enum CheckKind {
// flake8-blind-except
BlindExcept,
// flake8-bugbear
UnaryPrefixIncrement,
AssignmentToOsEnviron,
UnreliableCallableCheck,
StripWithMultiCharacters,
MutableArgumentDefault,
UnusedLoopControlVariable(String),
FunctionCallArgumentDefault(Option<String>),
GetAttrWithConstant,
SetAttrWithConstant,
DoNotAssertFalse,
JumpStatementInFinally(String),
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
UselessComparison,
CannotRaiseLiteral,
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
LoopVariableOverridesIterator(String),
FStringDocstring,
UselessContextlibSuppress,
AbstractBaseClassWithoutAbstractMethod(String),
AssignmentToOsEnviron,
CachedInstanceMethod,
CannotRaiseLiteral,
DoNotAssertFalse,
DuplicateHandlerException(Vec<String>),
DuplicateTryBlockException(String),
StarArgUnpackingAfterKeywordArg,
EmptyMethodWithoutAbstractDecorator(String),
FStringDocstring,
FunctionCallArgumentDefault(Option<String>),
FunctionUsesLoopVariable(String),
GetAttrWithConstant,
JumpStatementInFinally(String),
LoopVariableOverridesIterator(String),
MutableArgumentDefault,
NoAssertRaisesException,
RaiseWithoutFromInsideExcept,
RedundantTupleInExceptionHandler(String),
SetAttrWithConstant,
StarArgUnpackingAfterKeywordArg,
StripWithMultiCharacters,
UnaryPrefixIncrement,
UnreliableCallableCheck,
UnusedLoopControlVariable(String),
UselessComparison,
UselessContextlibSuppress,
UselessExpression,
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -458,6 +498,8 @@ pub enum CheckKind {
UnnecessarySubscriptReversal(String),
UnnecessaryComprehension(String),
UnnecessaryMap(String),
// flake8-debugger
Debugger(DebuggerUsingType),
// flake8-tidy-imports
BannedRelativeImport(Strictness),
// flake8-print
@@ -644,6 +686,24 @@ impl CheckCode {
}
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckCode::F501 => CheckKind::PercentFormatInvalidFormat("...".to_string()),
CheckCode::F502 => CheckKind::PercentFormatExpectedMapping,
CheckCode::F503 => CheckKind::PercentFormatExpectedSequence,
CheckCode::F504 => CheckKind::PercentFormatExtraNamedArguments(vec!["...".to_string()]),
CheckCode::F505 => CheckKind::PercentFormatMissingArgument(vec!["...".to_string()]),
CheckCode::F506 => CheckKind::PercentFormatMixedPositionalAndNamed,
CheckCode::F507 => CheckKind::PercentFormatPositionalCountMismatch(4, 2),
CheckCode::F508 => CheckKind::PercentFormatStarRequiresSequence,
CheckCode::F509 => CheckKind::PercentFormatUnsupportedFormatCharacter('c'),
CheckCode::F521 => CheckKind::StringDotFormatInvalidFormat("...".to_string()),
CheckCode::F522 => {
CheckKind::StringDotFormatExtraNamedArguments(vec!["...".to_string()])
}
CheckCode::F523 => {
CheckKind::StringDotFormatExtraPositionalArguments(vec!["...".to_string()])
}
CheckCode::F524 => CheckKind::StringDotFormatMissingArguments(vec!["...".to_string()]),
CheckCode::F525 => CheckKind::StringDotFormatMixingAutomatic,
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
@@ -695,10 +755,12 @@ impl CheckCode {
CheckCode::B020 => CheckKind::LoopVariableOverridesIterator("...".to_string()),
CheckCode::B021 => CheckKind::FStringDocstring,
CheckCode::B022 => CheckKind::UselessContextlibSuppress,
CheckCode::B023 => CheckKind::FunctionUsesLoopVariable("...".to_string()),
CheckCode::B024 => CheckKind::AbstractBaseClassWithoutAbstractMethod("...".to_string()),
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
CheckCode::B904 => CheckKind::RaiseWithoutFromInsideExcept,
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -729,6 +791,8 @@ impl CheckCode {
}
CheckCode::C416 => CheckKind::UnnecessaryComprehension("(list|set)".to_string()),
CheckCode::C417 => CheckKind::UnnecessaryMap("(list|set|dict)".to_string()),
// flake8-debugger
CheckCode::T100 => CheckKind::Debugger(DebuggerUsingType::Import("...".to_string())),
// flake8-tidy-imports
CheckCode::I252 => CheckKind::BannedRelativeImport(Strictness::All),
// flake8-print
@@ -909,6 +973,20 @@ impl CheckCode {
CheckCode::F405 => CheckCategory::Pyflakes,
CheckCode::F406 => CheckCategory::Pyflakes,
CheckCode::F407 => CheckCategory::Pyflakes,
CheckCode::F501 => CheckCategory::Pyflakes,
CheckCode::F502 => CheckCategory::Pyflakes,
CheckCode::F503 => CheckCategory::Pyflakes,
CheckCode::F504 => CheckCategory::Pyflakes,
CheckCode::F505 => CheckCategory::Pyflakes,
CheckCode::F506 => CheckCategory::Pyflakes,
CheckCode::F507 => CheckCategory::Pyflakes,
CheckCode::F508 => CheckCategory::Pyflakes,
CheckCode::F509 => CheckCategory::Pyflakes,
CheckCode::F521 => CheckCategory::Pyflakes,
CheckCode::F522 => CheckCategory::Pyflakes,
CheckCode::F523 => CheckCategory::Pyflakes,
CheckCode::F524 => CheckCategory::Pyflakes,
CheckCode::F525 => CheckCategory::Pyflakes,
CheckCode::F541 => CheckCategory::Pyflakes,
CheckCode::F601 => CheckCategory::Pyflakes,
CheckCode::F602 => CheckCategory::Pyflakes,
@@ -954,10 +1032,12 @@ impl CheckCode {
CheckCode::B020 => CheckCategory::Flake8Bugbear,
CheckCode::B021 => CheckCategory::Flake8Bugbear,
CheckCode::B022 => CheckCategory::Flake8Bugbear,
CheckCode::B023 => CheckCategory::Flake8Bugbear,
CheckCode::B024 => CheckCategory::Flake8Bugbear,
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::B904 => CheckCategory::Flake8Bugbear,
CheckCode::BLE001 => CheckCategory::Flake8BlindExcept,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
@@ -975,6 +1055,7 @@ impl CheckCode {
CheckCode::C415 => CheckCategory::Flake8Comprehensions,
CheckCode::C416 => CheckCategory::Flake8Comprehensions,
CheckCode::C417 => CheckCategory::Flake8Comprehensions,
CheckCode::T100 => CheckCategory::Flake8Debugger,
CheckCode::I252 => CheckCategory::Flake8TidyImports,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
@@ -1130,8 +1211,22 @@ impl CheckKind {
CheckKind::NoneComparison(_) => &CheckCode::E711,
CheckKind::NotInTest => &CheckCode::E713,
CheckKind::NotIsTest => &CheckCode::E714,
CheckKind::PercentFormatExpectedMapping => &CheckCode::F502,
CheckKind::PercentFormatExpectedSequence => &CheckCode::F503,
CheckKind::PercentFormatExtraNamedArguments(_) => &CheckCode::F504,
CheckKind::PercentFormatInvalidFormat(_) => &CheckCode::F501,
CheckKind::PercentFormatMissingArgument(_) => &CheckCode::F505,
CheckKind::PercentFormatMixedPositionalAndNamed => &CheckCode::F506,
CheckKind::PercentFormatPositionalCountMismatch(..) => &CheckCode::F507,
CheckKind::PercentFormatStarRequiresSequence => &CheckCode::F508,
CheckKind::PercentFormatUnsupportedFormatCharacter(_) => &CheckCode::F509,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::StringDotFormatExtraNamedArguments(_) => &CheckCode::F522,
CheckKind::StringDotFormatExtraPositionalArguments(_) => &CheckCode::F523,
CheckKind::StringDotFormatInvalidFormat(_) => &CheckCode::F521,
CheckKind::StringDotFormatMissingArguments(_) => &CheckCode::F524,
CheckKind::StringDotFormatMixingAutomatic => &CheckCode::F525,
CheckKind::SyntaxError(_) => &CheckCode::E999,
CheckKind::ExpressionsInStarAssignment => &CheckCode::F621,
CheckKind::TrueFalseComparison(..) => &CheckCode::E712,
@@ -1151,31 +1246,33 @@ impl CheckKind {
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
// flake8-bugbear
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
CheckKind::AssignmentToOsEnviron => &CheckCode::B003,
CheckKind::UnreliableCallableCheck => &CheckCode::B004,
CheckKind::StripWithMultiCharacters => &CheckCode::B005,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::FunctionCallArgumentDefault(_) => &CheckCode::B008,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::JumpStatementInFinally(_) => &CheckCode::B012,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::UselessComparison => &CheckCode::B015,
CheckKind::CannotRaiseLiteral => &CheckCode::B016,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::AbstractBaseClassWithoutAbstractMethod(_) => &CheckCode::B024,
CheckKind::AssignmentToOsEnviron => &CheckCode::B003,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::CannotRaiseLiteral => &CheckCode::B016,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::EmptyMethodWithoutAbstractDecorator(_) => &CheckCode::B027,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::FunctionCallArgumentDefault(_) => &CheckCode::B008,
CheckKind::FunctionUsesLoopVariable(_) => &CheckCode::B023,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::JumpStatementInFinally(_) => &CheckCode::B012,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::RaiseWithoutFromInsideExcept => &CheckCode::B904,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::StripWithMultiCharacters => &CheckCode::B005,
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
CheckKind::UnreliableCallableCheck => &CheckCode::B004,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::UselessComparison => &CheckCode::B015,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::UselessExpression => &CheckCode::B018,
// flake8-blind-except
CheckKind::BlindExcept => &CheckCode::BLE001,
// flake8-comprehensions
@@ -1195,6 +1292,8 @@ impl CheckKind {
CheckKind::UnnecessarySubscriptReversal(_) => &CheckCode::C415,
CheckKind::UnnecessaryComprehension(..) => &CheckCode::C416,
CheckKind::UnnecessaryMap(_) => &CheckCode::C417,
// flake8-debugger
CheckKind::Debugger(_) => &CheckCode::T100,
// flake8-tidy-imports
CheckKind::BannedRelativeImport(_) => &CheckCode::I252,
// flake8-print
@@ -1411,12 +1510,59 @@ impl CheckKind {
},
CheckKind::NotInTest => "Test for membership should be `not in`".to_string(),
CheckKind::NotIsTest => "Test for object identity should be `is not`".to_string(),
CheckKind::PercentFormatInvalidFormat(message) => {
format!("'...' % ... has invalid format string: {message}")
}
CheckKind::PercentFormatUnsupportedFormatCharacter(char) => {
format!("'...' % ... has unsupported format character '{char}'")
}
CheckKind::PercentFormatExpectedMapping => {
"'...' % ... expected mapping but got sequence".to_string()
}
CheckKind::PercentFormatExpectedSequence => {
"'...' % ... expected sequence but got mapping".to_string()
}
CheckKind::PercentFormatExtraNamedArguments(missing) => {
let message = missing.join(", ");
format!("'...' % ... has unused named argument(s): {message}")
}
CheckKind::PercentFormatMissingArgument(missing) => {
let message = missing.join(", ");
format!("'...' % ... is missing argument(s) for placeholder(s): {message}")
}
CheckKind::PercentFormatMixedPositionalAndNamed => {
"'...' % ... has mixed positional and named placeholders".to_string()
}
CheckKind::PercentFormatPositionalCountMismatch(wanted, got) => {
format!("'...' % ... has {wanted} placeholder(s) but {got} substitution(s)")
}
CheckKind::PercentFormatStarRequiresSequence => {
"'...' % ... `*` specifier requires sequence".to_string()
}
CheckKind::RaiseNotImplemented => {
"`raise NotImplemented` should be `raise NotImplementedError`".to_string()
}
CheckKind::ReturnOutsideFunction => {
"`return` statement outside of a function/method".to_string()
}
CheckKind::StringDotFormatExtraNamedArguments(missing) => {
let message = missing.join(", ");
format!("'...'.format(...) has unused named argument(s): {message}")
}
CheckKind::StringDotFormatExtraPositionalArguments(missing) => {
let message = missing.join(", ");
format!("'...'.format(...) has unused arguments at position(s): {message}")
}
CheckKind::StringDotFormatInvalidFormat(message) => {
format!("'...'.format(...) has invalid format string: {message}")
}
CheckKind::StringDotFormatMissingArguments(missing) => {
let message = missing.join(", ");
format!("'...'.format(...) is missing argument(s) for placeholder(s): {message}")
}
CheckKind::StringDotFormatMixingAutomatic => {
"'...'.format(...) mixes automatic and manual numbering".to_string()
}
CheckKind::SyntaxError(message) => format!("SyntaxError: {message}"),
CheckKind::ExpressionsInStarAssignment => {
"Too many expressions in star-unpacking assignment".to_string()
@@ -1502,6 +1648,9 @@ impl CheckKind {
"Do not perform function call in argument defaults".to_string()
}
}
CheckKind::FunctionUsesLoopVariable(name) => {
format!("Function definition does not bind loop variable `{name}`")
}
CheckKind::GetAttrWithConstant => "Do not call `getattr` with a constant attribute \
value. It is not any safer than normal property \
access."
@@ -1580,6 +1729,11 @@ impl CheckKind {
decorator"
)
}
CheckKind::RaiseWithoutFromInsideExcept => {
"Within an except clause, raise exceptions with raise ... from err or raise ... \
from None to distinguish them from errors in exception handling"
.to_string()
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()
@@ -1653,6 +1807,11 @@ impl CheckKind {
format!("Unnecessary `map` usage (rewrite using a `{obj_type}` comprehension)")
}
}
// flake8-debugger
CheckKind::Debugger(using_type) => match using_type {
DebuggerUsingType::Call(name) => format!("Trace found: `{name}` used"),
DebuggerUsingType::Import(name) => format!("Import for `{name}` found"),
},
// flake8-tidy-imports
CheckKind::BannedRelativeImport(strictness) => match strictness {
Strictness::Parents => {

View File

@@ -59,10 +59,14 @@ pub enum CheckCodePrefix {
B020,
B021,
B022,
B023,
B024,
B025,
B026,
B027,
B9,
B90,
B904,
BLE,
BLE0,
BLE00,
@@ -183,6 +187,12 @@ pub enum CheckCodePrefix {
F406,
F407,
F5,
F52,
F521,
F522,
F523,
F524,
F525,
F54,
F541,
F6,
@@ -280,6 +290,9 @@ pub enum CheckCodePrefix {
S106,
S107,
T,
T1,
T10,
T100,
T2,
T20,
T201,
@@ -412,10 +425,12 @@ impl CheckCodePrefix {
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
CheckCode::B904,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -439,6 +454,7 @@ impl CheckCodePrefix {
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
@@ -488,6 +504,7 @@ impl CheckCodePrefix {
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
@@ -496,10 +513,14 @@ impl CheckCodePrefix {
CheckCodePrefix::B020 => vec![CheckCode::B020],
CheckCodePrefix::B021 => vec![CheckCode::B021],
CheckCodePrefix::B022 => vec![CheckCode::B022],
CheckCodePrefix::B023 => vec![CheckCode::B023],
CheckCodePrefix::B024 => vec![CheckCode::B024],
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::B027 => vec![CheckCode::B027],
CheckCodePrefix::B9 => vec![CheckCode::B904],
CheckCodePrefix::B90 => vec![CheckCode::B904],
CheckCodePrefix::B904 => vec![CheckCode::B904],
CheckCodePrefix::BLE => vec![CheckCode::BLE001],
CheckCodePrefix::BLE0 => vec![CheckCode::BLE001],
CheckCodePrefix::BLE00 => vec![CheckCode::BLE001],
@@ -840,6 +861,20 @@ impl CheckCodePrefix {
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F501,
CheckCode::F502,
CheckCode::F503,
CheckCode::F504,
CheckCode::F505,
CheckCode::F506,
CheckCode::F507,
CheckCode::F508,
CheckCode::F509,
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
@@ -887,7 +922,26 @@ impl CheckCodePrefix {
CheckCodePrefix::F405 => vec![CheckCode::F405],
CheckCodePrefix::F406 => vec![CheckCode::F406],
CheckCodePrefix::F407 => vec![CheckCode::F407],
CheckCodePrefix::F5 => vec![CheckCode::F541],
CheckCodePrefix::F5 => vec![
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
CheckCode::F541,
],
CheckCodePrefix::F52 => vec![
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
],
CheckCodePrefix::F521 => vec![CheckCode::F521],
CheckCodePrefix::F522 => vec![CheckCode::F522],
CheckCodePrefix::F523 => vec![CheckCode::F523],
CheckCodePrefix::F524 => vec![CheckCode::F524],
CheckCodePrefix::F525 => vec![CheckCode::F525],
CheckCodePrefix::F54 => vec![CheckCode::F541],
CheckCodePrefix::F541 => vec![CheckCode::F541],
CheckCodePrefix::F6 => vec![
@@ -1107,7 +1161,10 @@ impl CheckCodePrefix {
CheckCodePrefix::S105 => vec![CheckCode::S105],
CheckCodePrefix::S106 => vec![CheckCode::S106],
CheckCodePrefix::S107 => vec![CheckCode::S107],
CheckCodePrefix::T => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T => vec![CheckCode::T100, CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T1 => vec![CheckCode::T100],
CheckCodePrefix::T10 => vec![CheckCode::T100],
CheckCodePrefix::T100 => vec![CheckCode::T100],
CheckCodePrefix::T2 => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T20 => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T201 => vec![CheckCode::T201],
@@ -1281,10 +1338,14 @@ impl CheckCodePrefix {
CheckCodePrefix::B020 => PrefixSpecificity::Explicit,
CheckCodePrefix::B021 => PrefixSpecificity::Explicit,
CheckCodePrefix::B022 => PrefixSpecificity::Explicit,
CheckCodePrefix::B023 => PrefixSpecificity::Explicit,
CheckCodePrefix::B024 => PrefixSpecificity::Explicit,
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
CheckCodePrefix::B027 => PrefixSpecificity::Explicit,
CheckCodePrefix::B9 => PrefixSpecificity::Hundreds,
CheckCodePrefix::B90 => PrefixSpecificity::Tens,
CheckCodePrefix::B904 => PrefixSpecificity::Explicit,
CheckCodePrefix::BLE => PrefixSpecificity::Category,
CheckCodePrefix::BLE0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::BLE00 => PrefixSpecificity::Tens,
@@ -1405,6 +1466,12 @@ impl CheckCodePrefix {
CheckCodePrefix::F406 => PrefixSpecificity::Explicit,
CheckCodePrefix::F407 => PrefixSpecificity::Explicit,
CheckCodePrefix::F5 => PrefixSpecificity::Hundreds,
CheckCodePrefix::F52 => PrefixSpecificity::Tens,
CheckCodePrefix::F521 => PrefixSpecificity::Explicit,
CheckCodePrefix::F522 => PrefixSpecificity::Explicit,
CheckCodePrefix::F523 => PrefixSpecificity::Explicit,
CheckCodePrefix::F524 => PrefixSpecificity::Explicit,
CheckCodePrefix::F525 => PrefixSpecificity::Explicit,
CheckCodePrefix::F54 => PrefixSpecificity::Tens,
CheckCodePrefix::F541 => PrefixSpecificity::Explicit,
CheckCodePrefix::F6 => PrefixSpecificity::Hundreds,
@@ -1502,6 +1569,9 @@ impl CheckCodePrefix {
CheckCodePrefix::S106 => PrefixSpecificity::Explicit,
CheckCodePrefix::S107 => PrefixSpecificity::Explicit,
CheckCodePrefix::T => PrefixSpecificity::Category,
CheckCodePrefix::T1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::T10 => PrefixSpecificity::Tens,
CheckCodePrefix::T100 => PrefixSpecificity::Explicit,
CheckCodePrefix::T2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::T20 => PrefixSpecificity::Tens,
CheckCodePrefix::T201 => PrefixSpecificity::Explicit,
@@ -1551,3 +1621,25 @@ impl CheckCodePrefix {
}
}
}
pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::A,
CheckCodePrefix::ANN,
CheckCodePrefix::B,
CheckCodePrefix::BLE,
CheckCodePrefix::C,
CheckCodePrefix::D,
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::M,
CheckCodePrefix::N,
CheckCodePrefix::Q,
CheckCodePrefix::RUF,
CheckCodePrefix::S,
CheckCodePrefix::T,
CheckCodePrefix::U,
CheckCodePrefix::W,
CheckCodePrefix::YTT,
];

View File

@@ -1,6 +1,5 @@
use std::path::PathBuf;
use anyhow::Result;
use clap::{command, Parser};
use regex::Regex;
use rustc_hash::FxHashMap;
@@ -9,14 +8,14 @@ use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::logging::LogLevel;
use crate::printer::SerializationFormat;
use crate::settings::types::{PatternPrefixPair, PerFileIgnore, PythonVersion};
use crate::settings::types::{FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion};
#[derive(Debug, Parser)]
#[command(author, about = "Ruff: An extremely fast Python linter.")]
#[command(version)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
#[arg(required = true)]
#[arg(required_unless_present_any = ["explain", "generate_shell_completion"])]
pub files: Vec<PathBuf>,
/// Path to the `pyproject.toml` file to use for configuration.
#[arg(long)]
@@ -61,11 +60,11 @@ pub struct Cli {
pub extend_ignore: Vec<CheckCodePrefix>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<String>,
pub exclude: Vec<FilePattern>,
/// Like --exclude, but adds additional files and directories on top of the
/// excluded ones.
#[arg(long, value_delimiter = ',')]
pub extend_exclude: Vec<String>,
pub extend_exclude: Vec<FilePattern>,
/// List of error codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',')]
@@ -78,7 +77,7 @@ pub struct Cli {
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Vec<PatternPrefixPair>,
/// Output serialization format for error messages.
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
#[arg(long, value_enum, default_value_t = SerializationFormat::Text)]
pub format: SerializationFormat,
/// Show violations with source code.
#[arg(long)]
@@ -115,6 +114,9 @@ pub struct Cli {
/// Explain a rule.
#[arg(long)]
pub explain: Option<CheckCode>,
/// Generate shell completion
#[arg(long, hide = true, value_name = "SHELL")]
pub generate_shell_completion: Option<clap_complete_command::Shell>,
}
impl Cli {
@@ -149,10 +151,7 @@ pub fn extract_log_level(cli: &Cli) -> LogLevel {
}
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
project_root: Option<&PathBuf>,
) -> Result<Vec<PerFileIgnore>> {
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FxHashMap<String, Vec<CheckCodePrefix>> = FxHashMap::default();
for pair in pairs {
per_file_ignores
@@ -161,7 +160,7 @@ pub fn collect_per_file_ignores(
.push(pair.prefix);
}
per_file_ignores
.iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, prefixes, project_root))
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
.collect()
}

66
src/commands.rs Normal file
View File

@@ -0,0 +1,66 @@
use std::path::PathBuf;
use anyhow::Result;
use serde::Serialize;
use walkdir::DirEntry;
use crate::checks::CheckCode;
use crate::fs::iter_python_files;
use crate::printer::SerializationFormat;
use crate::{Configuration, Settings};
/// Print the user-facing configuration settings.
pub fn show_settings(
configuration: &Configuration,
project_root: Option<&PathBuf>,
pyproject: Option<&PathBuf>,
) {
println!("Resolved configuration: {configuration:#?}");
println!("Found project root at: {project_root:?}");
println!("Found pyproject.toml at: {pyproject:?}");
}
/// Show the list of files to be checked based on current settings.
pub fn show_files(files: &[PathBuf], settings: &Settings) {
let mut entries: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
for entry in entries {
println!("{}", entry.path().to_string_lossy());
}
}
#[derive(Serialize)]
struct Explanation<'a> {
code: &'a str,
category: &'a str,
summary: &'a str,
}
/// Explain a `CheckCode` to the user.
pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
match format {
SerializationFormat::Text => {
println!(
"{} ({}): {}",
code.as_ref(),
code.category().title(),
code.kind().summary()
);
}
SerializationFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&Explanation {
code: code.as_ref(),
category: code.category().title(),
summary: &code.kind().summary(),
})?
);
}
};
Ok(())
}

View File

@@ -21,7 +21,7 @@ where
fn visit_stmt(&mut self, stmt: &'b Stmt) {
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
// No recurse.
// Don't recurse.
}
StmtKind::Return { value } => self.returns.push(value.as_ref().map(|expr| &**expr)),
_ => visitor::walk_stmt(self, stmt),

View File

@@ -0,0 +1,232 @@
use rustc_hash::FxHashSet;
use rustpython_ast::{Comprehension, Expr, ExprContext, ExprKind, Stmt, StmtKind};
use crate::ast::helpers::collect_arg_names;
use crate::ast::types::{Node, Range};
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
#[derive(Default)]
struct LoadedNamesVisitor<'a> {
// Tuple of: name, defining expression, and defining range.
names: Vec<(&'a str, &'a Expr, Range)>,
// If we're in an f-string, the range of the defining expression.
in_f_string: Option<Range>,
}
/// `Visitor` to collect all used identifiers in a statement.
impl<'a, 'b> Visitor<'b> for LoadedNamesVisitor<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::JoinedStr { .. } => {
let prev_in_f_string = self.in_f_string;
self.in_f_string = Some(Range::from_located(expr));
visitor::walk_expr(self, expr);
self.in_f_string = prev_in_f_string;
}
ExprKind::Name { id, ctx } if matches!(ctx, ExprContext::Load) => {
self.names.push((
id,
expr,
self.in_f_string
.unwrap_or_else(|| Range::from_located(expr)),
));
}
_ => visitor::walk_expr(self, expr),
}
}
}
#[derive(Default)]
struct SuspiciousVariablesVisitor<'a> {
names: Vec<(&'a str, &'a Expr, Range)>,
}
/// `Visitor` to collect all suspicious variables (those referenced in
/// functions, but not bound as arguments).
impl<'a, 'b> Visitor<'b> for SuspiciousVariablesVisitor<'a>
where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
match &stmt.node {
StmtKind::FunctionDef { args, body, .. }
| StmtKind::AsyncFunctionDef { args, body, .. } => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
for stmt in body {
visitor.visit_stmt(stmt);
}
// Collect all argument names.
let arg_names = collect_arg_names(args);
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.names
.into_iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
}
_ => visitor::walk_stmt(self, stmt),
}
}
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Lambda { args, body } => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
visitor.visit_expr(body);
// Collect all argument names.
let arg_names = collect_arg_names(args);
// Treat any non-arguments as "suspicious".
self.names.extend(
visitor
.names
.into_iter()
.filter(|(id, ..)| !arg_names.contains(id)),
);
}
_ => visitor::walk_expr(self, expr),
}
}
}
#[derive(Default)]
struct NamesFromAssignmentsVisitor<'a> {
names: FxHashSet<&'a str>,
}
/// `Visitor` to collect all names used in an assignment expression.
impl<'a, 'b> Visitor<'b> for NamesFromAssignmentsVisitor<'a>
where
'b: 'a,
{
fn visit_expr(&mut self, expr: &'b Expr) {
match &expr.node {
ExprKind::Name { id, .. } => {
self.names.insert(id.as_str());
}
ExprKind::Starred { value, .. } => {
self.visit_expr(value);
}
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
for expr in elts {
self.visit_expr(expr);
}
}
_ => {}
}
}
}
#[derive(Default)]
struct AssignedNamesVisitor<'a> {
names: FxHashSet<&'a str>,
}
/// `Visitor` to collect all used identifiers in a statement.
impl<'a, 'b> Visitor<'b> for AssignedNamesVisitor<'a>
where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
if matches!(
&stmt.node,
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. }
) {
// Don't recurse.
return;
}
match &stmt.node {
StmtKind::Assign { targets, .. } => {
let mut visitor = NamesFromAssignmentsVisitor::default();
for expr in targets {
visitor.visit_expr(expr);
}
self.names.extend(visitor.names);
}
StmtKind::AugAssign { target, .. }
| StmtKind::AnnAssign { target, .. }
| StmtKind::For { target, .. }
| StmtKind::AsyncFor { target, .. } => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(target);
self.names.extend(visitor.names);
}
_ => {}
}
visitor::walk_stmt(self, stmt);
}
fn visit_expr(&mut self, expr: &'b Expr) {
if matches!(&expr.node, ExprKind::Lambda { .. }) {
// Don't recurse.
return;
}
visitor::walk_expr(self, expr);
}
fn visit_comprehension(&mut self, comprehension: &'b Comprehension) {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(&comprehension.target);
self.names.extend(visitor.names);
visitor::walk_comprehension(self, comprehension);
}
}
/// B023
pub fn function_uses_loop_variable<'a, 'b>(checker: &'a mut Checker<'b>, node: &Node<'b>)
where
'b: 'a,
{
// Identify any "suspicious" variables. These are defined as variables that are
// referenced in a function or lambda body, but aren't bound as arguments.
let suspicious_variables = {
let mut visitor = SuspiciousVariablesVisitor::<'b>::default();
match node {
Node::Stmt(stmt) => visitor.visit_stmt(stmt),
Node::Expr(expr) => visitor.visit_expr(expr),
}
visitor.names
};
if !suspicious_variables.is_empty() {
// Identify any variables that are assigned in the loop (ignoring functions).
let reassigned_in_loop = {
let mut visitor = AssignedNamesVisitor::<'b>::default();
match node {
Node::Stmt(stmt) => visitor.visit_stmt(stmt),
Node::Expr(expr) => visitor.visit_expr(expr),
}
visitor.names
};
// If a variable was used in a function or lambda body, and assigned in the
// loop, flag it.
for (name, expr, range) in suspicious_variables {
if reassigned_in_loop.contains(name) {
if !checker.seen_b023.contains(&expr) {
checker.seen_b023.push(expr);
checker.add_check(Check::new(
CheckKind::FunctionUsesLoopVariable(name.to_string()),
range,
));
}
}
}
}
}

View File

@@ -7,10 +7,12 @@ pub use cannot_raise_literal::cannot_raise_literal;
pub use duplicate_exceptions::duplicate_exceptions;
pub use f_string_docstring::f_string_docstring;
pub use function_call_argument_default::function_call_argument_default;
pub use function_uses_loop_variable::function_uses_loop_variable;
pub use getattr_with_constant::getattr_with_constant;
pub use jump_statement_in_finally::jump_statement_in_finally;
pub use loop_variable_overrides_iterator::loop_variable_overrides_iterator;
pub use mutable_argument_default::mutable_argument_default;
pub use raise_without_from_inside_except::raise_without_from_inside_except;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use setattr_with_constant::setattr_with_constant;
pub use star_arg_unpacking_after_keyword_arg::star_arg_unpacking_after_keyword_arg;
@@ -31,10 +33,12 @@ mod cannot_raise_literal;
mod duplicate_exceptions;
mod f_string_docstring;
mod function_call_argument_default;
mod function_uses_loop_variable;
mod getattr_with_constant;
mod jump_statement_in_finally;
mod loop_variable_overrides_iterator;
mod mutable_argument_default;
mod raise_without_from_inside_except;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;
mod star_arg_unpacking_after_keyword_arg;

View File

@@ -0,0 +1,56 @@
use rustpython_ast::{ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor::Visitor;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::python::string::is_lower;
struct RaiseVisitor {
checks: Vec<Check>,
}
impl<'a> Visitor<'a> for RaiseVisitor {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match &stmt.node {
StmtKind::Raise { exc, cause } => {
if cause.is_none() {
if let Some(exc) = exc {
match &exc.node {
ExprKind::Name { id, .. } if is_lower(id) => {}
_ => {
self.checks.push(Check::new(
CheckKind::RaiseWithoutFromInsideExcept,
Range::from_located(stmt),
));
}
}
}
}
}
StmtKind::ClassDef { .. }
| StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::Try { .. } => {}
StmtKind::If { body, .. }
| StmtKind::While { body, .. }
| StmtKind::With { body, .. }
| StmtKind::AsyncWith { body, .. }
| StmtKind::For { body, .. }
| StmtKind::AsyncFor { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
}
_ => {}
}
}
}
pub fn raise_without_from_inside_except(checker: &mut Checker, body: &[Stmt]) {
let mut visitor = RaiseVisitor { checks: vec![] };
for stmt in body {
visitor.visit_stmt(stmt);
}
checker.add_checks(visitor.checks.into_iter());
}

View File

@@ -39,9 +39,7 @@ pub fn fix_unnecessary_generator_list(
let call = match_call(body)?;
let arg = match_arg(call)?;
let generator_exp = if let Expression::GeneratorExp(generator_exp) = &arg.value {
generator_exp
} else {
let Expression::GeneratorExp(generator_exp) = &arg.value else {
return Err(anyhow::anyhow!(
"Expected node to be: Expression::GeneratorExp"
));
@@ -82,9 +80,7 @@ pub fn fix_unnecessary_generator_set(
let call = match_call(body)?;
let arg = match_arg(call)?;
let generator_exp = if let Expression::GeneratorExp(generator_exp) = &arg.value {
generator_exp
} else {
let Expression::GeneratorExp(generator_exp) = &arg.value else {
return Err(anyhow::anyhow!(
"Expected node to be: Expression::GeneratorExp"
));
@@ -126,28 +122,20 @@ pub fn fix_unnecessary_generator_dict(
let arg = match_arg(call)?;
// Extract the (k, v) from `(k, v) for ...`.
let generator_exp = if let Expression::GeneratorExp(generator_exp) = &arg.value {
generator_exp
} else {
let Expression::GeneratorExp(generator_exp) = &arg.value else {
return Err(anyhow::anyhow!(
"Expected node to be: Expression::GeneratorExp"
));
};
let tuple = if let Expression::Tuple(tuple) = &generator_exp.elt.as_ref() {
tuple
} else {
let Expression::Tuple(tuple) = &generator_exp.elt.as_ref() else {
return Err(anyhow::anyhow!("Expected node to be: Expression::Tuple"));
};
let key = if let Some(Element::Simple { value, .. }) = &tuple.elements.get(0) {
value
} else {
let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else {
return Err(anyhow::anyhow!(
"Expected tuple to contain a key as the first element"
));
};
let value = if let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) {
value
} else {
let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else {
return Err(anyhow::anyhow!(
"Expected tuple to contain a key as the second element"
));
@@ -192,9 +180,7 @@ pub fn fix_unnecessary_list_comprehension_set(
let call = match_call(body)?;
let arg = match_arg(call)?;
let list_comp = if let Expression::ListComp(list_comp) = &arg.value {
list_comp
} else {
let Expression::ListComp(list_comp) = &arg.value else {
return Err(anyhow::anyhow!("Expected node to be: Expression::ListComp"));
};
@@ -233,25 +219,18 @@ pub fn fix_unnecessary_list_comprehension_dict(
let call = match_call(body)?;
let arg = match_arg(call)?;
let list_comp = if let Expression::ListComp(list_comp) = &arg.value {
list_comp
} else {
let Expression::ListComp(list_comp) = &arg.value else {
return Err(anyhow::anyhow!("Expected node to be: Expression::ListComp"));
};
let tuple = if let Expression::Tuple(tuple) = &*list_comp.elt {
tuple
} else {
let Expression::Tuple(tuple) = &*list_comp.elt else {
return Err(anyhow::anyhow!("Expected node to be: Expression::Tuple"));
};
let (key, comma, value) = match &tuple.elements[..] {
[Element::Simple {
let [Element::Simple {
value: key,
comma: Some(comma),
}, Element::Simple { value, .. }] => (key, comma, value),
_ => return Err(anyhow::anyhow!("Expected tuple with two elements")),
};
}, Element::Simple { value, .. }] = &tuple.elements[..] else { return Err(anyhow::anyhow!("Expected tuple with two elements")) };
body.value = Expression::DictComp(Box::new(DictComp {
key: Box::new(key.clone()),
@@ -409,9 +388,7 @@ pub fn fix_unnecessary_collection_call(
let mut tree = match_module(&module_text)?;
let mut body = match_expr(&mut tree)?;
let call = match_call(body)?;
let name = if let Expression::Name(name) = &call.func.as_ref() {
name
} else {
let Expression::Name(name) = &call.func.as_ref() else {
return Err(anyhow::anyhow!("Expected node to be: Expression::Name"));
};

View File

@@ -0,0 +1,64 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Expr, Stmt};
use crate::ast::helpers::{collect_call_paths, dealias_call_path, match_call_path};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::flake8_debugger::types::DebuggerUsingType;
const DEBUGGERS: &[(&str, &str)] = &[
("pdb", "set_trace"),
("pudb", "set_trace"),
("ipdb", "set_trace"),
("ipdb", "sset_trace"),
("IPython.terminal.embed", "InteractiveShellEmbed"),
("IPython.frontend.terminal.embed", "InteractiveShellEmbed"),
("celery.contrib.rdb", "set_trace"),
("builtins", "breakpoint"),
("", "breakpoint"),
];
/// Checks for the presence of a debugger call.
pub fn debugger_call(
expr: &Expr,
func: &Expr,
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
) -> Option<Check> {
let call_path = dealias_call_path(collect_call_paths(func), import_aliases);
if DEBUGGERS
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
{
Some(Check::new(
CheckKind::Debugger(DebuggerUsingType::Call(call_path.join("."))),
Range::from_located(expr),
))
} else {
None
}
}
/// Checks for the presence of a debugger import.
pub fn debugger_import(stmt: &Stmt, module: Option<&str>, name: &str) -> Option<Check> {
if let Some(module) = module {
if let Some((module_name, member)) = DEBUGGERS
.iter()
.find(|(module_name, member)| module_name == &module && member == &name)
{
return Some(Check::new(
CheckKind::Debugger(DebuggerUsingType::Import(format!("{module_name}.{member}"))),
Range::from_located(stmt),
));
}
} else if DEBUGGERS
.iter()
.any(|(module_name, ..)| module_name == &name)
{
return Some(Check::new(
CheckKind::Debugger(DebuggerUsingType::Import(name.to_string())),
Range::from_located(stmt),
));
}
None
}

View File

@@ -0,0 +1,2 @@
pub mod checks;
pub mod types;

View File

@@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DebuggerUsingType {
Call(String),
Import(String),
}

152
src/fs.rs
View File

@@ -1,16 +1,16 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use globset::GlobMatcher;
use log::debug;
use path_absolutize::{path_dedot, Absolutize};
use rustc_hash::FxHashSet;
use walkdir::{DirEntry, WalkDir};
use crate::checks::CheckCode;
use crate::settings::types::{FilePattern, PerFileIgnore};
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
@@ -25,32 +25,8 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
Ok((file_path, file_basename))
}
fn is_excluded<'a, T>(file_path: &str, file_basename: &str, exclude: T) -> bool
where
T: Iterator<Item = &'a FilePattern>,
{
for pattern in exclude {
match pattern {
FilePattern::Simple(basename) => {
if *basename == file_basename {
return true;
}
}
FilePattern::Complex(absolute, basename) => {
if absolute.matches(file_path) {
return true;
}
if basename
.as_ref()
.map(|pattern| pattern.matches(file_basename))
.unwrap_or_default()
{
return true;
}
}
};
}
false
fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet) -> bool {
exclude.is_match(file_path) || exclude.is_match(file_basename)
}
fn is_included(path: &Path) -> bool {
@@ -60,18 +36,12 @@ fn is_included(path: &Path) -> bool {
pub fn iter_python_files<'a>(
path: &'a Path,
exclude: &'a [FilePattern],
extend_exclude: &'a [FilePattern],
exclude: &'a globset::GlobSet,
extend_exclude: &'a globset::GlobSet,
) -> impl Iterator<Item = Result<DirEntry, walkdir::Error>> + 'a {
// Run some checks over the provided patterns, to enable optimizations below.
let has_exclude = !exclude.is_empty();
let has_extend_exclude = !extend_exclude.is_empty();
let exclude_simple = exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
let extend_exclude_simple = extend_exclude
.iter()
.all(|pattern| matches!(pattern, FilePattern::Simple(_)));
WalkDir::new(normalize_path(path))
.into_iter()
@@ -83,17 +53,11 @@ pub fn iter_python_files<'a>(
let path = entry.path();
match extract_path_names(path) {
Ok((file_path, file_basename)) => {
let file_type = entry.file_type();
if has_exclude
&& (!exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, exclude.iter())
{
if has_exclude && is_excluded(file_path, file_basename, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
&& (!extend_exclude_simple || file_type.is_dir())
&& is_excluded(file_path, file_basename, extend_exclude.iter())
&& is_excluded(file_path, file_basename, extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
@@ -119,19 +83,15 @@ pub fn iter_python_files<'a>(
/// Create tree set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [PerFileIgnore],
) -> Result<FxHashSet<&'a CheckCode>> {
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)],
) -> Result<BTreeSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()
.filter(|pattern_code_pair| {
is_excluded(
file_path,
file_basename,
[&pattern_code_pair.pattern].into_iter(),
)
.filter(|(absolute, basename, _)| {
basename.is_match(file_basename) || absolute.is_match(file_path)
})
.flat_map(|pattern_code_pair| &pattern_code_pair.codes)
.flat_map(|(_, _, codes)| codes)
.collect())
}
@@ -171,9 +131,10 @@ pub(crate) fn read_file(path: &Path) -> Result<String> {
#[cfg(test)]
mod tests {
use std::path::Path;
use std::path::{Path, PathBuf};
use anyhow::Result;
use globset::GlobSet;
use path_absolutize::Absolutize;
use crate::fs::{extract_path_names, is_excluded, is_included};
@@ -194,73 +155,86 @@ mod tests {
assert!(!is_included(&path));
}
fn make_exclusion(file_pattern: FilePattern, project_root: Option<&PathBuf>) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
file_pattern.add_to(&mut builder, project_root).unwrap();
builder.build().unwrap()
}
#[test]
fn exclusions() -> Result<()> {
let project_root = Path::new("/tmp/");
let path = Path::new("foo").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("foo".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"bar",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("bar".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz.py",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("baz.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("foo/bar".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/baz.py",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("foo/bar/baz.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"foo/bar/*.py",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("foo/bar/*.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(is_excluded(file_path, file_basename, exclude.iter()));
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = vec![FilePattern::from_user(
"baz",
Some(&project_root.to_path_buf()),
)?];
let exclude = FilePattern::User("baz".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(file_path, file_basename, exclude.iter()));
assert!(!is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
Ok(())
}

View File

@@ -35,6 +35,7 @@ pub mod checks;
pub mod checks_gen;
pub mod cli;
pub mod code_gen;
pub mod commands;
mod cst;
mod directives;
mod docstrings;
@@ -46,6 +47,7 @@ pub mod flake8_boolean_trap;
pub mod flake8_bugbear;
mod flake8_builtins;
mod flake8_comprehensions;
mod flake8_debugger;
mod flake8_print;
pub mod flake8_quotes;
pub mod flake8_tidy_imports;
@@ -70,6 +72,7 @@ pub mod settings;
pub mod source_code_locator;
#[cfg(feature = "update-informer")]
pub mod updates;
mod vendored;
pub mod visibility;
/// Run Ruff over Python source code directly.
@@ -86,10 +89,10 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_configuration(Configuration::from_pyproject(
pyproject.as_ref(),
let settings = Settings::from_configuration(
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?,
project_root.as_ref(),
)?);
)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
@@ -113,6 +116,7 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
&directives,
&settings,
autofix,
false,
)?;
Ok(checks)

View File

@@ -7,6 +7,7 @@ use std::path::Path;
use anyhow::Result;
#[cfg(not(target_family = "wasm"))]
use log::debug;
use nohash_hasher::IntMap;
use rustpython_parser::lexer::LexResult;
use crate::ast::types::Range;
@@ -46,6 +47,7 @@ impl AddAssign for Diagnostics {
/// Generate a list of `Check` violations from the source code contents at the
/// given `Path`.
#[allow(clippy::too_many_arguments)]
pub(crate) fn check_path(
path: &Path,
contents: &str,
@@ -54,6 +56,7 @@ pub(crate) fn check_path(
directives: &Directives,
settings: &Settings,
autofix: bool,
ignore_noqa: bool,
) -> Result<Vec<Check>> {
// Aggregate all checks.
let mut checks: Vec<Check> = vec![];
@@ -113,6 +116,7 @@ pub(crate) fn check_path(
&directives.noqa_line_for,
settings,
autofix,
ignore_noqa,
);
// Create path ignores.
@@ -179,6 +183,7 @@ pub fn lint_path(
&directives,
settings,
autofix.into(),
false,
)?;
// Apply autofix.
@@ -242,15 +247,19 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
directives::Flags::from_settings(settings),
);
// Generate checks.
// Generate checks, ignoring any existing `noqa` directives.
let checks = check_path(
path,
&contents,
tokens,
&locator,
&directives,
&Directives {
noqa_line_for: IntMap::default(),
isort_exclusions: directives.isort_exclusions,
},
settings,
false,
true,
)?;
add_noqa(&checks, &contents, &directives.noqa_line_for, path)
@@ -313,6 +322,7 @@ pub fn lint_stdin(
&directives,
settings,
autofix.into(),
false,
)?;
// Apply autofix.
@@ -373,6 +383,7 @@ pub fn test_path(path: &Path, settings: &Settings, autofix: bool) -> Result<Vec<
&directives,
settings,
autofix,
false,
)
}
@@ -413,10 +424,12 @@ mod tests {
#[test_case(CheckCode::B020, Path::new("B020.py"); "B020")]
#[test_case(CheckCode::B021, Path::new("B021.py"); "B021")]
#[test_case(CheckCode::B022, Path::new("B022.py"); "B022")]
#[test_case(CheckCode::B023, Path::new("B023.py"); "B023")]
#[test_case(CheckCode::B024, Path::new("B024.py"); "B024")]
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
#[test_case(CheckCode::B904, Path::new("B904.py"); "B904")]
#[test_case(CheckCode::BLE001, Path::new("BLE.py"); "BLE001")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
@@ -505,6 +518,24 @@ mod tests {
#[test_case(CheckCode::F405, Path::new("F405.py"); "F405")]
#[test_case(CheckCode::F406, Path::new("F406.py"); "F406")]
#[test_case(CheckCode::F407, Path::new("F407.py"); "F407")]
#[test_case(CheckCode::F501, Path::new("F50x.py"); "F501")]
#[test_case(CheckCode::F502, Path::new("F50x.py"); "F502_0")]
#[test_case(CheckCode::F502, Path::new("F502.py"); "F502_1")]
#[test_case(CheckCode::F503, Path::new("F50x.py"); "F503_0")]
#[test_case(CheckCode::F503, Path::new("F503.py"); "F503_1")]
#[test_case(CheckCode::F504, Path::new("F50x.py"); "F504_0")]
#[test_case(CheckCode::F504, Path::new("F504.py"); "F504_1")]
#[test_case(CheckCode::F505, Path::new("F50x.py"); "F505_0")]
#[test_case(CheckCode::F505, Path::new("F504.py"); "F505_1")]
#[test_case(CheckCode::F506, Path::new("F50x.py"); "F506")]
#[test_case(CheckCode::F507, Path::new("F50x.py"); "F507")]
#[test_case(CheckCode::F508, Path::new("F50x.py"); "F508")]
#[test_case(CheckCode::F509, Path::new("F50x.py"); "F509")]
#[test_case(CheckCode::F521, Path::new("F521.py"); "F521")]
#[test_case(CheckCode::F522, Path::new("F522.py"); "F522")]
#[test_case(CheckCode::F523, Path::new("F523.py"); "F523")]
#[test_case(CheckCode::F524, Path::new("F524.py"); "F524")]
#[test_case(CheckCode::F525, Path::new("F525.py"); "F525")]
#[test_case(CheckCode::F541, Path::new("F541.py"); "F541")]
#[test_case(CheckCode::F601, Path::new("F601.py"); "F601")]
#[test_case(CheckCode::F602, Path::new("F602.py"); "F602")]
@@ -551,6 +582,7 @@ mod tests {
#[test_case(CheckCode::S105, Path::new("S105.py"); "S105")]
#[test_case(CheckCode::S106, Path::new("S106.py"); "S106")]
#[test_case(CheckCode::S107, Path::new("S107.py"); "S107")]
#[test_case(CheckCode::T100, Path::new("T100.py"); "T100")]
#[test_case(CheckCode::T201, Path::new("T201.py"); "T201")]
#[test_case(CheckCode::T203, Path::new("T203.py"); "T203")]
#[test_case(CheckCode::U001, Path::new("U001.py"); "U001")]
@@ -564,6 +596,7 @@ mod tests {
#[test_case(CheckCode::U009, Path::new("U009_1.py"); "U009_1")]
#[test_case(CheckCode::U009, Path::new("U009_2.py"); "U009_2")]
#[test_case(CheckCode::U009, Path::new("U009_3.py"); "U009_3")]
#[test_case(CheckCode::U009, Path::new("U009_4.py"); "U009_4")]
#[test_case(CheckCode::U010, Path::new("U010.py"); "U010")]
#[test_case(CheckCode::U011, Path::new("U011_0.py"); "U011_0")]
#[test_case(CheckCode::U011, Path::new("U011_1.py"); "U011_1")]

View File

@@ -17,30 +17,26 @@ use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use ::ruff::cache;
use ::ruff::checks::{CheckCode, CheckKind};
use ::ruff::cli::{collect_per_file_ignores, extract_log_level, Cli};
use ::ruff::fs::iter_python_files;
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin};
use ::ruff::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::message::Message;
use ::ruff::printer::{Printer, SerializationFormat};
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::FilePattern;
use ::ruff::settings::user::UserConfiguration;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use ::ruff::{cache, commands};
use anyhow::Result;
use clap::Parser;
use clap::{CommandFactory, Parser};
use colored::Colorize;
use log::{debug, error};
use notify::{raw_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use ruff::linter::Diagnostics;
use rustpython_ast::Location;
use serde::Serialize;
use walkdir::DirEntry;
/// Shim that calls `par_iter` except for wasm because there's no wasm support
@@ -57,60 +53,6 @@ fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
fn show_settings(
configuration: Configuration,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) {
println!(
"{:#?}",
UserConfiguration::from_configuration(configuration, project_root, pyproject)
);
}
#[derive(Serialize)]
struct Explanation<'a> {
code: &'a str,
category: &'a str,
summary: &'a str,
}
fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
match format {
SerializationFormat::Text => {
println!(
"{} ({}): {}",
code.as_ref(),
code.category().title(),
code.kind().summary()
);
}
SerializationFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&Explanation {
code: code.as_ref(),
category: code.category().title(),
summary: &code.kind().summary(),
})?
);
}
};
Ok(())
}
fn show_files(files: &[PathBuf], settings: &Settings) {
let mut entries: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
entries.sort_by(|a, b| a.path().cmp(b.path()));
for entry in entries {
println!("{}", entry.path().to_string_lossy());
}
}
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
@@ -251,6 +193,16 @@ fn inner_main() -> Result<ExitCode> {
let log_level = extract_log_level(&cli);
set_up_logging(&log_level)?;
if let Some(code) = cli.explain {
commands::explain(&code, cli.format)?;
return Ok(ExitCode::SUCCESS);
}
if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut std::io::stdout());
return Ok(ExitCode::SUCCESS);
}
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&cli.files);
match &project_root {
@@ -266,28 +218,16 @@ fn inner_main() -> Result<ExitCode> {
};
// Reconcile configuration from pyproject.toml and command-line arguments.
let exclude: Vec<FilePattern> = cli
.exclude
.iter()
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
.collect::<Result<_>>()?;
let extend_exclude: Vec<FilePattern> = cli
.extend_exclude
.iter()
.map(|path| FilePattern::from_user(path, project_root.as_ref()))
.collect::<Result<_>>()?;
let mut configuration =
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?;
if !exclude.is_empty() {
configuration.exclude = exclude;
if !cli.exclude.is_empty() {
configuration.exclude = cli.exclude;
}
if !extend_exclude.is_empty() {
configuration.extend_exclude = extend_exclude;
if !cli.extend_exclude.is_empty() {
configuration.extend_exclude = cli.extend_exclude;
}
if !cli.per_file_ignores.is_empty() {
configuration.per_file_ignores =
collect_per_file_ignores(cli.per_file_ignores, project_root.as_ref())?;
configuration.per_file_ignores = collect_per_file_ignores(cli.per_file_ignores);
}
if !cli.select.is_empty() {
configuration.select = cli.select;
@@ -326,26 +266,21 @@ fn inner_main() -> Result<ExitCode> {
configuration.show_source = true;
}
if let Some(code) = cli.explain {
explain(&code, cli.format)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
}
if cli.show_settings {
show_settings(configuration, project_root, pyproject);
commands::show_settings(&configuration, project_root.as_ref(), pyproject.as_ref());
return Ok(ExitCode::SUCCESS);
}
// Extract settings for internal use.
let fix_enabled: bool = configuration.fix;
let settings = Settings::from_configuration(configuration);
let settings = Settings::from_configuration(configuration, project_root.as_ref())?;
if cli.show_files {
show_files(&cli.files, &settings);
commands::show_files(&cli.files, &settings);
return Ok(ExitCode::SUCCESS);
}

View File

@@ -99,18 +99,42 @@ fn add_noqa_inner(
Some(codes) => {
match extract_noqa_directive(line) {
Directive::None => {
output.push_str(line);
// Add existing content.
output.push_str(line.trim_end());
// Add `noqa` directive.
output.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> = codes.iter().map(AsRef::as_ref).collect();
let suffix = codes.join(", ");
output.push_str(&suffix);
output.push('\n');
count += 1;
}
Directive::All(_, start, _) | Directive::Codes(_, start, ..) => {
output.push_str(&line[..start]);
output.push_str("# noqa: ");
let mut new_line = String::new();
// Add existing content.
new_line.push_str(line[..start].trim_end());
// Add `noqa` directive.
new_line.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> = codes.iter().map(AsRef::as_ref).collect();
let suffix = codes.join(", ");
new_line.push_str(&suffix);
output.push_str(&new_line);
output.push('\n');
// Only count if the new line is an actual edit.
if &new_line != line {
count += 1;
}
}
};
let codes: Vec<&str> = codes.iter().map(AsRef::as_ref).collect();
output.push_str(&codes.join(", "));
output.push('\n');
count += 1;
}
}
}

View File

@@ -4,7 +4,7 @@ use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashSet;
use rustpython_ast::{Arg, Constant, ExprKind, Location, StmtKind};
use rustpython_ast::{Constant, ExprKind, Location, StmtKind};
use crate::ast::types::Range;
use crate::ast::whitespace;
@@ -15,7 +15,7 @@ use crate::docstrings::constants;
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::docstrings::sections::{section_contexts, SectionContext};
use crate::docstrings::styles::SectionStyle;
use crate::visibility::{is_init, is_magic, is_overload, is_staticmethod, Visibility};
use crate::visibility::{is_init, is_magic, is_overload, is_override, is_staticmethod, Visibility};
/// D100, D101, D102, D103, D104, D105, D106, D107
pub fn not_missing(
@@ -88,7 +88,7 @@ pub fn not_missing(
}
}
DefinitionKind::Method(stmt) => {
if is_overload(stmt) {
if is_overload(stmt) || is_override(stmt) {
true
} else if is_magic(stmt) {
if checker.settings.enabled.contains(&CheckCode::D105) {
@@ -1303,8 +1303,9 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args:
args: arguments, ..
} = &parent.node
{
// Collect all the arguments into a single vector.
let mut all_arguments: Vec<&Arg> = arguments
// Look for arguments that weren't included in the docstring.
let mut missing_arg_names: BTreeSet<String> = BTreeSet::default();
for arg in arguments
.args
.iter()
.chain(arguments.posonlyargs.iter())
@@ -1316,33 +1317,38 @@ fn missing_args(checker: &mut Checker, definition: &Definition, docstrings_args:
&& !is_staticmethod(parent),
),
)
.collect();
{
let arg_name = arg.node.arg.as_str();
if !arg_name.starts_with('_') && !docstrings_args.contains(&arg_name) {
missing_arg_names.insert(arg_name.to_string());
}
}
// Check specifically for `vararg` and `kwarg`, which can be prefixed with a
// single or double star, respectively.
if let Some(arg) = &arguments.vararg {
all_arguments.push(arg);
let arg_name = arg.node.arg.as_str();
let starred_arg_name = format!("*{arg_name}");
if !arg_name.starts_with('_')
&& !docstrings_args.contains(&arg_name)
&& !docstrings_args.contains(&starred_arg_name.as_str())
{
missing_arg_names.insert(starred_arg_name);
}
}
if let Some(arg) = &arguments.kwarg {
all_arguments.push(arg);
}
// Look for arguments that weren't included in the docstring.
let mut missing_args: BTreeSet<&str> = BTreeSet::default();
for arg in all_arguments {
let arg_name = arg.node.arg.as_str();
if arg_name.starts_with('_') {
continue;
let starred_arg_name = format!("**{arg_name}");
if !arg_name.starts_with('_')
&& !docstrings_args.contains(&arg_name)
&& !docstrings_args.contains(&starred_arg_name.as_str())
{
missing_arg_names.insert(starred_arg_name);
}
if docstrings_args.contains(&arg_name) {
continue;
}
missing_args.insert(arg_name);
}
if !missing_args.is_empty() {
let names = missing_args
.into_iter()
.map(String::from)
.sorted()
.collect();
if !missing_arg_names.is_empty() {
let names = missing_arg_names.into_iter().sorted().collect();
checker.add_check(Check::new(
CheckKind::DocumentAllArguments(names),
Range::from_located(parent),

102
src/pyflakes/cformat.rs Normal file
View File

@@ -0,0 +1,102 @@
//! Implements helper functions for using vendored/cformat.rs
use std::convert::TryFrom;
use std::str::FromStr;
use rustc_hash::FxHashSet;
use crate::vendored::cformat::{
CFormatError, CFormatPart, CFormatQuantity, CFormatSpec, CFormatString,
};
pub(crate) struct CFormatSummary {
pub starred: bool,
pub num_positional: usize,
pub keywords: FxHashSet<String>,
}
impl TryFrom<&str> for CFormatSummary {
type Error = CFormatError;
fn try_from(literal: &str) -> Result<Self, Self::Error> {
let format_string = CFormatString::from_str(literal)?;
let mut starred = false;
let mut num_positional = 0;
let mut keywords = FxHashSet::default();
for format_part in format_string.parts {
if let CFormatPart::Spec(CFormatSpec {
mapping_key,
min_field_width,
precision,
..
}) = format_part.1
{
match mapping_key {
Some(k) => {
keywords.insert(k);
}
None => {
num_positional += 1;
}
};
if min_field_width == Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
}
if precision == Some(CFormatQuantity::FromValuesTuple) {
num_positional += 1;
starred = true;
}
}
}
Ok(CFormatSummary {
starred,
num_positional,
keywords,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cformat_summary() {
let literal = "%(foo)s %s %d %(bar)x";
let expected_positional = 2;
let expected_keywords = ["foo", "bar"].into_iter().map(String::from).collect();
let format_summary = CFormatSummary::try_from(literal).unwrap();
assert!(!format_summary.starred);
assert_eq!(format_summary.num_positional, expected_positional);
assert_eq!(format_summary.keywords, expected_keywords);
}
#[test]
fn test_cformat_summary_starred() {
let format_summary1 = CFormatSummary::try_from("%*s %*d").unwrap();
assert!(format_summary1.starred);
assert_eq!(format_summary1.num_positional, 4);
let format_summary2 = CFormatSummary::try_from("%s %.*d").unwrap();
assert!(format_summary2.starred);
assert_eq!(format_summary2.num_positional, 3);
let format_summary3 = CFormatSummary::try_from("%s %*.*d").unwrap();
assert!(format_summary3.starred);
assert_eq!(format_summary3.num_positional, 4);
let format_summary4 = CFormatSummary::try_from("%s %1d").unwrap();
assert!(!format_summary4.starred);
}
#[test]
fn test_cformat_summary_invalid() {
assert!(CFormatSummary::try_from("%").is_err());
assert!(CFormatSummary::try_from("%(foo).").is_err());
}
}

View File

@@ -1,11 +1,353 @@
use std::string::ToString;
use regex::Regex;
use rustc_hash::FxHashSet;
use rustpython_ast::{Keyword, KeywordData};
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
};
use crate::ast::types::{BindingKind, FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pyflakes::cformat::CFormatSummary;
use crate::pyflakes::format::FormatSummary;
fn has_star_star_kwargs(keywords: &[Keyword]) -> bool {
keywords.iter().any(|k| {
let KeywordData { arg, .. } = &k.node;
arg.is_none()
})
}
fn has_star_args(args: &[Expr]) -> bool {
args.iter()
.any(|a| matches!(&a.node, ExprKind::Starred { .. }))
}
/// F502
pub(crate) fn percent_format_expected_mapping(
summary: &CFormatSummary,
right: &Expr,
location: Range,
) -> Option<Check> {
if summary.keywords.is_empty() {
None
} else {
// Tuple, List, Set (+comprehensions)
match right.node {
ExprKind::List { .. }
| ExprKind::Tuple { .. }
| ExprKind::Set { .. }
| ExprKind::ListComp { .. }
| ExprKind::SetComp { .. }
| ExprKind::GeneratorExp { .. } => Some(Check::new(
CheckKind::PercentFormatExpectedMapping,
location,
)),
_ => None,
}
}
}
/// F503
pub(crate) fn percent_format_expected_sequence(
summary: &CFormatSummary,
right: &Expr,
location: Range,
) -> Option<Check> {
if summary.num_positional <= 1 {
None
} else {
match right.node {
ExprKind::Dict { .. } | ExprKind::DictComp { .. } => Some(Check::new(
CheckKind::PercentFormatExpectedSequence,
location,
)),
_ => None,
}
}
}
/// F504
pub(crate) fn percent_format_extra_named_arguments(
summary: &CFormatSummary,
right: &Expr,
location: Range,
) -> Option<Check> {
if summary.num_positional > 0 {
return None;
}
if let ExprKind::Dict { keys, values } = &right.node {
if values.len() > keys.len() {
return None; // contains **x splat
}
let missing: Vec<&String> = keys
.iter()
.filter_map(|k| match &k.node {
// We can only check that string literals exist
ExprKind::Constant {
value: Constant::Str(value),
..
} => {
if summary.keywords.contains(value) {
None
} else {
Some(value)
}
}
_ => None,
})
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::PercentFormatExtraNamedArguments(
missing.iter().map(|&s| s.clone()).collect(),
),
location,
))
}
} else {
None
}
}
/// F505
pub(crate) fn percent_format_missing_arguments(
summary: &CFormatSummary,
right: &Expr,
location: Range,
) -> Option<Check> {
if summary.num_positional > 0 {
return None;
}
if let ExprKind::Dict { keys, values } = &right.node {
if values.len() > keys.len() {
return None; // contains **x splat
}
let mut keywords = FxHashSet::default();
for key in keys {
match &key.node {
ExprKind::Constant {
value: Constant::Str(value),
..
} => {
keywords.insert(value);
}
_ => {
return None; // Dynamic keys present
}
}
}
let missing: Vec<&String> = summary
.keywords
.iter()
.filter(|k| !keywords.contains(k))
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::PercentFormatMissingArgument(
missing.iter().map(|&s| s.clone()).collect(),
),
location,
))
}
} else {
None
}
}
/// F506
pub(crate) fn percent_format_mixed_positional_and_named(
summary: &CFormatSummary,
location: Range,
) -> Option<Check> {
if summary.num_positional == 0 || summary.keywords.is_empty() {
None
} else {
Some(Check::new(
CheckKind::PercentFormatMixedPositionalAndNamed,
location,
))
}
}
/// F507
pub(crate) fn percent_format_positional_count_mismatch(
summary: &CFormatSummary,
right: &Expr,
location: Range,
) -> Option<Check> {
if !summary.keywords.is_empty() {
return None;
}
match &right.node {
ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } | ExprKind::Set { elts, .. } => {
let mut found = 0;
for elt in elts {
if let ExprKind::Starred { .. } = &elt.node {
return None;
}
found += 1;
}
if found == summary.num_positional {
None
} else {
Some(Check::new(
CheckKind::PercentFormatPositionalCountMismatch(summary.num_positional, found),
location,
))
}
}
_ => None,
}
}
/// F508
pub(crate) fn percent_format_star_requires_sequence(
summary: &CFormatSummary,
right: &Expr,
location: Range,
) -> Option<Check> {
if summary.starred {
match &right.node {
ExprKind::Dict { .. } | ExprKind::DictComp { .. } => Some(Check::new(
CheckKind::PercentFormatStarRequiresSequence,
location,
)),
_ => None,
}
} else {
None
}
}
/// F522
pub(crate) fn string_dot_format_extra_named_arguments(
summary: &FormatSummary,
keywords: &[Keyword],
location: Range,
) -> Option<Check> {
if has_star_star_kwargs(keywords) {
return None;
}
let keywords = keywords.iter().filter_map(|k| {
let KeywordData { arg, .. } = &k.node;
arg.as_ref()
});
let missing: Vec<&String> = keywords
.filter(|&k| !summary.keywords.contains(k))
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::StringDotFormatExtraNamedArguments(
missing.iter().map(|&s| s.clone()).collect(),
),
location,
))
}
}
/// F523
pub(crate) fn string_dot_format_extra_positional_arguments(
summary: &FormatSummary,
args: &[Expr],
location: Range,
) -> Option<Check> {
if has_star_args(args) {
return None;
}
let missing: Vec<String> = (0..args.len())
.filter(|i| !(summary.autos.contains(i) || summary.indexes.contains(i)))
.map(|i| i.to_string())
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::StringDotFormatExtraPositionalArguments(missing),
location,
))
}
}
/// F524
pub(crate) fn string_dot_format_missing_argument(
summary: &FormatSummary,
args: &[Expr],
keywords: &[Keyword],
location: Range,
) -> Option<Check> {
if has_star_args(args) || has_star_star_kwargs(keywords) {
return None;
}
let keywords: FxHashSet<_> = keywords
.iter()
.filter_map(|k| {
let KeywordData { arg, .. } = &k.node;
arg.as_ref()
})
.collect();
let missing: Vec<String> = summary
.autos
.iter()
.chain(summary.indexes.iter())
.filter(|&&i| i >= args.len())
.map(ToString::to_string)
.chain(
summary
.keywords
.iter()
.filter(|k| !keywords.contains(k))
.cloned(),
)
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::StringDotFormatMissingArguments(missing),
location,
))
}
}
/// F525
pub(crate) fn string_dot_format_mixing_automatic(
summary: &FormatSummary,
location: Range,
) -> Option<Check> {
if summary.autos.is_empty() || summary.indexes.is_empty() {
None
} else {
Some(Check::new(
CheckKind::StringDotFormatMixingAutomatic,
location,
))
}
}
/// F631
pub fn assert_tuple(test: &Expr, location: Range) -> Option<Check> {

View File

@@ -22,14 +22,10 @@ pub fn remove_unused_imports(
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
body
} else {
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple"));
};
let body = if let Some(SmallStatement::Import(body)) = body.body.first_mut() {
body
} else {
let Some(SmallStatement::Import(body)) = body.body.first_mut() else {
return Err(anyhow::anyhow!(
"Expected node to be: SmallStatement::ImportFrom"
));
@@ -80,22 +76,16 @@ pub fn remove_unused_import_froms(
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
body
} else {
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple"));
};
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
body
} else {
let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() else {
return Err(anyhow::anyhow!(
"Expected node to be: SmallStatement::ImportFrom"
));
};
let aliases = if let ImportNames::Aliases(aliases) = &mut body.names {
aliases
} else {
let ImportNames::Aliases(aliases) = &mut body.names else {
return Err(anyhow::anyhow!("Expected node to be: Aliases"));
};

134
src/pyflakes/format.rs Normal file
View File

@@ -0,0 +1,134 @@
//! Implements helper functions for using vendored/format.rs
use std::convert::TryFrom;
use std::fmt;
use rustc_hash::FxHashSet;
use crate::vendored::format::{
FieldName, FieldType, FormatParseError, FormatPart, FormatString, FromTemplate,
};
impl fmt::Display for FormatParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
FormatParseError::EmptyAttribute => "Empty attribute in format string",
FormatParseError::InvalidCharacterAfterRightBracket => {
"Only '.' or '[' may follow ']' in format field specifier"
}
FormatParseError::InvalidFormatSpecifier => "Max string recursion exceeded",
FormatParseError::MissingStartBracket => "Single '}' encountered in format string",
FormatParseError::MissingRightBracket => "Expected '}' before end of string",
FormatParseError::UnmatchedBracket => "Single '{' encountered in format string",
_ => "Unexpected error parsing format string",
};
write!(f, "{message}")
}
}
pub(crate) struct FormatSummary {
pub autos: FxHashSet<usize>,
pub indexes: FxHashSet<usize>,
pub keywords: FxHashSet<String>,
}
impl TryFrom<&str> for FormatSummary {
type Error = FormatParseError;
fn try_from(literal: &str) -> Result<Self, Self::Error> {
let format_string = FormatString::from_str(literal)?;
let mut autos = FxHashSet::default();
let mut indexes = FxHashSet::default();
let mut keywords = FxHashSet::default();
for format_part in format_string.format_parts {
if let FormatPart::Field {
field_name,
format_spec,
..
} = format_part
{
let parsed = FieldName::parse(&field_name)?;
match parsed.field_type {
FieldType::Auto => autos.insert(autos.len()),
FieldType::Index(i) => indexes.insert(i),
FieldType::Keyword(k) => keywords.insert(k),
};
let nested = FormatString::from_str(&format_spec)?;
for nested_part in nested.format_parts {
if let FormatPart::Field { field_name, .. } = nested_part {
let parsed = FieldName::parse(&field_name)?;
match parsed.field_type {
FieldType::Auto => autos.insert(autos.len()),
FieldType::Index(i) => indexes.insert(i),
FieldType::Keyword(k) => keywords.insert(k),
};
}
}
}
}
Ok(FormatSummary {
autos,
indexes,
keywords,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vendored::format::FromTemplate;
#[test]
fn test_format_summary() {
let literal = "foo{foo}a{}b{2}c{2}d{1}{}{}e{bar}{foo}f{spam}";
let expected_autos = [0usize, 1usize, 2usize].into_iter().collect();
let expected_indexes = [1usize, 2usize].into_iter().collect();
let expected_keywords = ["foo", "bar", "spam"]
.into_iter()
.map(String::from)
.collect();
let format_summary = FormatSummary::try_from(literal).unwrap();
assert_eq!(format_summary.autos, expected_autos);
assert_eq!(format_summary.indexes, expected_indexes);
assert_eq!(format_summary.keywords, expected_keywords);
}
#[test]
fn test_format_summary_nested() {
let literal = "foo{foo}a{:{}{}}b{2:{3}{4}}c{2}d{1}{}e{bar:{spam}{eggs}}";
let expected_autos = [0usize, 1usize, 2usize, 3usize].into_iter().collect();
let expected_indexes = [1usize, 2usize, 3usize, 4usize].into_iter().collect();
let expected_keywords = ["foo", "bar", "spam", "eggs"]
.into_iter()
.map(String::from)
.collect();
let format_summary = FormatSummary::try_from(literal).unwrap();
assert_eq!(format_summary.autos, expected_autos);
assert_eq!(format_summary.indexes, expected_indexes);
assert_eq!(format_summary.keywords, expected_keywords);
}
#[test]
fn test_format_summary_invalid() {
assert!(FormatSummary::try_from("{").is_err());
let literal = "{foo}a{}b{bar..}";
assert!(FormatString::from_str(literal).is_ok());
assert!(FormatSummary::try_from(literal).is_err());
let literal_nested = "{foo}a{}b{bar:{spam..}}";
assert!(FormatString::from_str(literal_nested).is_ok());
assert!(FormatSummary::try_from(literal_nested).is_err());
}
}

View File

@@ -1,3 +1,5 @@
pub mod cformat;
pub mod checks;
pub mod fixes;
pub mod format;
pub mod plugins;

View File

@@ -209,15 +209,21 @@ pub enum SubscriptKind {
PEP593AnnotatedSubscript,
}
pub fn match_annotated_subscript(
pub fn match_annotated_subscript<F>(
expr: &Expr,
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
) -> Option<SubscriptKind> {
is_builtin: F,
) -> Option<SubscriptKind>
where
F: Fn(&str) -> bool,
{
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
if !call_path.is_empty() {
for (module, member) in SUBSCRIPTS {
if match_call_path(&call_path, module, member, from_imports) {
if match_call_path(&call_path, module, member, from_imports)
&& (!module.is_empty() || is_builtin(member))
{
return Some(SubscriptKind::AnnotatedSubscript);
}
}

View File

@@ -111,10 +111,7 @@ pub fn remove_super_arguments(locator: &SourceCodeLocator, expr: &Expr) -> Optio
let range = Range::from_located(expr);
let contents = locator.slice_source_code_range(&range);
let mut tree = match libcst_native::parse_module(&contents, None) {
Ok(m) => m,
Err(_) => return None,
};
let mut tree = libcst_native::parse_module(&contents, None).ok()?;
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
@@ -150,22 +147,16 @@ pub fn remove_unnecessary_future_import(
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
body
} else {
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple"));
};
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
body
} else {
let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() else {
return Err(anyhow::anyhow!(
"Expected node to be: SmallStatement::ImportFrom"
));
};
let aliases = if let ImportNames::Aliases(aliases) = &mut body.names {
aliases
} else {
let ImportNames::Aliases(aliases) = &mut body.names else {
return Err(anyhow::anyhow!("Expected node to be: Aliases"));
};

View File

@@ -2,7 +2,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Result};
use log::error;
use rustpython_ast::{Constant, Expr, ExprKind, Located, Location};
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, KeywordData, Location};
use rustpython_parser::lexer;
use rustpython_parser::token::Tok;
@@ -14,6 +14,7 @@ use crate::checks::{Check, CheckCode, CheckKind};
use crate::source_code_locator::SourceCodeLocator;
const OPEN_FUNC_NAME: &str = "open";
const MODE_KEYWORD_ARGUMENT: &str = "mode";
enum OpenMode {
U,
@@ -56,15 +57,19 @@ impl OpenMode {
}
}
fn match_open(expr: &Expr) -> Option<&Expr> {
if let ExprKind::Call { func, args, .. } = &expr.node {
// TODO(andberger): Verify that "open" is still bound to the built-in function.
fn match_open(expr: &Expr) -> (Option<&Expr>, Vec<Keyword>) {
if let ExprKind::Call {
func,
args,
keywords,
} = &expr.node
{
if match_name_or_attr(func, OPEN_FUNC_NAME) {
// Return the "open mode" parameter.
return args.get(1);
// Return the "open mode" parameter and keywords.
return (args.get(1), keywords.clone());
}
}
None
(None, vec![])
}
fn create_check(
@@ -101,19 +106,36 @@ fn create_remove_param_fix(
location: expr.location,
end_location: expr.end_location.unwrap(),
});
// Find the last comma before mode_param
// and delete that comma as well as mode_param.
// Find the last comma before mode_param and create a deletion fix
// starting from the comma and ending after mode_param.
let mut fix_start: Option<Location> = None;
let mut fix_end: Option<Location> = None;
let mut is_first_arg: bool = false;
let mut delete_first_arg: bool = false;
for (start, tok, end) in lexer::make_tokenizer(&content).flatten() {
let start = helpers::to_absolute(start, expr.location);
let end = helpers::to_absolute(end, expr.location);
if start == mode_param.location {
if is_first_arg {
delete_first_arg = true;
continue;
}
fix_end = Some(end);
break;
}
if delete_first_arg && matches!(tok, Tok::Name { .. }) {
fix_end = Some(start);
break;
}
if matches!(tok, Tok::Lpar) {
is_first_arg = true;
fix_start = Some(end);
}
if matches!(tok, Tok::Comma) {
fix_start = Some(start);
is_first_arg = false;
if !delete_first_arg {
fix_start = Some(start);
}
}
}
match (fix_start, fix_end) {
@@ -126,20 +148,45 @@ fn create_remove_param_fix(
/// U015
pub fn redundant_open_modes(checker: &mut Checker, expr: &Expr) {
// TODO(andberger): Add "mode" keyword argument handling to handle invocations
// on the following formats:
// - `open("foo", mode="U")`
// - `open(name="foo", mode="U")`
// - `open(mode="U", name="foo")`
if let Some(mode_param) = match_open(expr) {
if let Located {
node:
ExprKind::Constant {
value: Constant::Str(mode_param_value),
..
},
// If `open` has been rebound, skip this check entirely.
if !checker.is_builtin(OPEN_FUNC_NAME) {
return;
}
let (mode_param, keywords): (Option<&Expr>, Vec<Keyword>) = match_open(expr);
if mode_param.is_none() && !keywords.is_empty() {
if let Some(value) = keywords.iter().find_map(|keyword| {
let KeywordData { arg, value } = &keyword.node;
if arg
.as_ref()
.map(|arg| arg == MODE_KEYWORD_ARGUMENT)
.unwrap_or_default()
{
Some(value)
} else {
None
}
}) {
if let ExprKind::Constant {
value: Constant::Str(mode_param_value),
..
} = &value.node
{
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
checker.add_check(create_check(
expr,
value,
mode.replacement_value(),
checker.locator,
checker.patch(&CheckCode::U015),
));
}
}
}
} else if let Some(mode_param) = mode_param {
if let ExprKind::Constant {
value: Constant::Str(mode_param_value),
..
} = mode_param
} = &mode_param.node
{
if let Ok(mode) = OpenMode::from_str(mode_param_value.as_str()) {
checker.add_check(create_check(

View File

@@ -21,12 +21,7 @@ fn is_module_star_imported(checker: &Checker, module: &str) -> bool {
/// Return `true` if `exit` is (still) bound as a built-in in the current scope.
fn has_builtin_exit_in_scope(checker: &Checker) -> bool {
!is_module_star_imported(checker, "sys")
&& checker
.current_scopes()
.find_map(|scope| scope.values.get("exit"))
.map(|binding| matches!(binding.kind, BindingKind::Builtin))
.unwrap_or_default()
!is_module_star_imported(checker, "sys") && checker.is_builtin("exit")
}
/// Return the appropriate `sys.exit` reference based on the current set of

View File

@@ -9,7 +9,7 @@ use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::checks_gen::{CheckCodePrefix, CATEGORIES};
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{
@@ -46,25 +46,25 @@ pub struct Configuration {
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
FilePattern::Builtin(".bzr"),
FilePattern::Builtin(".direnv"),
FilePattern::Builtin(".eggs"),
FilePattern::Builtin(".git"),
FilePattern::Builtin(".hg"),
FilePattern::Builtin(".mypy_cache"),
FilePattern::Builtin(".nox"),
FilePattern::Builtin(".pants.d"),
FilePattern::Builtin(".ruff_cache"),
FilePattern::Builtin(".svn"),
FilePattern::Builtin(".tox"),
FilePattern::Builtin(".venv"),
FilePattern::Builtin("__pypackages__"),
FilePattern::Builtin("_build"),
FilePattern::Builtin("buck-out"),
FilePattern::Builtin("build"),
FilePattern::Builtin("dist"),
FilePattern::Builtin("node_modules"),
FilePattern::Builtin("venv"),
]
});
@@ -103,50 +103,21 @@ impl Configuration {
},
),
target_version: options.target_version.unwrap_or(PythonVersion::Py310),
exclude: options
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.transpose()?
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
exclude: options.exclude.map_or_else(
|| DEFAULT_EXCLUDE.clone(),
|paths| paths.into_iter().map(FilePattern::User).collect(),
),
extend_exclude: options
.extend_exclude
.unwrap_or_default()
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect::<Result<_>>()?,
.map(|paths| paths.into_iter().map(FilePattern::User).collect())
.unwrap_or_default(),
extend_ignore: options.extend_ignore.unwrap_or_default(),
select: options
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F]),
extend_select: options.extend_select.unwrap_or_default(),
fix: options.fix.unwrap_or_default(),
fixable: options.fixable.unwrap_or_else(|| {
// TODO(charlie): Autogenerate this list.
vec![
CheckCodePrefix::A,
CheckCodePrefix::B,
CheckCodePrefix::BLE,
CheckCodePrefix::C,
CheckCodePrefix::D,
CheckCodePrefix::E,
CheckCodePrefix::F,
CheckCodePrefix::I,
CheckCodePrefix::M,
CheckCodePrefix::N,
CheckCodePrefix::Q,
CheckCodePrefix::RUF,
CheckCodePrefix::S,
CheckCodePrefix::T,
CheckCodePrefix::U,
CheckCodePrefix::W,
CheckCodePrefix::YTT,
]
}),
fixable: options.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
unfixable: options.unfixable.unwrap_or_default(),
ignore: options.ignore.unwrap_or_default(),
line_length: options.line_length.unwrap_or(88),
@@ -154,13 +125,10 @@ impl Configuration {
.per_file_ignores
.map(|per_file_ignores| {
per_file_ignores
.iter()
.map(|(pattern, prefixes)| {
PerFileIgnore::new(pattern, prefixes, project_root)
})
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
.collect()
})
.transpose()?
.unwrap_or_default(),
show_source: options.show_source.unwrap_or_default(),
// Plugins

View File

@@ -2,9 +2,12 @@
//! command-line options. Structure is optimized for internal usage, as opposed
//! to external visibility or parsing.
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use anyhow::Result;
use globset::{Glob, GlobMatcher, GlobSet};
use path_absolutize::path_dedot;
use regex::Regex;
use rustc_hash::FxHashSet;
@@ -14,7 +17,7 @@ use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, isort, mccabe,
flake8_annotations, flake8_bugbear, flake8_quotes, flake8_tidy_imports, fs, isort, mccabe,
pep8_naming,
};
@@ -22,17 +25,16 @@ pub mod configuration;
pub mod options;
pub mod pyproject;
pub mod types;
pub mod user;
#[derive(Debug)]
pub struct Settings {
pub dummy_variable_rgx: Regex,
pub enabled: FxHashSet<CheckCode>,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub exclude: GlobSet,
pub extend_exclude: GlobSet,
pub fixable: FxHashSet<CheckCode>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)>,
pub show_source: bool,
pub src: Vec<PathBuf>,
pub target_version: PythonVersion,
@@ -47,8 +49,11 @@ pub struct Settings {
}
impl Settings {
pub fn from_configuration(config: Configuration) -> Self {
Self {
pub fn from_configuration(
config: Configuration,
project_root: Option<&PathBuf>,
) -> Result<Self> {
Ok(Self {
dummy_variable_rgx: config.dummy_variable_rgx,
enabled: resolve_codes(
&config
@@ -62,8 +67,8 @@ impl Settings {
.chain(config.extend_ignore.into_iter())
.collect::<Vec<_>>(),
),
exclude: config.exclude,
extend_exclude: config.extend_exclude,
exclude: resolve_globset(config.exclude, project_root)?,
extend_exclude: resolve_globset(config.extend_exclude, project_root)?,
fixable: resolve_codes(&config.fixable, &config.unfixable),
flake8_annotations: config.flake8_annotations,
flake8_bugbear: config.flake8_bugbear,
@@ -73,11 +78,11 @@ impl Settings {
mccabe: config.mccabe,
line_length: config.line_length,
pep8_naming: config.pep8_naming,
per_file_ignores: config.per_file_ignores,
per_file_ignores: resolve_per_file_ignores(config.per_file_ignores, project_root)?,
src: config.src,
target_version: config.target_version,
show_source: config.show_source,
}
})
}
pub fn for_rule(check_code: CheckCode) -> Self {
@@ -85,8 +90,8 @@ impl Settings {
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: FxHashSet::from_iter([check_code.clone()]),
fixable: FxHashSet::from_iter([check_code]),
exclude: vec![],
extend_exclude: vec![],
exclude: GlobSet::empty(),
extend_exclude: GlobSet::empty(),
line_length: 88,
per_file_ignores: vec![],
src: vec![path_dedot::CWD.clone()],
@@ -107,8 +112,8 @@ impl Settings {
dummy_variable_rgx: Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap(),
enabled: FxHashSet::from_iter(check_codes.clone()),
fixable: FxHashSet::from_iter(check_codes),
exclude: vec![],
extend_exclude: vec![],
exclude: GlobSet::empty(),
extend_exclude: GlobSet::empty(),
line_length: 88,
per_file_ignores: vec![],
src: vec![path_dedot::CWD.clone()],
@@ -136,8 +141,10 @@ impl Hash for Settings {
value.hash(state);
}
self.line_length.hash(state);
for value in &self.per_file_ignores {
value.hash(state);
for (absolute, basename, codes) in &self.per_file_ignores {
absolute.glob().hash(state);
basename.glob().hash(state);
codes.hash(state);
}
self.show_source.hash(state);
self.target_version.hash(state);
@@ -152,6 +159,42 @@ impl Hash for Settings {
}
}
/// Given a list of patterns, create a `GlobSet`.
pub fn resolve_globset(
patterns: Vec<FilePattern>,
project_root: Option<&PathBuf>,
) -> Result<GlobSet> {
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
pattern.add_to(&mut builder, project_root)?;
}
builder.build().map_err(std::convert::Into::into)
}
/// Given a list of patterns, create a `GlobSet`.
pub fn resolve_per_file_ignores(
per_file_ignores: Vec<PerFileIgnore>,
project_root: Option<&PathBuf>,
) -> Result<Vec<(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)>> {
per_file_ignores
.into_iter()
.map(|per_file_ignore| {
// Construct absolute path matcher.
let path = Path::new(&per_file_ignore.pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
let absolute = Glob::new(&absolute_path.to_string_lossy())?.compile_matcher();
// Construct basename matcher.
let basename = Glob::new(&per_file_ignore.pattern)?.compile_matcher();
Ok((absolute, basename, per_file_ignore.codes))
})
.collect()
}
/// Given a set of selected and ignored prefixes, resolve the set of enabled
/// error codes.
fn resolve_codes(select: &[CheckCodePrefix], ignore: &[CheckCodePrefix]) -> FxHashSet<CheckCode> {

View File

@@ -4,7 +4,7 @@ use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, Result};
use glob::Pattern;
use globset::{Glob, GlobSetBuilder};
use serde::{de, Deserialize, Deserializer, Serialize};
use crate::checks::CheckCode;
@@ -44,46 +44,59 @@ impl FromStr for PythonVersion {
}
}
#[derive(Debug, Clone, Hash)]
#[derive(Debug, Clone)]
pub enum FilePattern {
Simple(&'static str),
Complex(Pattern, Option<Pattern>),
Builtin(&'static str),
User(String),
}
impl FilePattern {
pub fn from_user(pattern: &str, project_root: Option<&PathBuf>) -> Result<Self> {
let path = Path::new(pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
pub fn add_to(
self,
builder: &mut GlobSetBuilder,
project_root: Option<&PathBuf>,
) -> Result<()> {
match self {
FilePattern::Builtin(pattern) => {
builder.add(Glob::from_str(pattern)?);
}
FilePattern::User(pattern) => {
// Add absolute path.
let path = Path::new(&pattern);
let absolute_path = match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
};
builder.add(Glob::new(&absolute_path.to_string_lossy())?);
let absolute = Pattern::new(&absolute_path.to_string_lossy())?;
let basename = if pattern.contains(std::path::MAIN_SEPARATOR) {
None
} else {
Some(Pattern::new(pattern)?)
};
// Add basename path.
if !pattern.contains(std::path::MAIN_SEPARATOR) {
builder.add(Glob::from_str(&pattern)?);
}
}
}
Ok(())
}
}
Ok(FilePattern::Complex(absolute, basename))
impl FromStr for FilePattern {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::User(s.into()))
}
}
#[derive(Debug, Clone, Hash)]
pub struct PerFileIgnore {
pub pattern: FilePattern,
pub pattern: String,
pub codes: BTreeSet<CheckCode>,
}
impl PerFileIgnore {
pub fn new(
pattern: &str,
prefixes: &[CheckCodePrefix],
project_root: Option<&PathBuf>,
) -> Result<Self> {
let pattern = FilePattern::from_user(pattern, project_root)?;
pub fn new(pattern: String, prefixes: &[CheckCodePrefix]) -> Self {
let codes = prefixes.iter().flat_map(CheckCodePrefix::codes).collect();
Ok(Self { pattern, codes })
Self { pattern, codes }
}
}
@@ -115,9 +128,9 @@ impl<'de> Deserialize<'de> for PatternPrefixPair {
impl FromStr for PatternPrefixPair {
type Err = anyhow::Error;
fn from_str(string: &str) -> Result<Self, Self::Err> {
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (pattern_str, code_string) = {
let tokens = string.split(':').collect::<Vec<_>>();
let tokens = s.split(':').collect::<Vec<_>>();
if tokens.len() != 2 {
return Err(anyhow!("Expected {}", Self::EXPECTED_PATTERN));
}

View File

@@ -1,114 +0,0 @@
//! Structs to render user-facing settings.
use std::path::PathBuf;
use regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::{FilePattern, PythonVersion};
use crate::{
flake8_annotations, flake8_quotes, flake8_tidy_imports, isort, pep8_naming, Configuration,
};
/// Struct to render user-facing exclusion patterns.
#[derive(Debug)]
#[allow(dead_code)]
pub struct Exclusion {
basename: Option<String>,
absolute: Option<String>,
}
impl Exclusion {
pub fn from_file_pattern(file_pattern: FilePattern) -> Self {
match file_pattern {
FilePattern::Simple(basename) => Exclusion {
basename: Some(basename.to_string()),
absolute: None,
},
FilePattern::Complex(absolute, basename) => Exclusion {
basename: basename.map(|pattern| pattern.to_string()),
absolute: Some(absolute.to_string()),
},
}
}
}
/// Struct to render user-facing configuration.
#[derive(Debug)]
pub struct UserConfiguration {
pub dummy_variable_rgx: Regex,
pub exclude: Vec<Exclusion>,
pub extend_exclude: Vec<Exclusion>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub fix: bool,
pub fixable: Vec<CheckCodePrefix>,
pub ignore: Vec<CheckCodePrefix>,
pub line_length: usize,
pub per_file_ignores: Vec<(Exclusion, Vec<CheckCode>)>,
pub select: Vec<CheckCodePrefix>,
pub show_source: bool,
pub src: Vec<PathBuf>,
pub target_version: PythonVersion,
pub unfixable: Vec<CheckCodePrefix>,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
pub flake8_quotes: flake8_quotes::settings::Settings,
pub flake8_tidy_imports: flake8_tidy_imports::settings::Settings,
pub isort: isort::settings::Settings,
pub pep8_naming: pep8_naming::settings::Settings,
// Non-settings exposed to the user
pub project_root: Option<PathBuf>,
pub pyproject: Option<PathBuf>,
}
impl UserConfiguration {
pub fn from_configuration(
configuration: Configuration,
project_root: Option<PathBuf>,
pyproject: Option<PathBuf>,
) -> Self {
Self {
dummy_variable_rgx: configuration.dummy_variable_rgx,
exclude: configuration
.exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_exclude: configuration
.extend_exclude
.into_iter()
.map(Exclusion::from_file_pattern)
.collect(),
extend_ignore: configuration.extend_ignore,
extend_select: configuration.extend_select,
fix: configuration.fix,
fixable: configuration.fixable,
unfixable: configuration.unfixable,
ignore: configuration.ignore,
line_length: configuration.line_length,
per_file_ignores: configuration
.per_file_ignores
.into_iter()
.map(|per_file_ignore| {
(
Exclusion::from_file_pattern(per_file_ignore.pattern),
Vec::from_iter(per_file_ignore.codes),
)
})
.collect(),
select: configuration.select,
src: configuration.src,
target_version: configuration.target_version,
show_source: configuration.show_source,
flake8_annotations: configuration.flake8_annotations,
flake8_quotes: configuration.flake8_quotes,
flake8_tidy_imports: configuration.flake8_tidy_imports,
isort: configuration.isort,
pep8_naming: configuration.pep8_naming,
project_root,
pyproject,
}
}
}

View File

@@ -0,0 +1,158 @@
---
source: src/linter.rs
expression: checks
---
- kind:
FunctionUsesLoopVariable: x
location:
row: 12
column: 29
end_location:
row: 12
column: 30
fix: ~
- kind:
FunctionUsesLoopVariable: y
location:
row: 13
column: 29
end_location:
row: 13
column: 30
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 16
column: 15
end_location:
row: 16
column: 16
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 28
column: 18
end_location:
row: 28
column: 19
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 29
column: 18
end_location:
row: 29
column: 19
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 30
column: 18
end_location:
row: 30
column: 19
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 31
column: 21
end_location:
row: 31
column: 22
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 40
column: 33
end_location:
row: 40
column: 34
fix: ~
- kind:
FunctionUsesLoopVariable: x
location:
row: 42
column: 13
end_location:
row: 42
column: 14
fix: ~
- kind:
FunctionUsesLoopVariable: a
location:
row: 50
column: 29
end_location:
row: 50
column: 30
fix: ~
- kind:
FunctionUsesLoopVariable: a_
location:
row: 51
column: 29
end_location:
row: 51
column: 31
fix: ~
- kind:
FunctionUsesLoopVariable: b
location:
row: 52
column: 29
end_location:
row: 52
column: 30
fix: ~
- kind:
FunctionUsesLoopVariable: c
location:
row: 53
column: 29
end_location:
row: 53
column: 30
fix: ~
- kind:
FunctionUsesLoopVariable: j
location:
row: 61
column: 16
end_location:
row: 61
column: 17
fix: ~
- kind:
FunctionUsesLoopVariable: k
location:
row: 61
column: 20
end_location:
row: 61
column: 21
fix: ~
- kind:
FunctionUsesLoopVariable: l
location:
row: 68
column: 9
end_location:
row: 68
column: 10
fix: ~
- kind:
FunctionUsesLoopVariable: i
location:
row: 82
column: 12
end_location:
row: 82
column: 18
fix: ~

View File

@@ -0,0 +1,29 @@
---
source: src/linter.rs
expression: checks
---
- kind: RaiseWithoutFromInsideExcept
location:
row: 10
column: 8
end_location:
row: 10
column: 23
fix: ~
- kind: RaiseWithoutFromInsideExcept
location:
row: 11
column: 4
end_location:
row: 11
column: 21
fix: ~
- kind: RaiseWithoutFromInsideExcept
location:
row: 16
column: 4
end_location:
row: 16
column: 39
fix: ~

View File

@@ -4,10 +4,10 @@ expression: checks
---
- kind: PublicClass
location:
row: 14
row: 15
column: 0
end_location:
row: 67
row: 72
column: 0
fix: ~

View File

@@ -4,26 +4,26 @@ expression: checks
---
- kind: PublicMethod
location:
row: 22
row: 23
column: 4
end_location:
row: 25
row: 26
column: 4
fix: ~
- kind: PublicMethod
location:
row: 51
row: 56
column: 4
end_location:
row: 54
row: 59
column: 4
fix: ~
- kind: PublicMethod
location:
row: 63
row: 68
column: 4
end_location:
row: 67
row: 72
column: 0
fix: ~

View File

@@ -4,10 +4,10 @@ expression: checks
---
- kind: PublicFunction
location:
row: 395
row: 400
column: 0
end_location:
row: 396
row: 401
column: 0
fix: ~

View File

@@ -4,10 +4,10 @@ expression: checks
---
- kind: MagicMethod
location:
row: 59
row: 64
column: 4
end_location:
row: 62
row: 67
column: 4
fix: ~

View File

@@ -4,18 +4,18 @@ expression: checks
---
- kind: PublicInit
location:
row: 55
row: 60
column: 4
end_location:
row: 58
row: 63
column: 4
fix: ~
- kind: PublicInit
location:
row: 529
row: 534
column: 4
end_location:
row: 533
row: 538
column: 0
fix: ~

View File

@@ -5,69 +5,69 @@ expression: checks
- kind:
NoBlankLineBeforeFunction: 1
location:
row: 132
row: 137
column: 4
end_location:
row: 132
row: 137
column: 24
fix:
patch:
content: ""
location:
row: 131
row: 136
column: 0
end_location:
row: 132
row: 137
column: 0
- kind:
NoBlankLineBeforeFunction: 1
location:
row: 146
row: 151
column: 4
end_location:
row: 146
row: 151
column: 37
fix:
patch:
content: ""
location:
row: 145
row: 150
column: 0
end_location:
row: 146
row: 151
column: 0
- kind:
NoBlankLineBeforeFunction: 1
location:
row: 541
row: 546
column: 4
end_location:
row: 544
row: 549
column: 7
fix:
patch:
content: ""
location:
row: 540
row: 545
column: 0
end_location:
row: 541
row: 546
column: 0
- kind:
NoBlankLineBeforeFunction: 1
location:
row: 563
row: 568
column: 4
end_location:
row: 566
row: 571
column: 7
fix:
patch:
content: ""
location:
row: 562
row: 567
column: 0
end_location:
row: 563
row: 568
column: 0

View File

@@ -5,69 +5,69 @@ expression: checks
- kind:
NoBlankLineAfterFunction: 1
location:
row: 137
row: 142
column: 4
end_location:
row: 137
row: 142
column: 24
fix:
patch:
content: ""
location:
row: 138
row: 143
column: 0
end_location:
row: 139
row: 144
column: 0
- kind:
NoBlankLineAfterFunction: 1
location:
row: 146
row: 151
column: 4
end_location:
row: 146
row: 151
column: 37
fix:
patch:
content: ""
location:
row: 147
row: 152
column: 0
end_location:
row: 148
row: 153
column: 0
- kind:
NoBlankLineAfterFunction: 1
location:
row: 550
row: 555
column: 4
end_location:
row: 553
row: 558
column: 7
fix:
patch:
content: ""
location:
row: 554
row: 559
column: 0
end_location:
row: 555
row: 560
column: 0
- kind:
NoBlankLineAfterFunction: 1
location:
row: 563
row: 568
column: 4
end_location:
row: 566
row: 571
column: 7
fix:
patch:
content: ""
location:
row: 567
row: 572
column: 0
end_location:
row: 568
row: 573
column: 0

View File

@@ -5,52 +5,52 @@ expression: checks
- kind:
OneBlankLineBeforeClass: 0
location:
row: 156
row: 161
column: 4
end_location:
row: 156
row: 161
column: 32
fix:
patch:
content: "\n"
location:
row: 156
row: 161
column: 0
end_location:
row: 156
row: 161
column: 0
- kind:
OneBlankLineBeforeClass: 0
location:
row: 187
row: 192
column: 4
end_location:
row: 187
row: 192
column: 45
fix:
patch:
content: "\n"
location:
row: 187
row: 192
column: 0
end_location:
row: 187
row: 192
column: 0
- kind:
OneBlankLineBeforeClass: 0
location:
row: 521
row: 526
column: 4
end_location:
row: 527
row: 532
column: 7
fix:
patch:
content: "\n"
location:
row: 521
row: 526
column: 0
end_location:
row: 521
row: 526
column: 0

View File

@@ -5,35 +5,35 @@ expression: checks
- kind:
OneBlankLineAfterClass: 0
location:
row: 176
row: 181
column: 4
end_location:
row: 176
row: 181
column: 24
fix:
patch:
content: "\n"
location:
row: 177
row: 182
column: 0
end_location:
row: 177
row: 182
column: 0
- kind:
OneBlankLineAfterClass: 0
location:
row: 187
row: 192
column: 4
end_location:
row: 187
row: 192
column: 45
fix:
patch:
content: "\n"
location:
row: 188
row: 193
column: 0
end_location:
row: 188
row: 193
column: 0

View File

@@ -4,34 +4,34 @@ expression: checks
---
- kind: BlankLineAfterSummary
location:
row: 195
row: 200
column: 4
end_location:
row: 198
row: 203
column: 7
fix:
patch:
content: "\n"
location:
row: 196
row: 201
column: 0
end_location:
row: 196
row: 201
column: 0
- kind: BlankLineAfterSummary
location:
row: 205
row: 210
column: 4
end_location:
row: 210
row: 215
column: 7
fix:
patch:
content: "\n"
location:
row: 206
row: 211
column: 0
end_location:
row: 208
row: 213
column: 0

View File

@@ -4,34 +4,34 @@ expression: checks
---
- kind: NoUnderIndentation
location:
row: 227
row: 232
column: 0
end_location:
row: 227
row: 232
column: 0
fix:
patch:
content: " "
location:
row: 227
row: 232
column: 0
end_location:
row: 227
row: 232
column: 0
- kind: NoUnderIndentation
location:
row: 435
row: 440
column: 0
end_location:
row: 435
row: 440
column: 0
fix:
patch:
content: " "
location:
row: 435
row: 440
column: 0
end_location:
row: 435
row: 440
column: 4

View File

@@ -4,50 +4,50 @@ expression: checks
---
- kind: NoOverIndentation
location:
row: 247
row: 252
column: 0
end_location:
row: 247
row: 252
column: 0
fix:
patch:
content: " "
location:
row: 247
row: 252
column: 0
end_location:
row: 247
row: 252
column: 7
- kind: NoOverIndentation
location:
row: 259
row: 264
column: 0
end_location:
row: 259
row: 264
column: 0
fix:
patch:
content: " "
location:
row: 259
row: 264
column: 0
end_location:
row: 259
row: 264
column: 8
- kind: NoOverIndentation
location:
row: 267
row: 272
column: 0
end_location:
row: 267
row: 272
column: 0
fix:
patch:
content: " "
location:
row: 267
row: 272
column: 0
end_location:
row: 267
row: 272
column: 8

View File

@@ -4,18 +4,18 @@ expression: checks
---
- kind: NewLineAfterLastParagraph
location:
row: 276
row: 281
column: 4
end_location:
row: 278
row: 283
column: 19
fix:
patch:
content: "\n "
location:
row: 278
row: 283
column: 16
end_location:
row: 278
row: 283
column: 16

View File

@@ -4,50 +4,50 @@ expression: checks
---
- kind: NoSurroundingWhitespace
location:
row: 283
row: 288
column: 4
end_location:
row: 283
row: 288
column: 33
fix:
patch:
content: Whitespace at the end.
location:
row: 283
row: 288
column: 7
end_location:
row: 283
row: 288
column: 30
- kind: NoSurroundingWhitespace
location:
row: 288
row: 293
column: 4
end_location:
row: 288
row: 293
column: 37
fix:
patch:
content: Whitespace at everywhere.
location:
row: 288
row: 293
column: 7
end_location:
row: 288
row: 293
column: 34
- kind: NoSurroundingWhitespace
location:
row: 294
row: 299
column: 4
end_location:
row: 297
row: 302
column: 7
fix:
patch:
content: Whitespace at the beginning.
location:
row: 294
row: 299
column: 7
end_location:
row: 294
row: 299
column: 36

View File

@@ -5,35 +5,35 @@ expression: checks
- kind:
NoBlankLineBeforeClass: 1
location:
row: 165
row: 170
column: 4
end_location:
row: 165
row: 170
column: 29
fix:
patch:
content: ""
location:
row: 164
row: 169
column: 0
end_location:
row: 165
row: 170
column: 0
- kind:
NoBlankLineBeforeClass: 1
location:
row: 176
row: 181
column: 4
end_location:
row: 176
row: 181
column: 24
fix:
patch:
content: ""
location:
row: 175
row: 180
column: 0
end_location:
row: 176
row: 181
column: 0

View File

@@ -4,10 +4,10 @@ expression: checks
---
- kind: MultiLineSummaryFirstLine
location:
row: 124
row: 129
column: 4
end_location:
row: 126
row: 131
column: 7
fix: ~

View File

@@ -4,154 +4,154 @@ expression: checks
---
- kind: MultiLineSummarySecondLine
location:
row: 195
row: 200
column: 4
end_location:
row: 198
row: 203
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 205
column: 4
end_location:
row: 210
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
column: 4
end_location:
row: 215
column: 4
end_location:
row: 219
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 225
row: 220
column: 4
end_location:
row: 229
row: 224
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 235
row: 230
column: 4
end_location:
row: 239
row: 234
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 240
column: 4
end_location:
row: 244
column: 3
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 245
row: 250
column: 4
end_location:
row: 249
row: 254
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 255
row: 260
column: 4
end_location:
row: 259
row: 264
column: 11
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 265
row: 270
column: 4
end_location:
row: 269
row: 274
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 276
row: 281
column: 4
end_location:
row: 278
row: 283
column: 19
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 294
row: 299
column: 4
end_location:
row: 297
row: 302
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 338
column: 4
end_location:
row: 343
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 378
column: 4
end_location:
row: 381
row: 348
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 387
row: 383
column: 4
end_location:
row: 391
row: 386
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 433
row: 392
column: 4
end_location:
row: 396
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 438
column: 36
end_location:
row: 436
row: 441
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 445
row: 450
column: 4
end_location:
row: 449
row: 454
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 521
row: 526
column: 4
end_location:
row: 527
row: 532
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 541
row: 546
column: 4
end_location:
row: 544
row: 549
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 550
row: 555
column: 4
end_location:
row: 553
row: 558
column: 7
fix: ~
- kind: MultiLineSummarySecondLine
location:
row: 563
row: 568
column: 4
end_location:
row: 566
row: 571
column: 7
fix: ~

View File

@@ -2,14 +2,6 @@
source: src/linter.rs
expression: checks
---
- kind: UsesTripleQuotes
location:
row: 302
column: 4
end_location:
row: 302
column: 19
fix: ~
- kind: UsesTripleQuotes
location:
row: 307
@@ -24,7 +16,7 @@ expression: checks
column: 4
end_location:
row: 312
column: 15
column: 19
fix: ~
- kind: UsesTripleQuotes
location:
@@ -36,10 +28,18 @@ expression: checks
fix: ~
- kind: UsesTripleQuotes
location:
row: 323
row: 322
column: 4
end_location:
row: 323
row: 322
column: 15
fix: ~
- kind: UsesTripleQuotes
location:
row: 328
column: 4
end_location:
row: 328
column: 16
fix: ~

View File

@@ -4,60 +4,52 @@ expression: checks
---
- kind: EndsInPeriod
location:
row: 350
row: 355
column: 4
end_location:
row: 350
row: 355
column: 17
fix: ~
- kind: EndsInPeriod
location:
row: 401
row: 406
column: 24
end_location:
row: 401
row: 406
column: 39
fix: ~
- kind: EndsInPeriod
location:
row: 405
row: 410
column: 4
end_location:
row: 405
row: 410
column: 24
fix: ~
- kind: EndsInPeriod
location:
row: 411
row: 416
column: 4
end_location:
row: 411
row: 416
column: 24
fix: ~
- kind: EndsInPeriod
location:
row: 417
row: 422
column: 34
end_location:
row: 417
row: 422
column: 49
fix: ~
- kind: EndsInPeriod
location:
row: 424
row: 429
column: 48
end_location:
row: 424
row: 429
column: 63
fix: ~
- kind: EndsInPeriod
location:
row: 465
column: 4
end_location:
row: 465
column: 24
fix: ~
- kind: EndsInPeriod
location:
row: 470
@@ -76,34 +68,42 @@ expression: checks
fix: ~
- kind: EndsInPeriod
location:
row: 482
row: 480
column: 4
end_location:
row: 482
row: 480
column: 24
fix: ~
- kind: EndsInPeriod
location:
row: 504
row: 487
column: 4
end_location:
row: 504
row: 487
column: 24
fix: ~
- kind: EndsInPeriod
location:
row: 509
column: 4
end_location:
row: 509
column: 34
fix: ~
- kind: EndsInPeriod
location:
row: 509
row: 514
column: 4
end_location:
row: 509
row: 514
column: 33
fix: ~
- kind: EndsInPeriod
location:
row: 515
row: 520
column: 4
end_location:
row: 515
row: 520
column: 32
fix: ~

View File

@@ -4,10 +4,10 @@ expression: checks
---
- kind: NoSignature
location:
row: 373
row: 378
column: 4
end_location:
row: 373
row: 378
column: 30
fix: ~

View File

@@ -4,60 +4,52 @@ expression: checks
---
- kind: EndsInPunctuation
location:
row: 350
row: 355
column: 4
end_location:
row: 350
row: 355
column: 17
fix: ~
- kind: EndsInPunctuation
location:
row: 401
row: 406
column: 24
end_location:
row: 401
row: 406
column: 39
fix: ~
- kind: EndsInPunctuation
location:
row: 405
row: 410
column: 4
end_location:
row: 405
row: 410
column: 24
fix: ~
- kind: EndsInPunctuation
location:
row: 411
row: 416
column: 4
end_location:
row: 411
row: 416
column: 24
fix: ~
- kind: EndsInPunctuation
location:
row: 417
row: 422
column: 34
end_location:
row: 417
row: 422
column: 49
fix: ~
- kind: EndsInPunctuation
location:
row: 424
row: 429
column: 48
end_location:
row: 424
row: 429
column: 63
fix: ~
- kind: EndsInPunctuation
location:
row: 465
column: 4
end_location:
row: 465
column: 24
fix: ~
- kind: EndsInPunctuation
location:
row: 470
@@ -76,26 +68,34 @@ expression: checks
fix: ~
- kind: EndsInPunctuation
location:
row: 482
row: 480
column: 4
end_location:
row: 482
row: 480
column: 24
fix: ~
- kind: EndsInPunctuation
location:
row: 504
row: 487
column: 4
end_location:
row: 504
row: 487
column: 24
fix: ~
- kind: EndsInPunctuation
location:
row: 509
column: 4
end_location:
row: 509
column: 34
fix: ~
- kind: EndsInPunctuation
location:
row: 515
row: 520
column: 4
end_location:
row: 515
row: 520
column: 32
fix: ~

View File

@@ -4,26 +4,26 @@ expression: checks
---
- kind: SkipDocstring
location:
row: 33
row: 34
column: 4
end_location:
row: 37
row: 38
column: 4
fix: ~
- kind: SkipDocstring
location:
row: 85
row: 90
column: 4
end_location:
row: 89
row: 94
column: 4
fix: ~
- kind: SkipDocstring
location:
row: 105
row: 110
column: 0
end_location:
row: 110
row: 115
column: 0
fix: ~

View File

@@ -4,26 +4,26 @@ expression: checks
---
- kind: NonEmpty
location:
row: 19
row: 20
column: 8
end_location:
row: 19
row: 20
column: 14
fix: ~
- kind: NonEmpty
location:
row: 69
row: 74
column: 4
end_location:
row: 69
row: 74
column: 11
fix: ~
- kind: NonEmpty
location:
row: 75
row: 80
column: 8
end_location:
row: 75
row: 80
column: 10
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/linter.rs
expression: checks
---
- kind:
PercentFormatInvalidFormat: incomplete format
location:
row: 1
column: 9
end_location:
row: 1
column: 25
fix: ~

View File

@@ -0,0 +1,61 @@
---
source: src/linter.rs
expression: checks
---
- kind: PercentFormatExpectedMapping
location:
row: 6
column: 10
end_location:
row: 6
column: 19
fix: ~
- kind: PercentFormatExpectedMapping
location:
row: 7
column: 10
end_location:
row: 7
column: 20
fix: ~
- kind: PercentFormatExpectedMapping
location:
row: 8
column: 10
end_location:
row: 8
column: 19
fix: ~
- kind: PercentFormatExpectedMapping
location:
row: 9
column: 10
end_location:
row: 9
column: 22
fix: ~
- kind: PercentFormatExpectedMapping
location:
row: 11
column: 10
end_location:
row: 11
column: 37
fix: ~
- kind: PercentFormatExpectedMapping
location:
row: 12
column: 10
end_location:
row: 12
column: 37
fix: ~
- kind: PercentFormatExpectedMapping
location:
row: 13
column: 10
end_location:
row: 13
column: 37
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/linter.rs
expression: checks
---
- kind: PercentFormatExpectedMapping
location:
row: 9
column: 10
end_location:
row: 9
column: 21
fix: ~

Some files were not shown because too many files have changed in this diff Show More