Compare commits

...

16 Commits

Author SHA1 Message Date
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
40 changed files with 1169 additions and 537 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.174
rev: v0.0.176
hooks:
- id: ruff

16
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.174-dev.0"
version = "0.0.176-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1821,7 +1821,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.174"
version = "0.0.176"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1874,7 +1874,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.174"
version = "0.0.176"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1892,7 +1892,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.174"
version = "0.0.176"
dependencies = [
"proc-macro2",
"quote",
@@ -1935,7 +1935,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 +1945,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 +1968,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 +1985,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.174"
version = "0.0.176"
edition = "2021"
rust-version = "1.65.0"
@@ -41,11 +41,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.174", path = "ruff_macros" }
ruff_macros = { version = "0.0.176", 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)

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)
@@ -153,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.174
rev: v0.0.176
hooks:
- id: ruff
```
@@ -368,7 +370,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.
@@ -418,14 +420,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 | |
@@ -715,7 +717,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)()`) | 🛠 |
@@ -770,6 +772,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.
@@ -815,6 +825,7 @@ 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 | |
@@ -822,6 +833,7 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
| 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)

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.174"
version = "0.0.176"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.174"
version = "0.0.176"
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.174-dev.0"
version = "0.0.176-dev.0"
edition = "2021"
[lib]

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,

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

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

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.174"
version = "0.0.176"
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.174"
version = "0.0.176"
edition = "2021"
[lib]

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,
};
@@ -1059,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) {
@@ -1155,7 +1158,13 @@ where
// 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.contains_key(name) {
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,
@@ -1207,7 +1216,13 @@ where
// 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.contains_key(name) {
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,
@@ -1481,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,
);
}
}
}
@@ -1966,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,
);
}
}
}
@@ -2119,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),
@@ -2758,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];

View File

@@ -206,6 +206,8 @@ pub enum CheckCode {
YTT301,
YTT302,
YTT303,
// flake8-simplify
SIM118,
// pyupgrade
UP001,
UP003,
@@ -337,6 +339,7 @@ pub enum CheckCategory {
Flake8Print,
Flake8Quotes,
Flake8Return,
Flake8Simplify,
Flake8TidyImports,
Flake8UnusedArguments,
Eradicate,
@@ -377,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",
@@ -406,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],
@@ -481,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,
@@ -746,6 +755,8 @@ pub enum CheckKind {
SysVersion0Referenced,
SysVersionCmpStr10,
SysVersionSlice1Referenced,
// flake8-simplify
KeyInDict(String, String),
// pyupgrade
TypeOfPrimitive(Primitive),
UselessMetaclassType,
@@ -1082,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),
@@ -1442,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,
@@ -1650,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,
@@ -2316,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())
@@ -2660,6 +2680,7 @@ impl CheckKind {
| CheckKind::ImplicitReturn
| CheckKind::ImplicitReturnValue
| CheckKind::IsLiteral
| CheckKind::KeyInDict(..)
| CheckKind::MisplacedComparisonConstant(..)
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(..)
@@ -2675,6 +2696,7 @@ impl CheckKind {
| CheckKind::NotIsTest
| CheckKind::OneBlankLineAfterClass(..)
| CheckKind::OneBlankLineBeforeClass(..)
| CheckKind::PercentFormatExtraNamedArguments(..)
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::PPrintFound
| CheckKind::PrintFound
@@ -2687,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

@@ -382,6 +382,10 @@ pub enum CheckCodePrefix {
S105,
S106,
S107,
SIM,
SIM1,
SIM11,
SIM118,
T,
T1,
T10,
@@ -1494,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],
@@ -2203,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,
@@ -2303,6 +2315,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::RET,
CheckCodePrefix::RUF,
CheckCodePrefix::S,
CheckCodePrefix::SIM,
CheckCodePrefix::T,
CheckCodePrefix::TID,
CheckCodePrefix::UP,

View File

@@ -47,42 +47,44 @@ 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,
@@ -121,9 +123,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 +176,51 @@ 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.
#[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 {

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

View File

@@ -53,6 +53,7 @@ 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;

View File

@@ -19,7 +19,7 @@ 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::cli::{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::logging::{set_up_logging, LogLevel};
@@ -199,8 +199,7 @@ fn autoformat(files: &[PathBuf], settings: &Settings) -> usize {
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)?;
@@ -220,54 +219,7 @@ fn inner_main() -> Result<ExitCode> {
// 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;
}
configuration.merge(overrides);
if cli.show_settings && cli.show_files {
eprintln!("Error: specify --show-settings or show-files (not both).");
@@ -278,14 +230,16 @@ fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::SUCCESS);
}
// Extract settings for internal use.
let autofix = if configuration.fix {
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
let fix = if configuration.fix {
fixer::Mode::Apply
} else if matches!(configuration.format, SerializationFormat::Json) {
fixer::Mode::Generate
} else {
fixer::Mode::None
};
let format = configuration.format;
let settings = Settings::from_configuration(configuration, project_root.as_ref())?;
// Now that we've inferred the appropriate log level, add some debug
@@ -300,7 +254,7 @@ fn inner_main() -> Result<ExitCode> {
};
if let Some(code) = cli.explain {
commands::explain(&code, settings.format)?;
commands::explain(&code, format)?;
return Ok(ExitCode::SUCCESS);
}
@@ -316,9 +270,9 @@ 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) {
if matches!(fix, fixer::Mode::Generate | fixer::Mode::Apply) {
eprintln!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
@@ -327,7 +281,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.");
}
@@ -383,16 +337,16 @@ 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)?
run_once_stdin(&settings, path, &fix)?
} else {
run_once(&cli.files, &settings, cache_enabled, &autofix)
run_once(&cli.files, &settings, cache_enabled, &fix)
};
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(autofix, fixer::Mode::Apply)) {
printer.write_once(&diagnostics, &autofix)?;
if !(is_stdin && matches!(fix, fixer::Mode::Apply)) {
printer.write_once(&diagnostics, &fix)?;
}
// Check for updates if we're in a non-silent log level.

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

@@ -93,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")]
@@ -411,6 +412,16 @@ mod tests {
"#,
&[],
)?;
// 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(())
}

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

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

View File

@@ -11,6 +11,7 @@ use regex::Regex;
use rustc_hash::FxHashSet;
use crate::checks_gen::{CheckCodePrefix, CATEGORIES};
use crate::cli::{collect_per_file_ignores, Overrides};
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
use crate::{
@@ -185,4 +186,55 @@ impl Configuration {
.unwrap_or_default(),
})
}
pub fn merge(&mut self, overrides: Overrides) {
if let Some(dummy_variable_rgx) = overrides.dummy_variable_rgx {
self.dummy_variable_rgx = dummy_variable_rgx;
}
if let Some(exclude) = overrides.exclude {
self.exclude = exclude;
}
if let Some(extend_exclude) = overrides.extend_exclude {
self.extend_exclude = extend_exclude;
}
if let Some(extend_ignore) = overrides.extend_ignore {
self.extend_ignore = extend_ignore;
}
if let Some(extend_select) = overrides.extend_select {
self.extend_select = extend_select;
}
if let Some(fix) = overrides.fix {
self.fix = fix;
}
if let Some(fixable) = overrides.fixable {
self.fixable = fixable;
}
if let Some(format) = overrides.format {
self.format = format;
}
if let Some(ignore) = overrides.ignore {
self.ignore = ignore;
}
if let Some(line_length) = overrides.line_length {
self.line_length = line_length;
}
if let Some(max_complexity) = overrides.max_complexity {
self.mccabe.max_complexity = max_complexity;
}
if let Some(per_file_ignores) = overrides.per_file_ignores {
self.per_file_ignores = collect_per_file_ignores(per_file_ignores);
}
if let Some(select) = overrides.select {
self.select = select;
}
if let Some(show_source) = overrides.show_source {
self.show_source = show_source;
}
if let Some(target_version) = overrides.target_version {
self.target_version = target_version;
}
if let Some(unfixable) = overrides.unfixable {
self.unfixable = unfixable;
}
}
}

View File

@@ -15,7 +15,7 @@ use rustc_hash::FxHashSet;
use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, SuffixLength};
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion, SerializationFormat};
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{
flake8_annotations, flake8_bugbear, flake8_import_conventions, flake8_quotes,
flake8_tidy_imports, fs, isort, mccabe, pep8_naming, pyupgrade,
@@ -36,7 +36,6 @@ pub struct Settings {
pub extend_exclude: GlobSet,
pub external: FxHashSet<String>,
pub fixable: FxHashSet<CheckCode>,
pub format: SerializationFormat,
pub ignore_init_module_imports: bool,
pub line_length: usize,
pub per_file_ignores: Vec<(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)>,
@@ -79,7 +78,6 @@ impl Settings {
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,
@@ -107,7 +105,6 @@ impl Settings {
extend_exclude: GlobSet::empty(),
external: FxHashSet::default(),
fixable: FxHashSet::from_iter([check_code]),
format: SerializationFormat::Text,
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: vec![],
@@ -135,7 +132,6 @@ impl Settings {
extend_exclude: GlobSet::empty(),
external: FxHashSet::default(),
fixable: FxHashSet::from_iter(check_codes),
format: SerializationFormat::Text,
ignore_init_module_imports: false,
line_length: 88,
per_file_ignores: vec![],

View File

@@ -20,7 +20,7 @@ pub struct Options {
`RUF002`, and `RUF003`.
"#,
default = r#"[]"#,
value_type = "Vec<char>",
value_type = "Vec<a test>",
example = r#"
# Allow minus-sign (U+2212), greek-small-letter-rho (U+03C1), and the asterisk-operator (U+2217),
# which could be confused for "-", "p", and "*", respectively.