Compare commits

...

45 Commits

Author SHA1 Message Date
Charlie Marsh
f3e11a30cb Bump version to 0.0.178 2022-12-12 22:06:04 -05:00
Charlie Marsh
2f3b5367ff Add a note on extends to README 2022-12-12 21:36:39 -05:00
Charlie Marsh
92bc417e4e Add support for glob patterns in src (#1225) 2022-12-12 21:35:03 -05:00
Charlie Marsh
9853b0728b Enable configuration files to "extend" other configuration files (#1219) 2022-12-12 20:28:22 -05:00
Charlie Marsh
77709dcc41 Remove underscore from extend_exclude 2022-12-12 16:34:16 -05:00
Charlie Marsh
b0cb5fc7ef Document current behavior around pyproject.toml discovery (#1213) 2022-12-12 11:49:21 -05:00
Charlie Marsh
d6f51e55dd Remove extraneous test_project 2022-12-12 10:53:12 -05:00
Charlie Marsh
4bb6b4851a Rename p to path 2022-12-12 10:51:24 -05:00
Charlie Marsh
54c5ded938 Move settings path discovery into its own function 2022-12-12 10:50:08 -05:00
Charlie Marsh
0157fedab5 Move Python file resolution into resolver.rs (#1211) 2022-12-12 10:43:50 -05:00
Charlie Marsh
cd69610741 Use --config everywhere if provided (#1210) 2022-12-12 10:28:00 -05:00
Charlie Marsh
a3d06d0005 Move more commands into commands.rs (#1209) 2022-12-12 10:22:47 -05:00
Charlie Marsh
ac6fa1dc88 Simplify some logic around configuration detection (#1197) 2022-12-12 10:15:05 -05:00
Charlie Marsh
73794fc299 Resolve hierarchical settings and Python files in a single filesystem pass (#1205) 2022-12-12 10:13:52 -05:00
Charlie Marsh
0adc9ed259 Support hierarchical settings for nested directories (#1190) 2022-12-12 10:12:23 -05:00
Charlie Marsh
19e9eb1af8 Bump version to 0.0.177 2022-12-11 22:38:52 -05:00
Anders Kaseorg
e57044800c Fix quotes in SIM118 error message (#1204) 2022-12-11 22:30:39 -05:00
Charlie Marsh
ae8ff7cb7f Add notes around python-lsp-ruff (#1202) 2022-12-11 17:36:20 -05:00
Charlie Marsh
c05914f222 Avoid inserting extra newlines for comment-delimited import blocks (#1201) 2022-12-11 17:13:09 -05:00
Charlie Marsh
24179655b8 Fix 'a test' reference 2022-12-11 13:31:14 -05:00
Charlie Marsh
d27b419e68 Run cargo dev generate-options 2022-12-11 10:34:52 -05:00
Charlie Marsh
9fc7a32a24 Sort list in README 2022-12-11 10:25:27 -05:00
Charlie Marsh
9161b866b5 Bump version to 0.0.176 2022-12-11 10:19:50 -05:00
Reiner Gerecke
38141a6f14 Check for outdated auto-generated files in CI (#1192) 2022-12-11 10:18:57 -05:00
Charlie Marsh
aa5402fc0e Add flake8-simplify to flake8-to-ruff 2022-12-11 10:10:46 -05:00
Charlie Marsh
99f077aa4e Add missing hash in README comment 2022-12-11 10:05:32 -05:00
Reiner Gerecke
7f25d1ec70 Implement SIM118 (key in dict) of flake8-simplify (#1195) 2022-12-11 10:05:11 -05:00
Charlie Marsh
360b033e04 Avoid F821 false positive on annotated global (#1196) 2022-12-11 10:04:06 -05:00
Reiner Gerecke
247dcc9f9c Mark C413 as fixable (#1191) 2022-12-11 09:07:51 -05:00
Charlie Marsh
c86e52193c Bump version to 0.0.175 2022-12-10 21:23:19 -05:00
Harutaka Kawamura
efdc4e801d Upgrade RustPython to fix end location of implicitly concatenated strings (#1187) 2022-12-10 19:16:01 -05:00
Charlie Marsh
f8f2eeed35 Enable --no-show-source for consistency (#1189) 2022-12-10 19:09:49 -05:00
Charlie Marsh
8fa414b67e Move configuration-CLI resolution into dedicated methods (#1188) 2022-12-10 19:07:38 -05:00
Charlie Marsh
484d7a30bd Add TODO around nested globals 2022-12-10 17:44:08 -05:00
Charlie Marsh
fb681c614a Move string formatting checks to plugins (#1185) 2022-12-10 16:43:21 -05:00
Reiner Gerecke
06ed125771 Add autofix for F504 and F522 (#1184) 2022-12-10 16:33:09 -05:00
Charlie Marsh
74668915b0 Remove serialization format from Settings struct (#1183) 2022-12-10 13:38:59 -05:00
Charlie Marsh
6da3de25ba Add jupyter_server to README (#1182) 2022-12-10 12:10:27 -05:00
Charlie Marsh
63b3e00c97 Bump version to 0.0.174 2022-12-10 12:08:48 -05:00
Charlie Marsh
39440aa274 Create function and lambda scopes eagerly (#1181) 2022-12-10 12:08:33 -05:00
Charlie Marsh
add96d3dc5 Implement E0117 (nonlocal-without-binding) (#1180) 2022-12-10 11:41:57 -05:00
Charlie Marsh
6f8e0224d0 Implement W0602 (global-variable-not-assigned) (#1179) 2022-12-10 11:33:24 -05:00
Charlie Marsh
b8bbafd85b Flag global usages prior to global declarations (#1178) 2022-12-10 11:19:24 -05:00
Charlie Marsh
40b54d3e8c Ignore imports in class scopes (#1176) 2022-12-10 10:23:33 -05:00
Charlie Marsh
2b44941d63 Add pacman instructions to README (#1175) 2022-12-10 10:00:01 -05:00
87 changed files with 3348 additions and 1428 deletions

View File

@@ -20,6 +20,9 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
override: true
components: rustfmt
- uses: actions/cache@v3
env:
cache-name: cache-cargo
@@ -33,6 +36,12 @@ jobs:
${{ runner.os }}-build-
${{ runner.os }}-
- run: cargo build --all --release
- run: ./target/release/ruff_dev generate-rules-table
- run: ./target/release/ruff_dev generate-options
- run: git diff --quiet README.md || echo "::error file=README.md::This file is outdated. You may have to rerun 'cargo dev generate-options' and/or 'cargo dev generate-rules-table'."
- run: ./target/release/ruff_dev generate-check-code-prefix && cargo fmt -- src/checks_gen.rs
- run: git diff --quiet src/checks_gen.rs || echo "::error file=src/checks_gen.rs::This file is outdated. You may have to rerun 'cargo dev generate-check-code-prefix'."
- run: git diff --exit-code -- README.md src/checks_gen.rs
cargo_fmt:
name: "cargo fmt"

View File

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

10
BREAKING_CHANGES.md Normal file
View File

@@ -0,0 +1,10 @@
# Breaking Changes
## 0.0.178
### Configuration files are now resolved hierarchically ([#1190](https://github.com/charliermarsh/ruff/pull/1190))
`pyproject.toml` files are now resolved hierarchically, such that for each Python file, we find
the first `pyproject.toml` file in its path, and use that to determine its lint settings.
See the [README](https://github.com/charliermarsh/ruff#pyprojecttoml-discovery) for more.

23
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.173-dev.0"
version = "0.0.178-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -796,6 +796,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "globset"
version = "0.4.9"
@@ -1821,7 +1827,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.173"
version = "0.0.178"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1841,6 +1847,7 @@ dependencies = [
"fern",
"filetime",
"getrandom 0.2.8",
"glob",
"globset",
"insta",
"itertools",
@@ -1874,7 +1881,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.173"
version = "0.0.178"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1892,7 +1899,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.173"
version = "0.0.178"
dependencies = [
"proc-macro2",
"quote",
@@ -1935,7 +1942,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -1945,7 +1952,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -1968,7 +1975,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"bincode",
"bitflags",
@@ -1985,7 +1992,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"ahash",
"anyhow",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.173"
version = "0.0.178"
edition = "2021"
rust-version = "1.65.0"
@@ -28,6 +28,7 @@ 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 = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
@@ -41,11 +42,11 @@ quick-junit = { version = "0.3.2" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.173", path = "ruff_macros" }
ruff_macros = { version = "0.0.178", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }

25
LICENSE
View File

@@ -438,6 +438,31 @@ are:
SOFTWARE.
"""
- flake8-simplify, licensed as follows:
"""
MIT License
Copyright (c) 2020 Martin Thoma
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.
"""
- isort, licensed as follows:
"""
The MIT License (MIT)

173
README.md
View File

@@ -41,6 +41,7 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Pydantic](https://github.com/pydantic/pydantic)
- [Saleor](https://github.com/saleor/saleor)
- [Hatch](https://github.com/pypa/hatch)
- [Jupyter Server](https://github.com/jupyter-server/jupyter_server)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -89,6 +90,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-quotes (Q)](#flake8-quotes-q)
1. [flake8-return (RET)](#flake8-return-ret)
1. [flake8-simplify (SIM)](#flake8-simplify-sim)
1. [flake8-tidy-imports (TID)](#flake8-tidy-imports-tid)
1. [flake8-unused-arguments (ARG)](#flake8-unused-arguments-arg)
1. [eradicate (ERA)](#eradicate-era)
@@ -120,12 +122,18 @@ For **macOS Homebrew** and **Linuxbrew** users, Ruff is also available as [`ruff
brew install ruff
```
For Conda users, Ruff is also available as [`ruff`](https://anaconda.org/conda-forge/ruff) on `conda-forge`:
For **Conda** users, Ruff is also available as [`ruff`](https://anaconda.org/conda-forge/ruff) on `conda-forge`:
```shell
conda install -c conda-forge ruff
```
For **Arch Linux** users, Ruff is also available as [`ruff`](https://archlinux.org/packages/community/x86_64/ruff/) on the official repositories:
```shell
pacman -S ruff
```
### Usage
To run Ruff, try any of the following:
@@ -147,7 +155,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.173
rev: v0.0.178
hooks:
- id: ruff
```
@@ -323,6 +331,40 @@ Options:
Print version information
```
### `pyproject.toml` discovery
Similar to [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),
Ruff supports hierarchical configuration, such that the "closest" `pyproject.toml` file in the
directory hierarchy is used for every individual file, with all paths in the `pyproject.toml` file
(e.g., `exclude` globs, `src` paths) being resolved relative to the directory containing the
`pyproject.toml` file.
There are a few exceptions to these rules:
1. If a configuration file is passed directly via `--config`, those settings are used for across
files. Any relative paths in that configuration file (like `exclude` globs or `src` paths) are
resolved relative to the _current working directory_.
2. If no `pyproject.toml` file is found in the filesystem hierarchy, Ruff will fall back to using
a default configuration. If a user-specific configuration file exists at `${config_dir}/ruff/pyproject.toml`,
that file will be used instead of the default configuration, with `${config_dir}` being determined
via the [`dirs](https://docs.rs/dirs/4.0.0/dirs/fn.config_dir.html) crate, and all relative paths
being again resolved relative to the _current working directory_.
3. Any `pyproject.toml`-supported settings that are provided on the command-line (e.g., via
`--select`) will override the settings in _every_ resolved configuration file.
Unlike [ESLint](https://eslint.org/docs/latest/user-guide/configuring/configuration-files#cascading-and-hierarchy),
Ruff does not merge settings across configuration files; instead, the "closest" configuration file
is used, and any parent configuration files are ignored. In lieu of this implicit cascade, Ruff
supports an [`extend`](#extend) field, which allows you to inherit the settings from another
`pyproject.toml` file, like so:
```toml
# Extend the `pyproject.toml` file in the parent directory.
extend = "../pyproject.toml"
# But use a different line length.
line-length = 100
```
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via [`ignore`](#ignore) or
@@ -362,7 +404,7 @@ For targeted exclusions across entire files (e.g., "Ignore all F841 violations i
### "Action Comments"
Ruff respects `isort`'s ["Action Comments"](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
(`# isort: skip_file`, `# isort: on`, `# isort: off`, `# isort: skip`, and `isort: split`), which
(`# isort: skip_file`, `# isort: on`, `# isort: off`, `# isort: skip`, and `# isort: split`), which
enable selectively enabling and disabling import sorting for blocks of code and other inline
configuration.
@@ -412,14 +454,14 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| 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): ... | |
| 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): ... | |
| 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 | |
@@ -709,7 +751,7 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary `(list\|tuple)` literal passed to `tuple()` (remove the outer call to `tuple()`) | 🛠 |
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary `(list\|tuple)` literal passed to `list()` (rewrite as a `list` literal) | 🛠 |
| C411 | UnnecessaryListCall | Unnecessary `list` call (remove the outer call to `list()`) | 🛠 |
| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | |
| C413 | UnnecessaryCallAroundSorted | Unnecessary `(list\|reversed)` call around `sorted()` | 🛠 |
| C414 | UnnecessaryDoubleCastOrProcess | Unnecessary `(list\|reversed\|set\|sorted\|tuple)` call within `(list\|set\|sorted\|tuple)()` | |
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within `(reversed\|set\|sorted)()` | |
| C416 | UnnecessaryComprehension | Unnecessary `(list\|set)` comprehension (rewrite using `(list\|set)()`) | 🛠 |
@@ -764,6 +806,14 @@ For more, see [flake8-return](https://pypi.org/project/flake8-return/1.2.0/) on
| RET507 | SuperfluousElseContinue | Unnecessary `else` after `continue` statement | |
| RET508 | SuperfluousElseBreak | Unnecessary `else` after `break` statement | |
### flake8-simplify (SIM)
For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM118 | KeyInDict | Use `key in dict` instead of `key in dict.keys()` | 🛠 |
### flake8-tidy-imports (TID)
For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/4.8.0/) on PyPI.
@@ -809,12 +859,15 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
| PLC0414 | UselessImportAlias | Import alias does not rename original package | 🛠 |
| PLC2201 | MisplacedComparisonConstant | Comparison should be ... | 🛠 |
| PLC3002 | UnnecessaryDirectLambdaCall | Lambda expression called directly. Execute the expression inline instead. | |
| PLE0117 | NonlocalWithoutBinding | Nonlocal name `...` found without binding | |
| PLE0118 | UsedPriorGlobalDeclaration | Name `...` is used prior to global declaration on line 1 | |
| PLE1142 | AwaitOutsideAsync | `await` should be used within an async function | |
| PLR0206 | PropertyWithParameters | Cannot have defined parameters for properties | |
| PLR0402 | ConsiderUsingFromImport | Use `from ... import ...` in lieu of alias | |
| PLR1701 | ConsiderMergingIsinstance | Merge these isinstance calls: `isinstance(..., (...))` | |
| PLR1722 | UseSysExit | Use `sys.exit()` instead of `exit` | 🛠 |
| PLW0120 | UselessElseOnLoop | Else clause on loop without a break statement, remove the else and de-indent all the code inside it | |
| PLW0602 | GlobalVariableNotAssigned | Using global for `...` but no assignment is done | |
### Ruff-specific rules (RUF)
@@ -833,6 +886,37 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
Download the [Ruff VS Code extension](https://marketplace.visualstudio.com/items?itemName=charliermarsh.ruff).
### Language Server Protocol
Ruff is available as a [Language Server Protocol (LSP)](https://microsoft.github.io/language-server-protocol/)
server, distributed as the [`python-lsp-ruff`](https://github.com/python-lsp/python-lsp-ruff) plugin
for [`python-lsp-server`](https://github.com/python-lsp/python-lsp-server), both of which are
installable via [PyPI](https://pypi.org/project/python-lsp-ruff/):
```shell
pip install python-lsp-server python-lsp-ruff
```
The LSP server can be used with any editor that supports the Language Server Protocol. For example,
to use it with Neovim, you would add something like the following to your `init.lua`:
```lua
require'lspconfig'.pylsp.setup {
settings = {
pylsp = {
plugins = {
ruff = {
enabled = true
}
}
}
},
}
```
[`ruffd`](https://github.com/Seamooo/ruffd) is another implementation of the Language Server
Protocol (LSP) for Ruff, written in Rust.
### PyCharm
Ruff can be installed as an [External Tool](https://www.jetbrains.com/help/pycharm/configuring-third-party-tools.html)
@@ -845,10 +929,13 @@ Ruff should then appear as a runnable action:
![Ruff as a runnable action](https://user-images.githubusercontent.com/1309177/193156026-732b0aaf-3dd9-4549-9b4d-2de6d2168a33.png)
### Vim & Neovim (Unofficial)
### Vim & Neovim
Ruff is available as part of the [coc-pyright](https://github.com/fannheyward/coc-pyright) extension
for coc.nvim.
Ruff can be integrated into any editor that supports the Language Server Protocol (LSP) (see:
[Language Server Protocol](#language-server-protocol)).
Ruff is also available as part of the [coc-pyright](https://github.com/fannheyward/coc-pyright)
extension for `coc.nvim`.
<details>
<summary>Ruff can also be integrated via <a href="https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#efm"><code>efm</code></a> in just a <a href="https://github.com/JafarAbdi/myconfigs/blob/6f0b6b2450e92ec8fc50422928cd22005b919110/efm-langserver/config.yaml#L14-L20">few lines</a>.</summary>
@@ -904,11 +991,6 @@ null_ls.setup({
</details>
### Language Server Protocol (Unofficial)
[`ruffd`](https://github.com/Seamooo/ruffd) is a Rust-based language server for Ruff that implements
the Language Server Protocol (LSP).
### GitHub Actions
GitHub Actions has everything you need to run Ruff out-of-the-box:
@@ -956,9 +1038,8 @@ Under those conditions, Ruff implements every rule in Flake8.
Ruff also re-implements some of the most popular Flake8 plugins and related code quality tools
natively, including:
- [`isort`](https://pypi.org/project/isort/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
- [`eradicate`](https://pypi.org/project/eradicate/)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
@@ -976,12 +1057,13 @@ natively, including:
- [`flake8-return`](https://pypi.org/project/flake8-return/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`isort`](https://pypi.org/project/isort/)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`yesqa`](https://github.com/asottile/yesqa)
- [`eradicate`](https://pypi.org/project/eradicate/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (1/10)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (16/33)
- [`yesqa`](https://github.com/asottile/yesqa)
Note that, in some cases, Ruff uses different error code prefixes than would be found in the
originating Flake8 plugins. For example, Ruff uses `TID252` to represent the `I252` rule from
@@ -1012,8 +1094,6 @@ Pylint parity is being tracked in [#689](https://github.com/charliermarsh/ruff/i
Today, Ruff can be used to replace Flake8 when used with any of the following plugins:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
@@ -1032,6 +1112,8 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`mccabe`](https://pypi.org/project/mccabe/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
Ruff can also replace [`isort`](https://pypi.org/project/isort/),
[`yesqa`](https://github.com/asottile/yesqa), [`eradicate`](https://pypi.org/project/eradicate/),
@@ -1374,7 +1456,7 @@ Exclusions are based on globs, and can be either:
(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
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"]`
@@ -1390,6 +1472,30 @@ exclude = [".venv"]
---
#### [`extend`](#extend)
A path to a local `pyproject.toml` file to merge into this configuration.
To resolve the current `pyproject.toml` file, Ruff will first resolve this base
configuration file, then merge in any properties defined in the current configuration
file.
**Default value**: `None`
**Type**: `Path`
**Example usage**:
```toml
[tool.ruff]
# Extend the `pyproject.toml` file in the parent directory.
extend = "../pyproject.toml"
# But use a different line length.
line-length = 100
```
---
#### [`extend-exclude`](#extend-exclude)
A list of file patterns to omit from linting, in addition to those specified by `exclude`.
@@ -1647,6 +1753,25 @@ show-source = true
The source code paths to consider, e.g., when resolving first- vs. third-party imports.
As an example: given a Python package structure like:
```text
my_package/
pyproject.toml
src/
my_package/
__init__.py
foo.py
bar.py
```
The `src` directory should be included in `source` (e.g., `source = ["src"]`), such that
when resolving imports, `my_package.foo` is considered a first-party import.
This field supports globs. For example, if you have a series of Python packages in
a `python_modules` directory, `src = ["python_modules/*"]` would expand to incorporate
all of the packages in that directory.
**Default value**: `["."]`
**Type**: `Vec<PathBuf>`

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.173"
version = "0.0.178"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.173"
version = "0.0.178"
dependencies = [
"anyhow",
"bincode",
@@ -2028,7 +2028,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2038,7 +2038,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -2061,7 +2061,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"bincode",
"bitflags",
@@ -2078,7 +2078,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=28f9f65ccc625f00835d84bbb5fba274dce5aa89#28f9f65ccc625f00835d84bbb5fba274dce5aa89"
source = "git+https://github.com/RustPython/RustPython.git?rev=2edd0d264c50c7807bcff03a52a6509e8b7f187f#2edd0d264c50c7807bcff03a52a6509e8b7f187f"
dependencies = [
"ahash",
"anyhow",

View File

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

View File

@@ -246,6 +246,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -291,6 +292,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -336,6 +338,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -381,6 +384,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -426,6 +430,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -479,6 +484,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -560,6 +566,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,

View File

@@ -18,6 +18,7 @@ pub enum Plugin {
Flake8Print,
Flake8Quotes,
Flake8Return,
Flake8Simplify,
Flake8TidyImports,
McCabe,
PEP8Naming,
@@ -41,6 +42,7 @@ impl FromStr for Plugin {
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-return" => Ok(Plugin::Flake8Return),
"flake8-simplify" => Ok(Plugin::Flake8Simplify),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"mccabe" => Ok(Plugin::McCabe),
"pep8-naming" => Ok(Plugin::PEP8Naming),
@@ -65,6 +67,7 @@ impl Plugin {
Plugin::Flake8Print => CheckCodePrefix::T2,
Plugin::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Return => CheckCodePrefix::RET,
Plugin::Flake8Simplify => CheckCodePrefix::SIM,
Plugin::Flake8TidyImports => CheckCodePrefix::I25,
Plugin::McCabe => CheckCodePrefix::C9,
Plugin::PEP8Naming => CheckCodePrefix::N,
@@ -101,6 +104,7 @@ impl Plugin {
Plugin::Flake8Print => vec![CheckCodePrefix::T2],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Return => vec![CheckCodePrefix::RET],
Plugin::Flake8Simplify => vec![CheckCodePrefix::SIM],
Plugin::Flake8TidyImports => vec![CheckCodePrefix::I25],
Plugin::McCabe => vec![CheckCodePrefix::C9],
Plugin::PEP8Naming => vec![CheckCodePrefix::N],
@@ -397,6 +401,7 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Print,
Plugin::Flake8Quotes,
Plugin::Flake8Return,
Plugin::Flake8Simplify,
Plugin::Flake8TidyImports,
Plugin::PEP8Naming,
Plugin::Pyupgrade,

3
resources/test/fixtures/README.md vendored Normal file
View File

@@ -0,0 +1,3 @@
# fixtures
Fixture files used for snapshot testing.

View File

@@ -0,0 +1,12 @@
key in dict.keys() # SIM118
foo["bar"] in dict.keys() # SIM118
foo() in dict.keys() # SIM118
for key in dict.keys(): # SIM118
pass
for key in list(dict.keys()):
if some_property(key):
del dict[key]

View File

@@ -39,3 +39,27 @@ if True:
import collections
import typing
def f(): pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
# And another.
def f():
pass

View File

@@ -39,3 +39,27 @@ if True:
import collections
import typing
def f(): pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
def f():
pass
import os
# Comment goes here.
# And another.
def f():
pass

View File

@@ -4,3 +4,6 @@ a = "wrong"
hidden = {"a": "!"}
"%(a)s %(c)s" % {"x": 1, **hidden} # Ok (cannot see through splat)
"%(a)s" % {"a": 1, r"b": "!"} # F504 ("b" not used)
"%(a)s" % {'a': 1, u"b": "!"} # F504 ("b" not used)

View File

@@ -9,3 +9,4 @@ e = (
)
g = f"ghi{123:{45}}"
h = "x" "y" f"z"

View File

@@ -0,0 +1,17 @@
"""Test: annotated global."""
n: int
def f():
print(n)
def g():
global n
n = 1
g()
f()

View File

@@ -0,0 +1,32 @@
###
# Errors.
###
def f():
global x
def f():
global x
print(x)
###
# Non-errors.
###
def f():
global x
x = 1
def f():
global x
(x, y) = (1, 2)
def f():
global x
del x

View File

@@ -0,0 +1,19 @@
nonlocal x
def f():
nonlocal x
def f():
nonlocal y
def f():
x = 1
def f():
nonlocal x
def f():
nonlocal y

View File

@@ -0,0 +1,148 @@
###
# Errors.
###
def f():
print(x)
global x
print(x)
def f():
global x
print(x)
global x
print(x)
def f():
print(x)
global x, y
print(x)
def f():
global x, y
print(x)
global x, y
print(x)
def f():
x = 1
global x
x = 1
def f():
global x
x = 1
global x
x = 1
def f():
del x
global x, y
del x
def f():
global x, y
del x
global x, y
del x
def f():
del x
global x
del x
def f():
global x
del x
global x
del x
def f():
del x
global x, y
del x
def f():
global x, y
del x
global x, y
del x
###
# Non-errors.
###
def f():
global x
print(x)
def f():
global x, y
print(x)
def f():
global x
x = 1
def f():
global x, y
x = 1
def f():
global x
del x
def f():
global x, y
del x

View File

@@ -0,0 +1,73 @@
# project
An example multi-package Python project used to test setting resolution and other complex
behaviors.
## Expected behavior
Running from the repo root should pick up and enforce the appropriate settings for each package:
```
∴ cargo run resources/test/project/
Found 5 error(s).
resources/test/project/examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
3 potentially fixable with the --fix option.
```
Running from the project directory itself should exhibit the same behavior:
```
∴ cd resources/test/project/ && cargo run .
Found 5 error(s).
examples/docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
src/file.py:1:8: F401 `os` imported but unused
src/file.py:5:5: F841 Local variable `x` is assigned to but never used
src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
3 potentially fixable with the --fix option.
```
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
files:
```
∴ cd resources/test/project/examples/docs && cargo run .
Found 2 error(s).
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
1 potentially fixable with the --fix option.
```
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
file paths from the current working directory:
```
∴ cargo run -- --config=resources/test/project/pyproject.toml resources/test/project/
Found 9 error(s).
resources/test/project/examples/docs/docs/concepts/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/examples/docs/docs/file.py:1:8: F401 `os` imported but unused
resources/test/project/examples/docs/docs/file.py:3:8: F401 `numpy` imported but unused
resources/test/project/examples/docs/docs/file.py:4:27: F401 `docs.concepts.file` imported but unused
resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/file.py:1:8: F401 `os` imported but unused
resources/test/project/src/file.py:5:5: F841 Local variable `x` is assigned to but never used
resources/test/project/src/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
6 potentially fixable with the --fix option.
```
Running from a parent directory should this "ignore" the `exclude` (hence, `concepts/file.py` gets
included in the output):
```
∴ cd resources/test/project/examples && cargo run -- --config=docs/pyproject.toml .
Found 3 error(s).
docs/docs/concepts/file.py:5:5: F841 Local variable `x` is assigned to but never used
docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
1 potentially fixable with the --fix option.
```

View File

@@ -0,0 +1,5 @@
import os
def f():
x = 1

View File

@@ -0,0 +1,8 @@
import os
import numpy as np
from docs.concepts import file
def f():
x = 1

View File

@@ -0,0 +1,6 @@
[tool.ruff]
extend = "../../pyproject.toml"
src = ["."]
extend-select = ["I001"]
extend-ignore = ["F401"]
extend-exclude = ["./docs/concepts/file.py"]

View File

@@ -0,0 +1,3 @@
[tool.ruff]
src = [".", "python_modules/*"]
extend-select = ["I001"]

View File

View File

@@ -0,0 +1,5 @@
import os
def f():
x = 1

View File

@@ -0,0 +1,7 @@
import numpy as np
from app import app_file
from core import core_file
np.array([1, 2, 3])
app_file()
core_file()

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.173"
version = "0.0.178"
edition = "2021"
[dependencies]
@@ -11,8 +11,8 @@ itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
once_cell = { version = "1.16.0" }
ruff = { path = ".." }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "2edd0d264c50c7807bcff03a52a6509e8b7f187f" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.173"
version = "0.0.178"
edition = "2021"
[lib]

View File

@@ -1,9 +1,12 @@
use rustc_hash::FxHashMap;
use rustpython_ast::{Cmpop, Located};
use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
use crate::ast::types::{Binding, BindingKind, Scope};
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
/// Extract the names bound to a given __all__ assignment.
pub fn extract_all_names(stmt: &Stmt, scope: &Scope, bindings: &[Binding]) -> Vec<String> {
@@ -69,6 +72,38 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope, bindings: &[Binding]) -> Ve
names
}
#[derive(Default)]
struct GlobalVisitor<'a> {
globals: FxHashMap<&'a str, &'a Stmt>,
}
impl<'a> Visitor<'a> for GlobalVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match &stmt.node {
StmtKind::Global { names } => {
for name in names {
self.globals.insert(name, stmt);
}
}
StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::ClassDef { .. } => {
// Don't recurse.
}
_ => visitor::walk_stmt(self, stmt),
}
}
}
/// Extract a map from global name to its last-defining `Stmt`.
pub fn extract_globals(body: &[Stmt]) -> FxHashMap<&str, &Stmt> {
let mut visitor = GlobalVisitor::default();
for stmt in body {
visitor.visit_stmt(stmt);
}
visitor.globals
}
/// Check if a node is parent of a conditional branch.
pub fn on_conditional_branch<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool {
parents.any(|parent| {

View File

@@ -32,23 +32,29 @@ impl Range {
#[derive(Debug)]
pub struct FunctionDef<'a> {
// Properties derived from StmtKind::FunctionDef.
pub name: &'a str,
pub args: &'a Arguments,
pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
// pub returns: Option<&'a Expr>,
// pub type_comment: Option<&'a str>,
// Scope-specific properties.
// TODO(charlie): Create AsyncFunctionDef to mirror the AST.
pub async_: bool,
pub globals: FxHashMap<&'a str, &'a Stmt>,
}
#[derive(Debug)]
pub struct ClassDef<'a> {
// Properties derived from StmtKind::ClassDef.
pub name: &'a str,
pub bases: &'a [Expr],
pub keywords: &'a [Keyword],
// pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
// Scope-specific properties.
pub globals: FxHashMap<&'a str, &'a Stmt>,
}
#[derive(Debug)]

View File

@@ -10,7 +10,7 @@ use crate::autofix::Fix;
use crate::checks::Check;
use crate::source_code_locator::SourceCodeLocator;
#[derive(Hash)]
#[derive(Debug, Hash)]
pub enum Mode {
Generate,
Apply,

View File

@@ -37,7 +37,7 @@ use crate::visibility::{module_visibility, transition_scope, Modifier, Visibilit
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger,
flake8_import_conventions, flake8_print, flake8_return, flake8_tidy_imports,
flake8_import_conventions, flake8_print, flake8_return, flake8_simplify, flake8_tidy_imports,
flake8_unused_arguments, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
pylint, pyupgrade, visibility,
};
@@ -77,7 +77,7 @@ pub struct Checker<'a> {
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext<'a>)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext<'a>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, DeferralContext<'a>)>,
deferred_assignments: Vec<(usize, DeferralContext<'a>)>,
deferred_assignments: Vec<DeferralContext<'a>>,
// Internal, derivative state.
visible_scope: VisibleScope,
in_f_string: Option<Range>,
@@ -241,23 +241,6 @@ where
StmtKind::Global { names } => {
let scope_index = *self.scope_stack.last().expect("No current scope found");
if scope_index != GLOBAL_SCOPE_INDEX {
// If the binding doesn't already exist in the global scope, add it.
for name in names {
if !self.scopes[GLOBAL_SCOPE_INDEX]
.values
.contains_key(&name.as_str())
{
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Assignment,
used: None,
range: Range::from_located(stmt),
source: None,
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
}
// Add the binding to the current scope.
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
@@ -267,7 +250,7 @@ where
kind: BindingKind::Global,
used: usage,
range: Range::from_located(stmt),
source: None,
source: Some(RefEquality(stmt)),
});
scope.values.insert(name, index);
}
@@ -297,7 +280,7 @@ where
kind: BindingKind::Nonlocal,
used: usage,
range: Range::from_located(stmt),
source: None,
source: Some(RefEquality(stmt)),
});
scope.values.insert(name, index);
}
@@ -305,11 +288,23 @@ where
// Mark the binding in the defining scopes as used too. (Skip the global scope
// and the current scope.)
for name in names {
let mut exists = false;
for index in self.scope_stack.iter().skip(1).rev().skip(1) {
if let Some(index) = self.scopes[*index].values.get(&name.as_str()) {
exists = true;
self.bindings[*index].used = usage;
}
}
// Ensure that every nonlocal has an existing binding from a parent scope.
if !exists {
if self.settings.enabled.contains(&CheckCode::PLE0117) {
self.add_check(Check::new(
CheckKind::NonlocalWithoutBinding(name.to_string()),
Range::from_located(stmt),
));
}
}
}
}
@@ -589,12 +584,6 @@ where
for expr in decorator_list {
self.visit_expr(expr);
}
self.push_scope(Scope::new(ScopeKind::Class(ClassDef {
name,
bases,
keywords,
decorator_list,
})));
}
StmtKind::Import { names } => {
if self.settings.enabled.contains(&CheckCode::E402) {
@@ -1070,6 +1059,9 @@ where
if self.settings.enabled.contains(&CheckCode::PLW0120) {
pylint::plugins::useless_else_on_loop(self, stmt, body, orelse);
}
if self.settings.enabled.contains(&CheckCode::SIM118) {
flake8_simplify::plugins::key_in_dict_for(self, target, iter);
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
@@ -1135,7 +1127,20 @@ where
// Recurse.
let prev_visible_scope = self.visible_scope.clone();
match &stmt.node {
StmtKind::FunctionDef { body, .. } | StmtKind::AsyncFunctionDef { body, .. } => {
StmtKind::FunctionDef {
body,
name,
args,
decorator_list,
..
}
| StmtKind::AsyncFunctionDef {
body,
name,
args,
decorator_list,
..
} => {
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, body);
}
@@ -1150,13 +1155,50 @@ where
.push((definition, scope.visibility.clone()));
self.visible_scope = scope;
// If any global bindings don't already exist in the global scope, add it.
let globals = operations::extract_globals(body);
for (name, stmt) in operations::extract_globals(body) {
if self.scopes[GLOBAL_SCOPE_INDEX]
.values
.get(name)
.map_or(true, |index| {
matches!(self.bindings[*index].kind, BindingKind::Annotation)
})
{
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Assignment,
used: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
}
self.push_scope(Scope::new(ScopeKind::Function(FunctionDef {
name,
body,
args,
decorator_list,
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
globals,
})));
self.deferred_functions.push((
stmt,
(self.scope_stack.clone(), self.parents.clone()),
self.visible_scope.clone(),
));
}
StmtKind::ClassDef { body, .. } => {
StmtKind::ClassDef {
body,
name,
bases,
keywords,
decorator_list,
..
} => {
if self.settings.enabled.contains(&CheckCode::B021) {
flake8_bugbear::plugins::f_string_docstring(self, body);
}
@@ -1171,6 +1213,35 @@ where
.push((definition, scope.visibility.clone()));
self.visible_scope = scope;
// If any global bindings don't already exist in the global scope, add it.
let globals = operations::extract_globals(body);
for (name, stmt) in &globals {
if self.scopes[GLOBAL_SCOPE_INDEX]
.values
.get(name)
.map_or(true, |index| {
matches!(self.bindings[*index].kind, BindingKind::Annotation)
})
{
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Assignment,
used: None,
range: Range::from_located(stmt),
source: Some(RefEquality(stmt)),
});
self.scopes[GLOBAL_SCOPE_INDEX].values.insert(name, index);
}
}
self.push_scope(Scope::new(ScopeKind::Class(ClassDef {
name,
bases,
keywords,
decorator_list,
globals,
})));
for stmt in body {
self.visit_stmt(stmt);
}
@@ -1222,18 +1293,24 @@ where
self.visible_scope = prev_visible_scope;
// Post-visit.
if let StmtKind::ClassDef { name, .. } = &stmt.node {
self.pop_scope();
self.add_binding(
name,
Binding {
kind: BindingKind::ClassDefinition,
used: None,
range: Range::from_located(stmt),
source: Some(self.current_parent().clone()),
},
);
};
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
self.pop_scope();
}
StmtKind::ClassDef { name, .. } => {
self.pop_scope();
self.add_binding(
name,
Binding {
kind: BindingKind::ClassDefinition,
used: None,
range: Range::from_located(stmt),
source: Some(self.current_parent().clone()),
},
);
}
_ => {}
}
self.pop_parent();
}
@@ -1363,6 +1440,10 @@ where
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
if self.settings.enabled.contains(&CheckCode::PLE0118) {
pylint::plugins::used_prior_global_declaration(self, id, expr);
}
}
ExprKind::Attribute { attr, .. } => {
// Ex) typing.List[...]
@@ -1415,41 +1496,28 @@ where
}
Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F522) {
if let Some(check) = pyflakes::checks::string_dot_format_extra_named_arguments(
pyflakes::plugins::string_dot_format_extra_named_arguments(self,
&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(
pyflakes::plugins::string_dot_format_extra_positional_arguments(
self,
&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);
}
pyflakes::plugins::string_dot_format_missing_argument(
self, &summary, args, keywords, location,
);
}
if self.settings.enabled.contains(&CheckCode::F525) {
if let Some(check) =
pyflakes::checks::string_dot_format_mixing_automatic(
&summary, location,
)
{
self.add_check(check);
}
pyflakes::plugins::string_dot_format_mixing_automatic(
self, &summary, location,
);
}
}
}
@@ -1900,67 +1968,39 @@ where
}
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);
}
pyflakes::plugins::percent_format_expected_mapping(
self, &summary, right, location,
);
}
if self.settings.enabled.contains(&CheckCode::F503) {
if let Some(check) =
pyflakes::checks::percent_format_expected_sequence(
&summary, right, location,
)
{
self.add_check(check);
}
pyflakes::plugins::percent_format_expected_sequence(
self, &summary, right, location,
);
}
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);
}
pyflakes::plugins::percent_format_extra_named_arguments(
self, &summary, right, location,
);
}
if self.settings.enabled.contains(&CheckCode::F505) {
if let Some(check) =
pyflakes::checks::percent_format_missing_arguments(
&summary, right, location,
)
{
self.add_check(check);
}
pyflakes::plugins::percent_format_missing_arguments(
self, &summary, right, location,
);
}
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);
}
pyflakes::plugins::percent_format_mixed_positional_and_named(
self, &summary, location,
);
}
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);
}
pyflakes::plugins::percent_format_positional_count_mismatch(
self, &summary, right, location,
);
}
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);
}
pyflakes::plugins::percent_format_star_requires_sequence(
self, &summary, right, location,
);
}
}
}
@@ -2053,6 +2093,16 @@ where
comparators,
);
}
if self.settings.enabled.contains(&CheckCode::SIM118) {
flake8_simplify::plugins::key_in_dict_compare(
self,
expr,
left,
ops,
comparators,
);
}
}
ExprKind::Constant {
value: Constant::Str(value),
@@ -2692,6 +2742,7 @@ impl<'a> Checker<'a> {
let mut first_iter = true;
let mut in_generator = false;
let mut import_starred = false;
for scope_index in self.scope_stack.iter().rev() {
let scope = &self.scopes[*scope_index];
@@ -3054,27 +3105,8 @@ impl<'a> Checker<'a> {
self.visible_scope = visibility;
match &stmt.node {
StmtKind::FunctionDef {
name,
body,
args,
decorator_list,
..
}
| StmtKind::AsyncFunctionDef {
name,
body,
args,
decorator_list,
..
} => {
self.push_scope(Scope::new(ScopeKind::Function(FunctionDef {
name,
body,
args,
decorator_list,
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
})));
StmtKind::FunctionDef { body, args, .. }
| StmtKind::AsyncFunctionDef { body, args, .. } => {
self.visit_arguments(args);
for stmt in body {
self.visit_stmt(stmt);
@@ -3083,12 +3115,7 @@ impl<'a> Checker<'a> {
_ => unreachable!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
self.deferred_assignments.push((
*self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope();
self.deferred_assignments.push((scopes, parents));
}
}
@@ -3099,29 +3126,25 @@ impl<'a> Checker<'a> {
self.parents = parents.clone();
if let ExprKind::Lambda { args, body } = &expr.node {
self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
self.visit_arguments(args);
self.visit_expr(body);
} else {
unreachable!("Expected ExprKind::Lambda");
}
self.deferred_assignments.push((
*self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope();
self.deferred_assignments.push((scopes, parents));
}
}
fn check_deferred_assignments(&mut self) {
self.deferred_assignments.reverse();
while let Some((index, (scopes, _parents))) = self.deferred_assignments.pop() {
while let Some((scopes, _parents)) = self.deferred_assignments.pop() {
let scope_index = scopes[scopes.len() - 1];
let parent_scope_index = scopes[scopes.len() - 2];
if self.settings.enabled.contains(&CheckCode::F841) {
self.add_checks(
pyflakes::checks::unused_variable(
&self.scopes[index],
&self.scopes[scope_index],
&self.bindings,
&self.settings.dummy_variable_rgx,
)
@@ -3131,7 +3154,7 @@ impl<'a> Checker<'a> {
if self.settings.enabled.contains(&CheckCode::F842) {
self.add_checks(
pyflakes::checks::unused_annotation(
&self.scopes[index],
&self.scopes[scope_index],
&self.bindings,
&self.settings.dummy_variable_rgx,
)
@@ -3147,10 +3170,8 @@ impl<'a> Checker<'a> {
self.add_checks(
flake8_unused_arguments::plugins::unused_arguments(
self,
&self.scopes[*scopes
.last()
.expect("Expected parent scope above function scope")],
&self.scopes[index],
&self.scopes[parent_scope_index],
&self.scopes[scope_index],
&self.bindings,
)
.into_iter(),
@@ -3164,6 +3185,7 @@ impl<'a> Checker<'a> {
&& !self.settings.enabled.contains(&CheckCode::F405)
&& !self.settings.enabled.contains(&CheckCode::F811)
&& !self.settings.enabled.contains(&CheckCode::F822)
&& !self.settings.enabled.contains(&CheckCode::PLW0602)
{
return;
}
@@ -3175,6 +3197,24 @@ impl<'a> Checker<'a> {
.rev()
.map(|index| &self.scopes[*index])
{
// PLW0602
if self.settings.enabled.contains(&CheckCode::PLW0602) {
for (name, index) in &scope.values {
let binding = &self.bindings[*index];
if matches!(binding.kind, BindingKind::Global) {
checks.push(Check::new(
CheckKind::GlobalVariableNotAssigned((*name).to_string()),
binding.range,
));
}
}
}
// Imports in classes are public members.
if matches!(scope.kind, ScopeKind::Class(..)) {
continue;
}
let all_binding: Option<&Binding> = scope
.values
.get("__all__")
@@ -3202,9 +3242,9 @@ impl<'a> Checker<'a> {
}
}
// Look for any bindings that were redefined in another scope, and remain unused.
// Note that we only store references in `redefinitions` if the bindings are in
// different scopes.
// Look for any bindings that were redefined in another scope, and remain
// unused. Note that we only store references in `redefinitions` if
// the bindings are in different scopes.
if self.settings.enabled.contains(&CheckCode::F811) {
for (name, index) in &scope.values {
let binding = &self.bindings[*index];

View File

@@ -37,7 +37,7 @@ pub fn check_imports(
autofix: bool,
path: &Path,
) -> Vec<Check> {
let mut tracker = ImportTracker::new(directives, path);
let mut tracker = ImportTracker::new(locator, directives, path);
for stmt in python_ast {
tracker.visit_stmt(stmt);
}

View File

@@ -99,12 +99,15 @@ pub enum CheckCode {
PLC0414,
PLC2201,
PLC3002,
PLE0117,
PLE0118,
PLE1142,
PLR0206,
PLR0402,
PLR1701,
PLR1722,
PLW0120,
PLW0602,
// flake8-builtins
A001,
A002,
@@ -203,6 +206,8 @@ pub enum CheckCode {
YTT301,
YTT302,
YTT303,
// flake8-simplify
SIM118,
// pyupgrade
UP001,
UP003,
@@ -334,6 +339,7 @@ pub enum CheckCategory {
Flake8Print,
Flake8Quotes,
Flake8Return,
Flake8Simplify,
Flake8TidyImports,
Flake8UnusedArguments,
Eradicate,
@@ -374,6 +380,7 @@ impl CheckCategory {
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Return => "flake8-return",
CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8Simplify => "flake8-simplify",
CheckCategory::Flake8UnusedArguments => "flake8-unused-arguments",
CheckCategory::Isort => "isort",
CheckCategory::McCabe => "mccabe",
@@ -403,6 +410,7 @@ impl CheckCategory {
CheckCategory::Flake8Print => vec![CheckCodePrefix::T20],
CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q],
CheckCategory::Flake8Return => vec![CheckCodePrefix::RET],
CheckCategory::Flake8Simplify => vec![CheckCodePrefix::SIM],
CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::TID],
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Isort => vec![CheckCodePrefix::I],
@@ -478,6 +486,10 @@ impl CheckCategory {
"https://pypi.org/project/flake8-return/1.2.0/",
&Platform::PyPI,
)),
CheckCategory::Flake8Simplify => Some((
"https://pypi.org/project/flake8-simplify/0.19.3/",
&Platform::PyPI,
)),
CheckCategory::Flake8TidyImports => Some((
"https://pypi.org/project/flake8-tidy-imports/4.8.0/",
&Platform::PyPI,
@@ -635,15 +647,18 @@ pub enum CheckKind {
UnusedVariable(String),
YieldOutsideFunction(DeferralKeyword),
// pylint
ConsiderMergingIsinstance(String, Vec<String>),
UselessImportAlias,
MisplacedComparisonConstant(String),
UnnecessaryDirectLambdaCall,
PropertyWithParameters,
ConsiderUsingFromImport(String, String),
AwaitOutsideAsync,
UselessElseOnLoop,
ConsiderMergingIsinstance(String, Vec<String>),
ConsiderUsingFromImport(String, String),
GlobalVariableNotAssigned(String),
MisplacedComparisonConstant(String),
NonlocalWithoutBinding(String),
PropertyWithParameters,
UnnecessaryDirectLambdaCall,
UseSysExit(String),
UsedPriorGlobalDeclaration(String, usize),
UselessElseOnLoop,
UselessImportAlias,
// flake8-builtins
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
@@ -740,6 +755,8 @@ pub enum CheckKind {
SysVersion0Referenced,
SysVersionCmpStr10,
SysVersionSlice1Referenced,
// flake8-simplify
KeyInDict(String, String),
// pyupgrade
TypeOfPrimitive(Primitive),
UselessMetaclassType,
@@ -950,6 +967,8 @@ impl CheckCode {
CheckCode::PLC0414 => CheckKind::UselessImportAlias,
CheckCode::PLC2201 => CheckKind::MisplacedComparisonConstant("...".to_string()),
CheckCode::PLC3002 => CheckKind::UnnecessaryDirectLambdaCall,
CheckCode::PLE0117 => CheckKind::NonlocalWithoutBinding("...".to_string()),
CheckCode::PLE0118 => CheckKind::UsedPriorGlobalDeclaration("...".to_string(), 1),
CheckCode::PLE1142 => CheckKind::AwaitOutsideAsync,
CheckCode::PLR0402 => {
CheckKind::ConsiderUsingFromImport("...".to_string(), "...".to_string())
@@ -960,6 +979,7 @@ impl CheckCode {
}
CheckCode::PLR1722 => CheckKind::UseSysExit("exit".to_string()),
CheckCode::PLW0120 => CheckKind::UselessElseOnLoop,
CheckCode::PLW0602 => CheckKind::GlobalVariableNotAssigned("...".to_string()),
// flake8-builtins
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
@@ -1073,6 +1093,8 @@ impl CheckCode {
CheckCode::YTT303 => CheckKind::SysVersionSlice1Referenced,
// flake8-blind-except
CheckCode::BLE001 => CheckKind::BlindExcept("Exception".to_string()),
// flake8-simplify
CheckCode::SIM118 => CheckKind::KeyInDict("key".to_string(), "dict".to_string()),
// pyupgrade
CheckCode::UP001 => CheckKind::UselessMetaclassType,
CheckCode::UP003 => CheckKind::TypeOfPrimitive(Primitive::Str),
@@ -1402,12 +1424,15 @@ impl CheckCode {
CheckCode::PLC0414 => CheckCategory::Pylint,
CheckCode::PLC2201 => CheckCategory::Pylint,
CheckCode::PLC3002 => CheckCategory::Pylint,
CheckCode::PLE0117 => CheckCategory::Pylint,
CheckCode::PLE0118 => CheckCategory::Pylint,
CheckCode::PLE1142 => CheckCategory::Pylint,
CheckCode::PLR0206 => CheckCategory::Pylint,
CheckCode::PLR0402 => CheckCategory::Pylint,
CheckCode::PLR1701 => CheckCategory::Pylint,
CheckCode::PLR1722 => CheckCategory::Pylint,
CheckCode::PLW0120 => CheckCategory::Pylint,
CheckCode::PLW0602 => CheckCategory::Pylint,
CheckCode::Q000 => CheckCategory::Flake8Quotes,
CheckCode::Q001 => CheckCategory::Flake8Quotes,
CheckCode::Q002 => CheckCategory::Flake8Quotes,
@@ -1430,6 +1455,7 @@ impl CheckCode {
CheckCode::S105 => CheckCategory::Flake8Bandit,
CheckCode::S106 => CheckCategory::Flake8Bandit,
CheckCode::S107 => CheckCategory::Flake8Bandit,
CheckCode::SIM118 => CheckCategory::Flake8Simplify,
CheckCode::T100 => CheckCategory::Flake8Debugger,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
@@ -1530,15 +1556,18 @@ impl CheckKind {
CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292,
CheckKind::InvalidEscapeSequence(_) => &CheckCode::W605,
// pylint
CheckKind::UselessImportAlias => &CheckCode::PLC0414,
CheckKind::MisplacedComparisonConstant(..) => &CheckCode::PLC2201,
CheckKind::UnnecessaryDirectLambdaCall => &CheckCode::PLC3002,
CheckKind::AwaitOutsideAsync => &CheckCode::PLE1142,
CheckKind::ConsiderMergingIsinstance(..) => &CheckCode::PLR1701,
CheckKind::PropertyWithParameters => &CheckCode::PLR0206,
CheckKind::ConsiderUsingFromImport(..) => &CheckCode::PLR0402,
CheckKind::GlobalVariableNotAssigned(..) => &CheckCode::PLW0602,
CheckKind::MisplacedComparisonConstant(..) => &CheckCode::PLC2201,
CheckKind::PropertyWithParameters => &CheckCode::PLR0206,
CheckKind::UnnecessaryDirectLambdaCall => &CheckCode::PLC3002,
CheckKind::UseSysExit(_) => &CheckCode::PLR1722,
CheckKind::NonlocalWithoutBinding(..) => &CheckCode::PLE0117,
CheckKind::UsedPriorGlobalDeclaration(..) => &CheckCode::PLE0118,
CheckKind::UselessElseOnLoop => &CheckCode::PLW0120,
CheckKind::UselessImportAlias => &CheckCode::PLC0414,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
@@ -1635,6 +1664,8 @@ impl CheckKind {
CheckKind::SysVersion0Referenced => &CheckCode::YTT301,
CheckKind::SysVersionCmpStr10 => &CheckCode::YTT302,
CheckKind::SysVersionSlice1Referenced => &CheckCode::YTT303,
// flake8-simplify
CheckKind::KeyInDict(..) => &CheckCode::SIM118,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::UP003,
CheckKind::UselessMetaclassType => &CheckCode::UP001,
@@ -1947,6 +1978,9 @@ impl CheckKind {
CheckKind::MisplacedComparisonConstant(comparison) => {
format!("Comparison should be {comparison}")
}
CheckKind::NonlocalWithoutBinding(name) => {
format!("Nonlocal name `{name}` found without binding")
}
CheckKind::UnnecessaryDirectLambdaCall => "Lambda expression called directly. Execute \
the expression inline instead."
.to_string(),
@@ -1956,6 +1990,12 @@ impl CheckKind {
CheckKind::ConsiderUsingFromImport(module, name) => {
format!("Use `from {module} import {name}` in lieu of alias")
}
CheckKind::UsedPriorGlobalDeclaration(name, line) => {
format!("Name `{name}` is used prior to global declaration on line {line}")
}
CheckKind::GlobalVariableNotAssigned(name) => {
format!("Using global for `{name}` but no assignment is done")
}
CheckKind::AwaitOutsideAsync => {
"`await` should be used within an async function".to_string()
}
@@ -2292,6 +2332,10 @@ impl CheckKind {
CheckKind::SysVersionSlice1Referenced => {
"`sys.version[:1]` referenced (python10), use `sys.version_info`".to_string()
}
// flake8-simplify
CheckKind::KeyInDict(key, dict) => {
format!("Use `{key} in {dict}` instead of `{key} in {dict}.keys()`")
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -2636,6 +2680,7 @@ impl CheckKind {
| CheckKind::ImplicitReturn
| CheckKind::ImplicitReturnValue
| CheckKind::IsLiteral
| CheckKind::KeyInDict(..)
| CheckKind::MisplacedComparisonConstant(..)
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(..)
@@ -2651,6 +2696,7 @@ impl CheckKind {
| CheckKind::NotIsTest
| CheckKind::OneBlankLineAfterClass(..)
| CheckKind::OneBlankLineBeforeClass(..)
| CheckKind::PercentFormatExtraNamedArguments(..)
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::PPrintFound
| CheckKind::PrintFound
@@ -2663,9 +2709,11 @@ impl CheckKind {
| CheckKind::SectionUnderlineMatchesSectionLength(..)
| CheckKind::SectionUnderlineNotOverIndented(..)
| CheckKind::SetAttrWithConstant
| CheckKind::StringDotFormatExtraNamedArguments(..)
| CheckKind::SuperCallWithParameters
| CheckKind::TrueFalseComparison(..)
| CheckKind::TypeOfPrimitive(..)
| CheckKind::UnnecessaryCallAroundSorted(..)
| CheckKind::UnnecessaryCollectionCall(..)
| CheckKind::UnnecessaryComprehension(..)
| CheckKind::UnnecessaryEncodeUTF8

View File

@@ -315,6 +315,11 @@ pub enum CheckCodePrefix {
PLC300,
PLC3002,
PLE,
PLE0,
PLE01,
PLE011,
PLE0117,
PLE0118,
PLE1,
PLE11,
PLE114,
@@ -338,6 +343,9 @@ pub enum CheckCodePrefix {
PLW01,
PLW012,
PLW0120,
PLW06,
PLW060,
PLW0602,
Q,
Q0,
Q00,
@@ -374,6 +382,10 @@ pub enum CheckCodePrefix {
S105,
S106,
S107,
SIM,
SIM1,
SIM11,
SIM118,
T,
T1,
T10,
@@ -1343,7 +1355,14 @@ impl CheckCodePrefix {
CheckCodePrefix::PLC30 => vec![CheckCode::PLC3002],
CheckCodePrefix::PLC300 => vec![CheckCode::PLC3002],
CheckCodePrefix::PLC3002 => vec![CheckCode::PLC3002],
CheckCodePrefix::PLE => vec![CheckCode::PLE1142],
CheckCodePrefix::PLE => {
vec![CheckCode::PLE0117, CheckCode::PLE0118, CheckCode::PLE1142]
}
CheckCodePrefix::PLE0 => vec![CheckCode::PLE0117, CheckCode::PLE0118],
CheckCodePrefix::PLE01 => vec![CheckCode::PLE0117, CheckCode::PLE0118],
CheckCodePrefix::PLE011 => vec![CheckCode::PLE0117, CheckCode::PLE0118],
CheckCodePrefix::PLE0117 => vec![CheckCode::PLE0117],
CheckCodePrefix::PLE0118 => vec![CheckCode::PLE0118],
CheckCodePrefix::PLE1 => vec![CheckCode::PLE1142],
CheckCodePrefix::PLE11 => vec![CheckCode::PLE1142],
CheckCodePrefix::PLE114 => vec![CheckCode::PLE1142],
@@ -1367,11 +1386,14 @@ impl CheckCodePrefix {
CheckCodePrefix::PLR1701 => vec![CheckCode::PLR1701],
CheckCodePrefix::PLR172 => vec![CheckCode::PLR1722],
CheckCodePrefix::PLR1722 => vec![CheckCode::PLR1722],
CheckCodePrefix::PLW => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW0 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW => vec![CheckCode::PLW0120, CheckCode::PLW0602],
CheckCodePrefix::PLW0 => vec![CheckCode::PLW0120, CheckCode::PLW0602],
CheckCodePrefix::PLW01 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW012 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW0120 => vec![CheckCode::PLW0120],
CheckCodePrefix::PLW06 => vec![CheckCode::PLW0602],
CheckCodePrefix::PLW060 => vec![CheckCode::PLW0602],
CheckCodePrefix::PLW0602 => vec![CheckCode::PLW0602],
CheckCodePrefix::Q => vec![
CheckCode::Q000,
CheckCode::Q001,
@@ -1476,6 +1498,10 @@ impl CheckCodePrefix {
CheckCodePrefix::S105 => vec![CheckCode::S105],
CheckCodePrefix::S106 => vec![CheckCode::S106],
CheckCodePrefix::S107 => vec![CheckCode::S107],
CheckCodePrefix::SIM => vec![CheckCode::SIM118],
CheckCodePrefix::SIM1 => vec![CheckCode::SIM118],
CheckCodePrefix::SIM11 => vec![CheckCode::SIM118],
CheckCodePrefix::SIM118 => vec![CheckCode::SIM118],
CheckCodePrefix::T => vec![CheckCode::T100, CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T1 => vec![CheckCode::T100],
CheckCodePrefix::T10 => vec![CheckCode::T100],
@@ -2118,6 +2144,11 @@ impl CheckCodePrefix {
CheckCodePrefix::PLC300 => SuffixLength::Three,
CheckCodePrefix::PLC3002 => SuffixLength::Four,
CheckCodePrefix::PLE => SuffixLength::Zero,
CheckCodePrefix::PLE0 => SuffixLength::One,
CheckCodePrefix::PLE01 => SuffixLength::Two,
CheckCodePrefix::PLE011 => SuffixLength::Three,
CheckCodePrefix::PLE0117 => SuffixLength::Four,
CheckCodePrefix::PLE0118 => SuffixLength::Four,
CheckCodePrefix::PLE1 => SuffixLength::One,
CheckCodePrefix::PLE11 => SuffixLength::Two,
CheckCodePrefix::PLE114 => SuffixLength::Three,
@@ -2141,6 +2172,9 @@ impl CheckCodePrefix {
CheckCodePrefix::PLW01 => SuffixLength::Two,
CheckCodePrefix::PLW012 => SuffixLength::Three,
CheckCodePrefix::PLW0120 => SuffixLength::Four,
CheckCodePrefix::PLW06 => SuffixLength::Two,
CheckCodePrefix::PLW060 => SuffixLength::Three,
CheckCodePrefix::PLW0602 => SuffixLength::Four,
CheckCodePrefix::Q => SuffixLength::Zero,
CheckCodePrefix::Q0 => SuffixLength::One,
CheckCodePrefix::Q00 => SuffixLength::Two,
@@ -2177,6 +2211,10 @@ impl CheckCodePrefix {
CheckCodePrefix::S105 => SuffixLength::Three,
CheckCodePrefix::S106 => SuffixLength::Three,
CheckCodePrefix::S107 => SuffixLength::Three,
CheckCodePrefix::SIM => SuffixLength::Zero,
CheckCodePrefix::SIM1 => SuffixLength::One,
CheckCodePrefix::SIM11 => SuffixLength::Two,
CheckCodePrefix::SIM118 => SuffixLength::Three,
CheckCodePrefix::T => SuffixLength::Zero,
CheckCodePrefix::T1 => SuffixLength::One,
CheckCodePrefix::T10 => SuffixLength::Two,
@@ -2277,6 +2315,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::RET,
CheckCodePrefix::RUF,
CheckCodePrefix::S,
CheckCodePrefix::SIM,
CheckCodePrefix::T,
CheckCodePrefix::TID,
CheckCodePrefix::UP,

View File

@@ -1,4 +1,4 @@
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use clap::{command, Parser};
use regex::Regex;
@@ -6,6 +6,7 @@ use rustc_hash::FxHashMap;
use crate::checks::CheckCode;
use crate::checks_gen::CheckCodePrefix;
use crate::fs;
use crate::logging::LogLevel;
use crate::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
@@ -47,46 +48,48 @@ pub struct Cli {
pub no_cache: bool,
/// List of error codes to enable.
#[arg(long, value_delimiter = ',')]
pub select: Vec<CheckCodePrefix>,
pub select: Option<Vec<CheckCodePrefix>>,
/// Like --select, but adds additional error codes on top of the selected
/// ones.
#[arg(long, value_delimiter = ',')]
pub extend_select: Vec<CheckCodePrefix>,
pub extend_select: Option<Vec<CheckCodePrefix>>,
/// List of error codes to ignore.
#[arg(long, value_delimiter = ',')]
pub ignore: Vec<CheckCodePrefix>,
pub ignore: Option<Vec<CheckCodePrefix>>,
/// Like --ignore, but adds additional error codes on top of the ignored
/// ones.
#[arg(long, value_delimiter = ',')]
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
/// List of paths, used to exclude files and/or directories from checks.
#[arg(long, value_delimiter = ',')]
pub exclude: Vec<FilePattern>,
pub exclude: Option<Vec<FilePattern>>,
/// Like --exclude, but adds additional files and directories on top of the
/// excluded ones.
#[arg(long, value_delimiter = ',')]
pub extend_exclude: Vec<FilePattern>,
pub extend_exclude: Option<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 = ',')]
pub fixable: Vec<CheckCodePrefix>,
pub fixable: Option<Vec<CheckCodePrefix>>,
/// List of error codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',')]
pub unfixable: Vec<CheckCodePrefix>,
pub unfixable: Option<Vec<CheckCodePrefix>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Vec<PatternPrefixPair>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
/// Output serialization format for error messages.
#[arg(long, value_enum)]
pub format: Option<SerializationFormat>,
/// Show violations with source code.
#[arg(long)]
pub show_source: bool,
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
#[clap(long, overrides_with("show_source"), hide = true)]
no_show_source: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See Ruff's settings.
/// See the settings Ruff used for the first matching file.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
@@ -121,9 +124,47 @@ pub struct Cli {
}
impl Cli {
// See: https://github.com/clap-rs/clap/issues/3146
pub fn fix(&self) -> Option<bool> {
resolve_bool_arg(self.fix, self.no_fix)
/// Partition the CLI into command-line arguments and configuration
/// overrides.
pub fn partition(self) -> (Arguments, Overrides) {
(
Arguments {
add_noqa: self.add_noqa,
autoformat: self.autoformat,
config: self.config,
exit_zero: self.exit_zero,
explain: self.explain,
files: self.files,
generate_shell_completion: self.generate_shell_completion,
no_cache: self.no_cache,
quiet: self.quiet,
show_files: self.show_files,
show_settings: self.show_settings,
silent: self.silent,
stdin_filename: self.stdin_filename,
verbose: self.verbose,
watch: self.watch,
},
Overrides {
dummy_variable_rgx: self.dummy_variable_rgx,
exclude: self.exclude,
extend_exclude: self.extend_exclude,
extend_ignore: self.extend_ignore,
extend_select: self.extend_select,
fixable: self.fixable,
ignore: self.ignore,
line_length: self.line_length,
max_complexity: self.max_complexity,
per_file_ignores: self.per_file_ignores,
select: self.select,
show_source: resolve_bool_arg(self.show_source, self.no_show_source),
target_version: self.target_version,
unfixable: self.unfixable,
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
fix: resolve_bool_arg(self.fix, self.no_fix),
format: self.format,
},
)
}
}
@@ -136,8 +177,52 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
}
}
/// CLI settings that are distinct from configuration (commands, lists of files,
/// etc.).
#[allow(clippy::struct_excessive_bools)]
pub struct Arguments {
pub add_noqa: bool,
pub autoformat: bool,
pub config: Option<PathBuf>,
pub exit_zero: bool,
pub explain: Option<CheckCode>,
pub files: Vec<PathBuf>,
pub generate_shell_completion: Option<clap_complete_command::Shell>,
pub no_cache: bool,
pub quiet: bool,
pub show_files: bool,
pub show_settings: bool,
pub silent: bool,
pub stdin_filename: Option<String>,
pub verbose: bool,
pub watch: bool,
}
/// CLI settings that function as configuration overrides.
#[derive(Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct Overrides {
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
pub extend_select: Option<Vec<CheckCodePrefix>>,
pub fixable: Option<Vec<CheckCodePrefix>>,
pub ignore: Option<Vec<CheckCodePrefix>>,
pub line_length: Option<usize>,
pub max_complexity: Option<usize>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub select: Option<Vec<CheckCodePrefix>>,
pub show_source: Option<bool>,
pub target_version: Option<PythonVersion>,
pub unfixable: Option<Vec<CheckCodePrefix>>,
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub fix: Option<bool>,
pub format: Option<SerializationFormat>,
}
/// Map the CLI settings to a `LogLevel`.
pub fn extract_log_level(cli: &Cli) -> LogLevel {
pub fn extract_log_level(cli: &Arguments) -> LogLevel {
if cli.silent {
LogLevel::Silent
} else if cli.quiet {
@@ -160,6 +245,9 @@ pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgn
}
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
.map(|(pattern, prefixes)| {
let absolute = fs::normalize_path(Path::new(&pattern));
PerFileIgnore::new(pattern, absolute, &prefixes)
})
.collect()
}

View File

@@ -1,36 +1,207 @@
use std::path::PathBuf;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{bail, Result};
use itertools::Itertools;
use log::{debug, error};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustpython_ast::Location;
use serde::Serialize;
use walkdir::DirEntry;
use crate::checks::CheckCode;
use crate::fs::iter_python_files;
use crate::autofix::fixer;
use crate::checks::{CheckCode, CheckKind};
use crate::cli::Overrides;
use crate::iterators::par_iter;
use crate::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use crate::message::Message;
use crate::resolver;
use crate::resolver::Strategy;
use crate::settings::types::SerializationFormat;
use crate::{Configuration, Settings};
/// Run the linter over a collection of files.
pub fn run(
files: &[PathBuf],
strategy: &Strategy,
overrides: &Overrides,
cache: bool,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
// Collect all the files to check.
let start = Instant::now();
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut diagnostics: Diagnostics = par_iter(&paths)
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
let settings = resolver.resolve(path, strategy);
lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
e.path().map(Path::to_owned),
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, strategy);
if settings.enabled.contains(&CheckCode::E902) {
Diagnostics::new(vec![Message {
kind: CheckKind::IOError(message),
location: Location::default(),
end_location: Location::default(),
fix: None,
filename: path.to_string_lossy().to_string(),
source: None,
}])
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
Diagnostics::default()
}
} else {
error!("{message}");
Diagnostics::default()
}
})
})
.reduce(Diagnostics::default, |mut acc, item| {
acc += item;
acc
});
diagnostics.messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
Ok(diagnostics)
}
/// Read a `String` from `stdin`.
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
/// Run the linter over a single file, read from `stdin`.
pub fn run_stdin(
strategy: &Strategy,
filename: &Path,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?;
let settings = match strategy {
Strategy::Fixed(settings) => settings,
Strategy::Hierarchical(settings) => settings,
};
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)
}
/// Add `noqa` directives to a collection of files.
pub fn add_noqa(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
// Collect all the files to check.
let start = Instant::now();
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = par_iter(&paths)
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, strategy);
match add_noqa_to_path(path, settings) {
Ok(count) => Some(count),
Err(e) => {
error!("Failed to add noqa to {}: {e}", path.to_string_lossy());
None
}
}
})
.sum();
let duration = start.elapsed();
debug!("Added noqa to files in: {:?}", duration);
Ok(modifications)
}
/// Automatically format a collection of files.
pub fn autoformat(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<usize> {
// Collect all the files to format.
let start = Instant::now();
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications = par_iter(&paths)
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, strategy);
match autoformat_path(path, settings) {
Ok(()) => Some(()),
Err(e) => {
error!("Failed to autoformat {}: {e}", path.to_string_lossy());
None
}
}
})
.count();
let duration = start.elapsed();
debug!("Auto-formatted files in: {:?}", duration);
Ok(modifications)
}
/// 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:?}");
pub fn show_settings(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
// Print the list of files.
let Some(entry) = paths
.iter()
.flatten()
.sorted_by(|a, b| a.path().cmp(b.path())).next() else {
bail!("No files found under the given path");
};
let path = entry.path();
let settings = resolver.resolve(path, strategy);
println!("Resolved settings for: {path:?}");
println!("{settings:#?}");
Ok(())
}
/// 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
pub fn show_files(files: &[PathBuf], strategy: &Strategy, overrides: &Overrides) -> Result<()> {
// Collect all files in the hierarchy.
let (paths, _resolver) = resolver::resolve_python_files(files, strategy, overrides)?;
// Print the list of files.
for entry in paths
.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 {
.sorted_by(|a, b| a.path().cmp(b.path()))
{
println!("{}", entry.path().to_string_lossy());
}
Ok(())
}
#[derive(Serialize)]
@@ -41,7 +212,7 @@ struct Explanation<'a> {
}
/// Explain a `CheckCode` to the user.
pub fn explain(code: &CheckCode, format: SerializationFormat) -> Result<()> {
pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
match format {
SerializationFormat::Text | SerializationFormat::Grouped => {
println!(

View File

@@ -0,0 +1,29 @@
pub mod plugins;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::SIM118, Path::new("SIM118.py"); "SIM118")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_simplify")
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

View File

@@ -0,0 +1,74 @@
use rustpython_ast::{Cmpop, Expr, ExprKind};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
/// SIM118
fn key_in_dict(checker: &mut Checker, left: &Expr, right: &Expr, range: Range) {
let ExprKind::Call {
func,
args,
keywords,
} = &right.node else {
return;
};
if !(args.is_empty() && keywords.is_empty()) {
return;
}
let ExprKind::Attribute { attr, value, .. } = &func.node else {
return;
};
if attr != "keys" {
return;
}
let mut check = Check::new(
CheckKind::KeyInDict(left.to_string(), value.to_string()),
range,
);
if checker.patch(&CheckCode::SIM118) {
let content = right.to_string().replace(".keys()", "");
check.amend(Fix::replacement(
content,
right.location,
right.end_location.unwrap(),
));
}
checker.add_check(check);
}
/// SIM118 in a for loop
pub fn key_in_dict_for(checker: &mut Checker, target: &Expr, iter: &Expr) {
key_in_dict(
checker,
target,
iter,
Range {
location: target.location,
end_location: iter.end_location.unwrap(),
},
);
}
/// SIM118 in a comparison
pub fn key_in_dict_compare(
checker: &mut Checker,
expr: &Expr,
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
) {
if !matches!(ops[..], [Cmpop::In]) {
return;
}
if comparators.len() != 1 {
return;
}
let right = comparators.first().unwrap();
key_in_dict(checker, left, right, Range::from_located(expr));
}

View File

@@ -0,0 +1,3 @@
pub use key_in_dict::{key_in_dict_compare, key_in_dict_for};
mod key_in_dict;

View File

@@ -0,0 +1,77 @@
---
source: src/flake8_simplify/mod.rs
expression: checks
---
- kind:
KeyInDict:
- key
- dict
location:
row: 1
column: 0
end_location:
row: 1
column: 18
fix:
content: dict
location:
row: 1
column: 7
end_location:
row: 1
column: 18
- kind:
KeyInDict:
- "foo['bar']"
- dict
location:
row: 3
column: 0
end_location:
row: 3
column: 25
fix:
content: dict
location:
row: 3
column: 14
end_location:
row: 3
column: 25
- kind:
KeyInDict:
- foo()
- dict
location:
row: 5
column: 0
end_location:
row: 5
column: 20
fix:
content: dict
location:
row: 5
column: 9
end_location:
row: 5
column: 20
- kind:
KeyInDict:
- key
- dict
location:
row: 7
column: 4
end_location:
row: 7
column: 22
fix:
content: dict
location:
row: 7
column: 11
end_location:
row: 7
column: 22

172
src/fs.rs
View File

@@ -5,15 +5,13 @@ 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;
/// Extract the absolute path and basename (as strings) from a Path.
fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
pub fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
let file_path = path
.to_str()
.ok_or_else(|| anyhow!("Unable to parse filename: {:?}", path))?;
@@ -25,62 +23,7 @@ fn extract_path_names(path: &Path) -> Result<(&str, &str)> {
Ok((file_path, file_basename))
}
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 {
let file_name = path.to_string_lossy();
file_name.ends_with(".py") || file_name.ends_with(".pyi")
}
pub fn iter_python_files<'a>(
path: &'a Path,
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();
WalkDir::new(normalize_path(path))
.into_iter()
.filter_entry(move |entry| {
if !has_exclude && !has_extend_exclude {
return true;
}
let path = entry.path();
match extract_path_names(path) {
Ok((file_path, file_basename)) => {
if has_exclude && is_excluded(file_path, file_basename, exclude) {
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if has_extend_exclude
&& is_excluded(file_path, file_basename, extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
}
Err(e) => {
debug!("Ignored path due to error in parsing: {:?}: {}", path, e);
true
}
}
})
.filter(|entry| {
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 || is_included(entry.path()))
&& !entry.file_type().is_dir()
&& !(entry.file_type().is_symlink() && entry.path().is_dir())
})
})
}
/// Create tree set with codes matching the pattern/code pairs.
/// Create a set with codes matching the pattern/code pairs.
pub(crate) fn ignores_from_path<'a>(
path: &Path,
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)],
@@ -128,114 +71,3 @@ pub(crate) fn read_file(path: &Path) -> Result<String> {
buf_reader.read_to_string(&mut contents)?;
Ok(contents)
}
#[cfg(test)]
mod tests {
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};
use crate::settings::types::FilePattern;
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
assert!(is_included(&path));
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
assert!(!is_included(&path));
let path = Path::new("foo/bar/baz").absolutize().unwrap();
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 = FilePattern::User("foo".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
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 = FilePattern::User("bar".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
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 = FilePattern::User("baz.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
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 = FilePattern::User("foo/bar".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
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 = FilePattern::User("foo/bar/baz.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
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 = FilePattern::User("foo/bar/*.py".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
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 = FilePattern::User("baz".to_string());
let (file_path, file_basename) = extract_path_names(&path)?;
assert!(!is_excluded(
file_path,
file_basename,
&make_exclusion(exclude, Some(&project_root.to_path_buf()))
));
Ok(())
}
}

55
src/isort/helpers.rs Normal file
View File

@@ -0,0 +1,55 @@
use rustpython_ast::Stmt;
use crate::source_code_locator::SourceCodeLocator;
/// Return `true` if a `Stmt` is preceded by a "comment break"
pub fn has_comment_break(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
// Starting from the `Stmt` (`def f(): pass`), we want to detect patterns like
// this:
//
// import os
//
// # Detached comment.
//
// def f(): pass
// This should also be detected:
//
// import os
//
// # Detached comment.
//
// # Direct comment.
// def f(): pass
// But this should not:
//
// import os
//
// # Direct comment.
// def f(): pass
let mut seen_blank = false;
for line in locator
.slice_source_code_until(&stmt.location)
.lines()
.rev()
{
let line = line.trim();
if seen_blank {
if line.starts_with('#') {
return true;
} else if !line.is_empty() {
break;
}
} else {
if line.is_empty() {
seen_blank = true;
} else if line.starts_with('#') || line.starts_with('@') {
continue;
} else {
break;
}
}
}
false
}

View File

@@ -18,6 +18,7 @@ use crate::isort::types::{
mod categorize;
mod comments;
pub mod format;
mod helpers;
pub mod plugins;
pub mod settings;
mod sorting;

View File

@@ -49,32 +49,17 @@ expression: checks
column: 0
- kind: UnsortedImports
location:
row: 33
row: 52
column: 0
end_location:
row: 35
row: 54
column: 0
fix:
content: " import collections\n import typing\n\n"
content: "import os\n\n\n"
location:
row: 33
row: 52
column: 0
end_location:
row: 35
column: 0
- kind: UnsortedImports
location:
row: 39
column: 0
end_location:
row: 41
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 39
column: 0
end_location:
row: 41
row: 54
column: 0

View File

@@ -47,19 +47,4 @@ expression: checks
end_location:
row: 16
column: 0
- kind: UnsortedImports
location:
row: 33
column: 0
end_location:
row: 35
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 33
column: 0
end_location:
row: 35
column: 0

View File

@@ -25,7 +25,7 @@ expression: checks
row: 6
column: 13
fix:
content: " import os\n import sys\n\n"
content: " import os\n import sys\n"
location:
row: 5
column: 0

View File

@@ -8,20 +8,24 @@ use rustpython_ast::{
use crate::ast::visitor::Visitor;
use crate::directives::IsortDirectives;
use crate::isort::helpers;
use crate::source_code_locator::SourceCodeLocator;
#[derive(Debug)]
pub enum Trailer {
Sibling,
ClassDef,
FunctionDef,
}
#[derive(Default)]
#[derive(Debug, Default)]
pub struct Block<'a> {
pub imports: Vec<&'a Stmt>,
pub trailer: Option<Trailer>,
}
pub struct ImportTracker<'a> {
locator: &'a SourceCodeLocator<'a>,
directives: &'a IsortDirectives,
pyi: bool,
blocks: Vec<Block<'a>>,
@@ -30,8 +34,13 @@ pub struct ImportTracker<'a> {
}
impl<'a> ImportTracker<'a> {
pub fn new(directives: &'a IsortDirectives, path: &'a Path) -> Self {
pub fn new(
locator: &'a SourceCodeLocator<'a>,
directives: &'a IsortDirectives,
path: &'a Path,
) -> Self {
Self {
locator,
directives,
pyi: path.extension().map_or(false, |ext| ext == "pyi"),
blocks: vec![Block::default()],
@@ -46,31 +55,46 @@ impl<'a> ImportTracker<'a> {
}
fn trailer_for(&self, stmt: &'a Stmt) -> Option<Trailer> {
if self.pyi {
// Black treats interface files differently, limiting to one newline
// (`Trailing::Sibling`), and avoiding inserting any newlines in nested function
// blocks.
if self.nested
&& matches!(
stmt.node,
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. }
)
{
None
} else {
Some(Trailer::Sibling)
}
} else if self.nested {
Some(Trailer::Sibling)
} else {
Some(match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
Trailer::FunctionDef
}
StmtKind::ClassDef { .. } => Trailer::ClassDef,
_ => Trailer::Sibling,
})
// No need to compute trailers if we won't be finalizing anything.
let index = self.blocks.len() - 1;
if self.blocks[index].imports.is_empty() {
return None;
}
// Similar to isort, avoid enforcing any newline behaviors in nested blocks.
if self.nested {
return None;
}
Some(if self.pyi {
// Black treats interface files differently, limiting to one newline
// (`Trailing::Sibling`).
Trailer::Sibling
} else {
// If the import block is followed by a class or function, we want to enforce
// two blank lines. The exception: if, between the import and the class or
// function, we have at least one commented line, followed by at
// least one blank line. In that case, we treat it as a regular
// sibling (i.e., as if the comment is the next statement, as
// opposed to the class or function).
match &stmt.node {
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
if helpers::has_comment_break(stmt, self.locator) {
Trailer::Sibling
} else {
Trailer::FunctionDef
}
}
StmtKind::ClassDef { .. } => {
if helpers::has_comment_break(stmt, self.locator) {
Trailer::Sibling
} else {
Trailer::ClassDef
}
}
_ => Trailer::Sibling,
}
})
}
fn finalize(&mut self, trailer: Option<Trailer>) {

16
src/iterators.rs Normal file
View File

@@ -0,0 +1,16 @@
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
pub fn par_iter<T: Sync>(iterable: &[T]) -> impl Iterator<Item = &T> {
iterable.iter()
}

View File

@@ -14,13 +14,14 @@
use std::path::Path;
use anyhow::Result;
use log::debug;
use path_absolutize::path_dedot;
use rustpython_helpers::tokenize;
use rustpython_parser::lexer::LexResult;
use settings::{pyproject, Settings};
use crate::checks::Check;
use crate::linter::check_path;
use crate::resolver::Relativity;
use crate::settings::configuration::Configuration;
use crate::source_code_locator::SourceCodeLocator;
@@ -53,10 +54,12 @@ mod flake8_import_conventions;
mod flake8_print;
pub mod flake8_quotes;
mod flake8_return;
mod flake8_simplify;
pub mod flake8_tidy_imports;
mod flake8_unused_arguments;
pub mod fs;
mod isort;
pub mod iterators;
mod lex;
pub mod linter;
pub mod logging;
@@ -72,6 +75,7 @@ mod pygrep_hooks;
mod pylint;
mod python;
mod pyupgrade;
pub mod resolver;
mod ruff;
mod rustpython_helpers;
pub mod settings;
@@ -81,24 +85,24 @@ pub mod updates;
mod vendored;
pub mod visibility;
/// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> {
if let Some(pyproject) = pyproject::find_pyproject_toml(path) {
// First priority: `pyproject.toml` in the current `Path`.
resolver::resolve_settings(&pyproject, &Relativity::Parent, None)
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {
// Second priority: user-specific `pyproject.toml`.
resolver::resolve_settings(&pyproject, &Relativity::Cwd, None)
} else {
// Fallback: default settings.
Settings::from_configuration(Configuration::default(), &path_dedot::CWD)
}
}
/// Run Ruff over Python source code directly.
pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
// Find the project root and pyproject.toml.
let project_root = pyproject::find_project_root(&[path.to_path_buf()]);
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
let pyproject = pyproject::find_pyproject_toml(project_root.as_ref());
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
let settings = Settings::from_configuration(
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?,
project_root.as_ref(),
)?;
// Load the relevant `Settings` for the given `Path`.
let settings = resolve(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);

View File

@@ -211,7 +211,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
}
/// Apply autoformatting to the source code at the given `Path`.
pub fn autoformat_path(path: &Path) -> Result<()> {
pub fn autoformat_path(path: &Path, _settings: &Settings) -> Result<()> {
// Read the file from disk.
let contents = fs::read_file(path)?;

View File

@@ -11,301 +11,105 @@
clippy::too_many_lines
)]
use std::io::{self, Read};
use std::io::{self};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::sync::mpsc::channel;
use std::time::Instant;
use ::ruff::autofix::fixer;
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, Diagnostics};
use ::ruff::cli::{extract_log_level, Cli};
use ::ruff::logging::{set_up_logging, LogLevel};
use ::ruff::message::Message;
use ::ruff::printer::Printer;
use ::ruff::resolver::Strategy;
use ::ruff::settings::configuration::Configuration;
use ::ruff::settings::types::SerializationFormat;
use ::ruff::settings::{pyproject, Settings};
#[cfg(feature = "update-informer")]
use ::ruff::updates;
use ::ruff::{cache, commands, fs};
use ::ruff::{cache, commands};
use anyhow::Result;
use clap::{CommandFactory, Parser};
use colored::Colorize;
use log::{debug, error};
use notify::{recommended_watcher, RecursiveMode, Watcher};
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustpython_ast::Location;
use walkdir::DirEntry;
use path_absolutize::path_dedot;
use ruff::cli::Overrides;
use ruff::resolver::{resolve_settings, Relativity};
/// Shim that calls `par_iter` except for wasm because there's no wasm support
/// in rayon yet (there is a shim to be used for the web, but it requires js
/// cooperation) Unfortunately, `ParallelIterator` does not implement `Iterator`
/// so the signatures diverge
#[cfg(not(target_family = "wasm"))]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl ParallelIterator<Item = &T> {
iterable.par_iter()
}
#[cfg(target_family = "wasm")]
fn par_iter<T: Sync>(iterable: &Vec<T>) -> impl Iterator<Item = &T> {
iterable.iter()
}
fn read_from_stdin() -> Result<String> {
let mut buffer = String::new();
io::stdin().lock().read_to_string(&mut buffer)?;
Ok(buffer)
}
fn run_once_stdin(
settings: &Settings,
filename: &Path,
autofix: &fixer::Mode,
) -> Result<Diagnostics> {
let stdin = read_from_stdin()?;
let mut diagnostics = lint_stdin(filename, &stdin, settings, autofix)?;
diagnostics.messages.sort_unstable();
Ok(diagnostics)
}
fn run_once(
files: &[PathBuf],
settings: &Settings,
cache: bool,
autofix: &fixer::Mode,
) -> Diagnostics {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<Result<DirEntry, walkdir::Error>> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let mut diagnostics: Diagnostics = par_iter(&paths)
.map(|entry| {
match entry {
Ok(entry) => {
let path = entry.path();
lint_path(path, settings, &cache.into(), autofix)
.map_err(|e| (Some(path.to_owned()), e.to_string()))
}
Err(e) => Err((
e.path().map(Path::to_owned),
e.io_error()
.map_or_else(|| e.to_string(), io::Error::to_string),
)),
}
.unwrap_or_else(|(path, message)| {
if let Some(path) = path {
if settings.enabled.contains(&CheckCode::E902) {
Diagnostics::new(vec![Message {
kind: CheckKind::IOError(message),
location: Location::default(),
end_location: Location::default(),
fix: None,
filename: path.to_string_lossy().to_string(),
source: None,
}])
} else {
error!("Failed to check {}: {message}", path.to_string_lossy());
Diagnostics::default()
}
} else {
error!("{message}");
Diagnostics::default()
}
})
})
.reduce(Diagnostics::default, |mut acc, item| {
acc += item;
acc
});
diagnostics.messages.sort_unstable();
let duration = start.elapsed();
debug!("Checked files in: {:?}", duration);
diagnostics
}
fn add_noqa(files: &[PathBuf], settings: &Settings) -> usize {
// Collect all the files to check.
let start = Instant::now();
let paths: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications: usize = par_iter(&paths)
.filter_map(|entry| {
let path = entry.path();
match add_noqa_to_path(path, settings) {
Ok(count) => Some(count),
Err(e) => {
error!("Failed to add noqa to {}: {e}", path.to_string_lossy());
None
}
}
})
.sum();
let duration = start.elapsed();
debug!("Added noqa to files in: {:?}", duration);
modifications
}
fn autoformat(files: &[PathBuf], settings: &Settings) -> usize {
// Collect all the files to format.
let start = Instant::now();
let paths: Vec<DirEntry> = files
.iter()
.flat_map(|path| iter_python_files(path, &settings.exclude, &settings.extend_exclude))
.flatten()
.collect();
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
let start = Instant::now();
let modifications = par_iter(&paths)
.filter_map(|entry| {
let path = entry.path();
match autoformat_path(path) {
Ok(()) => Some(()),
Err(e) => {
error!("Failed to autoformat {}: {e}", path.to_string_lossy());
None
}
}
})
.count();
let duration = start.elapsed();
debug!("Auto-formatted files in: {:?}", duration);
modifications
/// Resolve the relevant settings strategy and defaults for the current
/// invocation.
fn resolve(config: Option<PathBuf>, overrides: &Overrides) -> Result<Strategy> {
if let Some(pyproject) = config {
// First priority: the user specified a `pyproject.toml` file. Use that
// `pyproject.toml` for _all_ configuration, and resolve paths relative to the
// current working directory. (This matches ESLint's behavior.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(Strategy::Fixed(settings))
} else if let Some(pyproject) = pyproject::find_pyproject_toml(path_dedot::CWD.as_path()) {
// Second priority: find a `pyproject.toml` file in the current working path,
// and resolve all paths relative to that directory. (With
// `Strategy::Hierarchical`, we'll end up finding the "closest" `pyproject.toml`
// file for every Python file later on, so these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
Ok(Strategy::Hierarchical(settings))
} else if let Some(pyproject) = pyproject::find_user_pyproject_toml() {
// Third priority: find a user-specific `pyproject.toml`, but resolve all paths
// relative the current working directory. (With `Strategy::Hierarchical`, we'll
// end up the "closest" `pyproject.toml` file for every Python file later on, so
// these act as the "default" settings.)
let settings = resolve_settings(&pyproject, &Relativity::Cwd, Some(overrides))?;
Ok(Strategy::Hierarchical(settings))
} else {
// Fallback: load Ruff's default settings, and resolve all paths relative to the
// current working directory. (With `Strategy::Hierarchical`, we'll end up the
// "closest" `pyproject.toml` file for every Python file later on, so these act
// as the "default" settings.)
let settings = Settings::from_configuration(Configuration::default(), &path_dedot::CWD)?;
Ok(Strategy::Hierarchical(settings))
}
}
fn inner_main() -> Result<ExitCode> {
// Extract command-line arguments.
let cli = Cli::parse();
let fix = cli.fix();
let (cli, overrides) = Cli::parse().partition();
let log_level = extract_log_level(&cli);
set_up_logging(&log_level)?;
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 config: Option<PathBuf> = cli.config;
let project_root = config.as_ref().map_or_else(
|| pyproject::find_project_root(&cli.files),
|config| config.parent().map(fs::normalize_path),
);
let pyproject = config.or_else(|| pyproject::find_pyproject_toml(project_root.as_ref()));
// Reconcile configuration from pyproject.toml and command-line arguments.
let mut configuration =
Configuration::from_pyproject(pyproject.as_ref(), project_root.as_ref())?;
if !cli.exclude.is_empty() {
configuration.exclude = cli.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);
}
if !cli.select.is_empty() {
configuration.select = cli.select;
}
if !cli.extend_select.is_empty() {
configuration.extend_select = cli.extend_select;
}
if !cli.ignore.is_empty() {
configuration.ignore = cli.ignore;
}
if !cli.extend_ignore.is_empty() {
configuration.extend_ignore = cli.extend_ignore;
}
if !cli.fixable.is_empty() {
configuration.fixable = cli.fixable;
}
if !cli.unfixable.is_empty() {
configuration.unfixable = cli.unfixable;
}
if let Some(format) = cli.format {
configuration.format = format;
}
if let Some(line_length) = cli.line_length {
configuration.line_length = line_length;
}
if let Some(max_complexity) = cli.max_complexity {
configuration.mccabe.max_complexity = max_complexity;
}
if let Some(target_version) = cli.target_version {
configuration.target_version = target_version;
}
if let Some(dummy_variable_rgx) = cli.dummy_variable_rgx {
configuration.dummy_variable_rgx = dummy_variable_rgx;
}
if let Some(fix) = fix {
configuration.fix = fix;
}
if cli.show_source {
configuration.show_source = true;
}
if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both).");
return Ok(ExitCode::FAILURE);
anyhow::bail!("specify --show-settings or show-files (not both)")
}
if cli.show_settings {
commands::show_settings(&configuration, project_root.as_ref(), pyproject.as_ref());
if let Some(shell) = cli.generate_shell_completion {
shell.generate(&mut Cli::command(), &mut io::stdout());
return Ok(ExitCode::SUCCESS);
}
// Extract settings for internal use.
let autofix = if configuration.fix {
// Construct the "default" settings. These are used when no `pyproject.toml`
// files are present, or files are injected from outside of the hierarchy.
let strategy = resolve(cli.config, &overrides)?;
// Extract options that are included in `Settings`, but only apply at the top
// level.
let (fix, format) = match &strategy {
Strategy::Fixed(settings) => (settings.fix, settings.format),
Strategy::Hierarchical(settings) => (settings.fix, settings.format),
};
let autofix = if fix {
fixer::Mode::Apply
} else if matches!(configuration.format, SerializationFormat::Json) {
} else if matches!(format, SerializationFormat::Json) {
fixer::Mode::Generate
} else {
fixer::Mode::None
};
let settings = Settings::from_configuration(configuration, project_root.as_ref())?;
// Now that we've inferred the appropriate log level, add some debug
// information.
match &project_root {
Some(path) => debug!("Found project root at: {:?}", path),
None => debug!("Unable to identify project root; assuming current directory..."),
};
match &pyproject {
Some(path) => debug!("Found pyproject.toml at: {:?}", path),
None => debug!("Unable to find pyproject.toml; using default settings..."),
};
if let Some(code) = cli.explain {
commands::explain(&code, settings.format)?;
commands::explain(&code, &format)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {
commands::show_settings(&cli.files, &strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_files {
commands::show_files(&cli.files, &settings);
commands::show_files(&cli.files, &strategy, &overrides)?;
return Ok(ExitCode::SUCCESS);
}
@@ -316,7 +120,7 @@ fn inner_main() -> Result<ExitCode> {
cache_enabled = false;
}
let printer = Printer::new(&settings.format, &log_level);
let printer = Printer::new(&format, &log_level);
if cli.watch {
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
eprintln!("Warning: --fix is not enabled in watch mode.");
@@ -327,7 +131,7 @@ fn inner_main() -> Result<ExitCode> {
if cli.autoformat {
eprintln!("Warning: --autoformat is not enabled in watch mode.");
}
if settings.format != SerializationFormat::Text {
if format != SerializationFormat::Text {
eprintln!("Warning: --format 'text' is used in watch mode.");
}
@@ -335,7 +139,13 @@ fn inner_main() -> Result<ExitCode> {
printer.clear_screen()?;
printer.write_to_user("Starting linter in watch mode...\n");
let messages = run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None);
let messages = commands::run(
&cli.files,
&strategy,
&overrides,
cache_enabled,
&fixer::Mode::None,
)?;
printer.write_continuously(&messages)?;
// Configure the file watcher.
@@ -347,32 +157,37 @@ fn inner_main() -> Result<ExitCode> {
loop {
match rx.recv() {
Ok(e) => {
let paths = e?.paths;
let py_changed = paths.iter().any(|p| {
p.extension()
.map(|ext| ext.eq_ignore_ascii_case("py"))
Ok(event) => {
let paths = event?.paths;
let py_changed = paths.iter().any(|path| {
path.extension()
.map(|ext| ext == "py" || ext == "pyi")
.unwrap_or_default()
});
if py_changed {
printer.clear_screen()?;
printer.write_to_user("File change detected...\n");
let messages =
run_once(&cli.files, &settings, cache_enabled, &fixer::Mode::None);
let messages = commands::run(
&cli.files,
&strategy,
&overrides,
cache_enabled,
&fixer::Mode::None,
)?;
printer.write_continuously(&messages)?;
}
}
Err(e) => return Err(e.into()),
Err(err) => return Err(err.into()),
}
}
} else if cli.add_noqa {
let modifications = add_noqa(&cli.files, &settings);
let modifications = commands::add_noqa(&cli.files, &strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Added {modifications} noqa directives.");
}
} else if cli.autoformat {
let modifications = autoformat(&cli.files, &settings);
let modifications = commands::autoformat(&cli.files, &strategy, &overrides)?;
if modifications > 0 && log_level >= LogLevel::Default {
println!("Formatted {modifications} files.");
}
@@ -383,9 +198,9 @@ fn inner_main() -> Result<ExitCode> {
let diagnostics = if is_stdin {
let filename = cli.stdin_filename.unwrap_or_else(|| "-".to_string());
let path = Path::new(&filename);
run_once_stdin(&settings, path, &autofix)?
commands::run_stdin(&strategy, path, &autofix)?
} else {
run_once(&cli.files, &settings, cache_enabled, &autofix)
commands::run(&cli.files, &strategy, &overrides, cache_enabled, &autofix)?
};
// Always try to print violations (the printer itself may suppress output),

View File

@@ -2,348 +2,12 @@ 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::{Binding, BindingKind, 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;
}
let ExprKind::Dict { keys, values } = &right.node else {
return None;
};
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() {
return None;
}
Some(Check::new(
CheckKind::PercentFormatExtraNamedArguments(missing.iter().map(|&s| s.clone()).collect()),
location,
))
}
/// 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

@@ -1,14 +1,18 @@
use anyhow::{bail, Result};
use libcst_native::{Codegen, CodegenState, ImportNames, SmallStatement, Statement};
use rustpython_ast::Stmt;
use libcst_native::{
Call, Codegen, CodegenState, Dict, DictElement, Expression, ImportNames, SmallStatement,
Statement,
};
use rustpython_ast::{Expr, Stmt};
use crate::ast::types::Range;
use crate::autofix::{helpers, Fix};
use crate::cst::helpers::compose_module_path;
use crate::cst::matchers::match_module;
use crate::cst::matchers::{match_expr, match_module};
use crate::python::string::strip_quotes_and_prefixes;
use crate::source_code_locator::SourceCodeLocator;
/// Generate a Fix to remove any unused imports from an `import` statement.
/// Generate a `Fix` to remove any unused imports from an `import` statement.
pub fn remove_unused_imports(
locator: &SourceCodeLocator,
unused_imports: &Vec<(&String, &Range)>,
@@ -73,3 +77,93 @@ pub fn remove_unused_imports(
))
}
}
/// Generate a `Fix` to remove unused keys from format dict.
pub fn remove_unused_format_arguments_from_dict(
locator: &SourceCodeLocator,
unused_arguments: &[&str],
stmt: &Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
let mut body = match_expr(&mut tree)?;
let new_dict = {
let Expression::Dict(dict) = &body.value else {
bail!("Expected Expression::Dict")
};
Dict {
lbrace: dict.lbrace.clone(),
lpar: dict.lpar.clone(),
rbrace: dict.rbrace.clone(),
rpar: dict.rpar.clone(),
elements: dict
.elements
.iter()
.filter_map(|e| match e {
DictElement::Simple {
key: Expression::SimpleString(name),
..
} if unused_arguments.contains(&strip_quotes_and_prefixes(name.value)) => None,
e => Some(e.clone()),
})
.collect(),
}
};
body.value = Expression::Dict(Box::new(new_dict));
let mut state = CodegenState::default();
tree.codegen(&mut state);
Ok(Fix::replacement(
state.to_string(),
stmt.location,
stmt.end_location.unwrap(),
))
}
/// Generate a `Fix` to remove unused keyword arguments from format call.
pub fn remove_unused_keyword_arguments_from_format_call(
locator: &SourceCodeLocator,
unused_arguments: &[&str],
location: Range,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&location);
let mut tree = match_module(&module_text)?;
let mut body = match_expr(&mut tree)?;
let new_call = {
let Expression::Call(call) = &body.value else {
bail!("Expected Expression::Call")
};
Call {
func: call.func.clone(),
lpar: call.lpar.clone(),
rpar: call.rpar.clone(),
whitespace_before_args: call.whitespace_before_args.clone(),
whitespace_after_func: call.whitespace_after_func.clone(),
args: call
.args
.iter()
.filter_map(|e| match &e.keyword {
Some(kw) if unused_arguments.contains(&kw.value) => None,
_ => Some(e.clone()),
})
.collect(),
}
};
body.value = Expression::Call(Box::new(new_call));
let mut state = CodegenState::default();
tree.codegen(&mut state);
Ok(Fix::replacement(
state.to_string(),
location.location,
location.end_location,
))
}

View File

@@ -1 +0,0 @@

View File

@@ -1,7 +1,6 @@
pub mod cformat;
pub mod checks;
pub mod fixes;
mod foo;
pub mod format;
pub mod plugins;
@@ -94,6 +93,7 @@ mod tests {
#[test_case(CheckCode::F821, Path::new("F821_3.py"); "F821_3")]
#[test_case(CheckCode::F821, Path::new("F821_4.py"); "F821_4")]
#[test_case(CheckCode::F821, Path::new("F821_5.py"); "F821_5")]
#[test_case(CheckCode::F821, Path::new("F821_6.py"); "F821_6")]
#[test_case(CheckCode::F822, Path::new("F822.py"); "F822")]
#[test_case(CheckCode::F823, Path::new("F823.py"); "F823")]
#[test_case(CheckCode::F831, Path::new("F831.py"); "F831")]
@@ -405,14 +405,23 @@ mod tests {
"#,
&[],
)?;
// Pyflakes allows this, but it causes other issues.
// flakes(
// r#"
// def c(): bar
// def b(): global bar; bar = 1
// "#,
// &[],
// )?;
flakes(
r#"
def c(): bar
def b(): global bar; bar = 1
"#,
&[],
)?;
// TODO(charlie): Extract globals recursively (such that we don't raise F821).
flakes(
r#"
def c(): bar
def d():
def b():
global bar; bar = 1
"#,
&[CheckCode::F821],
)?;
Ok(())
}
@@ -1482,7 +1491,7 @@ mod tests {
class bar:
import fu
"#,
&[CheckCode::F401],
&[],
)?;
flakes(
@@ -1492,7 +1501,7 @@ mod tests {
fu
"#,
&[CheckCode::F401, CheckCode::F821],
&[CheckCode::F821],
)?;
Ok(())
@@ -2097,7 +2106,7 @@ mod tests {
def fun(self):
fu
"#,
&[CheckCode::F401, CheckCode::F821],
&[CheckCode::F821],
)?;
Ok(())
@@ -2343,18 +2352,17 @@ mod tests {
Ok(())
}
#[ignore]
#[test]
fn imported_in_class() -> Result<()> {
// Imports in class scope can be used through self.
flakes(
r#"
class c:
class C:
import i
def __init__(self):
self.i
"#,
&[CheckCode::F401],
&[],
)?;
Ok(())

View File

@@ -3,9 +3,18 @@ pub use if_tuple::if_tuple;
pub use invalid_literal_comparisons::invalid_literal_comparison;
pub use invalid_print_syntax::invalid_print_syntax;
pub use raise_not_implemented::raise_not_implemented;
pub(crate) use strings::{
percent_format_expected_mapping, percent_format_expected_sequence,
percent_format_extra_named_arguments, percent_format_missing_arguments,
percent_format_mixed_positional_and_named, percent_format_positional_count_mismatch,
percent_format_star_requires_sequence, string_dot_format_extra_named_arguments,
string_dot_format_extra_positional_arguments, string_dot_format_missing_argument,
string_dot_format_mixing_automatic,
};
mod assert_tuple;
mod if_tuple;
mod invalid_literal_comparisons;
mod invalid_print_syntax;
mod raise_not_implemented;
mod strings;

View File

@@ -0,0 +1,369 @@
use std::string::ToString;
use rustc_hash::FxHashSet;
use rustpython_ast::{Keyword, KeywordData};
use rustpython_parser::ast::{Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::pyflakes::cformat::CFormatSummary;
use crate::pyflakes::fixes::{
remove_unused_format_arguments_from_dict, remove_unused_keyword_arguments_from_format_call,
};
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(|arg| matches!(&arg.node, ExprKind::Starred { .. }))
}
/// F502
pub(crate) fn percent_format_expected_mapping(
checker: &mut Checker,
summary: &CFormatSummary,
right: &Expr,
location: Range,
) {
if !summary.keywords.is_empty() {
// Tuple, List, Set (+comprehensions)
match right.node {
ExprKind::List { .. }
| ExprKind::Tuple { .. }
| ExprKind::Set { .. }
| ExprKind::ListComp { .. }
| ExprKind::SetComp { .. }
| ExprKind::GeneratorExp { .. } => checker.add_check(Check::new(
CheckKind::PercentFormatExpectedMapping,
location,
)),
_ => {}
}
}
}
/// F503
pub(crate) fn percent_format_expected_sequence(
checker: &mut Checker,
summary: &CFormatSummary,
right: &Expr,
location: Range,
) {
if summary.num_positional > 1
&& matches!(
right.node,
ExprKind::Dict { .. } | ExprKind::DictComp { .. }
)
{
checker.add_check(Check::new(
CheckKind::PercentFormatExpectedSequence,
location,
));
}
}
/// F504
pub(crate) fn percent_format_extra_named_arguments(
checker: &mut Checker,
summary: &CFormatSummary,
right: &Expr,
location: Range,
) {
if summary.num_positional > 0 {
return;
}
let ExprKind::Dict { keys, values } = &right.node else {
return;
};
if values.len() > keys.len() {
return; // contains **x splat
}
let missing: Vec<&str> = 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.as_str())
}
}
_ => None,
})
.collect();
if missing.is_empty() {
return;
}
let mut check = Check::new(
CheckKind::PercentFormatExtraNamedArguments(
missing.iter().map(|&arg| arg.to_string()).collect(),
),
location,
);
if checker.patch(check.kind.code()) {
if let Ok(fix) = remove_unused_format_arguments_from_dict(checker.locator, &missing, right)
{
check.amend(fix);
}
}
checker.add_check(check);
}
/// F505
pub(crate) fn percent_format_missing_arguments(
checker: &mut Checker,
summary: &CFormatSummary,
right: &Expr,
location: Range,
) {
if summary.num_positional > 0 {
return;
}
if let ExprKind::Dict { keys, values } = &right.node {
if values.len() > keys.len() {
return; // 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; // Dynamic keys present
}
}
}
let missing: Vec<&String> = summary
.keywords
.iter()
.filter(|k| !keywords.contains(k))
.collect();
if !missing.is_empty() {
checker.add_check(Check::new(
CheckKind::PercentFormatMissingArgument(
missing.iter().map(|&s| s.clone()).collect(),
),
location,
));
}
}
}
/// F506
pub(crate) fn percent_format_mixed_positional_and_named(
checker: &mut Checker,
summary: &CFormatSummary,
location: Range,
) {
if !(summary.num_positional == 0 || summary.keywords.is_empty()) {
checker.add_check(Check::new(
CheckKind::PercentFormatMixedPositionalAndNamed,
location,
));
}
}
/// F507
pub(crate) fn percent_format_positional_count_mismatch(
checker: &mut Checker,
summary: &CFormatSummary,
right: &Expr,
location: Range,
) {
if !summary.keywords.is_empty() {
return;
}
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;
}
found += 1;
}
if found != summary.num_positional {
checker.add_check(Check::new(
CheckKind::PercentFormatPositionalCountMismatch(summary.num_positional, found),
location,
));
}
}
_ => {}
}
}
/// F508
pub(crate) fn percent_format_star_requires_sequence(
checker: &mut Checker,
summary: &CFormatSummary,
right: &Expr,
location: Range,
) {
if summary.starred {
match &right.node {
ExprKind::Dict { .. } | ExprKind::DictComp { .. } => checker.add_check(Check::new(
CheckKind::PercentFormatStarRequiresSequence,
location,
)),
_ => {}
}
}
}
/// F522
pub(crate) fn string_dot_format_extra_named_arguments(
checker: &mut Checker,
summary: &FormatSummary,
keywords: &[Keyword],
location: Range,
) {
if has_star_star_kwargs(keywords) {
return;
}
let keywords = keywords.iter().filter_map(|k| {
let KeywordData { arg, .. } = &k.node;
arg.as_ref()
});
let missing: Vec<&str> = keywords
.filter_map(|arg| {
if summary.keywords.contains(arg) {
None
} else {
Some(arg.as_str())
}
})
.collect();
if missing.is_empty() {
return;
}
let mut check = Check::new(
CheckKind::StringDotFormatExtraNamedArguments(
missing.iter().map(|&arg| arg.to_string()).collect(),
),
location,
);
if checker.patch(check.kind.code()) {
if let Ok(fix) =
remove_unused_keyword_arguments_from_format_call(checker.locator, &missing, location)
{
check.amend(fix);
}
}
checker.add_check(check);
}
/// F523
pub(crate) fn string_dot_format_extra_positional_arguments(
checker: &mut Checker,
summary: &FormatSummary,
args: &[Expr],
location: Range,
) {
if has_star_args(args) {
return;
}
let missing: Vec<usize> = (0..args.len())
.filter(|i| !(summary.autos.contains(i) || summary.indexes.contains(i)))
.collect();
if missing.is_empty() {
return;
}
checker.add_check(Check::new(
CheckKind::StringDotFormatExtraPositionalArguments(
missing
.iter()
.map(std::string::ToString::to_string)
.collect::<Vec<String>>(),
),
location,
));
}
/// F524
pub(crate) fn string_dot_format_missing_argument(
checker: &mut Checker,
summary: &FormatSummary,
args: &[Expr],
keywords: &[Keyword],
location: Range,
) {
if has_star_args(args) || has_star_star_kwargs(keywords) {
return;
}
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() {
checker.add_check(Check::new(
CheckKind::StringDotFormatMissingArguments(missing),
location,
));
}
}
/// F525
pub(crate) fn string_dot_format_mixing_automatic(
checker: &mut Checker,
summary: &FormatSummary,
location: Range,
) {
if !(summary.autos.is_empty() || summary.indexes.is_empty()) {
checker.add_check(Check::new(
CheckKind::StringDotFormatMixingAutomatic,
location,
));
}
}

View File

@@ -11,5 +11,46 @@ expression: checks
end_location:
row: 3
column: 34
fix: ~
fix:
content: "{a: \"?\", }"
location:
row: 3
column: 16
end_location:
row: 3
column: 34
- kind:
PercentFormatExtraNamedArguments:
- b
location:
row: 8
column: 8
end_location:
row: 8
column: 29
fix:
content: "{\"a\": 1, }"
location:
row: 8
column: 10
end_location:
row: 8
column: 29
- kind:
PercentFormatExtraNamedArguments:
- b
location:
row: 9
column: 8
end_location:
row: 9
column: 29
fix:
content: "{'a': 1, }"
location:
row: 9
column: 10
end_location:
row: 9
column: 29

View File

@@ -11,5 +11,12 @@ expression: checks
end_location:
row: 8
column: 32
fix: ~
fix:
content: "{'bar': 1, }"
location:
row: 8
column: 12
end_location:
row: 8
column: 32

View File

@@ -11,7 +11,14 @@ expression: checks
end_location:
row: 1
column: 21
fix: ~
fix:
content: "\"{}\".format(1, )"
location:
row: 1
column: 0
end_location:
row: 1
column: 21
- kind:
StringDotFormatExtraNamedArguments:
- spam
@@ -21,7 +28,14 @@ expression: checks
end_location:
row: 2
column: 34
fix: ~
fix:
content: "\"{bar}{}\".format(1, bar=2, )"
location:
row: 2
column: 0
end_location:
row: 2
column: 34
- kind:
StringDotFormatExtraNamedArguments:
- eggs
@@ -32,5 +46,12 @@ expression: checks
end_location:
row: 4
column: 51
fix: ~
fix:
content: "\"{bar:{spam}}\".format(bar=2, spam=3, )"
location:
row: 4
column: 0
end_location:
row: 4
column: 51

View File

@@ -26,4 +26,12 @@ expression: checks
row: 7
column: 10
fix: ~
- kind: FStringMissingPlaceholders
location:
row: 12
column: 4
end_location:
row: 12
column: 16
fix: ~

View File

@@ -71,8 +71,8 @@ expression: checks
row: 89
column: 4
end_location:
row: 89
column: 8
row: 90
column: 10
fix: ~
- kind:
UndefinedName: PEP593Test123

View File

@@ -0,0 +1,6 @@
---
source: src/pyflakes/mod.rs
expression: checks
---
[]

View File

@@ -14,6 +14,8 @@ mod tests {
#[test_case(CheckCode::PLC0414, Path::new("import_aliasing.py"); "PLC0414")]
#[test_case(CheckCode::PLC2201, Path::new("misplaced_comparison_constant.py"); "PLC2201")]
#[test_case(CheckCode::PLC3002, Path::new("unnecessary_direct_lambda_call.py"); "PLC3002")]
#[test_case(CheckCode::PLE0117, Path::new("nonlocal_without_binding.py"); "PLE0117")]
#[test_case(CheckCode::PLE0118, Path::new("used_prior_global_declaration.py"); "PLE0118")]
#[test_case(CheckCode::PLE1142, Path::new("await_outside_async.py"); "PLE1142")]
#[test_case(CheckCode::PLR0206, Path::new("property_with_parameters.py"); "PLR0206")]
#[test_case(CheckCode::PLR0402, Path::new("import_aliasing.py"); "PLR0402")]
@@ -26,6 +28,7 @@ mod tests {
#[test_case(CheckCode::PLR1722, Path::new("consider_using_sys_exit_5.py"); "PLR1722_5")]
#[test_case(CheckCode::PLR1722, Path::new("consider_using_sys_exit_6.py"); "PLR1722_6")]
#[test_case(CheckCode::PLW0120, Path::new("useless_else_on_loop.py"); "PLW0120")]
#[test_case(CheckCode::PLW0602, Path::new("global_variable_not_assigned.py"); "PLW0602")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -5,6 +5,7 @@ pub use property_with_parameters::property_with_parameters;
pub use unnecessary_direct_lambda_call::unnecessary_direct_lambda_call;
pub use use_from_import::use_from_import;
pub use use_sys_exit::use_sys_exit;
pub use used_prior_global_declaration::used_prior_global_declaration;
pub use useless_else_on_loop::useless_else_on_loop;
pub use useless_import_alias::useless_import_alias;
@@ -15,5 +16,6 @@ mod property_with_parameters;
mod unnecessary_direct_lambda_call;
mod use_from_import;
mod use_sys_exit;
mod used_prior_global_declaration;
mod useless_else_on_loop;
mod useless_import_alias;

View File

@@ -0,0 +1,23 @@
use rustpython_ast::Expr;
use crate::ast::types::{Range, ScopeKind};
use crate::check_ast::Checker;
use crate::checks::CheckKind;
use crate::Check;
/// PLE0118
pub fn used_prior_global_declaration(checker: &mut Checker, name: &str, expr: &Expr) {
let globals = match &checker.current_scope().kind {
ScopeKind::Class(class_def) => &class_def.globals,
ScopeKind::Function(function_def) => &function_def.globals,
_ => return,
};
if let Some(stmt) = globals.get(name) {
if expr.location < stmt.location {
checker.add_check(Check::new(
CheckKind::UsedPriorGlobalDeclaration(name.to_string(), stmt.location.row()),
Range::from_located(expr),
));
}
}
}

View File

@@ -0,0 +1,32 @@
---
source: src/pylint/mod.rs
expression: checks
---
- kind:
NonlocalWithoutBinding: x
location:
row: 5
column: 4
end_location:
row: 5
column: 14
fix: ~
- kind:
NonlocalWithoutBinding: y
location:
row: 9
column: 4
end_location:
row: 9
column: 14
fix: ~
- kind:
NonlocalWithoutBinding: y
location:
row: 19
column: 8
end_location:
row: 19
column: 18
fix: ~

View File

@@ -0,0 +1,137 @@
---
source: src/pylint/mod.rs
expression: checks
---
- kind:
UsedPriorGlobalDeclaration:
- x
- 7
location:
row: 5
column: 10
end_location:
row: 5
column: 11
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 17
location:
row: 15
column: 10
end_location:
row: 15
column: 11
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 25
location:
row: 23
column: 10
end_location:
row: 23
column: 11
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 35
location:
row: 33
column: 10
end_location:
row: 33
column: 11
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 43
location:
row: 41
column: 4
end_location:
row: 41
column: 5
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 53
location:
row: 51
column: 4
end_location:
row: 51
column: 5
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 61
location:
row: 59
column: 8
end_location:
row: 59
column: 9
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 71
location:
row: 69
column: 8
end_location:
row: 69
column: 9
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 79
location:
row: 77
column: 8
end_location:
row: 77
column: 9
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 89
location:
row: 87
column: 8
end_location:
row: 87
column: 9
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 97
location:
row: 95
column: 8
end_location:
row: 95
column: 9
fix: ~
- kind:
UsedPriorGlobalDeclaration:
- x
- 107
location:
row: 105
column: 8
end_location:
row: 105
column: 9
fix: ~

View File

@@ -0,0 +1,23 @@
---
source: src/pylint/mod.rs
expression: checks
---
- kind:
GlobalVariableNotAssigned: x
location:
row: 5
column: 4
end_location:
row: 5
column: 12
fix: ~
- kind:
GlobalVariableNotAssigned: x
location:
row: 9
column: 4
end_location:
row: 9
column: 12
fix: ~

View File

@@ -1,3 +1,9 @@
use once_cell::sync::Lazy;
use regex::Regex;
pub static STRING_QUOTE_PREFIX_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r#"^(?i)[urb]*['"](?P<raw>.*)['"]$"#).unwrap());
pub fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
@@ -22,9 +28,22 @@ pub fn is_upper(s: &str) -> bool {
cased
}
/// Remove prefixes (u, r, b) and quotes around a string. This expects the given
/// string to be a valid Python string representation, it doesn't do any
/// validation.
pub fn strip_quotes_and_prefixes(s: &str) -> &str {
match STRING_QUOTE_PREFIX_REGEX.captures(s) {
Some(caps) => match caps.name("raw") {
Some(m) => m.as_str(),
None => s,
},
None => s,
}
}
#[cfg(test)]
mod tests {
use crate::python::string::{is_lower, is_upper};
use crate::python::string::{is_lower, is_upper, strip_quotes_and_prefixes};
#[test]
fn test_is_lower() {
@@ -47,4 +66,12 @@ mod tests {
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
#[test]
fn test_strip_quotes_and_prefixes() {
assert_eq!(strip_quotes_and_prefixes(r#"'a'"#), "a");
assert_eq!(strip_quotes_and_prefixes(r#"bur'a'"#), "a");
assert_eq!(strip_quotes_and_prefixes(r#"UrB'a'"#), "a");
assert_eq!(strip_quotes_and_prefixes(r#""a""#), "a");
}
}

414
src/resolver.rs Normal file
View File

@@ -0,0 +1,414 @@
//! Discover Python files, and their corresponding `Settings`, from the
//! filesystem.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use anyhow::{bail, Result};
use log::debug;
use path_absolutize::path_dedot;
use rustc_hash::FxHashSet;
use walkdir::{DirEntry, WalkDir};
use crate::cli::Overrides;
use crate::fs;
use crate::settings::configuration::Configuration;
use crate::settings::{pyproject, Settings};
/// The strategy for discovering a `pyproject.toml` file for each Python file.
#[derive(Debug)]
pub enum Strategy {
/// Use a fixed `pyproject.toml` file for all Python files (i.e., one
/// provided on the command-line).
Fixed(Settings),
/// Use the closest `pyproject.toml` file in the filesystem hierarchy, or
/// the default settings.
Hierarchical(Settings),
}
/// The strategy for resolving file paths in a `pyproject.toml`.
pub enum Relativity {
/// Resolve file paths relative to the current working directory.
Cwd,
/// Resolve file paths relative to the directory containing the
/// `pyproject.toml`.
Parent,
}
impl Relativity {
pub fn resolve(&self, path: &Path) -> PathBuf {
match self {
Relativity::Parent => path
.parent()
.expect("Expected pyproject.toml file to be in parent directory")
.to_path_buf(),
Relativity::Cwd => path_dedot::CWD.clone(),
}
}
}
#[derive(Default)]
pub struct Resolver {
settings: BTreeMap<PathBuf, Settings>,
}
impl Resolver {
/// Merge a `Resolver` into the current `Resolver`.
pub fn merge(&mut self, resolver: Resolver) {
self.settings.extend(resolver.settings);
}
/// Add a resolved `Settings` under a given `PathBuf` scope.
pub fn add(&mut self, path: PathBuf, settings: Settings) {
self.settings.insert(path, settings);
}
/// Return the appropriate `Settings` for a given `Path`.
pub fn resolve<'a>(&'a self, path: &Path, strategy: &'a Strategy) -> &'a Settings {
match strategy {
Strategy::Fixed(settings) => settings,
Strategy::Hierarchical(default) => self
.settings
.iter()
.rev()
.find_map(|(root, settings)| {
if path.starts_with(root) {
Some(settings)
} else {
None
}
})
.unwrap_or(default),
}
}
}
/// Recursively resolve a `Configuration` from a `pyproject.toml` file at the
/// specified `Path`.
// TODO(charlie): This whole system could do with some caching. Right now, if a
// configuration file extends another in the same path, we'll re-parse the same
// file at least twice (possibly more than twice, since we'll also parse it when
// resolving the "default" configuration).
pub fn resolve_configuration(
pyproject: &Path,
relativity: &Relativity,
overrides: Option<&Overrides>,
) -> Result<Configuration> {
let mut seen = FxHashSet::default();
let mut stack = vec![];
let mut next = Some(fs::normalize_path(pyproject));
while let Some(path) = next {
if seen.contains(&path) {
bail!("Circular dependency detected in pyproject.toml");
}
// Resolve the current path.
let options = pyproject::load_options(&path)?;
let project_root = relativity.resolve(&path);
let configuration = Configuration::from_options(options, &project_root)?;
// If extending, continue to collect.
next = configuration.extend.as_ref().map(|extend| {
fs::normalize_path_to(
extend,
path.parent()
.expect("Expected pyproject.toml file to be in parent directory"),
)
});
// Keep track of (1) the paths we've already resolved (to avoid cycles), and (2)
// the base configuration for every path.
seen.insert(path);
stack.push(configuration);
}
// Merge the configurations, in order.
stack.reverse();
let mut configuration = stack.pop().unwrap();
while let Some(extend) = stack.pop() {
configuration = configuration.combine(extend);
}
if let Some(overrides) = overrides {
configuration.apply(overrides.clone());
}
Ok(configuration)
}
/// Extract the project root (scope) and `Settings` from a given
/// `pyproject.toml`.
pub fn resolve_scoped_settings(
pyproject: &Path,
relativity: &Relativity,
overrides: Option<&Overrides>,
) -> Result<(PathBuf, Settings)> {
let project_root = relativity.resolve(pyproject);
let configuration = resolve_configuration(pyproject, relativity, overrides)?;
let settings = Settings::from_configuration(configuration, &project_root)?;
Ok((project_root, settings))
}
/// Extract the `Settings` from a given `pyproject.toml`.
pub fn resolve_settings(
pyproject: &Path,
relativity: &Relativity,
overrides: Option<&Overrides>,
) -> Result<Settings> {
let (_project_root, settings) = resolve_scoped_settings(pyproject, relativity, overrides)?;
Ok(settings)
}
/// Return `true` if the given file should be ignored based on the exclusion
/// criteria.
fn is_excluded(file_path: &str, file_basename: &str, exclude: &globset::GlobSet) -> bool {
exclude.is_match(file_path) || exclude.is_match(file_basename)
}
/// Return `true` if the `Path` appears to be that of a Python file.
fn is_python_file(path: &Path) -> bool {
path.extension()
.map_or(false, |ext| ext == "py" || ext == "pyi")
}
/// Find all Python (`.py` and `.pyi` files) in a set of `PathBuf`s.
pub fn resolve_python_files(
paths: &[PathBuf],
strategy: &Strategy,
overrides: &Overrides,
) -> Result<(Vec<Result<DirEntry, walkdir::Error>>, Resolver)> {
let mut files = Vec::new();
let mut resolver = Resolver::default();
for path in paths {
let (files_in_path, file_resolver) = python_files_in_path(path, strategy, overrides)?;
files.extend(files_in_path);
resolver.merge(file_resolver);
}
Ok((files, resolver))
}
/// Find all Python (`.py` and `.pyi` files) in a given `Path`.
fn python_files_in_path(
path: &Path,
strategy: &Strategy,
overrides: &Overrides,
) -> Result<(Vec<Result<DirEntry, walkdir::Error>>, Resolver)> {
let path = fs::normalize_path(path);
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();
for path in path.ancestors() {
if path.is_dir() {
let pyproject = path.join("pyproject.toml");
if pyproject.is_file() {
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))?;
resolver.add(root, settings);
}
}
}
// Collect all Python files.
let files: Vec<Result<DirEntry, walkdir::Error>> = WalkDir::new(path)
.into_iter()
.filter_entry(|entry| {
// Search for the `pyproject.toml` file in this directory, before we visit any
// of its contents.
if entry.file_type().is_dir() {
let pyproject = entry.path().join("pyproject.toml");
if pyproject.is_file() {
// TODO(charlie): Return a `Result` here.
let (root, settings) =
resolve_scoped_settings(&pyproject, &Relativity::Parent, Some(overrides))
.unwrap();
resolver.add(root, settings);
}
}
let path = entry.path();
let settings = resolver.resolve(path, strategy);
match fs::extract_path_names(path) {
Ok((file_path, file_basename)) => {
if !settings.exclude.is_empty()
&& is_excluded(file_path, file_basename, &settings.exclude)
{
debug!("Ignored path via `exclude`: {:?}", path);
false
} else if !settings.extend_exclude.is_empty()
&& is_excluded(file_path, file_basename, &settings.extend_exclude)
{
debug!("Ignored path via `extend-exclude`: {:?}", path);
false
} else {
true
}
}
Err(e) => {
debug!("Ignored path due to error in parsing: {:?}: {}", path, e);
true
}
}
})
.filter(|entry| {
entry.as_ref().map_or(true, |entry| {
(entry.depth() == 0 || is_python_file(entry.path()))
&& !entry.file_type().is_dir()
&& !(entry.file_type().is_symlink() && entry.path().is_dir())
})
})
.collect::<Vec<_>>();
Ok((files, resolver))
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use globset::GlobSet;
use path_absolutize::Absolutize;
use crate::fs;
use crate::resolver::{is_excluded, is_python_file};
use crate::settings::types::FilePattern;
#[test]
fn inclusions() {
let path = Path::new("foo/bar/baz.py").absolutize().unwrap();
assert!(is_python_file(&path));
let path = Path::new("foo/bar/baz.pyi").absolutize().unwrap();
assert!(is_python_file(&path));
let path = Path::new("foo/bar/baz.js").absolutize().unwrap();
assert!(!is_python_file(&path));
let path = Path::new("foo/bar/baz").absolutize().unwrap();
assert!(!is_python_file(&path));
}
fn make_exclusion(file_pattern: FilePattern) -> GlobSet {
let mut builder = globset::GlobSetBuilder::new();
file_pattern.add_to(&mut builder).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 = FilePattern::User(
"foo".to_string(),
Path::new("foo")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = FilePattern::User(
"bar".to_string(),
Path::new("bar")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User(
"baz.py".to_string(),
Path::new("baz.py")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
let path = Path::new("foo/bar").absolutize_from(project_root).unwrap();
let exclude = FilePattern::User(
"foo/bar".to_string(),
Path::new("foo/bar")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User(
"foo/bar/baz.py".to_string(),
Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User(
"foo/bar/*.py".to_string(),
Path::new("foo/bar/*.py")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
let path = Path::new("foo/bar/baz.py")
.absolutize_from(project_root)
.unwrap();
let exclude = FilePattern::User(
"baz".to_string(),
Path::new("baz")
.absolutize_from(project_root)
.unwrap()
.to_path_buf(),
);
let (file_path, file_basename) = fs::extract_path_names(&path)?;
assert!(!is_excluded(
file_path,
file_basename,
&make_exclusion(exclude,)
));
Ok(())
}
}

View File

@@ -5,12 +5,12 @@
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use glob::{glob, GlobError, Paths, PatternError};
use regex::Regex;
use rustc_hash::FxHashSet;
use crate::checks_gen::{CheckCodePrefix, CATEGORIES};
use crate::checks_gen::CheckCodePrefix;
use crate::cli::{collect_per_file_ignores, Overrides};
use crate::settings::options::Options;
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
use crate::{
@@ -18,171 +18,217 @@ use crate::{
flake8_tidy_imports, fs, isort, mccabe, pep8_naming, pyupgrade,
};
#[derive(Debug)]
#[derive(Debug, Default)]
pub struct Configuration {
pub allowed_confusables: FxHashSet<char>,
pub dummy_variable_rgx: Regex,
pub exclude: Vec<FilePattern>,
pub extend_exclude: Vec<FilePattern>,
pub extend_ignore: Vec<CheckCodePrefix>,
pub extend_select: Vec<CheckCodePrefix>,
pub external: Vec<String>,
pub fix: bool,
pub fixable: Vec<CheckCodePrefix>,
pub format: SerializationFormat,
pub ignore: Vec<CheckCodePrefix>,
pub ignore_init_module_imports: bool,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub show_source: bool,
pub src: Vec<PathBuf>,
pub target_version: PythonVersion,
pub unfixable: Vec<CheckCodePrefix>,
pub allowed_confusables: Option<Vec<char>>,
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend: Option<PathBuf>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
pub extend_select: Option<Vec<CheckCodePrefix>>,
pub external: Option<Vec<String>>,
pub fix: Option<bool>,
pub fixable: Option<Vec<CheckCodePrefix>>,
pub format: Option<SerializationFormat>,
pub ignore: Option<Vec<CheckCodePrefix>>,
pub ignore_init_module_imports: Option<bool>,
pub line_length: Option<usize>,
pub per_file_ignores: Option<Vec<PerFileIgnore>>,
pub select: Option<Vec<CheckCodePrefix>>,
pub show_source: Option<bool>,
pub src: Option<Vec<PathBuf>>,
pub target_version: Option<PythonVersion>,
pub unfixable: Option<Vec<CheckCodePrefix>>,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
pub flake8_bugbear: flake8_bugbear::settings::Settings,
pub flake8_import_conventions: flake8_import_conventions::settings::Settings,
pub flake8_quotes: flake8_quotes::settings::Settings,
pub flake8_tidy_imports: flake8_tidy_imports::settings::Settings,
pub isort: isort::settings::Settings,
pub mccabe: mccabe::settings::Settings,
pub pep8_naming: pep8_naming::settings::Settings,
pub pyupgrade: pyupgrade::settings::Settings,
pub flake8_annotations: Option<flake8_annotations::settings::Options>,
pub flake8_bugbear: Option<flake8_bugbear::settings::Options>,
pub flake8_import_conventions: Option<flake8_import_conventions::settings::Options>,
pub flake8_quotes: Option<flake8_quotes::settings::Options>,
pub flake8_tidy_imports: Option<flake8_tidy_imports::settings::Options>,
pub isort: Option<isort::settings::Options>,
pub mccabe: Option<mccabe::settings::Options>,
pub pep8_naming: Option<pep8_naming::settings::Options>,
pub pyupgrade: Option<pyupgrade::settings::Options>,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
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"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Configuration {
pub fn from_pyproject(
pyproject: Option<&PathBuf>,
project_root: Option<&PathBuf>,
) -> Result<Self> {
let options = load_options(pyproject)?;
pub fn from_pyproject(pyproject: &Path, project_root: &Path) -> Result<Self> {
Self::from_options(load_options(pyproject)?, project_root)
}
pub fn from_options(options: Options, project_root: &Path) -> Result<Self> {
Ok(Configuration {
allowed_confusables: FxHashSet::from_iter(
options.allowed_confusables.unwrap_or_default(),
),
dummy_variable_rgx: match options.dummy_variable_rgx {
Some(pattern) => Regex::new(&pattern)
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
src: options.src.map_or_else(
|| {
vec![match project_root {
Some(project_root) => project_root.clone(),
None => path_dedot::CWD.clone(),
}]
},
|src| {
src.iter()
.map(|path| {
let path = Path::new(path);
match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
}
})
.collect()
},
),
target_version: options.target_version.unwrap_or(PythonVersion::Py310),
exclude: options.exclude.map_or_else(
|| DEFAULT_EXCLUDE.clone(),
|paths| paths.into_iter().map(FilePattern::User).collect(),
),
extend_exclude: options
.extend_exclude
.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(),
external: options.external.unwrap_or_default(),
fix: options.fix.unwrap_or_default(),
fixable: options.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
unfixable: options.unfixable.unwrap_or_default(),
format: options.format.unwrap_or_default(),
ignore: options.ignore.unwrap_or_default(),
ignore_init_module_imports: options.ignore_init_module_imports.unwrap_or_default(),
line_length: options.line_length.unwrap_or(88),
per_file_ignores: options
.per_file_ignores
.map(|per_file_ignores| {
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
.collect()
})
.unwrap_or_default(),
show_source: options.show_source.unwrap_or_default(),
extend: options.extend.map(PathBuf::from),
allowed_confusables: options.allowed_confusables,
dummy_variable_rgx: options
.dummy_variable_rgx
.map(|pattern| Regex::new(&pattern))
.transpose()
.map_err(|e| anyhow!("Invalid `dummy-variable-rgx` value: {e}"))?,
src: options
.src
.map(|src| resolve_src(&src, project_root))
.transpose()?,
target_version: options.target_version,
exclude: options.exclude.map(|paths| {
paths
.into_iter()
.map(|pattern| {
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
FilePattern::User(pattern, absolute)
})
.collect()
}),
extend_exclude: options.extend_exclude.map(|paths| {
paths
.into_iter()
.map(|pattern| {
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
FilePattern::User(pattern, absolute)
})
.collect()
}),
extend_ignore: options.extend_ignore,
select: options.select,
extend_select: options.extend_select,
external: options.external,
fix: options.fix,
fixable: options.fixable,
unfixable: options.unfixable,
format: options.format,
ignore: options.ignore,
ignore_init_module_imports: options.ignore_init_module_imports,
line_length: options.line_length,
per_file_ignores: options.per_file_ignores.map(|per_file_ignores| {
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| {
let absolute = fs::normalize_path_to(Path::new(&pattern), project_root);
PerFileIgnore::new(pattern, absolute, &prefixes)
})
.collect()
}),
show_source: options.show_source,
// Plugins
flake8_annotations: options
.flake8_annotations
.map(flake8_annotations::settings::Settings::from_options)
.unwrap_or_default(),
flake8_bugbear: options
.flake8_bugbear
.map(flake8_bugbear::settings::Settings::from_options)
.unwrap_or_default(),
flake8_import_conventions: options
.flake8_import_conventions
.map(flake8_import_conventions::settings::Settings::from_options)
.unwrap_or_default(),
flake8_quotes: options
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_options)
.unwrap_or_default(),
flake8_tidy_imports: options
.flake8_tidy_imports
.map(flake8_tidy_imports::settings::Settings::from_options)
.unwrap_or_default(),
isort: options
.isort
.map(isort::settings::Settings::from_options)
.unwrap_or_default(),
mccabe: options
.mccabe
.as_ref()
.map(mccabe::settings::Settings::from_options)
.unwrap_or_default(),
pep8_naming: options
.pep8_naming
.map(pep8_naming::settings::Settings::from_options)
.unwrap_or_default(),
pyupgrade: options
.pyupgrade
.as_ref()
.map(pyupgrade::settings::Settings::from_options)
.unwrap_or_default(),
flake8_annotations: options.flake8_annotations,
flake8_bugbear: options.flake8_bugbear,
flake8_import_conventions: options.flake8_import_conventions,
flake8_quotes: options.flake8_quotes,
flake8_tidy_imports: options.flake8_tidy_imports,
isort: options.isort,
mccabe: options.mccabe,
pep8_naming: options.pep8_naming,
pyupgrade: options.pyupgrade,
})
}
#[must_use]
pub fn combine(self, config: Configuration) -> Self {
Self {
allowed_confusables: self.allowed_confusables.or(config.allowed_confusables),
dummy_variable_rgx: self.dummy_variable_rgx.or(config.dummy_variable_rgx),
exclude: self.exclude.or(config.exclude),
extend: self.extend.or(config.extend),
extend_exclude: self.extend_exclude.or(config.extend_exclude),
extend_ignore: self.extend_ignore.or(config.extend_ignore),
extend_select: self.extend_select.or(config.extend_select),
external: self.external.or(config.external),
fix: self.fix.or(config.fix),
fixable: self.fixable.or(config.fixable),
format: self.format.or(config.format),
ignore: self.ignore.or(config.ignore),
ignore_init_module_imports: self
.ignore_init_module_imports
.or(config.ignore_init_module_imports),
line_length: self.line_length.or(config.line_length),
per_file_ignores: self.per_file_ignores.or(config.per_file_ignores),
select: self.select.or(config.select),
show_source: self.show_source.or(config.show_source),
src: self.src.or(config.src),
target_version: self.target_version.or(config.target_version),
unfixable: self.unfixable.or(config.unfixable),
// Plugins
flake8_annotations: self.flake8_annotations.or(config.flake8_annotations),
flake8_bugbear: self.flake8_bugbear.or(config.flake8_bugbear),
flake8_import_conventions: self
.flake8_import_conventions
.or(config.flake8_import_conventions),
flake8_quotes: self.flake8_quotes.or(config.flake8_quotes),
flake8_tidy_imports: self.flake8_tidy_imports.or(config.flake8_tidy_imports),
isort: self.isort.or(config.isort),
mccabe: self.mccabe.or(config.mccabe),
pep8_naming: self.pep8_naming.or(config.pep8_naming),
pyupgrade: self.pyupgrade.or(config.pyupgrade),
}
}
pub fn apply(&mut self, overrides: Overrides) {
if let Some(dummy_variable_rgx) = overrides.dummy_variable_rgx {
self.dummy_variable_rgx = Some(dummy_variable_rgx);
}
if let Some(exclude) = overrides.exclude {
self.exclude = Some(exclude);
}
if let Some(extend_exclude) = overrides.extend_exclude {
self.extend_exclude = Some(extend_exclude);
}
if let Some(extend_ignore) = overrides.extend_ignore {
self.extend_ignore = Some(extend_ignore);
}
if let Some(extend_select) = overrides.extend_select {
self.extend_select = Some(extend_select);
}
if let Some(fix) = overrides.fix {
self.fix = Some(fix);
}
if let Some(fixable) = overrides.fixable {
self.fixable = Some(fixable);
}
if let Some(format) = overrides.format {
self.format = Some(format);
}
if let Some(ignore) = overrides.ignore {
self.ignore = Some(ignore);
}
if let Some(line_length) = overrides.line_length {
self.line_length = Some(line_length);
}
if let Some(max_complexity) = overrides.max_complexity {
self.mccabe = Some(mccabe::settings::Options {
max_complexity: Some(max_complexity),
});
}
if let Some(per_file_ignores) = overrides.per_file_ignores {
self.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores));
}
if let Some(select) = overrides.select {
self.select = Some(select);
}
if let Some(show_source) = overrides.show_source {
self.show_source = Some(show_source);
}
if let Some(target_version) = overrides.target_version {
self.target_version = Some(target_version);
}
if let Some(unfixable) = overrides.unfixable {
self.unfixable = Some(unfixable);
}
}
}
/// Given a list of source paths, which could include glob patterns, resolve the
/// matching paths.
pub fn resolve_src(src: &[String], project_root: &Path) -> Result<Vec<PathBuf>> {
let globs = src
.iter()
.map(Path::new)
.map(|path| fs::normalize_path_to(path, project_root))
.map(|path| glob(&path.to_string_lossy()))
.collect::<Result<Vec<Paths>, PatternError>>()?;
let paths: Vec<PathBuf> = globs
.into_iter()
.flatten()
.collect::<Result<Vec<PathBuf>, GlobError>>()?;
Ok(paths)
}

View File

@@ -8,17 +8,18 @@ use std::path::{Path, PathBuf};
use anyhow::Result;
use globset::{Glob, GlobMatcher, GlobSet};
use itertools::Itertools;
use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use regex::Regex;
use rustc_hash::FxHashSet;
use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, SuffixLength};
use crate::checks_gen::{CheckCodePrefix, SuffixLength, CATEGORIES};
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
use crate::{
flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes,
flake8_tidy_imports, fs, isort, mccabe, pep8_naming, pyupgrade,
flake8_tidy_imports, isort, mccabe, pep8_naming, pyupgrade,
};
pub mod configuration;
@@ -35,6 +36,7 @@ pub struct Settings {
pub exclude: GlobSet,
pub extend_exclude: GlobSet,
pub external: FxHashSet<String>,
pub fix: bool,
pub fixable: FxHashSet<CheckCode>,
pub format: SerializationFormat,
pub ignore_init_module_imports: bool,
@@ -55,46 +57,115 @@ pub struct Settings {
pub pyupgrade: pyupgrade::settings::Settings,
}
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
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"),
]
});
static DEFAULT_DUMMY_VARIABLE_RGX: Lazy<Regex> =
Lazy::new(|| Regex::new("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$").unwrap());
impl Settings {
pub fn from_configuration(
config: Configuration,
project_root: Option<&PathBuf>,
) -> Result<Self> {
pub fn from_configuration(config: Configuration, project_root: &Path) -> Result<Self> {
Ok(Self {
allowed_confusables: config.allowed_confusables,
dummy_variable_rgx: config.dummy_variable_rgx,
allowed_confusables: config
.allowed_confusables
.map(FxHashSet::from_iter)
.unwrap_or_default(),
dummy_variable_rgx: config
.dummy_variable_rgx
.unwrap_or_else(|| DEFAULT_DUMMY_VARIABLE_RGX.clone()),
enabled: resolve_codes(
&config
.select
.unwrap_or_else(|| vec![CheckCodePrefix::E, CheckCodePrefix::F])
.into_iter()
.chain(config.extend_select.into_iter())
.chain(config.extend_select.unwrap_or_default().into_iter())
.collect::<Vec<_>>(),
&config
.ignore
.unwrap_or_default()
.into_iter()
.chain(config.extend_ignore.into_iter())
.chain(config.extend_ignore.unwrap_or_default().into_iter())
.collect::<Vec<_>>(),
),
exclude: resolve_globset(config.exclude, project_root)?,
extend_exclude: resolve_globset(config.extend_exclude, project_root)?,
external: FxHashSet::from_iter(config.external),
fixable: resolve_codes(&config.fixable, &config.unfixable),
format: config.format,
flake8_annotations: config.flake8_annotations,
flake8_bugbear: config.flake8_bugbear,
flake8_import_conventions: config.flake8_import_conventions,
flake8_quotes: config.flake8_quotes,
flake8_tidy_imports: config.flake8_tidy_imports,
ignore_init_module_imports: config.ignore_init_module_imports,
isort: config.isort,
mccabe: config.mccabe,
line_length: config.line_length,
pep8_naming: config.pep8_naming,
pyupgrade: config.pyupgrade,
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,
exclude: resolve_globset(config.exclude.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()))?,
extend_exclude: resolve_globset(config.extend_exclude.unwrap_or_default())?,
external: FxHashSet::from_iter(config.external.unwrap_or_default()),
fix: config.fix.unwrap_or(false),
fixable: resolve_codes(
&config.fixable.unwrap_or_else(|| CATEGORIES.to_vec()),
&config.unfixable.unwrap_or_default(),
),
format: config.format.unwrap_or(SerializationFormat::Text),
ignore_init_module_imports: config.ignore_init_module_imports.unwrap_or_default(),
line_length: config.line_length.unwrap_or(88),
per_file_ignores: resolve_per_file_ignores(
config.per_file_ignores.unwrap_or_default(),
)?,
src: config
.src
.unwrap_or_else(|| vec![project_root.to_path_buf()]),
target_version: config.target_version.unwrap_or(PythonVersion::Py310),
show_source: config.show_source.unwrap_or_default(),
// Plugins
flake8_annotations: config
.flake8_annotations
.map(flake8_annotations::settings::Settings::from_options)
.unwrap_or_default(),
flake8_bugbear: config
.flake8_bugbear
.map(flake8_bugbear::settings::Settings::from_options)
.unwrap_or_default(),
flake8_import_conventions: config
.flake8_import_conventions
.map(flake8_import_conventions::settings::Settings::from_options)
.unwrap_or_default(),
flake8_quotes: config
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_options)
.unwrap_or_default(),
flake8_tidy_imports: config
.flake8_tidy_imports
.map(flake8_tidy_imports::settings::Settings::from_options)
.unwrap_or_default(),
isort: config
.isort
.map(isort::settings::Settings::from_options)
.unwrap_or_default(),
mccabe: config
.mccabe
.as_ref()
.map(mccabe::settings::Settings::from_options)
.unwrap_or_default(),
pep8_naming: config
.pep8_naming
.map(pep8_naming::settings::Settings::from_options)
.unwrap_or_default(),
pyupgrade: config
.pyupgrade
.as_ref()
.map(pyupgrade::settings::Settings::from_options)
.unwrap_or_default(),
})
}
@@ -106,6 +177,7 @@ impl Settings {
exclude: GlobSet::empty(),
extend_exclude: GlobSet::empty(),
external: FxHashSet::default(),
fix: false,
fixable: FxHashSet::from_iter([check_code]),
format: SerializationFormat::Text,
ignore_init_module_imports: false,
@@ -134,6 +206,7 @@ impl Settings {
exclude: GlobSet::empty(),
extend_exclude: GlobSet::empty(),
external: FxHashSet::default(),
fix: false,
fixable: FxHashSet::from_iter(check_codes),
format: SerializationFormat::Text,
ignore_init_module_imports: false,
@@ -196,13 +269,10 @@ 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> {
pub fn resolve_globset(patterns: Vec<FilePattern>) -> Result<GlobSet> {
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
pattern.add_to(&mut builder, project_root)?;
pattern.add_to(&mut builder)?;
}
builder.build().map_err(std::convert::Into::into)
}
@@ -210,21 +280,16 @@ pub fn resolve_globset(
/// 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, FxHashSet<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();
let absolute =
Glob::new(&per_file_ignore.absolute.to_string_lossy())?.compile_matcher();
// Construct basename matcher.
let basename = Glob::new(&per_file_ignore.pattern)?.compile_matcher();
let basename = Glob::new(&per_file_ignore.basename)?.compile_matcher();
Ok((absolute, basename, per_file_ignore.codes))
})

View File

@@ -55,7 +55,7 @@ pub struct Options {
(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
Note that you'll typically want to use [`extend-exclude`](#extend-exclude) to modify
the excluded paths.
"#,
default = r#"[".bzr", ".direnv", ".eggs", ".git", ".hg", ".mypy_cache", ".nox", ".pants.d", ".ruff_cache", ".svn", ".tox", ".venv", "__pypackages__", "_build", "buck-out", "build", "dist", "node_modules", "venv"]"#,
@@ -65,6 +65,24 @@ pub struct Options {
"#
)]
pub exclude: Option<Vec<String>>,
#[option(
doc = r#"
A path to a local `pyproject.toml` file to merge into this configuration.
To resolve the current `pyproject.toml` file, Ruff will first resolve this base
configuration file, then merge in any properties defined in the current configuration
file.
"#,
default = r#"None"#,
value_type = "Path",
example = r#"
# Extend the `pyproject.toml` file in the parent directory.
extend = "../pyproject.toml"
# But use a different line length.
line-length = 100
"#
)]
pub extend: Option<String>,
#[option(
doc = "A list of file patterns to omit from linting, in addition to those specified by \
`exclude`.",
@@ -217,8 +235,28 @@ pub struct Options {
)]
pub show_source: Option<bool>,
#[option(
doc = "The source code paths to consider, e.g., when resolving first- vs. third-party \
imports.",
doc = r#"
The source code paths to consider, e.g., when resolving first- vs. third-party imports.
As an example: given a Python package structure like:
```text
my_package/
pyproject.toml
src/
my_package/
__init__.py
foo.py
bar.py
```
The `src` directory should be included in `source` (e.g., `source = ["src"]`), such that
when resolving imports, `my_package.foo` is considered a first-party import.
This field supports globs. For example, if you have a series of Python packages in
a `python_modules` directory, `src = ["python_modules/*"]` would expand to incorporate
all of the packages in that directory.
"#,
default = r#"["."]"#,
value_type = "Vec<PathBuf>",
example = r#"

View File

@@ -3,9 +3,6 @@
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use common_path::common_path_all;
use log::debug;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::fs;
@@ -33,21 +30,21 @@ impl Pyproject {
fn parse_pyproject_toml(path: &Path) -> Result<Pyproject> {
let contents = fs::read_file(path)?;
Ok(toml::from_str(&contents)?)
toml::from_str(&contents).map_err(std::convert::Into::into)
}
pub fn find_pyproject_toml(path: Option<&PathBuf>) -> Option<PathBuf> {
if let Some(path) = path {
let path_pyproject_toml = path.join("pyproject.toml");
if path_pyproject_toml.is_file() {
return Some(path_pyproject_toml);
/// Find the nearest `pyproject.toml` file.
pub fn find_pyproject_toml(path: &Path) -> Option<PathBuf> {
for directory in path.ancestors() {
let pyproject = directory.join("pyproject.toml");
if pyproject.is_file() {
return Some(pyproject);
}
}
find_user_pyproject_toml()
None
}
fn find_user_pyproject_toml() -> Option<PathBuf> {
pub fn find_user_pyproject_toml() -> Option<PathBuf> {
let mut path = dirs::config_dir()?;
path.push("ruff");
path.push("pyproject.toml");
@@ -58,46 +55,17 @@ fn find_user_pyproject_toml() -> Option<PathBuf> {
}
}
pub fn find_project_root(sources: &[PathBuf]) -> Option<PathBuf> {
let absolute_sources: Vec<PathBuf> = sources
.iter()
.flat_map(|source| source.absolutize().map(|path| path.to_path_buf()))
.collect();
if let Some(prefix) = common_path_all(absolute_sources.iter().map(PathBuf::as_path)) {
for directory in prefix.ancestors() {
if directory.join(".git").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join(".hg").is_dir() {
return Some(directory.to_path_buf());
}
if directory.join("pyproject.toml").is_file() {
return Some(directory.to_path_buf());
}
}
}
None
}
pub fn load_options(pyproject: Option<&PathBuf>) -> Result<Options> {
if let Some(pyproject) = pyproject {
Ok(parse_pyproject_toml(pyproject)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default())
} else {
debug!("No pyproject.toml found.");
debug!("Falling back to default configuration...");
Ok(Options::default())
}
pub fn load_options(pyproject: &Path) -> Result<Options> {
Ok(parse_pyproject_toml(pyproject)
.map_err(|err| anyhow!("Failed to parse `{}`: {}", pyproject.to_string_lossy(), err))?
.tool
.and_then(|tool| tool.ruff)
.unwrap_or_default())
}
#[cfg(test)]
mod tests {
use std::env::current_dir;
use std::path::PathBuf;
use std::str::FromStr;
use anyhow::Result;
@@ -107,7 +75,7 @@ mod tests {
use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness;
use crate::settings::pyproject::{
find_project_root, find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools,
find_pyproject_toml, parse_pyproject_toml, Options, Pyproject, Tools,
};
use crate::settings::types::PatternPrefixPair;
use crate::{
@@ -140,6 +108,7 @@ mod tests {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -183,6 +152,7 @@ line-length = 79
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -226,6 +196,7 @@ exclude = ["foo.py"]
allowed_confusables: None,
line_length: None,
fix: None,
extend: None,
exclude: Some(vec!["foo.py".to_string()]),
extend_exclude: None,
select: None,
@@ -269,6 +240,7 @@ select = ["E501"]
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
@@ -313,6 +285,7 @@ ignore = ["E501"]
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: Some(vec![CheckCodePrefix::RUF100]),
@@ -376,14 +349,14 @@ other-attribute = 1
#[test]
fn find_and_parse_pyproject_toml() -> Result<()> {
let cwd = current_dir()?;
let project_root =
find_project_root(&[PathBuf::from("resources/test/fixtures/__init__.py")]).unwrap();
assert_eq!(project_root, cwd.join("resources/test/fixtures"));
let pyproject =
find_pyproject_toml(&cwd.join("resources/test/fixtures/__init__.py")).unwrap();
assert_eq!(
pyproject,
cwd.join("resources/test/fixtures/pyproject.toml")
);
let path = find_pyproject_toml(Some(&project_root)).unwrap();
assert_eq!(path, cwd.join("resources/test/fixtures/pyproject.toml"));
let pyproject = parse_pyproject_toml(&path)?;
let pyproject = parse_pyproject_toml(&pyproject)?;
let config = pyproject.tool.and_then(|tool| tool.ruff).unwrap();
assert_eq!(
config,
@@ -392,6 +365,7 @@ other-attribute = 1
line_length: Some(88),
fix: None,
exclude: None,
extend: None,
extend_exclude: Some(vec![
"excluded_file.py".to_string(),
"migrations".to_string(),

View File

@@ -49,27 +49,18 @@ impl FromStr for PythonVersion {
#[derive(Debug, Clone)]
pub enum FilePattern {
Builtin(&'static str),
User(String),
User(String, PathBuf),
}
impl FilePattern {
pub fn add_to(
self,
builder: &mut GlobSetBuilder,
project_root: Option<&PathBuf>,
) -> Result<()> {
pub fn add_to(self, builder: &mut GlobSetBuilder) -> 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())?);
FilePattern::User(pattern, absolute) => {
// Add the absolute path.
builder.add(Glob::new(&absolute.to_string_lossy())?);
// Add basename path.
if !pattern.contains(std::path::MAIN_SEPARATOR) {
@@ -85,20 +76,27 @@ impl FromStr for FilePattern {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::User(s.into()))
let pattern = s.to_string();
let absolute = fs::normalize_path(Path::new(&pattern));
Ok(Self::User(pattern, absolute))
}
}
#[derive(Debug, Clone)]
pub struct PerFileIgnore {
pub pattern: String,
pub basename: String,
pub absolute: PathBuf,
pub codes: FxHashSet<CheckCode>,
}
impl PerFileIgnore {
pub fn new(pattern: String, prefixes: &[CheckCodePrefix]) -> Self {
pub fn new(basename: String, absolute: PathBuf, prefixes: &[CheckCodePrefix]) -> Self {
let codes = prefixes.iter().flat_map(CheckCodePrefix::codes).collect();
Self { pattern, codes }
Self {
basename,
absolute,
codes,
}
}
}

View File

@@ -31,6 +31,12 @@ impl<'a> SourceCodeLocator<'a> {
Cow::from(rope.slice(offset..))
}
pub fn slice_source_code_until(&self, location: &Location) -> Cow<'_, str> {
let rope = self.get_or_init_rope();
let offset = rope.line_to_char(location.row() - 1) + location.column();
Cow::from(rope.slice(..offset))
}
pub fn slice_source_code_range(&self, range: &Range) -> Cow<'_, str> {
let rope = self.get_or_init_rope();
let start = rope.line_to_char(range.location.row() - 1) + range.location.column();