Compare commits

...

19 Commits

Author SHA1 Message Date
Charlie Marsh
560558b814 Bump version to 0.0.139 2022-11-25 18:38:26 -05:00
Andri Bergsson
bef601b994 Add keyword argument handling for redundant open modes. (#906) 2022-11-25 18:38:05 -05:00
Charlie Marsh
7445d00b88 Implement B023 (function uses loop variable) (#907) 2022-11-25 18:29:54 -05:00
Charlie Marsh
7c78d4e103 Use docstring comment for CheckCode 2022-11-25 13:15:43 -05:00
Oliver Margetts
8b14f1b8cc Implement F522-F525 (#899) 2022-11-25 13:14:31 -05:00
Xuan (Sean) Hu
5a6b51e623 Minor changes in README. (#903) 2022-11-25 09:49:16 -05:00
Charlie Marsh
0b60242fb7 Bump version to 0.0.138 2022-11-25 00:05:41 -05:00
Charlie Marsh
65b77feeb8 Bump LibCST version 2022-11-25 00:05:03 -05:00
Charlie Marsh
04b9c0a31d Fix cargo clippy 2022-11-24 23:40:43 -05:00
Harutaka Kawamura
49dc8231be Fix typo (#902) 2022-11-24 23:38:45 -05:00
Charlie Marsh
92ca114882 Move some main.rs subcommands to a new module (#901) 2022-11-24 22:43:43 -05:00
Charlie Marsh
553bc7443a Remove UserConfiguration struct (#900) 2022-11-24 22:39:07 -05:00
CelebrateVC
a3af6c1ea5 Implement GlobSet optimization for file path exclusions (#883) 2022-11-24 22:31:55 -05:00
Charlie Marsh
b50016fe89 Regenerate README.md 2022-11-24 18:10:07 -05:00
Oliver Margetts
2cf2805848 Implement F521 (#898) 2022-11-24 18:09:36 -05:00
Harutaka Kawamura
33fbef7700 Implement B904 (#892) 2022-11-24 09:49:57 -05:00
Charlie Marsh
68668a584b Bump version to 0.0.137 2022-11-23 20:28:45 -05:00
Charlie Marsh
6cd8655d29 Treat withitem variables as bindings (#897) 2022-11-23 20:28:37 -05:00
Charlie Marsh
72a9bd3cfb Revert "Upload wheels back to GitHub Releases (#884)"
This reverts commit bd08fc359d.
2022-11-23 20:27:33 -05:00
57 changed files with 2627 additions and 493 deletions

View File

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

View File

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

25
Cargo.lock generated
View File

@@ -670,7 +670,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.136-dev.0"
version = "0.0.139-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -769,10 +769,17 @@ dependencies = [
]
[[package]]
name = "glob"
version = "0.3.0"
name = "globset"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a"
dependencies = [
"aho-corasick",
"bstr 0.2.17",
"fnv",
"log",
"regex",
]
[[package]]
name = "half"
@@ -1029,7 +1036,7 @@ checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
[[package]]
name = "libcst"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
dependencies = [
"chic",
"itertools",
@@ -1044,7 +1051,7 @@ dependencies = [
[[package]]
name = "libcst_derive"
version = "0.1.0"
source = "git+https://github.com/charliermarsh/LibCST?rev=a13ec97dd4eb925bde4d426c6e422582793b260c#a13ec97dd4eb925bde4d426c6e422582793b260c"
source = "git+https://github.com/charliermarsh/LibCST?rev=f2f0b7a487a8725d161fe8b3ed73a6758b21e177#f2f0b7a487a8725d161fe8b3ed73a6758b21e177"
dependencies = [
"quote",
"syn",
@@ -1768,7 +1775,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.136"
version = "0.0.139"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1787,7 +1794,7 @@ dependencies = [
"fern",
"filetime",
"getrandom 0.2.8",
"glob",
"globset",
"insta",
"itertools",
"libcst",
@@ -1818,7 +1825,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.136"
version = "0.0.139"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.136"
version = "0.0.139"
edition = "2021"
rust-version = "1.65.0"
@@ -27,9 +27,9 @@ common-path = { version = "1.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
filetime = { version = "0.2.17" }
glob = { version = "0.3.0" }
globset = {version = "0.4.9" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
nohash-hasher = { version = "0.2.0" }
notify = { version = "4.0.17" }

25
LICENSE
View File

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

View File

@@ -56,6 +56,7 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
1. [pep8-naming (N)](#pep8-naming)
1. [flake8-bandit (S)](#flake8-bandit)
1. [flake8-comprehensions (C)](#flake8-comprehensions)
1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap)
1. [flake8-bugbear (B)](#flake8-bugbear)
1. [flake8-builtins (A)](#flake8-builtins)
1. [flake8-tidy-imports (I25)](#flake8-tidy-imports)
@@ -64,7 +65,6 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
1. [flake8-annotations (ANN)](#flake8-annotations)
1. [flake8-2020 (YTT)](#flake8-2020)
1. [flake8-blind-except (BLE)](#flake8-blind-except)
1. [flake8-boolean-trap (FBT)](#flake8-boolean-trap)
1. [mccabe (C90)](#mccabe)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules)
1. [Meta rules (M)](#meta-rules)
@@ -107,7 +107,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.136
rev: v0.0.139
hooks:
- id: ruff
```
@@ -285,7 +285,7 @@ Exclusions are based on globs, and can be either:
### Ignoring errors
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
either on the command-line or in your `project.toml` file.
either on the command-line or in your `project.toml` file.
To ignore an error in-line, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
@@ -350,6 +350,11 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | |
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | |
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | |
| F521 | StringDotFormatInvalidFormat | '...'.format(...) has invalid format string: ... | |
| F522 | StringDotFormatExtraNamedArguments | '...'.format(...) has unused named argument(s): ... | |
| F523 | StringDotFormatExtraPositionalArguments | '...'.format(...) has unused arguments at position(s): ... | |
| F524 | StringDotFormatMissingArguments | '...'.format(...) is missing argument(s) for placeholder(s): ... | |
| F525 | StringDotFormatMixingAutomatic | '...'.format(...) mixes automatic and manual numbering | |
| F541 | FStringMissingPlaceholders | f-string without any placeholders | |
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
@@ -570,10 +575,12 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B020 | LoopVariableOverridesIterator | Loop control variable `...` overrides iterable it iterates | |
| B021 | FStringDocstring | f-string used as docstring. This will be interpreted by python as a joined string rather than a docstring. | |
| B022 | UselessContextlibSuppress | No arguments passed to `contextlib.suppress`. No exceptions will be suppressed and therefore this context manager is redundant | |
| B023 | FunctionUsesLoopVariable | Function definition does not bind loop variable `...` | |
| B024 | AbstractBaseClassWithoutAbstractMethod | `...` is an abstract base class, but it has no abstract methods | |
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | |
| B026 | StarArgUnpackingAfterKeywordArg | Star-arg unpacking after a keyword argument is strongly discouraged | |
| B027 | EmptyMethodWithoutAbstractDecorator | `...` is an empty method in an abstract base class, but has no abstract decorator | |
| B904 | RaiseWithoutFromInsideExcept | Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling | |
### flake8-builtins
@@ -805,7 +812,7 @@ automatically convert your existing configuration.)
Ruff can be used as a (near) drop-in replacement for Flake8 when used (1) without or with a small
number of plugins, (2) alongside Black, and (3) on Python 3 code.
Under those conditions Ruff is missing 14 rules related to string `.format` calls, 1 rule related
Under those conditions Ruff is missing 9 rules related to `%` string formatting, 1 rule related
to docstring parsing, and 1 rule related to redefined variables.
Ruff re-implements some of the most popular Flake8 plugins and related code quality tools natively,
@@ -814,6 +821,8 @@ including:
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`yesqa`](https://github.com/asottile/yesqa)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
@@ -821,9 +830,7 @@ including:
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (25/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)
@@ -845,6 +852,8 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
@@ -853,8 +862,6 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-bandit`](https://pypi.org/project/flake8-bandit/) (6/40)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (26/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`flake8-blind-except`](https://pypi.org/project/flake8-blind-except/)
- [`flake8-boolean-trap`](https://pypi.org/project/flake8-boolean-trap/)

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -36,3 +36,35 @@ with open("foo", "U") as fa, open("bar", "U") as fb:
pass
with open("foo", "Ub") as fa, open("bar", "Ub") as fb:
pass
open("foo", mode="U")
open(name="foo", mode="U")
open(mode="U", name="foo")
with open("foo", mode="U") as f:
pass
with open(name="foo", mode="U") as f:
pass
with open(mode="U", name="foo") as f:
pass
open("foo", mode="Ub")
open(name="foo", mode="Ub")
open(mode="Ub", name="foo")
with open("foo", mode="Ub") as f:
pass
with open(name="foo", mode="Ub") as f:
pass
with open(mode="Ub", name="foo") as f:
pass
open(file="foo", mode='U', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='U')
open(file="foo", buffering=- 1, encoding=None, errors=None, mode='U', newline=None, closefd=True, opener=None)
open(mode='U', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", mode='Ub', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
open(file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None, mode='Ub')
open(file="foo", buffering=- 1, encoding=None, errors=None, mode='Ub', newline=None, closefd=True, opener=None)
open(mode='Ub', file="foo", buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

View File

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

View File

@@ -117,7 +117,8 @@ pub fn main(cli: &Cli) -> Result<()> {
// Construct the output contents.
let mut output = String::new();
output.push_str("//! File automatically generated by examples/generate_check_code_prefix.rs.");
output
.push_str("//! File automatically generated by `examples/generate_check_code_prefix.rs`.");
output.push('\n');
output.push('\n');
output.push_str("use serde::{{Serialize, Deserialize}};");

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Withitem;
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind,
KeywordData, Operator, Stmt, StmtKind, Suite,
@@ -18,10 +19,10 @@ use crate::ast::helpers::{
use crate::ast::operations::extract_all_names;
use crate::ast::relocate::relocate_expr;
use crate::ast::types::{
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, ImportKind, Range, Scope,
ScopeKind,
Binding, BindingContext, BindingKind, ClassScope, FunctionScope, ImportKind, Node, Range,
Scope, ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::visitor::{walk_excepthandler, walk_withitem, Visitor};
use crate::ast::{helpers, operations, visitor};
use crate::checks::{Check, CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
@@ -77,10 +78,13 @@ pub struct Checker<'a> {
in_deferred_string_annotation: bool,
in_literal: bool,
in_subscript: bool,
in_withitem: bool,
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
except_handlers: Vec<Vec<Vec<&'a str>>>,
// Check-specific state.
pub(crate) seen_b023: Vec<&'a Expr>,
}
impl<'a> Checker<'a> {
@@ -120,10 +124,13 @@ impl<'a> Checker<'a> {
in_deferred_string_annotation: false,
in_literal: false,
in_subscript: false,
in_withitem: false,
seen_import_boundary: false,
futures_allowed: true,
annotations_future_enabled: false,
except_handlers: vec![],
// Check-specific state.
seen_b023: vec![],
}
}
@@ -948,8 +955,16 @@ where
flake8_bugbear::plugins::assert_raises_exception(self, stmt, items);
}
}
StmtKind::While { .. } => {
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Stmt(stmt));
}
}
StmtKind::For {
target, body, iter, ..
}
| StmtKind::AsyncFor {
target, body, iter, ..
} => {
if self.settings.enabled.contains(&CheckCode::B007) {
flake8_bugbear::plugins::unused_loop_control_variable(self, target, body);
@@ -957,6 +972,9 @@ where
if self.settings.enabled.contains(&CheckCode::B020) {
flake8_bugbear::plugins::loop_variable_overrides_iterator(self, target, iter);
}
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Stmt(stmt));
}
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
@@ -1240,6 +1258,81 @@ where
args,
keywords,
} => {
// pyflakes
if self.settings.enabled.contains(&CheckCode::F521)
|| self.settings.enabled.contains(&CheckCode::F522)
|| self.settings.enabled.contains(&CheckCode::F523)
|| self.settings.enabled.contains(&CheckCode::F524)
|| self.settings.enabled.contains(&CheckCode::F525)
{
if let ExprKind::Attribute { value, attr, .. } = &func.node {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &value.node
{
if attr == "format" {
// "...".format(...) call
let location = Range::from_located(expr);
match pyflakes::format::FormatSummary::try_from(value.as_ref()) {
Err(e) => {
if self.settings.enabled.contains(&CheckCode::F521) {
self.add_check(Check::new(
CheckKind::StringDotFormatInvalidFormat(
e.to_string(),
),
location,
));
}
}
Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F522) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_named_arguments(
&summary, keywords, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F523) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_positional_arguments(
&summary, args, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F524) {
if let Some(check) =
pyflakes::checks::string_dot_format_missing_argument(
&summary, args, keywords, location,
)
{
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::F525) {
if let Some(check) =
pyflakes::checks::string_dot_format_mixing_automatic(
&summary, location,
)
{
self.add_check(check);
}
}
}
}
}
}
}
}
// pyupgrade
if self.settings.enabled.contains(&CheckCode::U005) {
pyupgrade::plugins::deprecated_unittest_alias(self, func);
}
@@ -1732,9 +1825,15 @@ where
self.add_check(check);
};
}
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Expr(expr));
}
self.push_scope(Scope::new(ScopeKind::Generator));
}
ExprKind::GeneratorExp { .. } | ExprKind::DictComp { .. } => {
if self.settings.enabled.contains(&CheckCode::B023) {
flake8_bugbear::plugins::function_uses_loop_variable(self, &Node::Expr(expr));
}
self.push_scope(Scope::new(ScopeKind::Generator));
}
_ => {}
@@ -1926,13 +2025,20 @@ where
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
match &excepthandler.node {
ExcepthandlerKind::ExceptHandler { type_, name, .. } => {
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
self.add_check(Check::new(
CheckKind::DoNotUseBareExcept,
Range::from_located(excepthandler),
));
}
if self.settings.enabled.contains(&CheckCode::B904) {
{
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
}
}
match name {
Some(name) => {
if self.settings.enabled.contains(&CheckCode::E741) {
@@ -2080,6 +2186,13 @@ where
self.check_builtin_arg_shadowing(&arg.node.arg, Range::from_located(arg));
}
fn visit_withitem(&mut self, withitem: &'b Withitem) {
let prev_in_withitem = self.in_withitem;
self.in_withitem = true;
walk_withitem(self, withitem);
self.in_withitem = prev_in_withitem;
}
}
fn try_mark_used(scope: &mut Scope, scope_id: usize, id: &str, expr: &Expr) -> bool {
@@ -2386,7 +2499,7 @@ impl<'a> Checker<'a> {
return;
}
if operations::is_unpacking_assignment(parent) {
if self.in_withitem || operations::is_unpacking_assignment(parent) {
self.add_binding(
id,
Binding {

View File

@@ -52,6 +52,11 @@ pub enum CheckCode {
F405,
F406,
F407,
F521,
F522,
F523,
F524,
F525,
F541,
F601,
F602,
@@ -99,10 +104,12 @@ pub enum CheckCode {
B020,
B021,
B022,
B023,
B024,
B025,
B026,
B027,
B904,
// flake8-blind-except
BLE001,
// flake8-comprehensions
@@ -402,6 +409,11 @@ pub enum CheckKind {
MultiValueRepeatedKeyVariable(String),
RaiseNotImplemented,
ReturnOutsideFunction,
StringDotFormatExtraNamedArguments(Vec<String>),
StringDotFormatExtraPositionalArguments(Vec<String>),
StringDotFormatInvalidFormat(String),
StringDotFormatMissingArguments(Vec<String>),
StringDotFormatMixingAutomatic,
TwoStarredExpressions,
UndefinedExport(String),
UndefinedLocal(String),
@@ -416,31 +428,33 @@ pub enum CheckKind {
// flake8-blind-except
BlindExcept,
// flake8-bugbear
UnaryPrefixIncrement,
AssignmentToOsEnviron,
UnreliableCallableCheck,
StripWithMultiCharacters,
MutableArgumentDefault,
UnusedLoopControlVariable(String),
FunctionCallArgumentDefault(Option<String>),
GetAttrWithConstant,
SetAttrWithConstant,
DoNotAssertFalse,
JumpStatementInFinally(String),
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
UselessComparison,
CannotRaiseLiteral,
NoAssertRaisesException,
UselessExpression,
CachedInstanceMethod,
LoopVariableOverridesIterator(String),
FStringDocstring,
UselessContextlibSuppress,
AbstractBaseClassWithoutAbstractMethod(String),
AssignmentToOsEnviron,
CachedInstanceMethod,
CannotRaiseLiteral,
DoNotAssertFalse,
DuplicateHandlerException(Vec<String>),
DuplicateTryBlockException(String),
StarArgUnpackingAfterKeywordArg,
EmptyMethodWithoutAbstractDecorator(String),
FStringDocstring,
FunctionCallArgumentDefault(Option<String>),
FunctionUsesLoopVariable(String),
GetAttrWithConstant,
JumpStatementInFinally(String),
LoopVariableOverridesIterator(String),
MutableArgumentDefault,
NoAssertRaisesException,
RaiseWithoutFromInsideExcept,
RedundantTupleInExceptionHandler(String),
SetAttrWithConstant,
StarArgUnpackingAfterKeywordArg,
StripWithMultiCharacters,
UnaryPrefixIncrement,
UnreliableCallableCheck,
UnusedLoopControlVariable(String),
UselessComparison,
UselessContextlibSuppress,
UselessExpression,
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -644,6 +658,15 @@ impl CheckCode {
}
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
CheckCode::F521 => CheckKind::StringDotFormatInvalidFormat("...".to_string()),
CheckCode::F522 => {
CheckKind::StringDotFormatExtraNamedArguments(vec!["...".to_string()])
}
CheckCode::F523 => {
CheckKind::StringDotFormatExtraPositionalArguments(vec!["...".to_string()])
}
CheckCode::F524 => CheckKind::StringDotFormatMissingArguments(vec!["...".to_string()]),
CheckCode::F525 => CheckKind::StringDotFormatMixingAutomatic,
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
@@ -695,10 +718,12 @@ impl CheckCode {
CheckCode::B020 => CheckKind::LoopVariableOverridesIterator("...".to_string()),
CheckCode::B021 => CheckKind::FStringDocstring,
CheckCode::B022 => CheckKind::UselessContextlibSuppress,
CheckCode::B023 => CheckKind::FunctionUsesLoopVariable("...".to_string()),
CheckCode::B024 => CheckKind::AbstractBaseClassWithoutAbstractMethod("...".to_string()),
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
CheckCode::B904 => CheckKind::RaiseWithoutFromInsideExcept,
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -909,6 +934,11 @@ impl CheckCode {
CheckCode::F405 => CheckCategory::Pyflakes,
CheckCode::F406 => CheckCategory::Pyflakes,
CheckCode::F407 => CheckCategory::Pyflakes,
CheckCode::F521 => CheckCategory::Pyflakes,
CheckCode::F522 => CheckCategory::Pyflakes,
CheckCode::F523 => CheckCategory::Pyflakes,
CheckCode::F524 => CheckCategory::Pyflakes,
CheckCode::F525 => CheckCategory::Pyflakes,
CheckCode::F541 => CheckCategory::Pyflakes,
CheckCode::F601 => CheckCategory::Pyflakes,
CheckCode::F602 => CheckCategory::Pyflakes,
@@ -954,10 +984,12 @@ impl CheckCode {
CheckCode::B020 => CheckCategory::Flake8Bugbear,
CheckCode::B021 => CheckCategory::Flake8Bugbear,
CheckCode::B022 => CheckCategory::Flake8Bugbear,
CheckCode::B023 => CheckCategory::Flake8Bugbear,
CheckCode::B024 => CheckCategory::Flake8Bugbear,
CheckCode::B025 => CheckCategory::Flake8Bugbear,
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::B904 => CheckCategory::Flake8Bugbear,
CheckCode::BLE001 => CheckCategory::Flake8BlindExcept,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
@@ -1132,6 +1164,11 @@ impl CheckKind {
CheckKind::NotIsTest => &CheckCode::E714,
CheckKind::RaiseNotImplemented => &CheckCode::F901,
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
CheckKind::StringDotFormatExtraNamedArguments(_) => &CheckCode::F522,
CheckKind::StringDotFormatExtraPositionalArguments(_) => &CheckCode::F523,
CheckKind::StringDotFormatInvalidFormat(_) => &CheckCode::F521,
CheckKind::StringDotFormatMissingArguments(_) => &CheckCode::F524,
CheckKind::StringDotFormatMixingAutomatic => &CheckCode::F525,
CheckKind::SyntaxError(_) => &CheckCode::E999,
CheckKind::ExpressionsInStarAssignment => &CheckCode::F621,
CheckKind::TrueFalseComparison(..) => &CheckCode::E712,
@@ -1151,31 +1188,33 @@ impl CheckKind {
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
// flake8-bugbear
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
CheckKind::AssignmentToOsEnviron => &CheckCode::B003,
CheckKind::UnreliableCallableCheck => &CheckCode::B004,
CheckKind::StripWithMultiCharacters => &CheckCode::B005,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::FunctionCallArgumentDefault(_) => &CheckCode::B008,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::JumpStatementInFinally(_) => &CheckCode::B012,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::UselessComparison => &CheckCode::B015,
CheckKind::CannotRaiseLiteral => &CheckCode::B016,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::UselessExpression => &CheckCode::B018,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::AbstractBaseClassWithoutAbstractMethod(_) => &CheckCode::B024,
CheckKind::AssignmentToOsEnviron => &CheckCode::B003,
CheckKind::CachedInstanceMethod => &CheckCode::B019,
CheckKind::CannotRaiseLiteral => &CheckCode::B016,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::EmptyMethodWithoutAbstractDecorator(_) => &CheckCode::B027,
CheckKind::FStringDocstring => &CheckCode::B021,
CheckKind::FunctionCallArgumentDefault(_) => &CheckCode::B008,
CheckKind::FunctionUsesLoopVariable(_) => &CheckCode::B023,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::JumpStatementInFinally(_) => &CheckCode::B012,
CheckKind::LoopVariableOverridesIterator(_) => &CheckCode::B020,
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::RaiseWithoutFromInsideExcept => &CheckCode::B904,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
CheckKind::StripWithMultiCharacters => &CheckCode::B005,
CheckKind::UnaryPrefixIncrement => &CheckCode::B002,
CheckKind::UnreliableCallableCheck => &CheckCode::B004,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::UselessComparison => &CheckCode::B015,
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::UselessExpression => &CheckCode::B018,
// flake8-blind-except
CheckKind::BlindExcept => &CheckCode::BLE001,
// flake8-comprehensions
@@ -1417,6 +1456,24 @@ impl CheckKind {
CheckKind::ReturnOutsideFunction => {
"`return` statement outside of a function/method".to_string()
}
CheckKind::StringDotFormatExtraNamedArguments(missing) => {
let message = missing.join(", ");
format!("'...'.format(...) has unused named argument(s): {message}")
}
CheckKind::StringDotFormatExtraPositionalArguments(missing) => {
let message = missing.join(", ");
format!("'...'.format(...) has unused arguments at position(s): {message}")
}
CheckKind::StringDotFormatInvalidFormat(message) => {
format!("'...'.format(...) has invalid format string: {message}")
}
CheckKind::StringDotFormatMissingArguments(missing) => {
let message = missing.join(", ");
format!("'...'.format(...) is missing argument(s) for placeholder(s): {message}")
}
CheckKind::StringDotFormatMixingAutomatic => {
"'...'.format(...) mixes automatic and manual numbering".to_string()
}
CheckKind::SyntaxError(message) => format!("SyntaxError: {message}"),
CheckKind::ExpressionsInStarAssignment => {
"Too many expressions in star-unpacking assignment".to_string()
@@ -1502,6 +1559,9 @@ impl CheckKind {
"Do not perform function call in argument defaults".to_string()
}
}
CheckKind::FunctionUsesLoopVariable(name) => {
format!("Function definition does not bind loop variable `{name}`")
}
CheckKind::GetAttrWithConstant => "Do not call `getattr` with a constant attribute \
value. It is not any safer than normal property \
access."
@@ -1580,6 +1640,11 @@ impl CheckKind {
decorator"
)
}
CheckKind::RaiseWithoutFromInsideExcept => {
"Within an except clause, raise exceptions with raise ... from err or raise ... \
from None to distinguish them from errors in exception handling"
.to_string()
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()

View File

@@ -59,10 +59,14 @@ pub enum CheckCodePrefix {
B020,
B021,
B022,
B023,
B024,
B025,
B026,
B027,
B9,
B90,
B904,
BLE,
BLE0,
BLE00,
@@ -183,6 +187,12 @@ pub enum CheckCodePrefix {
F406,
F407,
F5,
F52,
F521,
F522,
F523,
F524,
F525,
F54,
F541,
F6,
@@ -412,10 +422,12 @@ impl CheckCodePrefix {
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
CheckCode::B027,
CheckCode::B904,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -439,6 +451,7 @@ impl CheckCodePrefix {
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
@@ -488,6 +501,7 @@ impl CheckCodePrefix {
CheckCode::B020,
CheckCode::B021,
CheckCode::B022,
CheckCode::B023,
CheckCode::B024,
CheckCode::B025,
CheckCode::B026,
@@ -496,10 +510,14 @@ impl CheckCodePrefix {
CheckCodePrefix::B020 => vec![CheckCode::B020],
CheckCodePrefix::B021 => vec![CheckCode::B021],
CheckCodePrefix::B022 => vec![CheckCode::B022],
CheckCodePrefix::B023 => vec![CheckCode::B023],
CheckCodePrefix::B024 => vec![CheckCode::B024],
CheckCodePrefix::B025 => vec![CheckCode::B025],
CheckCodePrefix::B026 => vec![CheckCode::B026],
CheckCodePrefix::B027 => vec![CheckCode::B027],
CheckCodePrefix::B9 => vec![CheckCode::B904],
CheckCodePrefix::B90 => vec![CheckCode::B904],
CheckCodePrefix::B904 => vec![CheckCode::B904],
CheckCodePrefix::BLE => vec![CheckCode::BLE001],
CheckCodePrefix::BLE0 => vec![CheckCode::BLE001],
CheckCodePrefix::BLE00 => vec![CheckCode::BLE001],
@@ -840,6 +858,11 @@ impl CheckCodePrefix {
CheckCode::F405,
CheckCode::F406,
CheckCode::F407,
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
CheckCode::F541,
CheckCode::F601,
CheckCode::F602,
@@ -887,7 +910,26 @@ impl CheckCodePrefix {
CheckCodePrefix::F405 => vec![CheckCode::F405],
CheckCodePrefix::F406 => vec![CheckCode::F406],
CheckCodePrefix::F407 => vec![CheckCode::F407],
CheckCodePrefix::F5 => vec![CheckCode::F541],
CheckCodePrefix::F5 => vec![
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
CheckCode::F541,
],
CheckCodePrefix::F52 => vec![
CheckCode::F521,
CheckCode::F522,
CheckCode::F523,
CheckCode::F524,
CheckCode::F525,
],
CheckCodePrefix::F521 => vec![CheckCode::F521],
CheckCodePrefix::F522 => vec![CheckCode::F522],
CheckCodePrefix::F523 => vec![CheckCode::F523],
CheckCodePrefix::F524 => vec![CheckCode::F524],
CheckCodePrefix::F525 => vec![CheckCode::F525],
CheckCodePrefix::F54 => vec![CheckCode::F541],
CheckCodePrefix::F541 => vec![CheckCode::F541],
CheckCodePrefix::F6 => vec![
@@ -1281,10 +1323,14 @@ impl CheckCodePrefix {
CheckCodePrefix::B020 => PrefixSpecificity::Explicit,
CheckCodePrefix::B021 => PrefixSpecificity::Explicit,
CheckCodePrefix::B022 => PrefixSpecificity::Explicit,
CheckCodePrefix::B023 => PrefixSpecificity::Explicit,
CheckCodePrefix::B024 => PrefixSpecificity::Explicit,
CheckCodePrefix::B025 => PrefixSpecificity::Explicit,
CheckCodePrefix::B026 => PrefixSpecificity::Explicit,
CheckCodePrefix::B027 => PrefixSpecificity::Explicit,
CheckCodePrefix::B9 => PrefixSpecificity::Hundreds,
CheckCodePrefix::B90 => PrefixSpecificity::Tens,
CheckCodePrefix::B904 => PrefixSpecificity::Explicit,
CheckCodePrefix::BLE => PrefixSpecificity::Category,
CheckCodePrefix::BLE0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::BLE00 => PrefixSpecificity::Tens,
@@ -1405,6 +1451,12 @@ impl CheckCodePrefix {
CheckCodePrefix::F406 => PrefixSpecificity::Explicit,
CheckCodePrefix::F407 => PrefixSpecificity::Explicit,
CheckCodePrefix::F5 => PrefixSpecificity::Hundreds,
CheckCodePrefix::F52 => PrefixSpecificity::Tens,
CheckCodePrefix::F521 => PrefixSpecificity::Explicit,
CheckCodePrefix::F522 => PrefixSpecificity::Explicit,
CheckCodePrefix::F523 => PrefixSpecificity::Explicit,
CheckCodePrefix::F524 => PrefixSpecificity::Explicit,
CheckCodePrefix::F525 => PrefixSpecificity::Explicit,
CheckCodePrefix::F54 => PrefixSpecificity::Tens,
CheckCodePrefix::F541 => PrefixSpecificity::Explicit,
CheckCodePrefix::F6 => PrefixSpecificity::Hundreds,

View File

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

66
src/commands.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

152
src/fs.rs
View File

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

View File

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

View File

@@ -413,10 +413,12 @@ mod tests {
#[test_case(CheckCode::B020, Path::new("B020.py"); "B020")]
#[test_case(CheckCode::B021, Path::new("B021.py"); "B021")]
#[test_case(CheckCode::B022, Path::new("B022.py"); "B022")]
#[test_case(CheckCode::B023, Path::new("B023.py"); "B023")]
#[test_case(CheckCode::B024, Path::new("B024.py"); "B024")]
#[test_case(CheckCode::B025, Path::new("B025.py"); "B025")]
#[test_case(CheckCode::B026, Path::new("B026.py"); "B026")]
#[test_case(CheckCode::B027, Path::new("B027.py"); "B027")]
#[test_case(CheckCode::B904, Path::new("B904.py"); "B904")]
#[test_case(CheckCode::BLE001, Path::new("BLE.py"); "BLE001")]
#[test_case(CheckCode::C400, Path::new("C400.py"); "C400")]
#[test_case(CheckCode::C401, Path::new("C401.py"); "C401")]
@@ -505,6 +507,11 @@ mod tests {
#[test_case(CheckCode::F405, Path::new("F405.py"); "F405")]
#[test_case(CheckCode::F406, Path::new("F406.py"); "F406")]
#[test_case(CheckCode::F407, Path::new("F407.py"); "F407")]
#[test_case(CheckCode::F521, Path::new("F521.py"); "F521")]
#[test_case(CheckCode::F522, Path::new("F522.py"); "F522")]
#[test_case(CheckCode::F523, Path::new("F523.py"); "F523")]
#[test_case(CheckCode::F524, Path::new("F524.py"); "F524")]
#[test_case(CheckCode::F525, Path::new("F525.py"); "F525")]
#[test_case(CheckCode::F541, Path::new("F541.py"); "F541")]
#[test_case(CheckCode::F601, Path::new("F601.py"); "F601")]
#[test_case(CheckCode::F602, Path::new("F602.py"); "F602")]

View File

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

View File

@@ -1,11 +1,141 @@
use std::string::ToString;
use regex::Regex;
use rustc_hash::FxHashSet;
use rustpython_ast::{Keyword, KeywordData};
use rustpython_parser::ast::{
Arg, Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Stmt, StmtKind,
};
use crate::ast::types::{BindingKind, FunctionScope, Range, Scope, ScopeKind};
use crate::checks::{Check, CheckKind};
use crate::pyflakes::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 { .. }))
}
/// 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))
.cloned()
.collect();
if missing.is_empty() {
None
} else {
Some(Check::new(
CheckKind::StringDotFormatExtraNamedArguments(missing),
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> {

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

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

View File

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

View File

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

View File

@@ -46,25 +46,25 @@ pub struct Configuration {
static DEFAULT_EXCLUDE: Lazy<Vec<FilePattern>> = Lazy::new(|| {
vec![
FilePattern::Simple(".bzr"),
FilePattern::Simple(".direnv"),
FilePattern::Simple(".eggs"),
FilePattern::Simple(".git"),
FilePattern::Simple(".hg"),
FilePattern::Simple(".mypy_cache"),
FilePattern::Simple(".nox"),
FilePattern::Simple(".pants.d"),
FilePattern::Simple(".ruff_cache"),
FilePattern::Simple(".svn"),
FilePattern::Simple(".tox"),
FilePattern::Simple(".venv"),
FilePattern::Simple("__pypackages__"),
FilePattern::Simple("_build"),
FilePattern::Simple("buck-out"),
FilePattern::Simple("build"),
FilePattern::Simple("dist"),
FilePattern::Simple("node_modules"),
FilePattern::Simple("venv"),
FilePattern::Builtin(".bzr"),
FilePattern::Builtin(".direnv"),
FilePattern::Builtin(".eggs"),
FilePattern::Builtin(".git"),
FilePattern::Builtin(".hg"),
FilePattern::Builtin(".mypy_cache"),
FilePattern::Builtin(".nox"),
FilePattern::Builtin(".pants.d"),
FilePattern::Builtin(".ruff_cache"),
FilePattern::Builtin(".svn"),
FilePattern::Builtin(".tox"),
FilePattern::Builtin(".venv"),
FilePattern::Builtin("__pypackages__"),
FilePattern::Builtin("_build"),
FilePattern::Builtin("buck-out"),
FilePattern::Builtin("build"),
FilePattern::Builtin("dist"),
FilePattern::Builtin("node_modules"),
FilePattern::Builtin("venv"),
]
});
@@ -103,22 +103,14 @@ impl Configuration {
},
),
target_version: options.target_version.unwrap_or(PythonVersion::Py310),
exclude: options
.exclude
.map(|paths| {
paths
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect()
})
.transpose()?
.unwrap_or_else(|| DEFAULT_EXCLUDE.clone()),
exclude: options.exclude.map_or_else(
|| DEFAULT_EXCLUDE.clone(),
|paths| paths.into_iter().map(FilePattern::User).collect(),
),
extend_exclude: options
.extend_exclude
.unwrap_or_default()
.iter()
.map(|path| FilePattern::from_user(path, project_root))
.collect::<Result<_>>()?,
.map(|paths| paths.into_iter().map(FilePattern::User).collect())
.unwrap_or_default(),
extend_ignore: options.extend_ignore.unwrap_or_default(),
select: options
.select
@@ -154,13 +146,10 @@ impl Configuration {
.per_file_ignores
.map(|per_file_ignores| {
per_file_ignores
.iter()
.map(|(pattern, prefixes)| {
PerFileIgnore::new(pattern, prefixes, project_root)
})
.into_iter()
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes))
.collect()
})
.transpose()?
.unwrap_or_default(),
show_source: options.show_source.unwrap_or_default(),
// Plugins

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,68 @@
---
source: src/linter.rs
expression: checks
---
- kind:
StringDotFormatInvalidFormat: "Single '{' encountered in format string"
location:
row: 1
column: 0
end_location:
row: 1
column: 13
fix: ~
- kind:
StringDotFormatInvalidFormat: "Single '}' encountered in format string"
location:
row: 2
column: 0
end_location:
row: 2
column: 13
fix: ~
- kind:
StringDotFormatInvalidFormat: "Expected '}' before end of string"
location:
row: 3
column: 0
end_location:
row: 3
column: 22
fix: ~
- kind:
StringDotFormatInvalidFormat: Max string recursion exceeded
location:
row: 5
column: 0
end_location:
row: 5
column: 26
fix: ~
- kind:
StringDotFormatInvalidFormat: Empty attribute in format string
location:
row: 7
column: 0
end_location:
row: 7
column: 29
fix: ~
- kind:
StringDotFormatInvalidFormat: Empty attribute in format string
location:
row: 8
column: 0
end_location:
row: 8
column: 23
fix: ~
- kind:
StringDotFormatInvalidFormat: Empty attribute in format string
location:
row: 9
column: 0
end_location:
row: 9
column: 26
fix: ~

View File

@@ -0,0 +1,36 @@
---
source: src/linter.rs
expression: checks
---
- kind:
StringDotFormatExtraNamedArguments:
- bar
location:
row: 1
column: 0
end_location:
row: 1
column: 21
fix: ~
- kind:
StringDotFormatExtraNamedArguments:
- spam
location:
row: 2
column: 0
end_location:
row: 2
column: 34
fix: ~
- kind:
StringDotFormatExtraNamedArguments:
- eggs
- ham
location:
row: 4
column: 0
end_location:
row: 4
column: 51
fix: ~

View File

@@ -0,0 +1,77 @@
---
source: src/linter.rs
expression: checks
---
- kind:
StringDotFormatExtraPositionalArguments:
- "1"
location:
row: 2
column: 0
end_location:
row: 2
column: 18
fix: ~
- kind:
StringDotFormatExtraPositionalArguments:
- "0"
- "2"
location:
row: 3
column: 0
end_location:
row: 3
column: 21
fix: ~
- kind:
StringDotFormatExtraPositionalArguments:
- "2"
location:
row: 5
column: 0
end_location:
row: 5
column: 25
fix: ~
- kind:
StringDotFormatExtraPositionalArguments:
- "1"
location:
row: 6
column: 0
end_location:
row: 6
column: 21
fix: ~
- kind:
StringDotFormatExtraPositionalArguments:
- "1"
location:
row: 9
column: 0
end_location:
row: 9
column: 17
fix: ~
- kind:
StringDotFormatExtraPositionalArguments:
- "1"
- "2"
location:
row: 10
column: 0
end_location:
row: 10
column: 20
fix: ~
- kind:
StringDotFormatExtraPositionalArguments:
- "2"
location:
row: 12
column: 0
end_location:
row: 12
column: 23
fix: ~

View File

@@ -0,0 +1,67 @@
---
source: src/linter.rs
expression: checks
---
- kind:
StringDotFormatMissingArguments:
- "1"
location:
row: 1
column: 0
end_location:
row: 1
column: 17
fix: ~
- kind:
StringDotFormatMissingArguments:
- "2"
location:
row: 2
column: 0
end_location:
row: 2
column: 14
fix: ~
- kind:
StringDotFormatMissingArguments:
- bar
location:
row: 3
column: 0
end_location:
row: 3
column: 16
fix: ~
- kind:
StringDotFormatMissingArguments:
- bar
location:
row: 4
column: 0
end_location:
row: 4
column: 21
fix: ~
- kind:
StringDotFormatMissingArguments:
- "0"
- bar
location:
row: 5
column: 0
end_location:
row: 5
column: 20
fix: ~
- kind:
StringDotFormatMissingArguments:
- "0"
- bar
location:
row: 6
column: 0
end_location:
row: 6
column: 20
fix: ~

View File

@@ -0,0 +1,21 @@
---
source: src/linter.rs
expression: checks
---
- kind: StringDotFormatMixingAutomatic
location:
row: 1
column: 0
end_location:
row: 1
column: 21
fix: ~
- kind: StringDotFormatMixingAutomatic
location:
row: 2
column: 0
end_location:
row: 2
column: 21
fix: ~

View File

@@ -386,4 +386,324 @@ expression: checks
end_location:
row: 37
column: 46
- kind: RedundantOpenModes
location:
row: 40
column: 0
end_location:
row: 40
column: 21
fix:
patch:
content: ""
location:
row: 40
column: 10
end_location:
row: 40
column: 20
- kind: RedundantOpenModes
location:
row: 41
column: 0
end_location:
row: 41
column: 26
fix:
patch:
content: ""
location:
row: 41
column: 15
end_location:
row: 41
column: 25
- kind: RedundantOpenModes
location:
row: 42
column: 0
end_location:
row: 42
column: 26
fix:
patch:
content: ""
location:
row: 42
column: 5
end_location:
row: 42
column: 15
- kind: RedundantOpenModes
location:
row: 44
column: 5
end_location:
row: 44
column: 26
fix:
patch:
content: ""
location:
row: 44
column: 15
end_location:
row: 44
column: 25
- kind: RedundantOpenModes
location:
row: 46
column: 5
end_location:
row: 46
column: 31
fix:
patch:
content: ""
location:
row: 46
column: 20
end_location:
row: 46
column: 30
- kind: RedundantOpenModes
location:
row: 48
column: 5
end_location:
row: 48
column: 31
fix:
patch:
content: ""
location:
row: 48
column: 10
end_location:
row: 48
column: 20
- kind: RedundantOpenModes
location:
row: 51
column: 0
end_location:
row: 51
column: 22
fix:
patch:
content: "\"rb\""
location:
row: 51
column: 17
end_location:
row: 51
column: 21
- kind: RedundantOpenModes
location:
row: 52
column: 0
end_location:
row: 52
column: 27
fix:
patch:
content: "\"rb\""
location:
row: 52
column: 22
end_location:
row: 52
column: 26
- kind: RedundantOpenModes
location:
row: 53
column: 0
end_location:
row: 53
column: 27
fix:
patch:
content: "\"rb\""
location:
row: 53
column: 10
end_location:
row: 53
column: 14
- kind: RedundantOpenModes
location:
row: 55
column: 5
end_location:
row: 55
column: 27
fix:
patch:
content: "\"rb\""
location:
row: 55
column: 22
end_location:
row: 55
column: 26
- kind: RedundantOpenModes
location:
row: 57
column: 5
end_location:
row: 57
column: 32
fix:
patch:
content: "\"rb\""
location:
row: 57
column: 27
end_location:
row: 57
column: 31
- kind: RedundantOpenModes
location:
row: 59
column: 5
end_location:
row: 59
column: 32
fix:
patch:
content: "\"rb\""
location:
row: 59
column: 15
end_location:
row: 59
column: 19
- kind: RedundantOpenModes
location:
row: 62
column: 0
end_location:
row: 62
column: 110
fix:
patch:
content: ""
location:
row: 62
column: 15
end_location:
row: 62
column: 25
- kind: RedundantOpenModes
location:
row: 63
column: 0
end_location:
row: 63
column: 110
fix:
patch:
content: ""
location:
row: 63
column: 99
end_location:
row: 63
column: 109
- kind: RedundantOpenModes
location:
row: 64
column: 0
end_location:
row: 64
column: 110
fix:
patch:
content: ""
location:
row: 64
column: 58
end_location:
row: 64
column: 68
- kind: RedundantOpenModes
location:
row: 65
column: 0
end_location:
row: 65
column: 110
fix:
patch:
content: ""
location:
row: 65
column: 5
end_location:
row: 65
column: 15
- kind: RedundantOpenModes
location:
row: 67
column: 0
end_location:
row: 67
column: 111
fix:
patch:
content: "\"rb\""
location:
row: 67
column: 22
end_location:
row: 67
column: 26
- kind: RedundantOpenModes
location:
row: 68
column: 0
end_location:
row: 68
column: 111
fix:
patch:
content: "\"rb\""
location:
row: 68
column: 106
end_location:
row: 68
column: 110
- kind: RedundantOpenModes
location:
row: 69
column: 0
end_location:
row: 69
column: 111
fix:
patch:
content: "\"rb\""
location:
row: 69
column: 65
end_location:
row: 69
column: 69
- kind: RedundantOpenModes
location:
row: 70
column: 0
end_location:
row: 70
column: 111
fix:
patch:
content: "\"rb\""
location:
row: 70
column: 10
end_location:
row: 70
column: 14

360
src/vendored/format.rs Normal file
View File

@@ -0,0 +1,360 @@
//! Vendored from [format.rs in rustpython-vm](https://github.com/RustPython/RustPython/blob/f54b5556e28256763c5506813ea977c9e1445af0/vm/src/format.rs).
//! The only changes we make are to remove dead code and code involving the vm.
use itertools::{Itertools, PeekingNext};
#[derive(Debug, PartialEq)]
pub(crate) enum FormatParseError {
UnmatchedBracket,
MissingStartBracket,
UnescapedStartBracketInLiteral,
InvalidFormatSpecifier,
UnknownConversion,
EmptyAttribute,
MissingRightBracket,
InvalidCharacterAfterRightBracket,
}
#[derive(Debug, PartialEq)]
pub(crate) enum FieldNamePart {
Attribute(String),
Index(usize),
StringIndex(String),
}
impl FieldNamePart {
fn parse_part(
chars: &mut impl PeekingNext<Item = char>,
) -> Result<Option<FieldNamePart>, FormatParseError> {
chars
.next()
.map(|ch| match ch {
'.' => {
let mut attribute = String::new();
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
attribute.push(ch);
}
if attribute.is_empty() {
Err(FormatParseError::EmptyAttribute)
} else {
Ok(FieldNamePart::Attribute(attribute))
}
}
'[' => {
let mut index = String::new();
for ch in chars {
if ch == ']' {
return if index.is_empty() {
Err(FormatParseError::EmptyAttribute)
} else if let Ok(index) = index.parse::<usize>() {
Ok(FieldNamePart::Index(index))
} else {
Ok(FieldNamePart::StringIndex(index))
};
}
index.push(ch);
}
Err(FormatParseError::MissingRightBracket)
}
_ => Err(FormatParseError::InvalidCharacterAfterRightBracket),
})
.transpose()
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum FieldType {
Auto,
Index(usize),
Keyword(String),
}
#[derive(Debug, PartialEq)]
pub(crate) struct FieldName {
pub field_type: FieldType,
pub parts: Vec<FieldNamePart>,
}
impl FieldName {
pub(crate) fn parse(text: &str) -> Result<FieldName, FormatParseError> {
let mut chars = text.chars().peekable();
let mut first = String::new();
for ch in chars.peeking_take_while(|ch| *ch != '.' && *ch != '[') {
first.push(ch);
}
let field_type = if first.is_empty() {
FieldType::Auto
} else if let Ok(index) = first.parse::<usize>() {
FieldType::Index(index)
} else {
FieldType::Keyword(first)
};
let mut parts = Vec::new();
while let Some(part) = FieldNamePart::parse_part(&mut chars)? {
parts.push(part);
}
Ok(FieldName { field_type, parts })
}
}
#[derive(Debug, PartialEq)]
pub(crate) enum FormatPart {
Field {
field_name: String,
preconversion_spec: Option<char>,
format_spec: String,
},
Literal(String),
}
#[derive(Debug, PartialEq)]
pub(crate) struct FormatString {
pub format_parts: Vec<FormatPart>,
}
impl FormatString {
fn parse_literal_single(text: &str) -> Result<(char, &str), FormatParseError> {
let mut chars = text.chars();
// This should never be called with an empty str
let first_char = chars.next().unwrap();
// isn't this detectable only with bytes operation?
if first_char == '{' || first_char == '}' {
let maybe_next_char = chars.next();
// if we see a bracket, it has to be escaped by doubling up to be in a literal
return if maybe_next_char.is_none() || maybe_next_char.unwrap() != first_char {
Err(FormatParseError::UnescapedStartBracketInLiteral)
} else {
Ok((first_char, chars.as_str()))
};
}
Ok((first_char, chars.as_str()))
}
fn parse_literal(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
let mut cur_text = text;
let mut result_string = String::new();
while !cur_text.is_empty() {
match FormatString::parse_literal_single(cur_text) {
Ok((next_char, remaining)) => {
result_string.push(next_char);
cur_text = remaining;
}
Err(err) => {
return if result_string.is_empty() {
Err(err)
} else {
Ok((FormatPart::Literal(result_string), cur_text))
};
}
}
}
Ok((FormatPart::Literal(result_string), ""))
}
fn parse_part_in_brackets(text: &str) -> Result<FormatPart, FormatParseError> {
let parts: Vec<&str> = text.splitn(2, ':').collect();
// before the comma is a keyword or arg index, after the comma is maybe a spec.
let arg_part = parts[0];
let format_spec = if parts.len() > 1 {
parts[1].to_owned()
} else {
String::new()
};
// On parts[0] can still be the preconversor (!r, !s, !a)
let parts: Vec<&str> = arg_part.splitn(2, '!').collect();
// before the bang is a keyword or arg index, after the comma is maybe a
// conversor spec.
let arg_part = parts[0];
let preconversion_spec = parts
.get(1)
.map(|conversion| {
// conversions are only every one character
conversion
.chars()
.exactly_one()
.map_err(|_| FormatParseError::UnknownConversion)
})
.transpose()?;
Ok(FormatPart::Field {
field_name: arg_part.to_owned(),
preconversion_spec,
format_spec,
})
}
fn parse_spec(text: &str) -> Result<(FormatPart, &str), FormatParseError> {
let mut nested = false;
let mut end_bracket_pos = None;
let mut left = String::new();
// There may be one layer nesting brackets in spec
for (idx, c) in text.chars().enumerate() {
if idx == 0 {
if c != '{' {
return Err(FormatParseError::MissingStartBracket);
}
} else if c == '{' {
if nested {
return Err(FormatParseError::InvalidFormatSpecifier);
}
nested = true;
left.push(c);
continue;
} else if c == '}' {
if nested {
nested = false;
left.push(c);
continue;
}
end_bracket_pos = Some(idx);
break;
} else {
left.push(c);
}
}
if let Some(pos) = end_bracket_pos {
let (_, right) = text.split_at(pos);
let format_part = FormatString::parse_part_in_brackets(&left)?;
Ok((format_part, &right[1..]))
} else {
Err(FormatParseError::UnmatchedBracket)
}
}
}
pub(crate) trait FromTemplate<'a>: Sized {
type Err;
fn from_str(s: &'a str) -> Result<Self, Self::Err>;
}
impl<'a> FromTemplate<'a> for FormatString {
type Err = FormatParseError;
fn from_str(text: &'a str) -> Result<Self, Self::Err> {
let mut cur_text: &str = text;
let mut parts: Vec<FormatPart> = Vec::new();
while !cur_text.is_empty() {
// Try to parse both literals and bracketed format parts until we
// run out of text
cur_text = FormatString::parse_literal(cur_text)
.or_else(|_| FormatString::parse_spec(cur_text))
.map(|(part, new_text)| {
parts.push(part);
new_text
})?;
}
Ok(FormatString {
format_parts: parts,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_parse() {
let expected = Ok(FormatString {
format_parts: vec![
FormatPart::Literal("abcd".to_owned()),
FormatPart::Field {
field_name: "1".to_owned(),
preconversion_spec: None,
format_spec: String::new(),
},
FormatPart::Literal(":".to_owned()),
FormatPart::Field {
field_name: "key".to_owned(),
preconversion_spec: None,
format_spec: String::new(),
},
],
});
assert_eq!(FormatString::from_str("abcd{1}:{key}"), expected);
}
#[test]
fn test_format_parse_fail() {
assert_eq!(
FormatString::from_str("{s"),
Err(FormatParseError::UnmatchedBracket)
);
}
#[test]
fn test_format_parse_escape() {
let expected = Ok(FormatString {
format_parts: vec![
FormatPart::Literal("{".to_owned()),
FormatPart::Field {
field_name: "key".to_owned(),
preconversion_spec: None,
format_spec: String::new(),
},
FormatPart::Literal("}ddfe".to_owned()),
],
});
assert_eq!(FormatString::from_str("{{{key}}}ddfe"), expected);
}
#[test]
fn test_parse_field_name() {
assert_eq!(
FieldName::parse(""),
Ok(FieldName {
field_type: FieldType::Auto,
parts: Vec::new(),
})
);
assert_eq!(
FieldName::parse("0"),
Ok(FieldName {
field_type: FieldType::Index(0),
parts: Vec::new(),
})
);
assert_eq!(
FieldName::parse("key"),
Ok(FieldName {
field_type: FieldType::Keyword("key".to_owned()),
parts: Vec::new(),
})
);
assert_eq!(
FieldName::parse("key.attr[0][string]"),
Ok(FieldName {
field_type: FieldType::Keyword("key".to_owned()),
parts: vec![
FieldNamePart::Attribute("attr".to_owned()),
FieldNamePart::Index(0),
FieldNamePart::StringIndex("string".to_owned())
],
})
);
assert_eq!(
FieldName::parse("key.."),
Err(FormatParseError::EmptyAttribute)
);
assert_eq!(
FieldName::parse("key[]"),
Err(FormatParseError::EmptyAttribute)
);
assert_eq!(
FieldName::parse("key["),
Err(FormatParseError::MissingRightBracket)
);
assert_eq!(
FieldName::parse("key[0]after"),
Err(FormatParseError::InvalidCharacterAfterRightBracket)
);
}
}

1
src/vendored/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod format;