Compare commits

...

56 Commits

Author SHA1 Message Date
Charlie Marsh
a710e35ebc Bump version to 0.0.170 2022-12-08 11:36:24 -05:00
Charlie Marsh
49df43bb78 Use single newlines in .pyi import sorting (#1142) 2022-12-08 11:34:41 -05:00
Charlie Marsh
e338d9acbe Remove 'consider' language from check messages (#1135) 2022-12-07 20:10:36 -05:00
Charlie Marsh
5c8655f479 Bump ruff_macros to 0.0.169 2022-12-07 19:10:16 -05:00
Charlie Marsh
60987888a2 Re-increase max iterations to 100 2022-12-07 19:10:03 -05:00
Charlie Marsh
a81581c781 Bump ruff_macros to 0.0.168 2022-12-07 19:08:18 -05:00
Charlie Marsh
3152dd7a8e Don't prompt users to --fix if they ran with --fix (#1133) 2022-12-07 19:07:51 -05:00
Charlie Marsh
528416f07a Rename I252 to TID252; add redirects for all renamed codes (#1129) 2022-12-07 15:12:22 -05:00
Charlie Marsh
4405a6a903 Bump version to 0.0.168 2022-12-07 13:18:40 -05:00
Charlie Marsh
35fa2a3c32 Convert more BTree usages to Fx (#1112) 2022-12-07 12:21:12 -05:00
Charlie Marsh
bb67fbb73a Implement unused argument detection (ARG) (#1126)
Detect unused arguments
2022-12-07 12:15:41 -05:00
Charlie Marsh
d698c6123e Bump version to 0.0.167 2022-12-07 10:37:31 -05:00
Charlie Marsh
9579faffa8 Avoid flagging bare exception issues when exception is re-raised (#1124) 2022-12-07 10:37:08 -05:00
Phillip Verheyden
9c6e8c7644 Auto-generate the rules table of contents (#1121) 2022-12-07 10:03:42 -05:00
Charlie Marsh
7abecd4f0e Implement B905 (#1122) 2022-12-07 10:01:24 -05:00
Phillip Verheyden
b8ff209af8 Encode prefixes in README headings not just in TOC (#1109) 2022-12-07 09:24:49 -05:00
Jeong YunWon
c5451cd8ad Reduce indents (#1116) 2022-12-07 09:20:33 -05:00
Jonathan Plasse
92b9ab3010 Add aiter() and anext() to BUILTINS (#1118) 2022-12-07 09:20:06 -05:00
Edgar R. M
f2ac8c4ec2 Add flake8-import-conventions to TOC in readme (#1114) 2022-12-06 21:27:04 -05:00
Charlie Marsh
80e2f0c92e Bump version to 0.0.166 2022-12-06 16:06:19 -05:00
Edgar R. M
ea550abd3c Implement flake8-import-conventions (#1098) 2022-12-06 16:01:17 -05:00
Charlie Marsh
5c26777e4c Avoid flagging ANN errors in @overload implementations (#1110) 2022-12-06 12:46:38 -05:00
Laurent Baillet
6eb6b6eede Update readme in order to match pylint prefixes (#1105) 2022-12-06 08:59:45 -05:00
Charlie Marsh
f1d3e3698a Bump version to 0.0.165 2022-12-06 00:03:30 -05:00
Charlie Marsh
080411bc89 Re-create ruff snapshots 2022-12-06 00:03:14 -05:00
Charlie Marsh
f2ad915224 Bump version to 0.0.164 2022-12-05 23:37:22 -05:00
Charlie Marsh
71543eeabc Rename rules mod to ruff (#1104) 2022-12-05 23:35:36 -05:00
Charlie Marsh
44025f1c92 Improve F841's Flake8 parity for unpacking assignments (#1103) 2022-12-05 23:34:40 -05:00
Charlie Marsh
971bf6d232 Run cargo fmt 2022-12-05 23:01:47 -05:00
Charlie Marsh
0acc47386a Use pyproject.toml parent as project root when explicitly provided (#1101) 2022-12-05 23:00:59 -05:00
Reiner Gerecke
982ac6b0ad Auto-generate options in README from field attributes (#1015) 2022-12-05 22:34:40 -05:00
Charlie Marsh
541440f7a8 Re-support F841 detection for single context managers (#1099) 2022-12-05 22:09:45 -05:00
Charlie Marsh
66dde46e03 Track nested imports without column number detection (#1097) 2022-12-05 21:16:44 -05:00
Charlie Marsh
1339e2a002 Bump version to 0.0.163 2022-12-05 20:45:24 -05:00
Charlie Marsh
38ad10f60d Treat nested classes and functions as "standard" siblings (#1095) 2022-12-05 20:45:12 -05:00
Charlie Marsh
88e78c5cde Add missing D415 fixture 2022-12-05 20:42:21 -05:00
Charlie Marsh
436aeed20a Implement autofix for D400 and D415 (#1094) 2022-12-05 20:24:56 -05:00
Charlie Marsh
b94169a8bb Don't autofix D210 by introducing a syntax error (#1093) 2022-12-05 19:13:22 -05:00
Charlie Marsh
995994be3e Bump version to 0.0.162 2022-12-05 19:07:44 -05:00
Charlie Marsh
f001305b2e Only autofix D205 by deleting blank lines (#1091) 2022-12-05 19:01:32 -05:00
Charlie Marsh
e88093541f Avoid wrapping import-star statements (#1089) 2022-12-05 18:39:16 -05:00
Charlie Marsh
da41a495f1 Remove extraneous plugin creation script 2022-12-05 18:31:19 -05:00
Charlie Marsh
55b7ec8f85 Ignore newline enforcement when imports break indentation boundaries (#1085) 2022-12-05 18:02:41 -05:00
Charlie Marsh
4b41ae3f53 Bump version to 0.0.161 2022-12-05 17:02:05 -05:00
Charlie Marsh
f944e1e1cf Add action comments to README.md (#1082) 2022-12-05 16:56:28 -05:00
Charlie Marsh
4fbc1082de Support isort: split directive (#1081) 2022-12-05 16:48:10 -05:00
Charlie Marsh
cf2e887e38 Tweak summary message to include total error counts (#1067) 2022-12-05 16:12:12 -05:00
Charlie Marsh
ee994e8c07 Import compatibility with isort newline-insertion behavior (#1078) 2022-12-05 16:07:07 -05:00
Charlie Marsh
c69c4fd655 Support isort: skip_file directive (#1075) 2022-12-05 15:02:01 -05:00
Charlie Marsh
e01e45ca35 Remove extraneous test file 2022-12-05 14:58:54 -05:00
Charlie Marsh
4be74785fe Support unterminated isort: off directives (#1074) 2022-12-05 14:54:47 -05:00
Charlie Marsh
40b7c64f7d Bump version to 0.0.160 2022-12-05 12:56:38 -05:00
Jonathan Plasse
a76c5d1226 Add allowed-confusable settings (#1059) 2022-12-05 12:53:55 -05:00
Charlie Marsh
5aeddeb825 Include pyproject.toml path in error message (#1068) 2022-12-05 12:04:50 -05:00
Charlie Marsh
5f8294aea4 Preserve star imports when re-formatting import blocks (#1066) 2022-12-05 11:48:38 -05:00
Charlie Marsh
e07d3f6313 Fix clippy 2022-12-05 11:47:42 -05:00
185 changed files with 6740 additions and 1886 deletions

View File

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

View File

@@ -105,6 +105,8 @@ You may also want to add the new configuration option to the `flake8-to-ruff` to
responsible for converting `flake8` configuration files to Ruff's TOML format. This logic
lives in `flake8_to_ruff/src/converter.rs`.
To update the documentation for supported configuration options, run `cargo dev generate-options`.
## Release process
As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub

17
Cargo.lock generated
View File

@@ -724,7 +724,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.159-dev.0"
version = "0.0.170-dev.0"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1821,7 +1821,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.159"
version = "0.0.170"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1855,6 +1855,7 @@ dependencies = [
"rayon",
"regex",
"ropey",
"ruff_macros",
"rustc-hash",
"rustpython-ast",
"rustpython-common",
@@ -1873,7 +1874,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.159"
version = "0.0.170"
dependencies = [
"anyhow",
"clap 4.0.29",
@@ -1889,6 +1890,16 @@ dependencies = [
"strum_macros",
]
[[package]]
name = "ruff_macros"
version = "0.0.170"
dependencies = [
"proc-macro2",
"quote",
"syn",
"textwrap",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.159"
version = "0.0.170"
edition = "2021"
rust-version = "1.65.0"
@@ -41,6 +41,7 @@ quick-junit = { version = "0.3.2" }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.170", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "28f9f65ccc625f00835d84bbb5fba274dce5aa89" }

50
LICENSE
View File

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

771
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +0,0 @@
set -euxo pipefail
NAME=$1
mkdir -p src/$1
mkdir -p resources/test/fixtures/$1
touch src/$1/mod.rs
sed -i "" "s/mod flake8_print;/mod flake8_print; mod flake8_return;/g" src/lib.rs
sed -i "" "s|// flake8-print|// flake8-return\n// flake8-print|g" src/checks.rs

View File

@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8_to_ruff"
version = "0.0.159"
version = "0.0.170"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.159"
version = "0.0.170"
dependencies = [
"anyhow",
"bincode",

View File

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

View File

@@ -243,6 +243,7 @@ mod tests {
fn it_converts_empty() -> Result<()> {
let actual = convert(&HashMap::from([]), None)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -269,6 +270,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -286,6 +288,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -312,6 +315,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -329,6 +333,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -355,6 +360,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -372,6 +378,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -398,6 +405,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -415,6 +423,7 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -446,6 +455,7 @@ mod tests {
avoid_escape: None,
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -466,6 +476,7 @@ mod tests {
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -527,6 +538,7 @@ mod tests {
flake8_bugbear: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,
@@ -544,6 +556,7 @@ mod tests {
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
dummy_variable_rgx: None,
exclude: None,
extend_exclude: None,
@@ -576,6 +589,7 @@ mod tests {
avoid_escape: None,
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
isort: None,
mccabe: None,
pep8_naming: None,

View File

@@ -0,0 +1,49 @@
from typing import overload
@overload
def foo(i: int) -> "int":
...
@overload
def foo(i: "str") -> "str":
...
def foo(i):
return i
@overload
def bar(i: int) -> "int":
...
@overload
def bar(i: "str") -> "str":
...
class X:
def bar(i):
return i
# TODO(charlie): This third case should raise an error (as in Mypy), because we have a
# statement between the interfaces and implementation.
@overload
def baz(i: int) -> "int":
...
@overload
def baz(i: "str") -> "str":
...
x = 1
def baz(i):
return i

View File

@@ -53,3 +53,11 @@ try:
raise e
except Exception:
pass
try:
pass
except Exception as e:
raise bad
except BaseException:
pass

View File

@@ -0,0 +1,10 @@
zip()
zip(range(3))
zip("a", "b")
zip("a", "b", *zip("c"))
zip(zip("a"), strict=False)
zip(zip("a", strict=True))
zip(range(3), strict=True)
zip("a", "b", strict=False)
zip("a", "b", "c", strict=True)

View File

@@ -0,0 +1,25 @@
import math # not checked
import altair # unconventional
import dask.array # unconventional
import dask.dataframe # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import dask.array as darray # unconventional
import dask.dataframe as ddf # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import dask.array as da # conventional
import dask.dataframe as dd # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,19 @@
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,19 @@
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as np # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as nmp # conventional
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,19 @@
import math # not checked
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # not checked
import pandas # unconventional
import seaborn # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # not checked
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # not checked
import pandas as pd # conventional
import seaborn as sns # conventional

View File

@@ -0,0 +1,137 @@
from abc import abstractmethod
from typing_extensions import override
###
# Unused arguments on functions.
###
def f(self, x):
print("Hello, world!")
def f(cls, x):
print("Hello, world!")
def f(self, x):
...
def f(cls, x):
...
###
# Unused arguments on lambdas.
###
lambda x: print("Hello, world!")
class X:
###
# Unused arguments.
###
def f(self, x):
print("Hello, world!")
def f(self, /, x):
print("Hello, world!")
def f(cls, x):
print("Hello, world!")
@classmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
def f(x):
print("Hello, world!")
###
# Unused arguments attached to empty functions (OK).
###
def f(self, x):
...
def f(self, /, x):
...
def f(cls, x):
...
@classmethod
def f(cls, x):
...
@staticmethod
def f(cls, x):
...
@staticmethod
def f(x):
...
###
# Unused functions attached to abstract methods (OK).
###
@abstractmethod
def f(self, x):
print("Hello, world!")
@abstractmethod
def f(self, /, x):
print("Hello, world!")
@abstractmethod
def f(cls, x):
print("Hello, world!")
@classmethod
@abstractmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
@abstractmethod
def f(cls, x):
print("Hello, world!")
@staticmethod
@abstractmethod
def f(x):
print("Hello, world!")
###
# Unused functions attached to overrides (OK).
###
@override
def f(self, x):
print("Hello, world!")
@override
def f(self, /, x):
print("Hello, world!")
@override
def f(cls, x):
print("Hello, world!")
@classmethod
@override
def f(cls, x):
print("Hello, world!")
@staticmethod
@override
def f(cls, x):
print("Hello, world!")
@staticmethod
@override
def f(x):
print("Hello, world!")

View File

@@ -0,0 +1,41 @@
import a
import b
x = 1
import os
import sys
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
y = 1
import os
import sys
"""Docstring"""
if True:
import os
def f():
pass
if True:
import os
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
if True:
x = 1
import collections
import typing
def f(): pass

View File

@@ -0,0 +1,41 @@
import a
import b
x = 1
import os
import sys
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
y = 1
import os
import sys
"""Docstring"""
if True:
import os
def f():
pass
if True:
import os
def f():
pass
if True:
x = 1
import collections
import typing
class X: pass
if True:
x = 1
import collections
import typing
def f(): pass

View File

@@ -0,0 +1 @@
from .subscription import * # type: ignore # some very long comment explaining why this needs a type ignore

View File

@@ -0,0 +1,6 @@
from some_other_module import some_class
from some_other_module import *
# Above
from some_module import some_class # Aside
# Above
from some_module import * # Aside

View File

@@ -0,0 +1,10 @@
# isort: skip_file
import e
import f
# isort: split
import a
import b
import c
import d

View File

@@ -0,0 +1,9 @@
import e
import f
# isort: split
import a
import b
import c
import d

View File

@@ -571,3 +571,11 @@ def multiline_trailing_and_leading_space():
"""
pass
@expect('D210: No whitespaces allowed surrounding docstring text')
@expect("D400: First line should end with a period (not '\"')")
@expect("D415: First line should end with a period, question mark, "
"or exclamation point (not '\"')")
def endswith_quote():
"""Whitespace at the end, but also a quote" """

View File

@@ -0,0 +1,73 @@
def f():
"Here's a line without a period"
...
def f():
"""Here's a line without a period"""
...
def f():
"""
Here's a line without a period,
but here's the next line
"""
...
def f():
"""Here's a line without a period"""
...
def f():
"""
Here's a line without a period,
but here's the next line"""
...
def f():
"""
Here's a line without a period,
but here's the next line with trailing space """
...
def f():
r"Here's a line without a period"
...
def f():
r"""Here's a line without a period"""
...
def f():
r"""
Here's a line without a period,
but here's the next line
"""
...
def f():
r"""Here's a line without a period"""
...
def f():
r"""
Here's a line without a period,
but here's the next line"""
...
def f():
r"""
Here's a line without a period,
but here's the next line with trailing space """
...

View File

@@ -65,3 +65,8 @@ def f7():
with connect() as (connection, cursor):
cursor.execute("SELECT * FROM users")
def f8():
with open("file") as f, open("") as ((a, b)):
print("hello")

View File

@@ -0,0 +1,24 @@
def f(tup):
x, y = tup # this does NOT trigger F841
def f():
x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
def f():
(x, y) = coords = 1, 2 # this does NOT trigger F841
if x > 1:
print(coords)
def f():
(x, y) = coords = 1, 2 # this triggers F841 on coords
def f():
coords = (x, y) = 1, 2 # this triggers F841 on coords
def f():
(a, b) = (x, y) = 1, 2 # this triggers F841 on everything

View File

@@ -1,4 +1,5 @@
[tool.ruff]
allowed-confusables = ["", "ρ", ""]
line-length = 88
extend-exclude = [
"excluded_file.py",
@@ -35,13 +36,14 @@ ignore-names = [
"longMessage",
"maxDiff",
]
classmethod-decorators = [
"classmethod",
"pydantic.validator",
]
staticmethod-decorators = [
"staticmethod",
]
classmethod-decorators = ["classmethod", "pydantic.validator"]
staticmethod-decorators = ["staticmethod"]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"
[tool.ruff.flake8-import-conventions.aliases]
pandas = "pd"
[tool.ruff.flake8-import-conventions.extend-aliases]
"dask.dataframe" = "dd"

View File

@@ -1,7 +0,0 @@
x = "𝐁ad string"
def f():
"""Here's a docstring with an unusual parenthesis: """
# And here's a comment with an unusual punctuation mark:
...

View File

@@ -1,7 +0,0 @@
x = "𝐁ad string"
def f():
"""Here's a docstring with an unusual parenthesis: """
# And here's a comment with an unusual punctuation mark:
...

View File

@@ -1,7 +1,14 @@
x = "𝐁ad string"
y = ""
def f():
"""Here's a docstring with an unusual parenthesis: """
# And here's a comment with an unusual punctuation mark:
...
def g():
"""Here's a docstring with a greek rho: ρ"""
# And here's a comment with a greek alpha:
...

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.159"
version = "0.0.170"
edition = "2021"
[dependencies]

View File

@@ -8,7 +8,7 @@ use anyhow::Result;
use clap::Parser;
use codegen::{Scope, Type, Variant};
use itertools::Itertools;
use ruff::checks::{CheckCode, REDIRECTS};
use ruff::checks::{CheckCode, CODE_REDIRECTS, PREFIX_REDIRECTS};
use strum::IntoEnumIterator;
const FILE: &str = "src/checks_gen.rs";
@@ -39,34 +39,26 @@ pub fn main(cli: &Cli) -> Result<()> {
}
}
// Add any aliases (e.g., "U001" to "UP001").
for (alias, check_code) in REDIRECTS.iter() {
// Compute the length of the prefix and suffix for both codes.
let code_str: String = check_code.as_ref().to_string();
let code_prefix_len = code_str
.chars()
.take_while(|char| char.is_alphabetic())
.count();
let code_suffix_len = code_str.len() - code_prefix_len;
let alias_prefix_len = alias
.chars()
.take_while(|char| char.is_alphabetic())
.count();
let alias_suffix_len = alias.len() - alias_prefix_len;
assert_eq!(code_suffix_len, alias_suffix_len);
for i in 0..=code_suffix_len {
let source = code_str[..code_prefix_len + i].to_string();
let destination = alias[..alias_prefix_len + i].to_string();
if source != destination {
prefix_to_codes.insert(
destination,
prefix_to_codes
.get(&source)
.unwrap_or_else(|| panic!("Unknown CheckCode: {source:?}"))
.clone(),
);
}
}
// Add any prefix aliases (e.g., "U" to "UP").
for (alias, source) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(&(*source).to_string())
.unwrap_or_else(|| panic!("Unknown CheckCode: {source:?}"))
.clone(),
);
}
// Add any check code aliases (e.g., "U001" to "UP001").
for (alias, check_code) in CODE_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(&check_code.as_ref().to_string())
.unwrap_or_else(|| panic!("Unknown CheckCode: {alias:?}"))
.clone(),
);
}
let mut scope = Scope::new();
@@ -76,6 +68,7 @@ pub fn main(cli: &Cli) -> Result<()> {
.new_enum("CheckCodePrefix")
.vis("pub")
.derive("EnumString")
.derive("AsRefStr")
.derive("Debug")
.derive("PartialEq")
.derive("Eq")
@@ -112,10 +105,10 @@ pub fn main(cli: &Cli) -> Result<()> {
.line("#[allow(clippy::match_same_arms)]")
.line("match self {");
for (prefix, codes) in &prefix_to_codes {
if let Some(target) = REDIRECTS.get(&prefix.as_str()) {
if let Some(target) = CODE_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ eprintln!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been renamed to \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
target.as_ref(),
@@ -124,6 +117,18 @@ pub fn main(cli: &Cli) -> Result<()> {
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else if let Some(target) = PREFIX_REDIRECTS.get(&prefix.as_str()) {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => {{ eprintln!(\"{{}}{{}} {{}}\", \
\"warning\".yellow().bold(), \":\".bold(), \"`{}` has been remapped to \
`{}`\".bold()); \n vec![{}] }}",
prefix,
target,
codes
.iter()
.map(|code| format!("CheckCode::{}", code.as_ref()))
.join(", ")
));
} else {
gen = gen.line(format!(
"CheckCodePrefix::{prefix} => vec![{}],",
@@ -170,9 +175,9 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push('\n');
output.push_str("use colored::Colorize;");
output.push('\n');
output.push_str("use serde::{{Serialize, Deserialize}};");
output.push_str("use serde::{Deserialize, Serialize};");
output.push('\n');
output.push_str("use strum_macros::EnumString;");
output.push_str("use strum_macros::{AsRefStr, EnumString};");
output.push('\n');
output.push('\n');
output.push_str("use crate::checks::CheckCode;");
@@ -186,7 +191,9 @@ pub fn main(cli: &Cli) -> Result<()> {
output.push_str("pub const CATEGORIES: &[CheckCodePrefix] = &[");
output.push('\n');
for prefix in prefix_to_codes.keys() {
if prefix.chars().all(char::is_alphabetic) {
if prefix.chars().all(char::is_alphabetic)
&& !PREFIX_REDIRECTS.contains_key(&prefix.as_str())
{
output.push_str(&format!("CheckCodePrefix::{prefix},"));
output.push('\n');
}

View File

@@ -0,0 +1,125 @@
//! Generate a Markdown-compatible listing of configuration options.
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated options sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated options sections. -->";
#[derive(Args)]
pub struct Cli {
/// Write the generated table to stdout (rather than to `README.md`).
#[arg(long)]
dry_run: bool,
}
fn emit_field(output: &mut String, field: &OptionField, group_name: Option<&str>) {
output.push_str(&format!("#### [`{0}`](#{0})\n", field.name));
output.push('\n');
output.push_str(field.doc);
output.push_str("\n\n");
output.push_str(&format!("**Default value**: `{}`\n", field.default));
output.push('\n');
output.push_str(&format!("**Type**: `{}`\n", field.value_type));
output.push('\n');
output.push_str(&format!(
"**Example usage**:\n\n```toml\n[tool.ruff{}]\n{}\n```\n",
if group_name.is_some() {
format!(".{}", group_name.unwrap())
} else {
String::new()
},
field.example
));
output.push('\n');
}
pub fn main(cli: &Cli) -> Result<()> {
let mut output = String::new();
// Generate all the top-level fields.
for field in Options::get_available_options()
.into_iter()
.filter_map(|entry| {
if let OptionEntry::Field(field) = entry {
Some(field)
} else {
None
}
})
.sorted_by_key(|field| field.name)
{
emit_field(&mut output, &field, None);
output.push_str("---\n\n");
}
// Generate all the sub-groups.
for group in Options::get_available_options()
.into_iter()
.filter_map(|entry| {
if let OptionEntry::Group(group) = entry {
Some(group)
} else {
None
}
})
.sorted_by_key(|group| group.name)
{
output.push_str(&format!("### `{}`\n", group.name));
output.push('\n');
for field in group
.fields
.iter()
.filter_map(|entry| {
if let OptionEntry::Field(field) = entry {
Some(field)
} else {
None
}
})
.sorted_by_key(|field| field.name)
{
emit_field(&mut output, field, Some(group.name));
output.push_str("---\n\n");
}
}
if cli.dry_run {
print!("{output}");
} else {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(BEGIN_PRAGMA)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + BEGIN_PRAGMA.len()];
// Extract the suffix.
let index = existing
.find(END_PRAGMA)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{prefix}\n\n")?;
write!(f, "{output}")?;
write!(f, "{suffix}")?;
}
Ok(())
}

View File

@@ -7,11 +7,15 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::checks::{CheckCategory, CheckCode};
use strum::IntoEnumIterator;
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
const TABLE_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const TABLE_END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
const TOC_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated table of contents. -->";
const TOC_END_PRAGMA: &str = "<!-- End auto-generated table of contents. -->";
#[derive(Args)]
pub struct Cli {
@@ -22,73 +26,91 @@ pub struct Cli {
pub fn main(cli: &Cli) -> Result<()> {
// Generate the table string.
let mut output = String::new();
let mut table_out = String::new();
let mut toc_out = String::new();
for check_category in CheckCategory::iter() {
output.push_str(&format!("### {}", check_category.title()));
output.push('\n');
output.push('\n');
let codes_csv: String = check_category.codes().iter().map(AsRef::as_ref).join(", ");
table_out.push_str(&format!("### {} ({codes_csv})", check_category.title()));
table_out.push('\n');
table_out.push('\n');
toc_out.push_str(&format!(
" 1. [{} ({})](#{}-{})\n",
check_category.title(),
codes_csv,
check_category.title().to_lowercase().replace(' ', "-"),
codes_csv.to_lowercase().replace(',', "-").replace(' ', "")
));
if let Some((url, platform)) = check_category.url() {
output.push_str(&format!(
table_out.push_str(&format!(
"For more, see [{}]({}) on {}.",
check_category.title(),
url,
platform
));
output.push('\n');
output.push('\n');
table_out.push('\n');
table_out.push('\n');
}
output.push_str("| Code | Name | Message | Fix |");
output.push('\n');
output.push_str("| ---- | ---- | ------- | --- |");
output.push('\n');
table_out.push_str("| Code | Name | Message | Fix |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | --- |");
table_out.push('\n');
for check_code in CheckCode::iter() {
if check_code.category() == check_category {
let check_kind = check_code.kind();
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
output.push_str(&format!(
table_out.push_str(&format!(
"| {} | {} | {} | {} |",
check_kind.code().as_ref(),
check_kind.as_ref(),
check_kind.summary().replace('|', r"\|"),
fix_token
));
output.push('\n');
table_out.push('\n');
}
}
output.push('\n');
table_out.push('\n');
}
if cli.dry_run {
print!("{output}");
print!("Table of Contents: {toc_out}\n Rules Tables: {table_out}");
} else {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(BEGIN_PRAGMA)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + BEGIN_PRAGMA.len()];
// Extract the suffix.
let index = existing
.find(END_PRAGMA)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{prefix}\n\n")?;
write!(f, "{output}")?;
write!(f, "{suffix}")?;
// Extra newline in the markdown numbered list looks weird
replace_readme_section(toc_out.trim_end(), TOC_BEGIN_PRAGMA, TOC_END_PRAGMA)?;
replace_readme_section(&table_out, TABLE_BEGIN_PRAGMA, TABLE_END_PRAGMA)?;
}
Ok(())
}
fn replace_readme_section(content: &str, begin_pragma: &str, end_pragma: &str) -> Result<()> {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(begin_pragma)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + begin_pragma.len()];
// Extract the suffix.
let index = existing
.find(end_pragma)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
writeln!(f, "{prefix}")?;
write!(f, "{content}")?;
write!(f, "{suffix}")?;
Ok(())
}

View File

@@ -12,6 +12,7 @@
)]
pub mod generate_check_code_prefix;
pub mod generate_options;
pub mod generate_rules_table;
pub mod generate_source_code;
pub mod print_ast;

View File

@@ -14,8 +14,8 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff_dev::{
generate_check_code_prefix, generate_rules_table, generate_source_code, print_ast, print_cst,
print_tokens,
generate_check_code_prefix, generate_options, generate_rules_table, generate_source_code,
print_ast, print_cst, print_tokens,
};
#[derive(Parser)]
@@ -32,6 +32,8 @@ enum Commands {
GenerateCheckCodePrefix(generate_check_code_prefix::Cli),
/// Generate a Markdown-compatible table of supported lint rules.
GenerateRulesTable(generate_rules_table::Cli),
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions(generate_options::Cli),
/// Run round-trip source code generation on a given Python file.
GenerateSourceCode(generate_source_code::Cli),
/// Print the AST for a given Python file.
@@ -48,6 +50,7 @@ fn main() -> Result<()> {
Commands::GenerateCheckCodePrefix(args) => generate_check_code_prefix::main(args)?,
Commands::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Commands::GenerateSourceCode(args) => generate_source_code::main(args)?,
Commands::GenerateOptions(args) => generate_options::main(args)?,
Commands::PrintAST(args) => print_ast::main(args)?,
Commands::PrintCST(args) => print_cst::main(args)?,
Commands::PrintTokens(args) => print_tokens::main(args)?,

13
ruff_macros/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "ruff_macros"
version = "0.0.170"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = { version = "1.0.47" }
quote = { version = "1.0.21" }
syn = { version = "1.0.103", features = ["derive", "parsing"] }
textwrap = { version = "0.16.0" }

177
ruff_macros/src/lib.rs Normal file
View File

@@ -0,0 +1,177 @@
#![allow(
clippy::collapsible_else_if,
clippy::collapsible_if,
clippy::implicit_hasher,
clippy::match_same_arms,
clippy::missing_errors_doc,
clippy::missing_panics_doc,
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::similar_names,
clippy::too_many_lines
)]
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream};
use syn::token::Comma;
use syn::{
parse_macro_input, AngleBracketedGenericArguments, Attribute, Data, DataStruct, DeriveInput,
Field, Fields, Lit, LitStr, Path, PathArguments, PathSegment, Token, Type, TypePath,
};
#[proc_macro_derive(ConfigurationOptions, attributes(option, option_group))]
pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
derive_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput { ident, data, .. } = input;
match data {
Data::Struct(DataStruct {
fields: Fields::Named(fields),
..
}) => {
let mut output = vec![];
for field in fields.named.iter() {
if let Some(attr) = field.attrs.iter().find(|a| a.path.is_ident("option")) {
output.push(handle_option(field, attr)?);
};
if field.attrs.iter().any(|a| a.path.is_ident("option_group")) {
output.push(handle_option_group(field)?);
};
}
Ok(quote! {
use crate::settings::options_base::{OptionEntry, OptionField, OptionGroup, ConfigurationOptions};
#[automatically_derived]
impl ConfigurationOptions for #ident {
fn get_available_options() -> Vec<OptionEntry> {
vec![#(#output),*]
}
}
})
}
_ => Err(syn::Error::new(
ident.span(),
"Can only derive ConfigurationOptions from structs with named fields.",
)),
}
}
/// For a field with type `Option<Foobar>` where `Foobar` itself is a struct
/// deriving `ConfigurationOptions`, create code that calls retrieves options
/// from that group: `Foobar::get_available_options()`
fn handle_option_group(field: &Field) -> syn::Result<proc_macro2::TokenStream> {
// unwrap is safe because we're only going over named fields
let ident = field.ident.as_ref().unwrap();
match &field.ty {
Type::Path(TypePath {
path: Path { segments, .. },
..
}) => match segments.first() {
Some(PathSegment {
ident: type_ident,
arguments:
PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }),
..
}) if type_ident == "Option" => {
let path = &args[0];
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => OptionEntry::Group(OptionGroup {
name: #kebab_name,
fields: #path::get_available_options(),
})
))
}
_ => Err(syn::Error::new(
ident.span(),
"Expected `Option<_>` as type.",
)),
},
_ => Err(syn::Error::new(ident.span(), "Expected type.")),
}
}
/// Parse an `#[option(doc="...", default="...", value_type="...",
/// example="...")]` attribute and return data in the form of an `OptionField`.
fn handle_option(field: &Field, attr: &Attribute) -> syn::Result<proc_macro2::TokenStream> {
// unwrap is safe because we're only going over named fields
let ident = field.ident.as_ref().unwrap();
let FieldAttributes {
doc,
default,
value_type,
example,
} = attr.parse_args::<FieldAttributes>()?;
let kebab_name = LitStr::new(&ident.to_string().replace('_', "-"), ident.span());
Ok(quote_spanned!(
ident.span() => OptionEntry::Field(OptionField {
name: #kebab_name,
doc: &#doc,
default: &#default,
value_type: &#value_type,
example: &#example,
})
))
}
#[derive(Debug)]
struct FieldAttributes {
doc: String,
default: String,
value_type: String,
example: String,
}
impl Parse for FieldAttributes {
fn parse(input: ParseStream) -> syn::Result<Self> {
let doc = _parse_key_value(input, "doc")?;
input.parse::<Comma>()?;
let default = _parse_key_value(input, "default")?;
input.parse::<Comma>()?;
let value_type = _parse_key_value(input, "value_type")?;
input.parse::<Comma>()?;
let example = _parse_key_value(input, "example")?;
if !input.is_empty() {
input.parse::<Comma>()?;
}
Ok(FieldAttributes {
doc: textwrap::dedent(&doc).trim_matches('\n').to_string(),
default,
value_type,
example: textwrap::dedent(&example).trim_matches('\n').to_string(),
})
}
}
fn _parse_key_value(input: ParseStream, name: &str) -> syn::Result<String> {
let ident: proc_macro2::Ident = input.parse()?;
if ident != name {
return Err(syn::Error::new(
ident.span(),
format!("Expected `{name}` name"),
));
}
input.parse::<Token![=]>()?;
let value: Lit = input.parse()?;
match &value {
Lit::Str(v) => Ok(v.value()),
_ => Err(syn::Error::new(value.span(), "Expected literal string")),
}
}

9
src/ast/cast.rs Normal file
View File

@@ -0,0 +1,9 @@
use rustpython_ast::{Expr, Stmt, StmtKind};
pub fn decorator_list(stmt: &Stmt) -> &Vec<Expr> {
match &stmt.node {
StmtKind::FunctionDef { decorator_list, .. }
| StmtKind::AsyncFunctionDef { decorator_list, .. } => decorator_list,
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
}

65
src/ast/function_type.rs Normal file
View File

@@ -0,0 +1,65 @@
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::Expr;
use crate::ast::helpers::{
collect_call_paths, dealias_call_path, match_call_path, to_module_and_member,
};
use crate::ast::types::{Scope, ScopeKind};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [(&str, &str); 2] = [("", "type"), ("abc", "ABCMeta")];
pub enum FunctionType {
Function,
Method,
ClassMethod,
StaticMethod,
}
/// Classify a function based on its scope, name, and decorators.
pub fn classify(
scope: &Scope,
name: &str,
decorator_list: &[Expr],
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
import_aliases: &FxHashMap<&str, &str>,
classmethod_decorators: &[String],
staticmethod_decorators: &[String],
) -> FunctionType {
let ScopeKind::Class(scope) = &scope.kind else {
return FunctionType::Function;
};
// Special-case class method, like `__new__`.
if CLASS_METHODS.contains(&name)
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
METACLASS_BASES
.iter()
.any(|(module, member)| match_call_path(&call_path, module, member, from_imports))
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
classmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
})
{
FunctionType::ClassMethod
} else if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
let call_path = dealias_call_path(collect_call_paths(expr), import_aliases);
staticmethod_decorators.iter().any(|decorator| {
let (module, member) = to_module_and_member(decorator);
match_call_path(&call_path, module, member, from_imports)
})
}) {
FunctionType::StaticMethod
} else {
// It's an instance method.
FunctionType::Method
}
}

View File

@@ -301,6 +301,16 @@ pub fn match_trailing_content(stmt: &Stmt, locator: &SourceCodeLocator) -> bool
false
}
/// Return the number of trailing empty lines following a statement.
pub fn count_trailing_lines(stmt: &Stmt, locator: &SourceCodeLocator) -> usize {
let suffix =
locator.slice_source_code_at(&Location::new(stmt.end_location.unwrap().row() + 1, 0));
suffix
.lines()
.take_while(|line| line.trim().is_empty())
.count()
}
#[cfg(test)]
mod tests {
use anyhow::Result;

View File

@@ -1,3 +1,5 @@
pub mod cast;
pub mod function_type;
pub mod helpers;
pub mod operations;
pub mod relocate;

View File

@@ -94,23 +94,132 @@ pub fn in_nested_block<'a>(parents: &mut impl Iterator<Item = &'a Stmt>) -> bool
})
}
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
let StmtKind::Assign { targets, value, .. } = &stmt.node else {
return false;
};
if !targets.iter().any(|child| {
matches!(
child.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}) {
return false;
/// Returns `true` if `parent` contains `child`.
fn contains(parent: &Expr, child: &Expr) -> bool {
match &parent.node {
ExprKind::BoolOp { values, .. } => values.iter().any(|parent| contains(parent, child)),
ExprKind::NamedExpr { target, value } => contains(target, child) || contains(value, child),
ExprKind::BinOp { left, right, .. } => contains(left, child) || contains(right, child),
ExprKind::UnaryOp { operand, .. } => contains(operand, child),
ExprKind::Lambda { body, .. } => contains(body, child),
ExprKind::IfExp { test, body, orelse } => {
contains(test, child) || contains(body, child) || contains(orelse, child)
}
ExprKind::Dict { keys, values } => keys
.iter()
.chain(values.iter())
.any(|parent| contains(parent, child)),
ExprKind::Set { elts } => elts.iter().any(|parent| contains(parent, child)),
ExprKind::ListComp { elt, .. } => contains(elt, child),
ExprKind::SetComp { elt, .. } => contains(elt, child),
ExprKind::DictComp { key, value, .. } => contains(key, child) || contains(value, child),
ExprKind::GeneratorExp { elt, .. } => contains(elt, child),
ExprKind::Await { value } => contains(value, child),
ExprKind::Yield { value } => value.as_ref().map_or(false, |value| contains(value, child)),
ExprKind::YieldFrom { value } => contains(value, child),
ExprKind::Compare {
left, comparators, ..
} => contains(left, child) || comparators.iter().any(|parent| contains(parent, child)),
ExprKind::Call {
func,
args,
keywords,
} => {
contains(func, child)
|| args.iter().any(|parent| contains(parent, child))
|| keywords
.iter()
.any(|keyword| contains(&keyword.node.value, child))
}
ExprKind::FormattedValue {
value, format_spec, ..
} => {
contains(value, child)
|| format_spec
.as_ref()
.map_or(false, |value| contains(value, child))
}
ExprKind::JoinedStr { values } => values.iter().any(|parent| contains(parent, child)),
ExprKind::Constant { .. } => false,
ExprKind::Attribute { value, .. } => contains(value, child),
ExprKind::Subscript { value, slice, .. } => {
contains(value, child) || contains(slice, child)
}
ExprKind::Starred { value, .. } => contains(value, child),
ExprKind::Name { .. } => parent == child,
ExprKind::List { elts, .. } => elts.iter().any(|parent| contains(parent, child)),
ExprKind::Tuple { elts, .. } => elts.iter().any(|parent| contains(parent, child)),
ExprKind::Slice { lower, upper, step } => {
lower.as_ref().map_or(false, |value| contains(value, child))
|| upper.as_ref().map_or(false, |value| contains(value, child))
|| step.as_ref().map_or(false, |value| contains(value, child))
}
}
}
/// Check if a node represents an unpacking assignment.
pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool {
match &parent.node {
StmtKind::With { items, .. } => items.iter().any(|item| {
if let Some(optional_vars) = &item.optional_vars {
if matches!(optional_vars.node, ExprKind::Tuple { .. }) {
if contains(optional_vars, child) {
return true;
}
}
}
false
}),
StmtKind::Assign { targets, value, .. } => {
// In `(a, b) = (1, 2)`, `(1, 2)` is the target, and it is a tuple.
let value_is_tuple = matches!(
&value.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
);
// In `(a, b) = coords = (1, 2)`, `(a, b)` and `coords` are the targets, and
// `(a, b`) is a tuple. (We use "tuple" as a placeholder for any
// unpackable expression.)
let targets_are_tuples = targets.iter().all(|item| {
matches!(
item.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
});
// If we're looking at `a` in `(a, b) = coords = (1, 2)`, then we should
// identify that the current expression is in a tuple.
let child_in_tuple = targets_are_tuples
|| targets.iter().any(|item| {
matches!(
item.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
) && contains(item, child)
});
// If our child is a tuple, and value is not, it's always an unpacking
// expression. Ex) `x, y = tup`
if child_in_tuple && !value_is_tuple {
return true;
}
// If our child isn't a tuple, but value is, it's never an unpacking expression.
// Ex) `coords = (1, 2)`
if !child_in_tuple && value_is_tuple {
return false;
}
// If our target and the value are both tuples, then it's an unpacking
// expression assuming there's at least one non-tuple child.
// Ex) Given `(x, y) = coords = 1, 2`, `(x, y)` is considered an unpacking
// expression. Ex) Given `(x, y) = (a, b) = 1, 2`, `(x, y)` isn't
// considered an unpacking expression.
if child_in_tuple && value_is_tuple {
return !targets_are_tuples;
}
false
}
_ => false,
}
!matches!(
&value.node,
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
)
}
pub type LocatedCmpop<U = ()> = Located<Cmpop, U>;
@@ -118,9 +227,9 @@ pub type LocatedCmpop<U = ()> = Located<Cmpop, U>;
/// Extract all `Cmpop` operators from a source code snippet, with appropriate
/// ranges.
///
/// RustPython doesn't include line and column information on `Cmpop` nodes.
/// (CPython doesn't either.) This method iterates over the token stream and
/// re-identifies `Cmpop` nodes, annotating them with valid arnges.
/// `RustPython` doesn't include line and column information on `Cmpop` nodes.
/// `CPython` doesn't either. This method iterates over the token stream and
/// re-identifies `Cmpop` nodes, annotating them with valid ranges.
pub fn locate_cmpops(contents: &str) -> Vec<LocatedCmpop> {
let mut tok_iter = lexer::make_tokenizer(contents)
.flatten()

View File

@@ -1,7 +1,7 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use rustc_hash::FxHashMap;
use rustpython_ast::{Expr, Keyword, Stmt};
use rustpython_ast::{Arguments, Expr, Keyword, Stmt};
use rustpython_parser::ast::{Located, Location};
fn id() -> usize {
@@ -30,35 +30,49 @@ impl Range {
}
}
#[derive(Clone, Debug)]
pub struct FunctionScope {
#[derive(Debug)]
pub struct FunctionDef<'a> {
pub name: &'a str,
pub args: &'a Arguments,
pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
// pub returns: Option<&'a Expr>,
// pub type_comment: Option<&'a str>,
// TODO(charlie): Create AsyncFunctionDef to mirror the AST.
pub async_: bool,
pub uses_locals: bool,
}
#[derive(Clone, Debug)]
pub struct ClassScope<'a> {
#[derive(Debug)]
pub struct ClassDef<'a> {
pub name: &'a str,
pub bases: &'a [Expr],
pub keywords: &'a [Keyword],
// pub body: &'a [Stmt],
pub decorator_list: &'a [Expr],
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Lambda<'a> {
pub args: &'a Arguments,
pub body: &'a Expr,
}
#[derive(Debug)]
pub enum ScopeKind<'a> {
Class(ClassScope<'a>),
Function(FunctionScope),
Class(ClassDef<'a>),
Function(FunctionDef<'a>),
Generator,
Module,
Arg,
Lambda,
Lambda(Lambda<'a>),
}
#[derive(Clone, Debug)]
#[derive(Debug)]
pub struct Scope<'a> {
pub id: usize,
pub kind: ScopeKind<'a>,
pub import_starred: bool,
pub uses_locals: bool,
pub values: FxHashMap<&'a str, Binding>,
}
@@ -68,6 +82,7 @@ impl<'a> Scope<'a> {
id: id(),
kind,
import_starred: false,
uses_locals: false,
values: FxHashMap::default(),
}
}

View File

@@ -92,7 +92,7 @@ fn apply_fixes<'a>(
}
// Add the remaining content.
let slice = locator.slice_source_code_at(last_pos);
let slice = locator.slice_source_code_at(&last_pos);
output.append(&slice);
(Cow::from(output.finish()), num_fixed)

View File

@@ -139,20 +139,21 @@ pub fn get(
return None;
};
if let Ok(encoded) = read_sync(cache_key(path, settings, autofix)) {
match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
}) => {
if FileTime::from_last_modification_time(metadata).unix_seconds() == mtime {
return Some(messages);
}
}
Err(e) => error!("Failed to deserialize encoded cache entry: {e:?}"),
let encoded = read_sync(cache_key(path, settings, autofix)).ok()?;
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
metadata: CacheMetadata { mtime },
messages,
}) => (mtime, messages),
Err(e) => {
error!("Failed to deserialize encoded cache entry: {e:?}");
return None;
}
};
if FileTime::from_last_modification_time(metadata).unix_seconds() != mtime {
return None;
}
None
Some(messages)
}
/// Set a value in the cache.

View File

@@ -5,7 +5,6 @@ use std::path::Path;
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,9 +17,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, Node, Range, Scope, ScopeKind,
Binding, BindingContext, BindingKind, ClassDef, FunctionDef, Lambda, Node, Range, Scope,
ScopeKind,
};
use crate::ast::visitor::{walk_excepthandler, walk_withitem, Visitor};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{helpers, operations, visitor};
use crate::checks::{Check, CheckCode, CheckKind, DeferralKeyword};
use crate::docstrings::definition::{Definition, DefinitionKind, Documentable};
@@ -36,8 +36,9 @@ use crate::visibility::{module_visibility, transition_scope, Modifier, Visibilit
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_debugger,
flake8_print, flake8_return, flake8_tidy_imports, mccabe, pep8_naming, pycodestyle, pydocstyle,
pyflakes, pygrep_hooks, pylint, pyupgrade,
flake8_import_conventions, flake8_print, flake8_return, flake8_tidy_imports,
flake8_unused_arguments, mccabe, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks,
pylint, pyupgrade,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -72,7 +73,7 @@ pub struct Checker<'a> {
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, DeferralContext)>,
deferred_assignments: Vec<usize>,
deferred_assignments: Vec<(usize, DeferralContext)>,
// Internal, derivative state.
visible_scope: VisibleScope,
in_f_string: Option<Range>,
@@ -82,7 +83,6 @@ pub struct Checker<'a> {
in_deferred_type_definition: bool,
in_literal: bool,
in_subscript: bool,
in_withitem: bool,
seen_import_boundary: bool,
futures_allowed: bool,
annotations_future_enabled: bool,
@@ -130,7 +130,6 @@ impl<'a> Checker<'a> {
in_deferred_type_definition: false,
in_literal: false,
in_subscript: false,
in_withitem: false,
seen_import_boundary: false,
futures_allowed: true,
annotations_future_enabled: false,
@@ -145,15 +144,16 @@ impl<'a> Checker<'a> {
// If we're in an f-string, override the location. RustPython doesn't produce
// reliable locations for expressions within f-strings, so we use the
// span of the f-string itself as a best-effort default.
if let Some(range) = self.in_f_string {
self.checks.push(Check {
let check = if let Some(range) = self.in_f_string {
Check {
location: range.location,
end_location: range.end_location,
..check
});
}
} else {
self.checks.push(check);
}
check
};
self.checks.push(check);
}
/// Add multiple `Check` items to the `Checker`.
@@ -582,7 +582,7 @@ where
for expr in decorator_list {
self.visit_expr(expr);
}
self.push_scope(Scope::new(ScopeKind::Class(ClassScope {
self.push_scope(Scope::new(ScopeKind::Class(ClassDef {
name,
bases,
keywords,
@@ -673,7 +673,7 @@ where
pylint::plugins::useless_import_alias(self, alias);
}
if self.settings.enabled.contains(&CheckCode::PLR0402) {
pylint::plugins::consider_using_from_import(self, alias);
pylint::plugins::use_from_import(self, alias);
}
if let Some(asname) = &alias.node.asname {
@@ -730,6 +730,19 @@ where
}
}
}
if self.settings.enabled.contains(&CheckCode::ICN001) {
if let Some(check) =
flake8_import_conventions::checks::check_conventional_import(
stmt,
&alias.node.name,
alias.node.asname.as_deref(),
&self.settings.flake8_import_conventions.aliases,
)
{
self.add_check(check);
}
}
}
}
StmtKind::ImportFrom {
@@ -891,7 +904,7 @@ where
);
}
if self.settings.enabled.contains(&CheckCode::I252) {
if self.settings.enabled.contains(&CheckCode::TID252) {
if let Some(check) = flake8_tidy_imports::checks::banned_relative_import(
stmt,
level.as_ref(),
@@ -1066,9 +1079,6 @@ where
if self.settings.enabled.contains(&CheckCode::B013) {
flake8_bugbear::plugins::redundant_tuple_in_exception_handler(self, handlers);
}
if self.settings.enabled.contains(&CheckCode::BLE001) {
flake8_blind_except::plugins::blind_except(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
@@ -1398,20 +1408,18 @@ where
}
Ok(summary) => {
if self.settings.enabled.contains(&CheckCode::F522) {
if let Some(check) =
pyflakes::checks::string_dot_format_extra_named_arguments(
&summary, keywords, location,
)
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,
)
if let Some(check) = pyflakes::checks::string_dot_format_extra_positional_arguments(
&summary, args, location,
)
{
self.add_check(check);
}
@@ -1447,16 +1455,15 @@ where
if self.settings.enabled.contains(&CheckCode::UP005) {
pyupgrade::plugins::deprecated_unittest_alias(self, func);
}
if self.settings.enabled.contains(&CheckCode::UP012) {
pyupgrade::plugins::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
// flake8-super
if self.settings.enabled.contains(&CheckCode::UP008) {
pyupgrade::plugins::super_call_with_parameters(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::UP012) {
pyupgrade::plugins::unnecessary_encode_utf8(self, expr, func, args, keywords);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
|| self.settings.enabled.contains(&CheckCode::T203)
@@ -1464,6 +1471,7 @@ where
flake8_print::plugins::print_call(self, expr, func);
}
// flake8-bugbear
if self.settings.enabled.contains(&CheckCode::B004) {
flake8_bugbear::plugins::unreliable_callable_check(self, expr, func, args);
}
@@ -1478,7 +1486,7 @@ where
.scope_stack
.iter()
.rev()
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda))
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda(..)))
{
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
@@ -1491,6 +1499,15 @@ where
self, args, keywords,
);
}
if self.settings.enabled.contains(&CheckCode::B905)
&& self.settings.target_version >= PythonVersion::Py310
{
flake8_bugbear::plugins::zip_without_explicit_strict(
self, expr, func, keywords,
);
}
// flake8-bandit
if self.settings.enabled.contains(&CheckCode::S102) {
if let Some(check) = flake8_bandit::plugins::exec_used(expr, func) {
self.add_check(check);
@@ -1730,9 +1747,7 @@ where
if id == "locals" && matches!(ctx, ExprContext::Load) {
let scope = &mut self.scopes
[*(self.scope_stack.last().expect("No current scope found"))];
if let ScopeKind::Function(inner) = &mut scope.kind {
inner.uses_locals = true;
}
scope.uses_locals = true;
}
}
@@ -1758,7 +1773,7 @@ where
pylint::plugins::unnecessary_direct_lambda_call(self, expr, func);
}
if self.settings.enabled.contains(&CheckCode::PLR1722) {
pylint::plugins::consider_using_sys_exit(self, func);
pylint::plugins::use_sys_exit(self, func);
}
}
ExprKind::Dict { keys, .. } => {
@@ -2053,7 +2068,7 @@ where
}
}
}
ExprKind::Lambda { args, .. } => {
ExprKind::Lambda { args, body, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.
for arg in &args.posonlyargs {
if let Some(expr) = &arg.node.annotation {
@@ -2086,7 +2101,7 @@ where
for expr in &args.defaults {
self.visit_expr(expr);
}
self.push_scope(Scope::new(ScopeKind::Lambda));
self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
}
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
if self.settings.enabled.contains(&CheckCode::C416) {
@@ -2114,7 +2129,7 @@ where
}
ExprKind::BoolOp { op, values } => {
if self.settings.enabled.contains(&CheckCode::PLR1701) {
pylint::plugins::consider_merging_isinstance(self, expr, op, values);
pylint::plugins::merge_isinstance(self, expr, op, values);
}
}
_ => {}
@@ -2327,16 +2342,25 @@ where
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.enabled.contains(&CheckCode::E722) && type_.is_none() {
self.add_check(Check::new(
CheckKind::DoNotUseBareExcept,
if self.settings.enabled.contains(&CheckCode::E722) {
if let Some(check) = pycodestyle::checks::do_not_use_bare_except(
type_.as_deref(),
body,
Range::from_located(excepthandler),
));
) {
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::B904) {
{
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
}
flake8_bugbear::plugins::raise_without_from_inside_except(self, body);
}
if self.settings.enabled.contains(&CheckCode::BLE001) {
flake8_blind_except::plugins::blind_except(
self,
type_.as_deref(),
name.as_ref().map(String::as_str),
body,
);
}
match name {
Some(name) => {
@@ -2485,13 +2509,6 @@ 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 {
@@ -2798,7 +2815,7 @@ impl<'a> Checker<'a> {
return;
}
if self.in_withitem || operations::is_unpacking_assignment(parent) {
if operations::is_unpacking_assignment(parent, expr) {
self.add_binding(
id,
Binding {
@@ -2939,27 +2956,44 @@ impl<'a> Checker<'a> {
fn check_deferred_functions(&mut self) {
while let Some((stmt, (scopes, parents), visibility)) = self.deferred_functions.pop() {
self.scope_stack = scopes;
self.parent_stack = parents;
self.scope_stack = scopes.clone();
self.parent_stack = parents.clone();
self.visible_scope = visibility;
self.push_scope(Scope::new(ScopeKind::Function(FunctionScope {
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
uses_locals: false,
})));
match &stmt.node {
StmtKind::FunctionDef { body, args, .. }
| StmtKind::AsyncFunctionDef { body, args, .. } => {
StmtKind::FunctionDef {
name,
body,
args,
decorator_list,
..
}
| StmtKind::AsyncFunctionDef {
name,
body,
args,
decorator_list,
..
} => {
self.push_scope(Scope::new(ScopeKind::Function(FunctionDef {
name,
body,
args,
decorator_list,
async_: matches!(stmt.node, StmtKind::AsyncFunctionDef { .. }),
})));
self.visit_arguments(args);
for stmt in body {
self.visit_stmt(stmt);
}
}
_ => {}
_ => unreachable!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
}
self.deferred_assignments
.push(*self.scope_stack.last().expect("No current scope found"));
self.deferred_assignments.push((
*self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope();
}
@@ -2967,25 +3001,29 @@ impl<'a> Checker<'a> {
fn check_deferred_lambdas(&mut self) {
while let Some((expr, (scopes, parents))) = self.deferred_lambdas.pop() {
self.scope_stack = scopes;
self.parent_stack = parents;
self.push_scope(Scope::new(ScopeKind::Lambda));
self.scope_stack = scopes.clone();
self.parent_stack = parents.clone();
if let ExprKind::Lambda { args, body } = &expr.node {
self.push_scope(Scope::new(ScopeKind::Lambda(Lambda { args, body })));
self.visit_arguments(args);
self.visit_expr(body);
} else {
unreachable!("Expected ExprKind::Lambda");
}
self.deferred_assignments
.push(*self.scope_stack.last().expect("No current scope found"));
self.deferred_assignments.push((
*self.scope_stack.last().expect("No current scope found"),
(scopes, parents),
));
self.pop_scope();
}
}
fn check_deferred_assignments(&mut self) {
if self.settings.enabled.contains(&CheckCode::F841) {
while let Some(index) = self.deferred_assignments.pop() {
while let Some((index, (scopes, _parents))) = self.deferred_assignments.pop() {
if self.settings.enabled.contains(&CheckCode::F841) {
self.add_checks(
pyflakes::checks::unused_variables(
&self.scopes[index],
@@ -2994,6 +3032,23 @@ impl<'a> Checker<'a> {
.into_iter(),
);
}
if self.settings.enabled.contains(&CheckCode::ARG001)
|| self.settings.enabled.contains(&CheckCode::ARG002)
|| self.settings.enabled.contains(&CheckCode::ARG003)
|| self.settings.enabled.contains(&CheckCode::ARG004)
|| self.settings.enabled.contains(&CheckCode::ARG005)
{
self.add_checks(
flake8_unused_arguments::plugins::unused_arguments(
self,
&self.scopes[*scopes
.last()
.expect("Expected parent scope above function scope")],
&self.scopes[index],
)
.into_iter(),
);
}
}
}
@@ -3073,7 +3128,7 @@ impl<'a> Checker<'a> {
for (name, binding) in &scope.values {
let (BindingKind::Importation(_, full_name, context)
| BindingKind::SubmoduleImportation(_, full_name, context)
| BindingKind::FromImportation(_, full_name, context)) = &binding.kind else { continue };
| BindingKind::FromImportation(_, full_name, context)) = &binding.kind else { continue; };
// Skip used exports from `__all__`
if binding.used.is_some()
@@ -3142,6 +3197,8 @@ impl<'a> Checker<'a> {
}
fn check_definitions(&mut self) {
let mut overloaded_name: Option<String> = None;
self.definitions.reverse();
while let Some((definition, visibility)) = self.definitions.pop() {
// flake8-annotations
if self.settings.enabled.contains(&CheckCode::ANN001)
@@ -3156,7 +3213,21 @@ impl<'a> Checker<'a> {
|| self.settings.enabled.contains(&CheckCode::ANN206)
|| self.settings.enabled.contains(&CheckCode::ANN401)
{
flake8_annotations::plugins::definition(self, &definition, &visibility);
// TODO(charlie): This should be even stricter, in that an overload
// implementation should come immediately after the overloaded
// interfaces, without any AST nodes in between. Right now, we
// only error when traversing definition boundaries (functions,
// classes, etc.).
if !overloaded_name.map_or(false, |overloaded_name| {
flake8_annotations::helpers::is_overload_impl(
self,
&definition,
&overloaded_name,
)
}) {
flake8_annotations::plugins::definition(self, &definition, &visibility);
}
overloaded_name = flake8_annotations::helpers::overloaded_name(self, &definition);
}
// pydocstyle

View File

@@ -1,10 +1,12 @@
//! Lint rules based on import analysis.
use nohash_hasher::IntSet;
use std::path::Path;
use rustpython_parser::ast::Suite;
use crate::ast::visitor::Visitor;
use crate::checks::Check;
use crate::directives::IsortDirectives;
use crate::isort;
use crate::isort::track::ImportTracker;
use crate::settings::Settings;
@@ -18,7 +20,7 @@ fn check_import_blocks(
) -> Vec<Check> {
let mut checks = vec![];
for block in tracker.into_iter() {
if !block.is_empty() {
if !block.imports.is_empty() {
if let Some(check) = isort::plugins::check_imports(&block, locator, settings, autofix) {
checks.push(check);
}
@@ -30,11 +32,12 @@ fn check_import_blocks(
pub fn check_imports(
python_ast: &Suite,
locator: &SourceCodeLocator,
exclusions: &IntSet<usize>,
directives: &IsortDirectives,
settings: &Settings,
autofix: bool,
path: &Path,
) -> Vec<Check> {
let mut tracker = ImportTracker::new(exclusions);
let mut tracker = ImportTracker::new(directives, path);
for stmt in python_ast {
tracker.visit_stmt(stmt);
}

View File

@@ -7,7 +7,7 @@ use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks::{Check, CheckCode, CheckKind, REDIRECTS};
use crate::checks::{Check, CheckCode, CheckKind, CODE_REDIRECTS};
use crate::noqa;
use crate::noqa::{is_file_exempt, Directive};
use crate::settings::Settings;
@@ -20,19 +20,18 @@ static URL_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://\S+$").unwra
/// Whether the given line is too long and should be reported.
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
if length > limit {
let mut chunks = line.split_whitespace();
if let (Some(first), Some(_)) = (chunks.next(), chunks.next()) {
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
!(first == "#" && chunks.last().map_or(true, |c| URL_REGEX.is_match(c)))
} else {
// Single word / no printable chars - no way to make the line shorter
false
}
} else {
false
if length <= limit {
return false;
}
let mut chunks = line.split_whitespace();
let (Some(first), Some(_)) = (chunks.next(), chunks.next()) else {
// Single word / no printable chars - no way to make the line shorter
return false;
};
// Do not enforce the line length for commented lines that end with a URL
// or contain only a single word.
!(first == "#" && chunks.last().map_or(true, |c| URL_REGEX.is_match(c)))
}
pub fn check_lines(
@@ -210,7 +209,7 @@ pub fn check_lines(
let mut invalid_codes = vec![];
let mut valid_codes = vec![];
for code in codes {
let code = REDIRECTS.get(code).map_or(code, AsRef::as_ref);
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code.to_string());
} else {

View File

@@ -4,9 +4,9 @@ use rustpython_parser::lexer::{LexResult, Tok};
use crate::checks::{Check, CheckCode};
use crate::lex::docstring_detection::StateMachine;
use crate::rules::checks::Context;
use crate::ruff::checks::Context;
use crate::source_code_locator::SourceCodeLocator;
use crate::{eradicate, flake8_quotes, pycodestyle, rules, Settings};
use crate::{eradicate, flake8_quotes, pycodestyle, ruff, Settings};
pub fn check_tokens(
locator: &SourceCodeLocator,
@@ -37,7 +37,7 @@ pub fn check_tokens(
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment) {
checks.extend(rules::checks::ambiguous_unicode_character(
checks.extend(ruff::checks::ambiguous_unicode_character(
locator,
start,
end,

View File

@@ -10,6 +10,7 @@ use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::checks_gen::CheckCodePrefix;
use crate::flake8_debugger::types::DebuggerUsingType;
use crate::flake8_quotes::settings::Quote;
use crate::flake8_tidy_imports::settings::Strictness;
@@ -134,6 +135,7 @@ pub enum CheckCode {
B026,
B027,
B904,
B905,
// flake8-blind-except
BLE001,
// flake8-comprehensions
@@ -158,7 +160,7 @@ pub enum CheckCode {
// mccabe
C901,
// flake8-tidy-imports
I252,
TID252,
// flake8-return
RET501,
RET502,
@@ -290,6 +292,14 @@ pub enum CheckCode {
FBT001,
FBT002,
FBT003,
// flake8-unused-arguments
ARG001,
ARG002,
ARG003,
ARG004,
ARG005,
// flake8-import-conventions
ICN001,
// Ruff
RUF001,
RUF002,
@@ -317,10 +327,12 @@ pub enum CheckCategory {
Flake8Builtins,
Flake8Comprehensions,
Flake8Debugger,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
Flake8Return,
Flake8TidyImports,
Flake8UnusedArguments,
Eradicate,
PygrepHooks,
Pylint,
@@ -354,10 +366,12 @@ impl CheckCategory {
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Debugger => "flake8-debugger",
CheckCategory::Flake8ImportConventions => "flake8-import-conventions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Return => "flake8-return",
CheckCategory::Flake8TidyImports => "flake8-tidy-imports",
CheckCategory::Flake8UnusedArguments => "flake8-unused-arguments",
CheckCategory::Isort => "isort",
CheckCategory::McCabe => "mccabe",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -371,6 +385,42 @@ impl CheckCategory {
}
}
pub fn codes(&self) -> Vec<CheckCodePrefix> {
match self {
CheckCategory::Eradicate => vec![CheckCodePrefix::ERA],
CheckCategory::Flake82020 => vec![CheckCodePrefix::YTT],
CheckCategory::Flake8Annotations => vec![CheckCodePrefix::ANN],
CheckCategory::Flake8Bandit => vec![CheckCodePrefix::S],
CheckCategory::Flake8BlindExcept => vec![CheckCodePrefix::BLE],
CheckCategory::Flake8BooleanTrap => vec![CheckCodePrefix::FBT],
CheckCategory::Flake8Bugbear => vec![CheckCodePrefix::B],
CheckCategory::Flake8Builtins => vec![CheckCodePrefix::A],
CheckCategory::Flake8Comprehensions => vec![CheckCodePrefix::C4],
CheckCategory::Flake8Debugger => vec![CheckCodePrefix::T10],
CheckCategory::Flake8Print => vec![CheckCodePrefix::T20],
CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q],
CheckCategory::Flake8Return => vec![CheckCodePrefix::RET],
CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::TID],
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Isort => vec![CheckCodePrefix::I],
CheckCategory::McCabe => vec![CheckCodePrefix::C90],
CheckCategory::PEP8Naming => vec![CheckCodePrefix::N],
CheckCategory::Pycodestyle => vec![CheckCodePrefix::E, CheckCodePrefix::W],
CheckCategory::Pydocstyle => vec![CheckCodePrefix::D],
CheckCategory::Pyflakes => vec![CheckCodePrefix::F],
CheckCategory::PygrepHooks => vec![CheckCodePrefix::PGH],
CheckCategory::Pylint => vec![
CheckCodePrefix::PLC,
CheckCodePrefix::PLE,
CheckCodePrefix::PLR,
CheckCodePrefix::PLW,
],
CheckCategory::Pyupgrade => vec![CheckCodePrefix::UP],
CheckCategory::Flake8ImportConventions => vec![CheckCodePrefix::ICN],
CheckCategory::Ruff => vec![CheckCodePrefix::RUF],
}
}
pub fn url(&self) -> Option<(&'static str, &'static Platform)> {
match self {
CheckCategory::Eradicate => {
@@ -412,6 +462,7 @@ impl CheckCategory {
"https://pypi.org/project/flake8-debugger/4.1.2/",
&Platform::PyPI,
)),
CheckCategory::Flake8ImportConventions => None,
CheckCategory::Flake8Print => Some((
"https://pypi.org/project/flake8-print/5.0.0/",
&Platform::PyPI,
@@ -428,6 +479,10 @@ impl CheckCategory {
"https://pypi.org/project/flake8-tidy-imports/4.8.0/",
&Platform::PyPI,
)),
CheckCategory::Flake8UnusedArguments => Some((
"https://pypi.org/project/flake8-unused-arguments/0.0.12/",
&Platform::PyPI,
)),
CheckCategory::Isort => {
Some(("https://pypi.org/project/isort/5.10.1/", &Platform::PyPI))
}
@@ -583,13 +638,13 @@ pub enum CheckKind {
ConsiderUsingFromImport(String, String),
AwaitOutsideAsync,
UselessElseOnLoop,
ConsiderUsingSysExit,
UseSysExit(String),
// flake8-builtins
BuiltinVariableShadowing(String),
BuiltinArgumentShadowing(String),
BuiltinAttributeShadowing(String),
// flake8-blind-except
BlindExcept,
BlindExcept(String),
// flake8-bugbear
AbstractBaseClassWithoutAbstractMethod(String),
AssignmentToOsEnviron,
@@ -618,6 +673,7 @@ pub enum CheckKind {
UselessComparison,
UselessContextlibSuppress,
UselessExpression,
ZipWithoutExplicitStrict,
// flake8-comprehensions
UnnecessaryGeneratorList,
UnnecessaryGeneratorSet,
@@ -774,6 +830,14 @@ pub enum CheckKind {
BooleanPositionalValueInFunctionCall,
// pygrep-hooks
NoEval,
// flake8-unused-arguments
UnusedFunctionArgument(String),
UnusedMethodArgument(String),
UnusedClassMethodArgument(String),
UnusedStaticMethodArgument(String),
UnusedLambdaArgument(String),
// flake8-import-conventions
ImportAliasIsNotConventional(String, String),
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
@@ -886,7 +950,7 @@ impl CheckCode {
CheckCode::PLR1701 => {
CheckKind::ConsiderMergingIsinstance("...".to_string(), vec!["...".to_string()])
}
CheckCode::PLR1722 => CheckKind::ConsiderUsingSysExit,
CheckCode::PLR1722 => CheckKind::UseSysExit("exit".to_string()),
CheckCode::PLW0120 => CheckKind::UselessElseOnLoop,
// flake8-builtins
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
@@ -924,6 +988,7 @@ impl CheckCode {
CheckCode::B026 => CheckKind::StarArgUnpackingAfterKeywordArg,
CheckCode::B027 => CheckKind::EmptyMethodWithoutAbstractDecorator("...".to_string()),
CheckCode::B904 => CheckKind::RaiseWithoutFromInsideExcept,
CheckCode::B905 => CheckKind::ZipWithoutExplicitStrict,
// flake8-comprehensions
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
@@ -957,7 +1022,7 @@ impl CheckCode {
// flake8-debugger
CheckCode::T100 => CheckKind::Debugger(DebuggerUsingType::Import("...".to_string())),
// flake8-tidy-imports
CheckCode::I252 => CheckKind::BannedRelativeImport(Strictness::All),
CheckCode::TID252 => CheckKind::BannedRelativeImport(Strictness::All),
// flake8-return
CheckCode::RET501 => CheckKind::UnnecessaryReturnNone,
CheckCode::RET502 => CheckKind::ImplicitReturnValue,
@@ -999,7 +1064,7 @@ impl CheckCode {
CheckCode::YTT302 => CheckKind::SysVersionCmpStr10,
CheckCode::YTT303 => CheckKind::SysVersionSlice1Referenced,
// flake8-blind-except
CheckCode::BLE001 => CheckKind::BlindExcept,
CheckCode::BLE001 => CheckKind::BlindExcept("Exception".to_string()),
// pyupgrade
CheckCode::UP001 => CheckKind::UselessMetaclassType,
CheckCode::UP003 => CheckKind::TypeOfPrimitive(Primitive::Str),
@@ -1113,6 +1178,16 @@ impl CheckCode {
CheckCode::FBT003 => CheckKind::BooleanPositionalValueInFunctionCall,
// pygrep-hooks
CheckCode::PGH001 => CheckKind::NoEval,
// flake8-unused-arguments
CheckCode::ARG001 => CheckKind::UnusedFunctionArgument("...".to_string()),
CheckCode::ARG002 => CheckKind::UnusedMethodArgument("...".to_string()),
CheckCode::ARG003 => CheckKind::UnusedClassMethodArgument("...".to_string()),
CheckCode::ARG004 => CheckKind::UnusedStaticMethodArgument("...".to_string()),
CheckCode::ARG005 => CheckKind::UnusedLambdaArgument("...".to_string()),
// flake8-import-conventions
CheckCode::ICN001 => {
CheckKind::ImportAliasIsNotConventional("...".to_string(), "...".to_string())
}
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -1138,6 +1213,11 @@ impl CheckCode {
CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::ANN401 => CheckCategory::Flake8Annotations,
CheckCode::ARG001 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG002 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG003 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG004 => CheckCategory::Flake8UnusedArguments,
CheckCode::ARG005 => CheckCategory::Flake8UnusedArguments,
CheckCode::B002 => CheckCategory::Flake8Bugbear,
CheckCode::B003 => CheckCategory::Flake8Bugbear,
CheckCode::B004 => CheckCategory::Flake8Bugbear,
@@ -1165,6 +1245,7 @@ impl CheckCode {
CheckCode::B026 => CheckCategory::Flake8Bugbear,
CheckCode::B027 => CheckCategory::Flake8Bugbear,
CheckCode::B904 => CheckCategory::Flake8Bugbear,
CheckCode::B905 => CheckCategory::Flake8Bugbear,
CheckCode::BLE001 => CheckCategory::Flake8BlindExcept,
CheckCode::C400 => CheckCategory::Flake8Comprehensions,
CheckCode::C401 => CheckCategory::Flake8Comprehensions,
@@ -1288,7 +1369,8 @@ impl CheckCode {
CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap,
CheckCode::I001 => CheckCategory::Isort,
CheckCode::I252 => CheckCategory::Flake8TidyImports,
CheckCode::TID252 => CheckCategory::Flake8TidyImports,
CheckCode::ICN001 => CheckCategory::Flake8ImportConventions,
CheckCode::N801 => CheckCategory::PEP8Naming,
CheckCode::N802 => CheckCategory::PEP8Naming,
CheckCode::N803 => CheckCategory::PEP8Naming,
@@ -1441,7 +1523,7 @@ impl CheckKind {
CheckKind::ConsiderMergingIsinstance(..) => &CheckCode::PLR1701,
CheckKind::PropertyWithParameters => &CheckCode::PLR0206,
CheckKind::ConsiderUsingFromImport(..) => &CheckCode::PLR0402,
CheckKind::ConsiderUsingSysExit => &CheckCode::PLR1722,
CheckKind::UseSysExit(_) => &CheckCode::PLR1722,
CheckKind::UselessElseOnLoop => &CheckCode::PLW0120,
// flake8-builtins
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
@@ -1465,6 +1547,7 @@ impl CheckKind {
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::NoAssertRaisesException => &CheckCode::B017,
CheckKind::RaiseWithoutFromInsideExcept => &CheckCode::B904,
CheckKind::ZipWithoutExplicitStrict => &CheckCode::B905,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::StarArgUnpackingAfterKeywordArg => &CheckCode::B026,
@@ -1476,7 +1559,7 @@ impl CheckKind {
CheckKind::UselessContextlibSuppress => &CheckCode::B022,
CheckKind::UselessExpression => &CheckCode::B018,
// flake8-blind-except
CheckKind::BlindExcept => &CheckCode::BLE001,
CheckKind::BlindExcept(_) => &CheckCode::BLE001,
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
@@ -1497,7 +1580,7 @@ impl CheckKind {
// flake8-debugger
CheckKind::Debugger(_) => &CheckCode::T100,
// flake8-tidy-imports
CheckKind::BannedRelativeImport(_) => &CheckCode::I252,
CheckKind::BannedRelativeImport(_) => &CheckCode::TID252,
// flake8-return
CheckKind::UnnecessaryReturnNone => &CheckCode::RET501,
CheckKind::ImplicitReturnValue => &CheckCode::RET502,
@@ -1633,6 +1716,14 @@ impl CheckKind {
CheckKind::BooleanPositionalValueInFunctionCall => &CheckCode::FBT003,
// pygrep-hooks
CheckKind::NoEval => &CheckCode::PGH001,
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(..) => &CheckCode::ARG001,
CheckKind::UnusedMethodArgument(..) => &CheckCode::ARG002,
CheckKind::UnusedClassMethodArgument(..) => &CheckCode::ARG003,
CheckKind::UnusedStaticMethodArgument(..) => &CheckCode::ARG004,
CheckKind::UnusedLambdaArgument(..) => &CheckCode::ARG005,
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(..) => &CheckCode::ICN001,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1830,7 +1921,7 @@ impl CheckKind {
}
CheckKind::ConsiderMergingIsinstance(obj, types) => {
let types = types.join(", ");
format!("Consider merging these isinstance calls: `isinstance({obj}, ({types}))`")
format!("Merge these isinstance calls: `isinstance({obj}, ({types}))`")
}
CheckKind::MisplacedComparisonConstant(comprison) => {
format!("Comparison should be {comprison}")
@@ -1842,7 +1933,7 @@ impl CheckKind {
"Cannot have defined parameters for properties".to_string()
}
CheckKind::ConsiderUsingFromImport(module, name) => {
format!("Consider using `from {module} import {name}`")
format!("Use `from {module} import {name}` in lieu of alias")
}
CheckKind::AwaitOutsideAsync => {
"`await` should be used within an async function".to_string()
@@ -1850,7 +1941,7 @@ impl CheckKind {
CheckKind::UselessElseOnLoop => "Else clause on loop without a break statement, \
remove the else and de-indent all the code inside it"
.to_string(),
CheckKind::ConsiderUsingSysExit => "Consider using `sys.exit()`".to_string(),
CheckKind::UseSysExit(name) => format!("Use `sys.exit()` instead of `{name}`"),
// flake8-builtins
CheckKind::BuiltinVariableShadowing(name) => {
format!("Variable `{name}` is shadowing a python builtin")
@@ -1976,6 +2067,9 @@ impl CheckKind {
from None to distinguish them from errors in exception handling"
.to_string()
}
CheckKind::ZipWithoutExplicitStrict => {
"`zip()` without an explicit `strict=` parameter".to_string()
}
// flake8-comprehensions
CheckKind::UnnecessaryGeneratorList => {
"Unnecessary generator (rewrite as a `list` comprehension)".to_string()
@@ -2402,7 +2496,7 @@ impl CheckKind {
format!("Possible hardcoded password: `\"{string}\"`")
}
// flake8-blind-except
CheckKind::BlindExcept => "Blind except Exception: statement".to_string(),
CheckKind::BlindExcept(name) => format!("Do not catch blind exception: `{name}`"),
// mccabe
CheckKind::FunctionIsTooComplex(name, complexity) => {
format!("`{name}` is too complex ({complexity})")
@@ -2419,6 +2513,22 @@ impl CheckKind {
}
// pygrep-hooks
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(name) => {
format!("Unused function argument: `{name}`")
}
CheckKind::UnusedMethodArgument(name) => format!("Unused method argument: `{name}`"),
CheckKind::UnusedClassMethodArgument(name) => {
format!("Unused class method argument: `{name}`")
}
CheckKind::UnusedStaticMethodArgument(name) => {
format!("Unused static method argument: `{name}`")
}
CheckKind::UnusedLambdaArgument(name) => format!("Unused lambda argument: `{name}`"),
// flake8-import-conventions
CheckKind::ImportAliasIsNotConventional(name, asname) => {
format!("`{name}` should be imported as `{asname}`")
}
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -2489,7 +2599,6 @@ impl CheckKind {
| CheckKind::BlankLineBeforeSection(..)
| CheckKind::CapitalizeSectionName(..)
| CheckKind::CommentedOutCode
| CheckKind::ConsiderUsingSysExit
| CheckKind::ConvertNamedTupleFunctionalToClass(..)
| CheckKind::ConvertTypedDictFunctionalToClass(..)
| CheckKind::DashedUnderlineAfterSection(..)
@@ -2497,6 +2606,8 @@ impl CheckKind {
| CheckKind::DoNotAssertFalse
| CheckKind::DoNotAssignLambda
| CheckKind::DuplicateHandlerException(..)
| CheckKind::EndsInPeriod
| CheckKind::EndsInPunctuation
| CheckKind::GetAttrWithConstant
| CheckKind::ImplicitReturn
| CheckKind::ImplicitReturnValue
@@ -2553,6 +2664,7 @@ impl CheckKind {
| CheckKind::UnusedNOQA(..)
| CheckKind::UsePEP585Annotation(..)
| CheckKind::UsePEP604Annotation
| CheckKind::UseSysExit(..)
| CheckKind::UselessImportAlias
| CheckKind::UselessMetaclassType
| CheckKind::UselessObjectInheritance(..)
@@ -2584,7 +2696,7 @@ impl Check {
}
/// A hash map from deprecated to latest `CheckCode`.
pub static REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", CheckCode::UP001),
@@ -2601,6 +2713,25 @@ pub static REDIRECTS: Lazy<FxHashMap<&'static str, CheckCode>> = Lazy::new(|| {
("U013", CheckCode::UP013),
("U014", CheckCode::UP014),
("U015", CheckCode::UP015),
// TODO(charlie): Remove by 2023-02-01.
("I252", CheckCode::TID252),
("M001", CheckCode::RUF100),
])
});
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
pub static PREFIX_REDIRECTS: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U", "UP"),
("U0", "UP0"),
("U00", "UP00"),
("U01", "UP01"),
// TODO(charlie): Remove by 2023-02-01.
("I2", "TID2"),
("I25", "TID25"),
("M", "RUF100"),
("M0", "RUF100"),
])
});

View File

@@ -2,11 +2,13 @@
use colored::Colorize;
use serde::{Deserialize, Serialize};
use strum_macros::EnumString;
use strum_macros::{AsRefStr, EnumString};
use crate::checks::CheckCode;
#[derive(EnumString, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)]
#[derive(
EnumString, AsRefStr, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize,
)]
pub enum CheckCodePrefix {
A,
A0,
@@ -34,6 +36,14 @@ pub enum CheckCodePrefix {
ANN4,
ANN40,
ANN401,
ARG,
ARG0,
ARG00,
ARG001,
ARG002,
ARG003,
ARG004,
ARG005,
B,
B0,
B00,
@@ -68,6 +78,7 @@ pub enum CheckCodePrefix {
B9,
B90,
B904,
B905,
BLE,
BLE0,
BLE00,
@@ -256,6 +267,13 @@ pub enum CheckCodePrefix {
I2,
I25,
I252,
ICN,
ICN0,
ICN00,
ICN001,
M,
M0,
M001,
N,
N8,
N80,
@@ -360,6 +378,10 @@ pub enum CheckCodePrefix {
T20,
T201,
T203,
TID,
TID2,
TID25,
TID252,
U,
U0,
U00,
@@ -485,6 +507,32 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN4 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN40 => vec![CheckCode::ANN401],
CheckCodePrefix::ANN401 => vec![CheckCode::ANN401],
CheckCodePrefix::ARG => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG0 => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG00 => vec![
CheckCode::ARG001,
CheckCode::ARG002,
CheckCode::ARG003,
CheckCode::ARG004,
CheckCode::ARG005,
],
CheckCodePrefix::ARG001 => vec![CheckCode::ARG001],
CheckCodePrefix::ARG002 => vec![CheckCode::ARG002],
CheckCodePrefix::ARG003 => vec![CheckCode::ARG003],
CheckCodePrefix::ARG004 => vec![CheckCode::ARG004],
CheckCodePrefix::ARG005 => vec![CheckCode::ARG005],
CheckCodePrefix::B => vec![
CheckCode::B002,
CheckCode::B003,
@@ -513,6 +561,7 @@ impl CheckCodePrefix {
CheckCode::B026,
CheckCode::B027,
CheckCode::B904,
CheckCode::B905,
],
CheckCodePrefix::B0 => vec![
CheckCode::B002,
@@ -600,9 +649,10 @@ impl CheckCodePrefix {
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::B9 => vec![CheckCode::B904, CheckCode::B905],
CheckCodePrefix::B90 => vec![CheckCode::B904, CheckCode::B905],
CheckCodePrefix::B904 => vec![CheckCode::B904],
CheckCodePrefix::B905 => vec![CheckCode::B905],
CheckCodePrefix::BLE => vec![CheckCode::BLE001],
CheckCodePrefix::BLE0 => vec![CheckCode::BLE001],
CheckCodePrefix::BLE00 => vec![CheckCode::BLE001],
@@ -1131,13 +1181,68 @@ impl CheckCodePrefix {
CheckCodePrefix::FBT001 => vec![CheckCode::FBT001],
CheckCodePrefix::FBT002 => vec![CheckCode::FBT002],
CheckCodePrefix::FBT003 => vec![CheckCode::FBT003],
CheckCodePrefix::I => vec![CheckCode::I252, CheckCode::I001],
CheckCodePrefix::I => vec![CheckCode::I001],
CheckCodePrefix::I0 => vec![CheckCode::I001],
CheckCodePrefix::I00 => vec![CheckCode::I001],
CheckCodePrefix::I001 => vec![CheckCode::I001],
CheckCodePrefix::I2 => vec![CheckCode::I252],
CheckCodePrefix::I25 => vec![CheckCode::I252],
CheckCodePrefix::I252 => vec![CheckCode::I252],
CheckCodePrefix::I2 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`I2` has been remapped to `TID2`".bold()
);
vec![CheckCode::TID252]
}
CheckCodePrefix::I25 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`I25` has been remapped to `TID25`".bold()
);
vec![CheckCode::TID252]
}
CheckCodePrefix::I252 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`I252` has been remapped to `TID252`".bold()
);
vec![CheckCode::TID252]
}
CheckCodePrefix::ICN => vec![CheckCode::ICN001],
CheckCodePrefix::ICN0 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN00 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN001 => vec![CheckCode::ICN001],
CheckCodePrefix::M => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`M` has been remapped to `RUF100`".bold()
);
vec![CheckCode::RUF100]
}
CheckCodePrefix::M0 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`M0` has been remapped to `RUF100`".bold()
);
vec![CheckCode::RUF100]
}
CheckCodePrefix::M001 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`M001` has been remapped to `RUF100`".bold()
);
vec![CheckCode::RUF100]
}
CheckCodePrefix::N => vec![
CheckCode::N801,
CheckCode::N802,
@@ -1366,54 +1471,82 @@ impl CheckCodePrefix {
CheckCodePrefix::T20 => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T201 => vec![CheckCode::T201],
CheckCodePrefix::T203 => vec![CheckCode::T203],
CheckCodePrefix::U => vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
],
CheckCodePrefix::U0 => vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
],
CheckCodePrefix::U00 => vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
],
CheckCodePrefix::TID => vec![CheckCode::TID252],
CheckCodePrefix::TID2 => vec![CheckCode::TID252],
CheckCodePrefix::TID25 => vec![CheckCode::TID252],
CheckCodePrefix::TID252 => vec![CheckCode::TID252],
CheckCodePrefix::U => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U` has been remapped to `UP`".bold()
);
vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
]
}
CheckCodePrefix::U0 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U0` has been remapped to `UP0`".bold()
);
vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
]
}
CheckCodePrefix::U00 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U00` has been remapped to `UP00`".bold()
);
vec![
CheckCode::UP001,
CheckCode::UP003,
CheckCode::UP004,
CheckCode::UP005,
CheckCode::UP006,
CheckCode::UP007,
CheckCode::UP008,
CheckCode::UP009,
]
}
CheckCodePrefix::U001 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U001` has been renamed to `UP001`".bold()
"`U001` has been remapped to `UP001`".bold()
);
vec![CheckCode::UP001]
}
@@ -1422,7 +1555,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U003` has been renamed to `UP003`".bold()
"`U003` has been remapped to `UP003`".bold()
);
vec![CheckCode::UP003]
}
@@ -1431,7 +1564,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U004` has been renamed to `UP004`".bold()
"`U004` has been remapped to `UP004`".bold()
);
vec![CheckCode::UP004]
}
@@ -1440,7 +1573,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U005` has been renamed to `UP005`".bold()
"`U005` has been remapped to `UP005`".bold()
);
vec![CheckCode::UP005]
}
@@ -1449,7 +1582,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U006` has been renamed to `UP006`".bold()
"`U006` has been remapped to `UP006`".bold()
);
vec![CheckCode::UP006]
}
@@ -1458,7 +1591,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U007` has been renamed to `UP007`".bold()
"`U007` has been remapped to `UP007`".bold()
);
vec![CheckCode::UP007]
}
@@ -1467,7 +1600,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U008` has been renamed to `UP008`".bold()
"`U008` has been remapped to `UP008`".bold()
);
vec![CheckCode::UP008]
}
@@ -1476,24 +1609,32 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U009` has been renamed to `UP009`".bold()
"`U009` has been remapped to `UP009`".bold()
);
vec![CheckCode::UP009]
}
CheckCodePrefix::U01 => vec![
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
],
CheckCodePrefix::U01 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U01` has been remapped to `UP01`".bold()
);
vec![
CheckCode::UP010,
CheckCode::UP011,
CheckCode::UP012,
CheckCode::UP013,
CheckCode::UP014,
CheckCode::UP015,
]
}
CheckCodePrefix::U010 => {
eprintln!(
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U010` has been renamed to `UP010`".bold()
"`U010` has been remapped to `UP010`".bold()
);
vec![CheckCode::UP010]
}
@@ -1502,7 +1643,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U011` has been renamed to `UP011`".bold()
"`U011` has been remapped to `UP011`".bold()
);
vec![CheckCode::UP011]
}
@@ -1511,7 +1652,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U012` has been renamed to `UP012`".bold()
"`U012` has been remapped to `UP012`".bold()
);
vec![CheckCode::UP012]
}
@@ -1520,7 +1661,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U013` has been renamed to `UP013`".bold()
"`U013` has been remapped to `UP013`".bold()
);
vec![CheckCode::UP013]
}
@@ -1529,7 +1670,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U014` has been renamed to `UP014`".bold()
"`U014` has been remapped to `UP014`".bold()
);
vec![CheckCode::UP014]
}
@@ -1538,7 +1679,7 @@ impl CheckCodePrefix {
"{}{} {}",
"warning".yellow().bold(),
":".bold(),
"`U015` has been renamed to `UP015`".bold()
"`U015` has been remapped to `UP015`".bold()
);
vec![CheckCode::UP015]
}
@@ -1685,6 +1826,14 @@ impl CheckCodePrefix {
CheckCodePrefix::ANN4 => SuffixLength::One,
CheckCodePrefix::ANN40 => SuffixLength::Two,
CheckCodePrefix::ANN401 => SuffixLength::Three,
CheckCodePrefix::ARG => SuffixLength::Zero,
CheckCodePrefix::ARG0 => SuffixLength::One,
CheckCodePrefix::ARG00 => SuffixLength::Two,
CheckCodePrefix::ARG001 => SuffixLength::Three,
CheckCodePrefix::ARG002 => SuffixLength::Three,
CheckCodePrefix::ARG003 => SuffixLength::Three,
CheckCodePrefix::ARG004 => SuffixLength::Three,
CheckCodePrefix::ARG005 => SuffixLength::Three,
CheckCodePrefix::B => SuffixLength::Zero,
CheckCodePrefix::B0 => SuffixLength::One,
CheckCodePrefix::B00 => SuffixLength::Two,
@@ -1719,6 +1868,7 @@ impl CheckCodePrefix {
CheckCodePrefix::B9 => SuffixLength::One,
CheckCodePrefix::B90 => SuffixLength::Two,
CheckCodePrefix::B904 => SuffixLength::Three,
CheckCodePrefix::B905 => SuffixLength::Three,
CheckCodePrefix::BLE => SuffixLength::Zero,
CheckCodePrefix::BLE0 => SuffixLength::One,
CheckCodePrefix::BLE00 => SuffixLength::Two,
@@ -1907,6 +2057,13 @@ impl CheckCodePrefix {
CheckCodePrefix::I2 => SuffixLength::One,
CheckCodePrefix::I25 => SuffixLength::Two,
CheckCodePrefix::I252 => SuffixLength::Three,
CheckCodePrefix::ICN => SuffixLength::Zero,
CheckCodePrefix::ICN0 => SuffixLength::One,
CheckCodePrefix::ICN00 => SuffixLength::Two,
CheckCodePrefix::ICN001 => SuffixLength::Three,
CheckCodePrefix::M => SuffixLength::Zero,
CheckCodePrefix::M0 => SuffixLength::One,
CheckCodePrefix::M001 => SuffixLength::Three,
CheckCodePrefix::N => SuffixLength::Zero,
CheckCodePrefix::N8 => SuffixLength::One,
CheckCodePrefix::N80 => SuffixLength::Two,
@@ -2011,6 +2168,10 @@ impl CheckCodePrefix {
CheckCodePrefix::T20 => SuffixLength::Two,
CheckCodePrefix::T201 => SuffixLength::Three,
CheckCodePrefix::T203 => SuffixLength::Three,
CheckCodePrefix::TID => SuffixLength::Zero,
CheckCodePrefix::TID2 => SuffixLength::One,
CheckCodePrefix::TID25 => SuffixLength::Two,
CheckCodePrefix::TID252 => SuffixLength::Three,
CheckCodePrefix::U => SuffixLength::Zero,
CheckCodePrefix::U0 => SuffixLength::One,
CheckCodePrefix::U00 => SuffixLength::Two,
@@ -2078,6 +2239,7 @@ impl CheckCodePrefix {
pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::A,
CheckCodePrefix::ANN,
CheckCodePrefix::ARG,
CheckCodePrefix::B,
CheckCodePrefix::BLE,
CheckCodePrefix::C,
@@ -2087,6 +2249,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::F,
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::ICN,
CheckCodePrefix::N,
CheckCodePrefix::PGH,
CheckCodePrefix::PLC,
@@ -2098,7 +2261,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::RUF,
CheckCodePrefix::S,
CheckCodePrefix::T,
CheckCodePrefix::U,
CheckCodePrefix::TID,
CheckCodePrefix::UP,
CheckCodePrefix::W,
CheckCodePrefix::YTT,

View File

@@ -30,9 +30,15 @@ impl Flags {
}
}
#[derive(Default)]
pub struct IsortDirectives {
pub exclusions: IntSet<usize>,
pub splits: Vec<usize>,
}
pub struct Directives {
pub noqa_line_for: IntMap<usize, usize>,
pub isort_exclusions: IntSet<usize>,
pub isort: IsortDirectives,
}
pub fn extract_directives(
@@ -46,10 +52,10 @@ pub fn extract_directives(
} else {
IntMap::default()
},
isort_exclusions: if flags.contains(Flags::ISORT) {
extract_isort_exclusions(lxr, locator)
isort: if flags.contains(Flags::ISORT) {
extract_isort_directives(lxr, locator)
} else {
IntSet::default()
IsortDirectives::default()
},
}
}
@@ -73,63 +79,87 @@ pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
}
/// Extract a set of lines over which to disable isort.
pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) -> IntSet<usize> {
pub fn extract_isort_directives(lxr: &[LexResult], locator: &SourceCodeLocator) -> IsortDirectives {
let mut exclusions: IntSet<usize> = IntSet::default();
let mut splits: Vec<usize> = Vec::default();
let mut skip_file: bool = false;
let mut off: Option<Location> = None;
let mut last: Option<Location> = None;
for &(start, ref tok, end) in lxr.iter().flatten() {
last = Some(end);
// No need to keep processing, but we do need to determine the last token.
if skip_file {
continue;
}
if !matches!(tok, Tok::Comment) {
continue;
}
// TODO(charlie): Modify RustPython to include the comment text in the token.
if matches!(tok, Tok::Comment) {
let comment_text = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
if off.is_some() {
if comment_text == "# isort: on" {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
let comment_text = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
if comment_text == "# isort: split" {
splits.push(start.row());
} else if comment_text == "# isort: skip_file" {
skip_file = true;
} else if off.is_some() {
if comment_text == "# isort: on" {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
off = None;
}
} else {
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
exclusions.insert(start.row());
} else if comment_text == "# isort: off" {
off = Some(start);
}
off = None;
}
} else if matches!(tok, Tok::EndOfFile) {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
} else {
if comment_text.contains("isort: skip") {
exclusions.insert(start.row());
} else if comment_text == "# isort: off" {
off = Some(start);
}
break;
}
}
exclusions
if skip_file {
// Enforce `isort: skip_file`.
if let Some(end) = last {
for row in 1..=end.row() {
exclusions.insert(row);
}
}
} else if let Some(start) = off {
// Enforce unterminated `isort: off`.
if let Some(end) = last {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
}
}
IsortDirectives { exclusions, splits }
}
#[cfg(test)]
mod tests {
use nohash_hasher::IntMap;
use nohash_hasher::{IntMap, IntSet};
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::directives::extract_noqa_line_for;
use crate::directives::{extract_isort_directives, extract_noqa_line_for};
use crate::SourceCodeLocator;
#[test]
fn extraction() {
let empty: IntMap<usize, usize> = IntMap::default();
fn noqa_extraction() {
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"
@@ -138,7 +168,7 @@ y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
@@ -147,7 +177,7 @@ z = x + 1
",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
@@ -157,7 +187,7 @@ z = x + 1
",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
assert_eq!(extract_noqa_line_for(&lxr), IntMap::default());
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = '''abc
@@ -200,4 +230,106 @@ z = x + 1",
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
);
}
#[test]
fn isort_exclusions() {
let contents = "x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).exclusions,
IntSet::default()
);
let contents = "# isort: off
x = 1
y = 2
# isort: on
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).exclusions,
IntSet::from_iter([2, 3, 4])
);
let contents = "# isort: off
x = 1
# isort: off
y = 2
# isort: on
z = x + 1
# isort: on";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).exclusions,
IntSet::from_iter([2, 3, 4, 5])
);
let contents = "# isort: off
x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).exclusions,
IntSet::from_iter([2, 3, 4])
);
let contents = "# isort: skip_file
x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).exclusions,
IntSet::from_iter([1, 2, 3, 4])
);
let contents = "# isort: off
x = 1
# isort: on
y = 2
# isort: skip_file
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).exclusions,
IntSet::from_iter([1, 2, 3, 4, 5, 6])
);
}
#[test]
fn isort_splits() {
let contents = "x = 1
y = 2
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(
extract_isort_directives(&lxr, &locator).splits,
Vec::<usize>::new()
);
let contents = "x = 1
y = 2
# isort: split
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(extract_isort_directives(&lxr, &locator).splits, vec![3]);
let contents = "x = 1
y = 2 # isort: split
z = x + 1";
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
let locator = SourceCodeLocator::new(contents);
assert_eq!(extract_isort_directives(&lxr, &locator).splits, vec![2]);
}
}

View File

@@ -0,0 +1,63 @@
use rustpython_ast::{Arguments, Expr, Stmt, StmtKind};
use crate::ast::cast;
use crate::check_ast::Checker;
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::visibility;
pub(super) fn match_function_def(
stmt: &Stmt,
) -> (&str, &Arguments, &Option<Box<Expr>>, &Vec<Stmt>) {
match &stmt.node {
StmtKind::FunctionDef {
name,
args,
returns,
body,
..
}
| StmtKind::AsyncFunctionDef {
name,
args,
returns,
body,
..
} => (name, args, returns, body),
_ => panic!("Found non-FunctionDef in match_name"),
}
}
/// Return the name of the function, if it's overloaded.
pub fn overloaded_name(checker: &Checker, definition: &Definition) -> Option<String> {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if visibility::is_overload(checker, cast::decorator_list(stmt)) {
let (name, ..) = match_function_def(stmt);
Some(name.to_string())
} else {
None
}
} else {
None
}
}
/// Return `true` if the definition is the implementation for an overloaded
/// function.
pub fn is_overload_impl(checker: &Checker, definition: &Definition, overloaded_name: &str) -> bool {
if let DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) = definition.kind
{
if visibility::is_overload(checker, cast::decorator_list(stmt)) {
false
} else {
let (name, ..) = match_function_def(stmt);
name == overloaded_name
}
} else {
false
}
}

View File

@@ -1,3 +1,4 @@
pub mod helpers;
pub mod plugins;
pub mod settings;
@@ -134,4 +135,24 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn allow_overload() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_overload.py"),
&Settings {
..Settings::for_rules(vec![
CheckCode::ANN201,
CheckCode::ANN202,
CheckCode::ANN204,
CheckCode::ANN205,
CheckCode::ANN206,
])
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,11 +1,12 @@
use rustpython_ast::{Arguments, Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::ast::{cast, visitor};
use crate::check_ast::Checker;
use crate::checks::{CheckCode, CheckKind};
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::flake8_annotations::helpers::match_function_def;
use crate::visibility::Visibility;
use crate::{visibility, Check};
@@ -61,26 +62,6 @@ where
};
}
fn match_function_def(stmt: &Stmt) -> (&str, &Arguments, &Option<Box<Expr>>, &Vec<Stmt>) {
match &stmt.node {
StmtKind::FunctionDef {
name,
args,
returns,
body,
..
}
| StmtKind::AsyncFunctionDef {
name,
args,
returns,
body,
..
} => (name, args, returns, body),
_ => panic!("Found non-FunctionDef in match_name"),
}
}
/// Generate flake8-annotation checks for a given `Definition`.
pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &Visibility) {
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
@@ -211,7 +192,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(!visibility::is_staticmethod(checker, stmt)),
usize::from(!visibility::is_staticmethod(
checker,
cast::decorator_list(stmt),
)),
)
{
// ANN401 for dynamically typed arguments
@@ -283,10 +267,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
// ANN101, ANN102
if !visibility::is_staticmethod(checker, stmt) {
if !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, stmt) {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN102) {
checker.add_check(Check::new(
CheckKind::MissingTypeCls(arg.node.arg.to_string()),
@@ -319,14 +303,14 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
return;
}
if visibility::is_classmethod(checker, stmt) {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN206) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeClassMethod(name.to_string()),
Range::from_located(stmt),
));
}
} else if visibility::is_staticmethod(checker, stmt) {
} else if visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.enabled.contains(&CheckCode::ANN205) {
checker.add_check(Check::new(
CheckKind::MissingReturnTypeStaticMethod(name.to_string()),

View File

@@ -1,22 +1,51 @@
//! Settings for the `flake-annotations` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
/// Allow omission of a return type hint for `__init__` if at least one
/// argument is annotated.
#[option(
doc = r#"
Whether to allow the omission of a return type hint for `__init__` if at least one
argument is annotated.
"#,
default = "false",
value_type = "bool",
example = "mypy-init-return = true"
)]
pub mypy_init_return: Option<bool>,
/// Suppress ANN000-level errors for dummy arguments, like `_`.
#[option(
doc = r#"
Whether to suppress `ANN000`-level errors for arguments matching the "dummy" variable
regex (like `_`).
"#,
default = "false",
value_type = "bool",
example = "suppress-dummy-args = true"
)]
pub suppress_dummy_args: Option<bool>,
/// Suppress ANN200-level errors for functions that meet one of the
/// following criteria:
/// - Contain no `return` statement
/// - Explicit `return` statement(s) all return `None` (explicitly or
/// implicitly).
#[option(
doc = r#"
Whether to suppress `ANN200`-level errors for functions that meet either of the
following criteria:
- Contain no `return` statement.
- Explicit `return` statement(s) all return `None` (explicitly or implicitly).
"#,
default = "false",
value_type = "bool",
example = "suppress-none-returning = true"
)]
pub suppress_none_returning: Option<bool>,
/// Suppress ANN401 for dynamically typed *args and **kwargs.
#[option(
doc = "Whether to suppress `ANN401` for dynamically typed `*args` and `**kwargs` \
arguments.",
default = "false",
value_type = "bool",
example = "allow-star-arg-any = true"
)]
pub allow_star_arg_any: Option<bool>,
}

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_annotations/mod.rs
expression: checks
---
- kind:
MissingReturnTypePublicFunction: bar
location:
row: 29
column: 4
end_location:
row: 35
column: 0
fix: ~

View File

@@ -1,21 +1,42 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use rustpython_ast::{Expr, ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
pub fn blind_except(checker: &mut Checker, handlers: &[Excepthandler]) {
for handler in handlers {
let ExcepthandlerKind::ExceptHandler { type_: Some(type_), .. } = &handler.node else {
continue;
};
let ExprKind::Name { id, .. } = &type_.node else {
continue;
};
for exception in ["BaseException", "Exception"] {
if id == exception {
/// BLE001
pub fn blind_except(
checker: &mut Checker,
type_: Option<&Expr>,
name: Option<&str>,
body: &[Stmt],
) {
let Some(type_) = type_ else {
return;
};
let ExprKind::Name { id, .. } = &type_.node else {
return;
};
for exception in ["BaseException", "Exception"] {
if id == exception && checker.is_builtin(exception) {
// If the exception is re-raised, don't flag an error.
if !body.iter().any(|stmt| {
if let StmtKind::Raise { exc, .. } = &stmt.node {
if let Some(exc) = exc {
if let ExprKind::Name { id, .. } = &exc.node {
name.map_or(false, |name| name == id)
} else {
false
}
} else {
true
}
} else {
false
}
}) {
checker.add_check(Check::new(
CheckKind::BlindExcept,
CheckKind::BlindExcept(id.to_string()),
Range::from_located(type_),
));
}

View File

@@ -2,31 +2,8 @@
source: src/flake8_blind_except/mod.rs
expression: checks
---
- kind: BlindExcept
location:
row: 5
column: 7
end_location:
row: 5
column: 16
fix: ~
- kind: BlindExcept
location:
row: 13
column: 7
end_location:
row: 13
column: 20
fix: ~
- kind: BlindExcept
location:
row: 23
column: 7
end_location:
row: 23
column: 16
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: BaseException
location:
row: 25
column: 7
@@ -34,7 +11,8 @@ expression: checks
row: 25
column: 20
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: Exception
location:
row: 31
column: 7
@@ -42,15 +20,8 @@ expression: checks
row: 31
column: 16
fix: ~
- kind: BlindExcept
location:
row: 36
column: 11
end_location:
row: 36
column: 24
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: Exception
location:
row: 42
column: 7
@@ -58,7 +29,8 @@ expression: checks
row: 42
column: 16
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: BaseException
location:
row: 45
column: 11
@@ -66,15 +38,8 @@ expression: checks
row: 45
column: 24
fix: ~
- kind: BlindExcept
location:
row: 52
column: 11
end_location:
row: 52
column: 24
fix: ~
- kind: BlindExcept
- kind:
BlindExcept: Exception
location:
row: 54
column: 7
@@ -82,4 +47,22 @@ expression: checks
row: 54
column: 16
fix: ~
- kind:
BlindExcept: Exception
location:
row: 60
column: 7
end_location:
row: 60
column: 16
fix: ~
- kind:
BlindExcept: BaseException
location:
row: 62
column: 7
end_location:
row: 62
column: 20
fix: ~

View File

@@ -39,6 +39,7 @@ mod tests {
#[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::B905, Path::new("B905.py"); "B905")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -1,6 +1,5 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustc_hash::FxHashSet;
use rustpython_ast::{
Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Location, Stmt,
};
@@ -27,9 +26,9 @@ fn duplicate_handler_exceptions<'a>(
checker: &mut Checker,
expr: &'a Expr,
elts: &'a [Expr],
) -> BTreeSet<Vec<&'a str>> {
let mut seen: BTreeSet<Vec<&str>> = BTreeSet::default();
let mut duplicates: BTreeSet<Vec<&str>> = BTreeSet::default();
) -> FxHashSet<Vec<&'a str>> {
let mut seen: FxHashSet<Vec<&str>> = FxHashSet::default();
let mut duplicates: FxHashSet<Vec<&str>> = FxHashSet::default();
let mut unique_elts: Vec<&Expr> = Vec::default();
for type_ in elts {
let call_path = helpers::collect_call_paths(type_);
@@ -76,8 +75,8 @@ fn duplicate_handler_exceptions<'a>(
}
pub fn duplicate_exceptions(checker: &mut Checker, stmt: &Stmt, handlers: &[Excepthandler]) {
let mut seen: BTreeSet<Vec<&str>> = BTreeSet::default();
let mut duplicates: BTreeSet<Vec<&str>> = BTreeSet::default();
let mut seen: FxHashSet<Vec<&str>> = FxHashSet::default();
let mut duplicates: FxHashSet<Vec<&str>> = FxHashSet::default();
for handler in handlers {
let ExcepthandlerKind::ExceptHandler { type_: Some(type_), .. } = &handler.node else {
continue;

View File

@@ -23,6 +23,7 @@ pub use unused_loop_control_variable::unused_loop_control_variable;
pub use useless_comparison::useless_comparison;
pub use useless_contextlib_suppress::useless_contextlib_suppress;
pub use useless_expression::useless_expression;
pub use zip_without_explicit_strict::zip_without_explicit_strict;
mod abstract_base_class;
mod assert_false;
@@ -49,3 +50,4 @@ mod unused_loop_control_variable;
mod useless_comparison;
mod useless_contextlib_suppress;
mod useless_expression;
mod zip_without_explicit_strict;

View File

@@ -0,0 +1,31 @@
use rustpython_ast::{Expr, ExprKind, Keyword};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
/// B905
pub fn zip_without_explicit_strict(
checker: &mut Checker,
expr: &Expr,
func: &Expr,
kwargs: &[Keyword],
) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "zip"
&& checker.is_builtin("zip")
&& !kwargs.iter().any(|keyword| {
keyword
.node
.arg
.as_ref()
.map_or(false, |name| name == "strict")
})
{
checker.add_check(Check::new(
CheckKind::ZipWithoutExplicitStrict,
Range::from_located(expr),
));
}
}
}

View File

@@ -1,10 +1,23 @@
//! Settings for the `flake8-bugbear` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Additional callable functions to consider "immutable" when evaluating, e.g.,
`no-mutable-default-argument` checks (`B006`).
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
# Allow default arguments like, e.g., `data: List[str] = fastapi.Query(None)`.
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
"#
)]
pub extend_immutable_calls: Option<Vec<String>>,
}

View File

@@ -0,0 +1,61 @@
---
source: src/flake8_bugbear/mod.rs
expression: checks
---
- kind: ZipWithoutExplicitStrict
location:
row: 1
column: 0
end_location:
row: 1
column: 5
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 2
column: 0
end_location:
row: 2
column: 13
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 4
column: 15
end_location:
row: 4
column: 23
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 5
column: 4
end_location:
row: 5
column: 12
fix: ~
- kind: ZipWithoutExplicitStrict
location:
row: 6
column: 0
end_location:
row: 6
column: 26
fix: ~

View File

@@ -0,0 +1,36 @@
use rustc_hash::FxHashMap;
use rustpython_ast::Stmt;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
/// ICN001
pub fn check_conventional_import(
import_from: &Stmt,
name: &str,
asname: Option<&str>,
conventions: &FxHashMap<String, String>,
) -> Option<Check> {
let mut is_valid_import = true;
if let Some(expected_alias) = conventions.get(name) {
if !expected_alias.is_empty() {
if let Some(alias) = asname {
if expected_alias != alias {
is_valid_import = false;
}
} else {
is_valid_import = false;
}
}
if !is_valid_import {
return Some(Check::new(
CheckKind::ImportAliasIsNotConventional(
name.to_string(),
expected_alias.to_string(),
),
Range::from_located(import_from),
));
}
}
None
}

View File

@@ -0,0 +1,101 @@
pub mod checks;
pub mod settings;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use rustc_hash::FxHashMap;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::{flake8_import_conventions, Settings};
#[test]
fn defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/defaults.py"),
&Settings::for_rule(CheckCode::ICN001),
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("defaults", checks);
Ok(())
}
#[test]
fn custom() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/custom.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([
("dask.array".to_string(), "da".to_string()),
("dask.dataframe".to_string(), "dd".to_string()),
])),
},
),
..Settings::for_rule(CheckCode::ICN001)
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("custom", checks);
Ok(())
}
#[test]
fn remove_defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/remove_default.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: Some(FxHashMap::from_iter([
("altair".to_string(), "alt".to_string()),
("matplotlib.pyplot".to_string(), "plt".to_string()),
("pandas".to_string(), "pd".to_string()),
("seaborn".to_string(), "sns".to_string()),
])),
extend_aliases: None,
},
),
..Settings::for_rule(CheckCode::ICN001)
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("remove_default", checks);
Ok(())
}
#[test]
fn override_defaults() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/override_default.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([(
"numpy".to_string(),
"nmp".to_string(),
)])),
},
),
..Settings::for_rule(CheckCode::ICN001)
},
true,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!("override_default", checks);
Ok(())
}
}

View File

@@ -0,0 +1,94 @@
//! Settings for import conventions.
use std::hash::{Hash, Hasher};
use itertools::Itertools;
use ruff_macros::ConfigurationOptions;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
const CONVENTIONAL_ALIASES: &[(&str, &str)] = &[
("altair", "alt"),
("matplotlib.pyplot", "plt"),
("numpy", "np"),
("pandas", "pd"),
("seaborn", "sns"),
];
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = "The conventional aliases for imports. These aliases can be extended by the \
`extend_aliases` option.",
default = r#"{"altair": "alt", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns"}"#,
value_type = "FxHashMap<String, String>",
example = r#"
# Declare the default aliases.
altair = "alt"
matplotlib.pyplot = "plt"
numpy = "np"
pandas = "pd"
seaborn = "sns"
"#
)]
pub aliases: Option<FxHashMap<String, String>>,
#[option(
doc = "A mapping of modules to their conventional import aliases. These aliases will be \
added to the `aliases` mapping.",
default = r#"{}"#,
value_type = "FxHashMap<String, String>",
example = r#"
# Declare a custom alias for the `matplotlib` module.
"dask.dataframe" = "dd"
"#
)]
pub extend_aliases: Option<FxHashMap<String, String>>,
}
#[derive(Debug)]
pub struct Settings {
pub aliases: FxHashMap<String, String>,
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
for value in self.aliases.iter().sorted() {
value.hash(state);
}
}
}
fn default_aliases() -> FxHashMap<String, String> {
CONVENTIONAL_ALIASES
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect::<FxHashMap<_, _>>()
}
fn resolve_aliases(options: Options) -> FxHashMap<String, String> {
let mut aliases = match options.aliases {
Some(options_aliases) => options_aliases,
None => default_aliases(),
};
if let Some(extend_aliases) = options.extend_aliases {
aliases.extend(extend_aliases);
}
aliases
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
aliases: resolve_aliases(options),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
aliases: default_aliases(),
}
}
}

View File

@@ -0,0 +1,159 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.array
- da
location:
row: 4
column: 0
end_location:
row: 4
column: 17
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.dataframe
- dd
location:
row: 5
column: 0
end_location:
row: 5
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 6
column: 0
end_location:
row: 6
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 7
column: 0
end_location:
row: 7
column: 12
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 8
column: 0
end_location:
row: 8
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 9
column: 0
end_location:
row: 9
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 11
column: 0
end_location:
row: 11
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 12
column: 0
end_location:
row: 12
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.array
- da
location:
row: 13
column: 0
end_location:
row: 13
column: 27
fix: ~
- kind:
ImportAliasIsNotConventional:
- dask.dataframe
- dd
location:
row: 14
column: 0
end_location:
row: 14
column: 28
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 15
column: 0
end_location:
row: 15
column: 19
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 16
column: 0
end_location:
row: 16
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 17
column: 0
end_location:
row: 17
column: 22
fix: ~

View File

@@ -0,0 +1,115 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 5
column: 0
end_location:
row: 5
column: 12
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 6
column: 0
end_location:
row: 6
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 7
column: 0
end_location:
row: 7
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 10
column: 0
end_location:
row: 10
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- np
location:
row: 11
column: 0
end_location:
row: 11
column: 19
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 12
column: 0
end_location:
row: 12
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 13
column: 0
end_location:
row: 13
column: 22
fix: ~

View File

@@ -0,0 +1,115 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- nmp
location:
row: 5
column: 0
end_location:
row: 5
column: 12
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 6
column: 0
end_location:
row: 6
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 7
column: 0
end_location:
row: 7
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 10
column: 0
end_location:
row: 10
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- numpy
- nmp
location:
row: 11
column: 0
end_location:
row: 11
column: 18
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 12
column: 0
end_location:
row: 12
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 13
column: 0
end_location:
row: 13
column: 22
fix: ~

View File

@@ -0,0 +1,93 @@
---
source: src/flake8_import_conventions/mod.rs
expression: checks
---
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 3
column: 0
end_location:
row: 3
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 4
column: 0
end_location:
row: 4
column: 24
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 6
column: 0
end_location:
row: 6
column: 13
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 7
column: 0
end_location:
row: 7
column: 14
fix: ~
- kind:
ImportAliasIsNotConventional:
- altair
- alt
location:
row: 9
column: 0
end_location:
row: 9
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- matplotlib.pyplot
- plt
location:
row: 10
column: 0
end_location:
row: 10
column: 32
fix: ~
- kind:
ImportAliasIsNotConventional:
- pandas
- pd
location:
row: 12
column: 0
end_location:
row: 12
column: 21
fix: ~
- kind:
ImportAliasIsNotConventional:
- seaborn
- sns
location:
row: 13
column: 0
end_location:
row: 13
column: 22
fix: ~

View File

@@ -1,5 +1,6 @@
//! Settings for the `flake8-quotes` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -9,12 +10,55 @@ pub enum Quote {
Double,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Quote style to prefer for inline strings (either "single" (`'`) or "double" (`"`)).
"#,
default = r#""double""#,
value_type = "Quote",
example = r#"
inline-quotes = "single"
"#
)]
pub inline_quotes: Option<Quote>,
#[option(
doc = r#"
Quote style to prefer for multiline strings (either "single" (`'`) or "double" (`"`)).
"#,
default = r#""double""#,
value_type = "Quote",
example = r#"
multiline-quotes = "single"
"#
)]
pub multiline_quotes: Option<Quote>,
#[option(
doc = r#"
Quote style to prefer for docstrings (either "single" (`'`) or "double" (`"`)).
"#,
default = r#""double""#,
value_type = "Quote",
example = r#"
docstring-quotes = "single"
"#
)]
pub docstring_quotes: Option<Quote>,
#[option(
doc = r#"
Whether to avoid using single quotes if a string contains single quotes, or vice-versa
with double quotes, as per [PEP8](https://peps.python.org/pep-0008/#string-quotes).
This minimizes the need to escape quotation marks within strings.
"#,
default = r#"true"#,
value_type = "bool",
example = r#"
# Don't bother trying to avoid escapes.
avoid-escape = false
"#
)]
pub avoid_escape: Option<bool>,
}

View File

@@ -15,12 +15,12 @@ mod tests {
#[test]
fn ban_parent_imports() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/I252.py"),
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::Parents,
},
..Settings::for_rules(vec![CheckCode::I252])
..Settings::for_rules(vec![CheckCode::TID252])
},
true,
)?;
@@ -32,12 +32,12 @@ mod tests {
#[test]
fn ban_all_imports() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/I252.py"),
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID252.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::All,
},
..Settings::for_rules(vec![CheckCode::I252])
..Settings::for_rules(vec![CheckCode::TID252])
},
true,
)?;

View File

@@ -1,5 +1,6 @@
//! Settings for the `flake8-tidy-imports` plugin.
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
@@ -9,9 +10,21 @@ pub enum Strictness {
All,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Whether to ban all relative imports (`"all"`), or only those imports that extend into
the parent module and beyond (`"parents"`).
"#,
default = r#""parents""#,
value_type = "Strictness",
example = r#"
# Disallow all relative imports.
ban-relative-imports = "all"
"#
)]
pub ban_relative_imports: Option<Strictness>,
}

View File

@@ -0,0 +1,19 @@
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind};
pub fn is_empty(body: &[Stmt]) -> bool {
match &body {
[] => true,
// Also allow: raise NotImplementedError, raise NotImplemented
[stmt] => match &stmt.node {
StmtKind::Pass => true,
StmtKind::Expr { value } => match &value.node {
ExprKind::Constant { value, .. } => {
matches!(value, Constant::Str(_) | Constant::Ellipsis)
}
_ => false,
},
_ => false,
},
_ => false,
}
}

View File

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

View File

@@ -0,0 +1,184 @@
use std::iter;
use regex::Regex;
use rustc_hash::FxHashMap;
use rustpython_ast::{Arg, Arguments};
use crate::ast::function_type;
use crate::ast::function_type::FunctionType;
use crate::ast::helpers::collect_arg_names;
use crate::ast::types::{Binding, BindingKind, FunctionDef, Lambda, Scope, ScopeKind};
use crate::check_ast::Checker;
use crate::flake8_unused_arguments::helpers;
use crate::flake8_unused_arguments::types::Argumentable;
use crate::{visibility, Check};
/// Check a plain function for unused arguments.
fn function(
argumentable: &Argumentable,
args: &Arguments,
bindings: &FxHashMap<&str, Binding>,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for arg_name in collect_arg_names(args) {
if let Some(binding) = bindings.get(arg_name) {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg_name)
{
checks.push(Check::new(
argumentable.check_for(arg_name.to_string()),
binding.range,
));
}
}
}
checks
}
/// Check a method for unused arguments.
fn method(
argumentable: &Argumentable,
args: &Arguments,
bindings: &FxHashMap<&str, Binding>,
dummy_variable_rgx: &Regex,
) -> Vec<Check> {
let mut checks: Vec<Check> = vec![];
for arg in args
.posonlyargs
.iter()
.chain(args.args.iter())
.skip(1)
.chain(args.kwonlyargs.iter())
.chain(iter::once::<Option<&Arg>>(args.vararg.as_deref()).flatten())
.chain(iter::once::<Option<&Arg>>(args.kwarg.as_deref()).flatten())
{
if let Some(binding) = bindings.get(&arg.node.arg.as_str()) {
if binding.used.is_none()
&& matches!(binding.kind, BindingKind::Argument)
&& !dummy_variable_rgx.is_match(arg.node.arg.as_str())
{
checks.push(Check::new(
argumentable.check_for(arg.node.arg.to_string()),
binding.range,
));
}
}
}
checks
}
/// ARG001, ARG002, ARG003, ARG004, ARG005
pub fn unused_arguments(checker: &Checker, parent: &Scope, scope: &Scope) -> Vec<Check> {
match &scope.kind {
ScopeKind::Function(FunctionDef {
name,
args,
body,
decorator_list,
..
}) => {
match function_type::classify(
parent,
name,
decorator_list,
&checker.from_imports,
&checker.import_aliases,
&checker.settings.pep8_naming.classmethod_decorators,
&checker.settings.pep8_naming.staticmethod_decorators,
) {
FunctionType::Function => {
if checker
.settings
.enabled
.contains(Argumentable::Function.check_code())
{
function(
&Argumentable::Function,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::Method => {
if checker
.settings
.enabled
.contains(Argumentable::Method.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
method(
&Argumentable::Method,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::ClassMethod => {
if checker
.settings
.enabled
.contains(Argumentable::ClassMethod.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
method(
&Argumentable::ClassMethod,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
FunctionType::StaticMethod => {
if checker
.settings
.enabled
.contains(Argumentable::StaticMethod.check_code())
&& !helpers::is_empty(body)
&& !visibility::is_abstract(checker, decorator_list)
&& !visibility::is_override(checker, decorator_list)
{
function(
&Argumentable::StaticMethod,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
}
}
ScopeKind::Lambda(Lambda { args, .. }) => {
if checker
.settings
.enabled
.contains(Argumentable::Lambda.check_code())
{
function(
&Argumentable::Lambda,
args,
&scope.values,
&checker.settings.dummy_variable_rgx,
)
} else {
vec![]
}
}
_ => unreachable!("Expected ScopeKind::Function | ScopeKind::Lambda"),
}
}

View File

@@ -0,0 +1,77 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedFunctionArgument: self
location:
row: 8
column: 6
end_location:
row: 8
column: 10
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 8
column: 12
end_location:
row: 8
column: 13
fix: ~
- kind:
UnusedFunctionArgument: cls
location:
row: 12
column: 6
end_location:
row: 12
column: 9
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 12
column: 11
end_location:
row: 12
column: 12
fix: ~
- kind:
UnusedFunctionArgument: self
location:
row: 16
column: 6
end_location:
row: 16
column: 10
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 16
column: 12
end_location:
row: 16
column: 13
fix: ~
- kind:
UnusedFunctionArgument: cls
location:
row: 20
column: 6
end_location:
row: 20
column: 9
fix: ~
- kind:
UnusedFunctionArgument: x
location:
row: 20
column: 11
end_location:
row: 20
column: 12
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedMethodArgument: x
location:
row: 34
column: 16
end_location:
row: 34
column: 17
fix: ~
- kind:
UnusedMethodArgument: x
location:
row: 37
column: 19
end_location:
row: 37
column: 20
fix: ~
- kind:
UnusedMethodArgument: x
location:
row: 40
column: 15
end_location:
row: 40
column: 16
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedClassMethodArgument: x
location:
row: 44
column: 15
end_location:
row: 44
column: 16
fix: ~

View File

@@ -0,0 +1,32 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedStaticMethodArgument: cls
location:
row: 48
column: 10
end_location:
row: 48
column: 13
fix: ~
- kind:
UnusedStaticMethodArgument: x
location:
row: 48
column: 15
end_location:
row: 48
column: 16
fix: ~
- kind:
UnusedStaticMethodArgument: x
location:
row: 52
column: 10
end_location:
row: 52
column: 11
fix: ~

View File

@@ -0,0 +1,14 @@
---
source: src/flake8_unused_arguments/mod.rs
expression: checks
---
- kind:
UnusedLambdaArgument: x
location:
row: 27
column: 7
end_location:
row: 27
column: 8
fix: ~

View File

@@ -0,0 +1,32 @@
use crate::checks::{CheckCode, CheckKind};
/// An AST node that can contain arguments.
pub enum Argumentable {
Function,
Method,
ClassMethod,
StaticMethod,
Lambda,
}
impl Argumentable {
pub fn check_for(&self, name: String) -> CheckKind {
match self {
Argumentable::Function => CheckKind::UnusedFunctionArgument(name),
Argumentable::Method => CheckKind::UnusedMethodArgument(name),
Argumentable::ClassMethod => CheckKind::UnusedClassMethodArgument(name),
Argumentable::StaticMethod => CheckKind::UnusedStaticMethodArgument(name),
Argumentable::Lambda => CheckKind::UnusedLambdaArgument(name),
}
}
pub fn check_code(&self) -> &CheckCode {
match self {
Argumentable::Function => &CheckCode::ARG001,
Argumentable::Method => &CheckCode::ARG002,
Argumentable::ClassMethod => &CheckCode::ARG003,
Argumentable::StaticMethod => &CheckCode::ARG004,
Argumentable::Lambda => &CheckCode::ARG005,
}
}
}

View File

@@ -1,5 +1,4 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::{Path, PathBuf};
@@ -8,6 +7,7 @@ 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;
@@ -83,8 +83,8 @@ 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 [(GlobMatcher, GlobMatcher, BTreeSet<CheckCode>)],
) -> Result<BTreeSet<&'a CheckCode>> {
pattern_code_pairs: &'a [(GlobMatcher, GlobMatcher, FxHashSet<CheckCode>)],
) -> Result<FxHashSet<&'a CheckCode>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()
@@ -97,7 +97,7 @@ pub(crate) fn ignores_from_path<'a>(
/// Convert any path to an absolute path (based on the current working
/// directory).
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
pub fn normalize_path(path: &Path) -> PathBuf {
if let Ok(path) = path.absolutize() {
return path.to_path_buf();
}
@@ -105,7 +105,7 @@ pub(crate) fn normalize_path(path: &Path) -> PathBuf {
}
/// Convert any path to an absolute path (based on the specified project root).
pub(crate) fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
pub fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
if let Ok(path) = path.absolutize_from(project_root) {
return path.to_path_buf();
}
@@ -113,7 +113,7 @@ pub(crate) fn normalize_path_to(path: &Path, project_root: &Path) -> PathBuf {
}
/// Convert an absolute path to be relative to the current working directory.
pub(crate) fn relativize_path(path: &Path) -> Cow<str> {
pub fn relativize_path(path: &Path) -> Cow<str> {
if let Ok(path) = path.strip_prefix(&*path_dedot::CWD) {
return path.to_string_lossy();
}

View File

@@ -42,6 +42,15 @@ pub fn format_import_from(
force_wrap_aliases: bool,
is_first: bool,
) -> String {
if aliases.len() == 1
&& aliases
.iter()
.all(|(alias, _)| alias.name == "*" && alias.asname.is_none())
{
let (single_line, ..) = format_single_line(import_from, comments, aliases, is_first);
return single_line;
}
// We can only inline if: (1) none of the aliases have atop comments, and (3)
// only the last alias (if any) has inline comments.
if aliases
@@ -58,7 +67,7 @@ pub fn format_import_from(
{
let (single_line, import_length) =
format_single_line(import_from, comments, aliases, is_first);
if import_length <= line_length {
if import_length <= line_length || aliases.iter().any(|(alias, _)| alias.name == "*") {
return single_line;
}
}

View File

@@ -10,6 +10,7 @@ use rustpython_ast::{Stmt, StmtKind};
use crate::isort::categorize::{categorize, ImportType};
use crate::isort::comments::Comment;
use crate::isort::sorting::{member_key, module_key};
use crate::isort::track::{Block, Trailer};
use crate::isort::types::{
AliasData, CommentSet, ImportBlock, ImportFromData, Importable, OrderedImportBlock,
};
@@ -191,7 +192,18 @@ fn normalize_imports(imports: Vec<AnnotatedImport>, combine_as_imports: bool) ->
} => {
// Associate the comments with the first alias (best effort).
if let Some(alias) = names.first() {
if alias.asname.is_none() || combine_as_imports {
if alias.name == "*" {
let entry = block
.import_from_star
.entry(ImportFromData { module, level })
.or_default();
for comment in atop {
entry.atop.push(comment.value);
}
for comment in inline {
entry.inline.push(comment.value);
}
} else if alias.asname.is_none() || combine_as_imports {
let entry = &mut block
.import_from
.entry(ImportFromData { module, level })
@@ -225,7 +237,18 @@ fn normalize_imports(imports: Vec<AnnotatedImport>, combine_as_imports: bool) ->
// Create an entry for every alias.
for alias in names {
if alias.asname.is_none() || combine_as_imports {
if alias.name == "*" {
let entry = block
.import_from_star
.entry(ImportFromData { module, level })
.or_default();
for comment in alias.atop {
entry.atop.push(comment.value);
}
for comment in alias.inline {
entry.inline.push(comment.value);
}
} else if alias.asname.is_none() || combine_as_imports {
let entry = block
.import_from
.entry(ImportFromData { module, level })
@@ -323,6 +346,22 @@ fn categorize_imports<'a>(
.import_from_as
.insert((import_from, alias), comments);
}
// Categorize `StmtKind::ImportFrom` (with star).
for (import_from, comments) in block.import_from_star {
let classification = categorize(
&import_from.module_base(),
import_from.level,
src,
known_first_party,
known_third_party,
extra_standard_library,
);
block_by_type
.entry(classification)
.or_default()
.import_from_star
.insert(import_from, comments);
}
block_by_type
}
@@ -367,6 +406,33 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
)
}),
)
.chain(
// Include all star imports.
block
.import_from_star
.into_iter()
.map(|(import_from, comments)| {
(
import_from,
(
CommentSet {
atop: comments.atop,
inline: vec![],
},
FxHashMap::from_iter([(
AliasData {
name: "*",
asname: None,
},
CommentSet {
atop: vec![],
inline: comments.inline,
},
)]),
),
)
}),
)
.map(|(import_from, (comments, aliases))| {
// Within each `StmtKind::ImportFrom`, sort the members.
(
@@ -399,7 +465,7 @@ fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
#[allow(clippy::too_many_arguments)]
pub fn format_imports(
block: &[&Stmt],
block: &Block,
comments: Vec<Comment>,
line_length: usize,
src: &[PathBuf],
@@ -409,7 +475,8 @@ pub fn format_imports(
combine_as_imports: bool,
force_wrap_aliases: bool,
) -> String {
let block = annotate_imports(block, comments);
let trailer = &block.trailer;
let block = annotate_imports(&block.imports, comments);
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(block, combine_as_imports);
@@ -458,6 +525,16 @@ pub fn format_imports(
is_first_statement = false;
}
}
match trailer {
None => {}
Some(Trailer::Sibling) => {
output.append("\n");
}
Some(Trailer::FunctionDef | Trailer::ClassDef) => {
output.append("\n");
output.append("\n");
}
}
output.finish().to_string()
}
@@ -481,11 +558,15 @@ mod tests {
#[test_case(Path::new("fit_line_length_comment.py"))]
#[test_case(Path::new("force_wrap_aliases.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("insert_empty_lines.py"))]
#[test_case(Path::new("insert_empty_lines.pyi"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("no_wrap_star.py"))]
#[test_case(Path::new("order_by_type.py"))]
#[test_case(Path::new("order_relative_imports_by_level.py"))]
#[test_case(Path::new("preserve_comment_order.py"))]
#[test_case(Path::new("preserve_import_star.py"))]
#[test_case(Path::new("preserve_indentation.py"))]
#[test_case(Path::new("reorder_within_section.py"))]
#[test_case(Path::new("separate_first_party_imports.py"))]
@@ -493,7 +574,9 @@ mod tests {
#[test_case(Path::new("separate_local_folder_imports.py"))]
#[test_case(Path::new("separate_third_party_imports.py"))]
#[test_case(Path::new("skip.py"))]
#[test_case(Path::new("skip_file.py"))]
#[test_case(Path::new("sort_similar_imports.py"))]
#[test_case(Path::new("split.py"))]
#[test_case(Path::new("trailing_suffix.py"))]
#[test_case(Path::new("type_comments.py"))]
fn default(path: &Path) -> Result<()> {

View File

@@ -1,11 +1,12 @@
use rustpython_ast::{Location, Stmt};
use textwrap::{dedent, indent};
use crate::ast::helpers::{match_leading_content, match_trailing_content};
use crate::ast::helpers::{count_trailing_lines, match_leading_content, match_trailing_content};
use crate::ast::types::Range;
use crate::ast::whitespace::leading_space;
use crate::autofix::Fix;
use crate::checks::CheckKind;
use crate::isort::track::Block;
use crate::isort::{comments, format_imports};
use crate::{Check, Settings, SourceCodeLocator};
@@ -30,13 +31,13 @@ fn extract_indentation(body: &[&Stmt], locator: &SourceCodeLocator) -> String {
/// I001
pub fn check_imports(
body: &[&Stmt],
block: &Block,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: bool,
) -> Option<Check> {
let range = extract_range(body);
let indentation = extract_indentation(body, locator);
let range = extract_range(&block.imports);
let indentation = extract_indentation(&block.imports, locator);
// Extract comments. Take care to grab any inline comments from the last line.
let comments = comments::collect_comments(
@@ -48,12 +49,17 @@ pub fn check_imports(
);
// Special-cases: there's leading or trailing content in the import block.
let has_leading_content = match_leading_content(body.first().unwrap(), locator);
let has_trailing_content = match_trailing_content(body.last().unwrap(), locator);
let has_leading_content = match_leading_content(block.imports.first().unwrap(), locator);
let has_trailing_content = match_trailing_content(block.imports.last().unwrap(), locator);
let num_trailing_lines = if block.trailer.is_none() {
0
} else {
count_trailing_lines(block.imports.last().unwrap(), locator)
};
// Generate the sorted import block.
let expected = format_imports(
body,
block,
comments,
settings.line_length - indentation.len(),
&settings.src,
@@ -81,7 +87,7 @@ pub fn check_imports(
Location::new(range.location.row(), 0)
},
// TODO(charlie): Preserve trailing suffixes. Right now, we strip them.
Location::new(range.end_location.row() + 1, 0),
Location::new(range.end_location.row() + 1 + num_trailing_lines, 0),
));
}
Some(check)
@@ -89,7 +95,7 @@ pub fn check_imports(
// Expand the span the entire range, including leading and trailing space.
let range = Range {
location: Location::new(range.location.row(), 0),
end_location: Location::new(range.end_location.row() + 1, 0),
end_location: Location::new(range.end_location.row() + 1 + num_trailing_lines, 0),
};
let actual = dedent(&locator.slice_source_code_range(&range));
if actual == expected {

View File

@@ -2,15 +2,79 @@
use std::collections::BTreeSet;
use ruff_macros::ConfigurationOptions;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
#[option(
doc = r#"
Combines as imports on the same line. See isort's [`combine-as-imports`](https://pycqa.github.io/isort/docs/configuration/options.html#combine-as-imports)
option.
"#,
default = r#"false"#,
value_type = "bool",
example = r#"
combine-as-imports = true
"#
)]
pub combine_as_imports: Option<bool>,
#[option(
doc = r#"
Force `import from` statements with multiple members and at least one alias (e.g.,
`import A as B`) to wrap such that every line contains exactly one member. For example,
this formatting would be retained, rather than condensing to a single line:
```py
from .utils import (
test_directory as test_directory,
test_id as test_id
)
```
"#,
default = r#"false"#,
value_type = "bool",
example = r#"
force-wrap-aliases = true
"#
)]
pub force_wrap_aliases: Option<bool>,
#[option(
doc = r#"
A list of modules to consider first-party, regardless of whether they can be identified
as such via introspection of the local filesystem.
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
known-first-party = ["src"]
"#
)]
pub known_first_party: Option<Vec<String>>,
#[option(
doc = r#"
A list of modules to consider third-party, regardless of whether they can be identified
as such via introspection of the local filesystem.
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
known-third-party = ["src"]
"#
)]
pub known_third_party: Option<Vec<String>>,
#[option(
doc = r#"
A list of modules to consider standard-library, in addition to those known to Ruff in
advance.
"#,
default = r#"[]"#,
value_type = "Vec<String>",
example = r#"
extra-standard-library = ["path"]
"#
)]
pub extra_standard_library: Option<Vec<String>>,
}

View File

@@ -0,0 +1,80 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 3
column: 0
fix:
content: "import a\nimport b\n\n"
location:
row: 1
column: 0
end_location:
row: 3
column: 0
- kind: UnsortedImports
location:
row: 4
column: 0
end_location:
row: 6
column: 0
fix:
content: "import os\nimport sys\n\n\n"
location:
row: 4
column: 0
end_location:
row: 6
column: 0
- kind: UnsortedImports
location:
row: 14
column: 0
end_location:
row: 16
column: 0
fix:
content: "import os\nimport sys\n\n"
location:
row: 14
column: 0
end_location:
row: 16
column: 0
- kind: UnsortedImports
location:
row: 33
column: 0
end_location:
row: 35
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 33
column: 0
end_location:
row: 35
column: 0
- kind: UnsortedImports
location:
row: 39
column: 0
end_location:
row: 41
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 39
column: 0
end_location:
row: 41
column: 0

View File

@@ -0,0 +1,65 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 3
column: 0
fix:
content: "import a\nimport b\n\n"
location:
row: 1
column: 0
end_location:
row: 3
column: 0
- kind: UnsortedImports
location:
row: 4
column: 0
end_location:
row: 6
column: 0
fix:
content: "import os\nimport sys\n\n"
location:
row: 4
column: 0
end_location:
row: 6
column: 0
- kind: UnsortedImports
location:
row: 14
column: 0
end_location:
row: 16
column: 0
fix:
content: "import os\nimport sys\n\n"
location:
row: 14
column: 0
end_location:
row: 16
column: 0
- kind: UnsortedImports
location:
row: 33
column: 0
end_location:
row: 35
column: 0
fix:
content: " import collections\n import typing\n\n"
location:
row: 33
column: 0
end_location:
row: 35
column: 0

View File

@@ -10,12 +10,12 @@ expression: checks
row: 2
column: 9
fix:
content: "\nimport os\nimport sys\n"
content: "\nimport os\nimport sys\n\n"
location:
row: 1
column: 7
end_location:
row: 3
row: 4
column: 0
- kind: UnsortedImports
location:

View File

@@ -0,0 +1,20 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 2
column: 0
fix:
content: "from .subscription import * # type: ignore # some very long comment explaining why this needs a type ignore\n"
location:
row: 1
column: 0
end_location:
row: 2
column: 0

View File

@@ -0,0 +1,20 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 7
column: 0
fix:
content: "# Above\nfrom some_module import * # Aside\n\n# Above\nfrom some_module import some_class # Aside\nfrom some_other_module import *\nfrom some_other_module import some_class\n"
location:
row: 1
column: 0
end_location:
row: 7
column: 0

View File

@@ -2,6 +2,21 @@
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 7
column: 0
end_location:
row: 8
column: 0
fix:
content: "import sys\n\n"
location:
row: 7
column: 0
end_location:
row: 8
column: 0
- kind: UnsortedImports
location:
row: 9

View File

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

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