Compare commits

...

21 Commits

Author SHA1 Message Date
Charlie Marsh
a21fe716f2 Bump version to 0.0.113 2022-11-11 22:42:02 -05:00
Charlie Marsh
558883299a Default to isort's import sort logic (#691) 2022-11-11 22:41:39 -05:00
Charlie Marsh
048a13c795 Add a separate local folder category for imports (#690) 2022-11-11 22:12:48 -05:00
Anders Kaseorg
5a8b7c1d20 Implement flake8-2020 (sys.version, sys.version_info misuse) (#688) 2022-11-11 20:39:37 -05:00
Charlie Marsh
f8932ec12b Add some TODOs around import tracking 2022-11-11 19:07:40 -05:00
Charlie Marsh
2e7878ff48 Bump version to 0.0.112 2022-11-11 17:13:04 -05:00
Anders Kaseorg
5113ded22a Add ruff.__main__ wrapper to allow invocation as ‘python -m ruff’ (#687) 2022-11-11 15:53:42 -05:00
Anders Kaseorg
bf7bf7aa17 Only scan checks once in check_lines (#679) 2022-11-11 13:34:23 -05:00
Charlie Marsh
560c00ff9d Bump version to 0.0.111 2022-11-11 12:38:23 -05:00
Charlie Marsh
befe64a10e Support isort: skip, isort: on, and isort: off (#678) 2022-11-11 12:38:01 -05:00
Charlie Marsh
4eccfdeb69 Fix lambda handling for B010 (#685) 2022-11-11 11:18:23 -05:00
Charlie Marsh
4123ba9851 Add backticks around setattr 2022-11-11 11:08:22 -05:00
Harutaka Kawamura
e727c24f79 Implement autofix for B009 (#684) 2022-11-11 11:06:47 -05:00
Harutaka Kawamura
bd3b40688f Implement B010 (#683) 2022-11-11 10:26:37 -05:00
Charlie Marsh
b5549382a7 Clarify a few settings for isort behavior (#676) 2022-11-10 23:19:51 -05:00
Charlie Marsh
8cf745045f Bump version to 0.0.110 2022-11-10 19:22:45 -05:00
Charlie Marsh
f6992cc98c Add a test utility for running lint checks (#672) 2022-11-10 19:22:00 -05:00
Charlie Marsh
3cc74c0564 Implement import sorting (#633) 2022-11-10 19:05:56 -05:00
Charlie Marsh
887b9aa840 Rename some fixture files (#671) 2022-11-10 17:28:10 -05:00
Charlie Marsh
faf8556a5c Limit Ropey to newlines and carriage returns (#670) 2022-11-10 17:25:30 -05:00
Harutaka Kawamura
1888f6d41b Implement B009 (#669) 2022-11-10 13:52:20 -05:00
114 changed files with 3223 additions and 379 deletions

View File

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

14
Cargo.lock generated
View File

@@ -933,7 +933,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.109-dev.0"
version = "0.0.113-dev.0"
dependencies = [
"anyhow",
"clap 4.0.22",
@@ -1622,6 +1622,12 @@ dependencies = [
"libc",
]
[[package]]
name = "nohash-hasher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "nom"
version = "5.1.2"
@@ -2234,11 +2240,12 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.109"
version = "0.0.113"
dependencies = [
"anyhow",
"assert_cmd",
"bincode",
"bitflags",
"cacache",
"chrono",
"clap 4.0.22",
@@ -2255,6 +2262,7 @@ dependencies = [
"itertools",
"libcst",
"log",
"nohash-hasher",
"notify",
"num-bigint",
"once_cell",
@@ -2279,7 +2287,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.109"
version = "0.0.113"
dependencies = [
"anyhow",
"clap 4.0.22",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.109"
version = "0.0.113"
edition = "2021"
[lib]
@@ -15,6 +15,7 @@ name = "ruff"
[dependencies]
anyhow = { version = "1.0.66" }
bincode = { version = "1.3.3" }
bitflags = { version = "1.3.2" }
chrono = { version = "0.4.21" }
clap = { version = "4.0.1", features = ["derive"] }
colored = { version = "2.0.0" }
@@ -26,13 +27,14 @@ glob = { version = "0.3.0" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "a13ec97dd4eb925bde4d426c6e422582793b260c" }
log = { version = "0.4.17" }
nohash-hasher = { version = "0.2.0" }
notify = { version = "4.0.17" }
num-bigint = { version = "0.4.3" }
once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
rayon = { version = "1.5.3" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "27bf82a2251d7e6ac6cd75e6ad51be12a53d84bb" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "27bf82a2251d7e6ac6cd75e6ad51be12a53d84bb" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "27bf82a2251d7e6ac6cd75e6ad51be12a53d84bb" }
@@ -43,7 +45,7 @@ strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
titlecase = { version = "2.2.1" }
toml = { version = "0.5.9" }
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
update-informer = { version = "0.5.0", default-features = false, features = ["pypi"], optional = true }
walkdir = { version = "2.3.2" }
[target.'cfg(not(target_family = "wasm"))'.dependencies]

View File

@@ -27,9 +27,10 @@ An extremely fast Python linter, written in Rust.
Ruff aims to be orders of magnitude faster than alternative tools while integrating more
functionality behind a single, common interface. Ruff can be used to replace Flake8 (plus a variety
of plugins), [`pydocstyle`](https://pypi.org/project/pydocstyle/), [`yesqa`](https://github.com/asottile/yesqa),
and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/) and [`autoflake`](https://pypi.org/project/autoflake/)
all while executing tens or hundreds of times faster than any individual tool.
of plugins), [`isort`](https://pypi.org/project/isort/), [`pydocstyle`](https://pypi.org/project/pydocstyle/),
[`yesqa`](https://github.com/asottile/yesqa), and even a subset of [`pyupgrade`](https://pypi.org/project/pyupgrade/)
and [`autoflake`](https://pypi.org/project/autoflake/) all while executing tens or hundreds of times
faster than any individual tool.
(Coming from Flake8? Try [`flake8-to-ruff`](https://pypi.org/project/flake8-to-ruff/) to
automatically convert your existing configuration.)
@@ -56,8 +57,9 @@ Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-mu
9. [flake8-print](#flake8-print)
10. [flake8-quotes](#flake8-quotes)
11. [flake8-annotations](#flake8-annotations)
12. [Ruff-specific rules](#ruff-specific-rules)
13. [Meta rules](#meta-rules)
12. [flake8-2020](#flake8-2020)
13. [Ruff-specific rules](#ruff-specific-rules)
14. [Meta rules](#meta-rules)
5. [Editor Integrations](#editor-integrations)
6. [FAQ](#faq)
7. [Development](#development)
@@ -97,7 +99,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.109
rev: v0.0.113
hooks:
- id: ruff
```
@@ -285,16 +287,16 @@ Ruff supports several workflows to aid in `noqa` management.
First, Ruff provides a special error code, `M001`, to enforce that your `noqa` directives are
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
thus suppressed). **You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
directives.**
thus suppressed). You can run `ruff /path/to/file.py --extend-select M001` to flag unused `noqa`
directives.
Second, Ruff can _automatically remove_ unused `noqa` directives via its autofix functionality.
**You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
`noqa` directives.**
You can run `ruff /path/to/file.py --extend-select M001 --fix` to automatically remove unused
`noqa` directives.
Third, Ruff can _automatically add_ `noqa` directives to all failing lines. This is useful when
migrating a new codebase to Ruff. **You can run `ruff /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate error codes.**
migrating a new codebase to Ruff. You can run `ruff /path/to/file.py --add-noqa` to automatically
add `noqa` directives to all failing lines, with the appropriate error codes.
## Supported Rules
@@ -365,6 +367,14 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| W292 | NoNewLineAtEndOfFile | No newline at end of file | |
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | |
### isort
For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 |
### pydocstyle
For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
@@ -492,6 +502,8 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
| B006 | MutableArgumentDefault | Do not use mutable data structures for argument defaults. | |
| B007 | UnusedLoopControlVariable | Loop control variable `i` not used within the loop body. | 🛠 |
| B008 | FunctionCallArgumentDefault | Do not perform function calls in argument defaults. | |
| B009 | GetAttrWithConstant | Do not call `getattr` with a constant attribute value, it is not any safer than normal property access. | 🛠 |
| B010 | SetAttrWithConstant | Do not call `setattr` with a constant attribute value, it is not any safer than normal property access. | |
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B013 | RedundantTupleInExceptionHandler | A length-one tuple literal is redundant. Write `except ValueError:` instead of `except (ValueError,):`. | |
| B014 | DuplicateHandlerException | Exception handler with duplicate exception: `ValueError` | 🛠 |
@@ -550,6 +562,23 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2
| ANN206 | MissingReturnTypeClassMethod | Missing return type annotation for classmethod `...` | |
| ANN401 | DynamicallyTypedExpression | Dynamically typed expressions (typing.Any) are disallowed in `...` | |
### flake8-2020
For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| YTT101 | SysVersionSlice3Referenced | `sys.version[:3]` referenced (python3.10), use `sys.version_info` | |
| YTT102 | SysVersion2Referenced | `sys.version[2]` referenced (python3.10), use `sys.version_info` | |
| YTT103 | SysVersionCmpStr3 | `sys.version` compared to string (python3.10), use `sys.version_info` | |
| YTT201 | SysVersionInfo0Eq3Referenced | `sys.version_info[0] == 3` referenced (python4), use `>=` | |
| YTT202 | SixPY3Referenced | `six.PY3` referenced (python4), use `not six.PY2` | |
| YTT203 | SysVersionInfo1CmpInt | `sys.version_info[1]` compared to integer (python4), compare `sys.version_info` to tuple | |
| YTT204 | SysVersionInfoMinorCmpInt | `sys.version_info.minor` compared to integer (python4), compare `sys.version_info` to tuple | |
| YTT301 | SysVersion0Referenced | `sys.version[0]` referenced (python10), use `sys.version_info` | |
| YTT302 | SysVersionCmpStr10 | `sys.version` compared to string (python10), use `sys.version_info` | |
| YTT303 | SysVersionSlice1Referenced | `sys.version[:1]` referenced (python10), use `sys.version_info` | |
### Ruff-specific rules
| Code | Name | Message | Fix |
@@ -655,7 +684,8 @@ including:
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (18/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (20/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34)
- [`autoflake`](https://pypi.org/project/autoflake/) (1/7)
@@ -678,9 +708,10 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-annotations`](https://pypi.org/project/flake8-annotations/)
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (18/32)
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (20/32)
- [`flake8-2020`](https://pypi.org/project/flake8-2020/)
Ruff also implements the functionality that you get from [`yesqa`](https://github.com/asottile/yesqa),
Ruff can also replace [`isort`](https://pypi.org/project/isort/), [`yesqa`](https://github.com/asottile/yesqa),
and a subset of the rules implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (14/34).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
@@ -701,6 +732,34 @@ on Rust at all.
Ruff does not yet support third-party plugins, though a plugin system is within-scope for the
project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
Ruff's import sorting is intended to be equivalent to `isort` when used `profile = "black"`, and a
few other settings (`combine_as_imports = true`, `order_by_type = false`, and
`case_sensitive` = true`).
Like `isort`, Ruff's import sorting is compatible with Black.
Ruff is less configurable than `isort`, but supports the `known-first-party`, `known-third-party`,
`extra-standard-library`, and `src` settings, like so:
```toml
[tool.ruff]
select = [
# Pyflakes
"F",
# Pycodestyle
"E",
"W",
# isort
"I"
]
src = ["src", "tests"]
[tool.ruff.isort]
known-first-party = ["my_module1", "my_module2"]
```
### Does Ruff support NumPy- or Google-style docstrings?
Yes! To enable a specific docstring convention, start by enabling all `pydocstyle` error codes, and

View File

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

View File

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

View File

@@ -208,6 +208,7 @@ mod tests {
let actual = convert(&HashMap::from([]), None)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -224,6 +225,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -239,6 +241,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -255,6 +258,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -270,6 +274,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: Some(100),
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -286,6 +291,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -301,6 +307,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -317,6 +324,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -332,6 +340,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -353,6 +362,7 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -371,6 +381,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -422,6 +433,7 @@ mod tests {
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);
@@ -437,6 +449,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
line_length: None,
src: None,
fix: None,
exclude: None,
extend_exclude: None,
@@ -459,6 +472,7 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
isort: None,
pep8_naming: None,
});
assert_eq!(actual, expected);

View File

@@ -32,3 +32,7 @@ build-backend = "maturin"
bindings = "bin"
sdist-include = ["Cargo.lock"]
strip = true
[tool.isort]
profile = "black"
known_third_party = ["fastapi", "pydantic", "starlette"]

36
resources/test/fixtures/B009_B010.py vendored Normal file
View File

@@ -0,0 +1,36 @@
"""
Should emit:
B009 - Line 18, 19, 20, 21, 22
B010 - Line 33, 34, 35, 36
"""
# Valid getattr usage
getattr(foo, bar)
getattr(foo, "bar", None)
getattr(foo, "bar{foo}".format(foo="a"), None)
getattr(foo, "bar{foo}".format(foo="a"))
getattr(foo, bar, None)
getattr(foo, "123abc")
getattr(foo, r"123\abc")
getattr(foo, "except")
# Invalid usage
getattr(foo, "bar")
getattr(foo, "_123abc")
getattr(foo, "abc123")
getattr(foo, r"abc123")
_ = lambda x: getattr(x, "bar")
# Valid setattr usage
setattr(foo, bar, None)
setattr(foo, "bar{foo}".format(foo="a"), None)
setattr(foo, "123abc", None)
setattr(foo, r"123\abc", None)
setattr(foo, "except", None)
_ = lambda x: setattr(x, "bar", 1)
# Invalid usage
setattr(foo, "bar", None)
setattr(foo, "_123abc", None)
setattr(foo, "abc123", None)
setattr(foo, r"abc123", None)

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

@@ -0,0 +1,13 @@
import sys
from sys import version, version as v
print(sys.version)
print(sys.version[:3])
print(version[:3])
# ignore from imports with aliases, patches welcome
print(v[:3])
# the tool is timid and only flags certain numeric slices
i = 3
print(sys.version[:i])

5
resources/test/fixtures/YTT102.py vendored Normal file
View File

@@ -0,0 +1,5 @@
import sys
from sys import version
py_minor = sys.version[2]
py_minor = version[2]

8
resources/test/fixtures/YTT103.py vendored Normal file
View File

@@ -0,0 +1,8 @@
import sys
from sys import version
version < "3.5"
sys.version < "3.5"
sys.version <= "3.5"
sys.version > "3.5"
sys.version >= "3.5"

10
resources/test/fixtures/YTT201.py vendored Normal file
View File

@@ -0,0 +1,10 @@
import sys
from sys import version_info
print("{}.{}".format(*sys.version_info))
PY3 = sys.version_info[0] >= 3
PY3 = sys.version_info[0] == 3
PY3 = version_info[0] == 3
PY2 = sys.version_info[0] != 3
PY2 = version_info[0] != 3

7
resources/test/fixtures/YTT202.py vendored Normal file
View File

@@ -0,0 +1,7 @@
import six
from six import PY3
if six.PY3:
print("3")
if PY3:
print("3")

5
resources/test/fixtures/YTT203.py vendored Normal file
View File

@@ -0,0 +1,5 @@
import sys
from sys import version_info
sys.version_info[1] >= 5
version_info[1] < 6

5
resources/test/fixtures/YTT204.py vendored Normal file
View File

@@ -0,0 +1,5 @@
import sys
from sys import version_info
sys.version_info.minor <= 7
version_info.minor > 8

5
resources/test/fixtures/YTT301.py vendored Normal file
View File

@@ -0,0 +1,5 @@
import sys
from sys import version
py_major = sys.version[0]
py_major = version[0]

8
resources/test/fixtures/YTT302.py vendored Normal file
View File

@@ -0,0 +1,8 @@
import sys
from sys import version
version < "3"
sys.version < "3"
sys.version <= "3"
sys.version > "3"
sys.version >= "3"

5
resources/test/fixtures/YTT303.py vendored Normal file
View File

@@ -0,0 +1,5 @@
import sys
from sys import version
print(sys.version[:1])
print(version[:1])

View File

@@ -1,9 +0,0 @@
a = "abc"
b = f"ghi{'jkl'}"
c = f"def"
d = f"def" + "ghi"
e = (
f"def" +
"ghi"
)

View File

@@ -0,0 +1,5 @@
from collections import Awaitable
from collections import AsyncIterable
from collections import Collection
from collections import ChainMap
from collections import MutableSequence, MutableMapping

View File

@@ -0,0 +1,4 @@
import os
import os
import os as os1
import os as os2

View File

@@ -0,0 +1 @@
from collections import Collection

View File

@@ -0,0 +1,2 @@
from collections import Collection
import os

View File

@@ -0,0 +1,6 @@
x = 1; import sys
import os
if True:
x = 1; import sys
import os

View File

@@ -0,0 +1,3 @@
# OK
import os
import sys

View File

@@ -0,0 +1,12 @@
import StringIO
import glob
import os
import shutil
import tempfile
import time
from subprocess import PIPE, Popen, STDOUT
from module import Class, CONSTANT, function, BASIC, Apple
import foo
import FOO
import BAR
import bar

View File

@@ -0,0 +1,6 @@
if True:
import sys
import os
else:
import sys
import os

View File

@@ -0,0 +1,2 @@
[tool.ruff]
line-length = 88

View File

@@ -0,0 +1,2 @@
import sys
import os

View File

@@ -0,0 +1,5 @@
import sys
import leading_prefix
import numpy as np
import os
from leading_prefix import Class

View File

@@ -0,0 +1,3 @@
import sys
import os
from __future__ import annotations

View File

@@ -0,0 +1,4 @@
import sys
import leading_prefix
import os
from . import leading_prefix

View File

@@ -0,0 +1,4 @@
import pandas as pd
import sys
import numpy as np
import os

10
resources/test/fixtures/isort/skip.py vendored Normal file
View File

@@ -0,0 +1,10 @@
# isort: off
import sys
import os
import collections
# isort: on
import sys
import os # isort: skip
import collections
import abc

View File

@@ -0,0 +1,6 @@
import sys
import os; x = 1
if True:
import sys
import os; x = 1

View File

@@ -1,9 +1,9 @@
[tool.ruff]
line-length = 88
extend-exclude = [
"excluded.py",
"excluded_file.py",
"migrations",
"directory/also_excluded.py",
"with_excluded_file/other_excluded_file.py",
]
per-file-ignores = { "__init__.py" = ["F401"] }

7
ruff/__main__.py Normal file
View File

@@ -0,0 +1,7 @@
import os
import sys
import sysconfig
if __name__ == "__main__":
ruff = os.path.join(sysconfig.get_path("scripts"), "ruff")
os.spawnv(os.P_WAIT, ruff, [ruff, *sys.argv[1:]])

View File

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

View File

@@ -46,6 +46,7 @@ pub enum ScopeKind<'a> {
Generator,
Module,
Arg,
Lambda,
}
#[derive(Clone, Debug)]

View File

@@ -32,17 +32,19 @@ use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
use crate::visibility::{module_visibility, transition_scope, Modifier, Visibility, VisibleScope};
use crate::{
docstrings, flake8_annotations, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_print, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade,
docstrings, flake8_2020, flake8_annotations, flake8_bugbear, flake8_builtins,
flake8_comprehensions, flake8_print, pep8_naming, pycodestyle, pydocstyle, pyflakes, pyupgrade,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
const TRACK_FROM_IMPORTS: [&str; 10] = [
const TRACK_FROM_IMPORTS: [&str; 12] = [
"collections",
"collections.abc",
"contextlib",
"functools",
"re",
"six",
"sys",
"typing",
"typing.io",
"typing.re",
@@ -115,6 +117,7 @@ impl<'a> Checker<'a> {
deferred_functions: Default::default(),
deferred_lambdas: Default::default(),
deferred_assignments: Default::default(),
// Internal, derivative state.
visible_scope: VisibleScope {
modifier: Modifier::Module,
visibility: module_visibility(path),
@@ -983,6 +986,14 @@ where
if self.match_typing_module(value, "Literal") {
self.in_literal = true;
}
if self.settings.enabled.contains(&CheckCode::YTT101)
|| self.settings.enabled.contains(&CheckCode::YTT102)
|| self.settings.enabled.contains(&CheckCode::YTT301)
|| self.settings.enabled.contains(&CheckCode::YTT303)
{
flake8_2020::plugins::subscript(self, value, slice);
}
}
ExprKind::Tuple { elts, ctx } | ExprKind::List { elts, ctx } => {
if matches!(ctx, ExprContext::Store) {
@@ -1000,34 +1011,40 @@ where
}
}
}
ExprKind::Name { id, ctx } => match ctx {
ExprContext::Load => {
// Ex) List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, self.from_imports.get("typing"))
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
self.handle_node_load(expr);
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
self.add_check(check);
ExprKind::Name { id, ctx } => {
match ctx {
ExprContext::Load => {
// Ex) List[...]
if self.settings.enabled.contains(&CheckCode::U006)
&& self.settings.target_version >= PythonVersion::Py39
&& typing::is_pep585_builtin(expr, self.from_imports.get("typing"))
{
pyupgrade::plugins::use_pep585_annotation(self, expr, id);
}
self.handle_node_load(expr);
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
self.add_check(check);
}
}
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.check_builtin_shadowing(id, Range::from_located(expr), true);
self.handle_node_store(expr, self.current_parent());
self.handle_node_store(expr, self.current_parent());
}
ExprContext::Del => self.handle_node_delete(expr),
}
ExprContext::Del => self.handle_node_delete(expr),
},
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
}
ExprKind::Attribute { attr, .. } => {
// Ex) typing.List[...]
if self.settings.enabled.contains(&CheckCode::U006)
@@ -1036,6 +1053,10 @@ where
{
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
}
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
}
ExprKind::Call {
func,
@@ -1064,6 +1085,19 @@ where
if self.settings.enabled.contains(&CheckCode::B005) {
flake8_bugbear::plugins::strip_with_multi_characters(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::B009) {
flake8_bugbear::plugins::getattr_with_constant(self, expr, func, args);
}
if self.settings.enabled.contains(&CheckCode::B010) {
if !self
.scope_stack
.iter()
.rev()
.any(|index| matches!(self.scopes[*index].kind, ScopeKind::Lambda))
{
flake8_bugbear::plugins::setattr_with_constant(self, expr, func, args);
}
}
if self.settings.enabled.contains(&CheckCode::B026) {
flake8_bugbear::plugins::star_arg_unpacking_after_keyword_arg(
self, args, keywords,
@@ -1408,6 +1442,15 @@ where
.into_iter(),
);
}
if self.settings.enabled.contains(&CheckCode::YTT103)
|| self.settings.enabled.contains(&CheckCode::YTT201)
|| self.settings.enabled.contains(&CheckCode::YTT203)
|| self.settings.enabled.contains(&CheckCode::YTT204)
|| self.settings.enabled.contains(&CheckCode::YTT302)
{
flake8_2020::plugins::compare(self, left, ops, comparators);
}
}
ExprKind::Constant {
value: Constant::Str(value),
@@ -1451,8 +1494,8 @@ where
for expr in &args.defaults {
self.visit_expr(expr);
}
self.push_scope(Scope::new(ScopeKind::Lambda))
}
ExprKind::ListComp { elt, generators } | ExprKind::SetComp { elt, generators } => {
if self.settings.enabled.contains(&CheckCode::C416) {
if let Some(check) = flake8_comprehensions::checks::unnecessary_comprehension(
@@ -1468,7 +1511,6 @@ where
}
self.push_scope(Scope::new(ScopeKind::Generator))
}
ExprKind::GeneratorExp { .. } | ExprKind::DictComp { .. } => {
self.push_scope(Scope::new(ScopeKind::Generator))
}
@@ -1639,7 +1681,8 @@ where
// Post-visit.
match &expr.node {
ExprKind::GeneratorExp { .. }
ExprKind::Lambda { .. }
| ExprKind::GeneratorExp { .. }
| ExprKind::ListComp { .. }
| ExprKind::DictComp { .. }
| ExprKind::SetComp { .. } => {
@@ -2231,7 +2274,7 @@ impl<'a> Checker<'a> {
while let Some((expr, scopes, parents)) = self.deferred_lambdas.pop() {
self.parent_stack = parents;
self.scope_stack = scopes;
self.push_scope(Scope::new(ScopeKind::Function(Default::default())));
self.push_scope(Scope::new(ScopeKind::Lambda));
if let ExprKind::Lambda { args, body } = &expr.node {
self.visit_arguments(args);
@@ -2588,5 +2631,8 @@ pub fn check_ast(
// Check docstrings.
checker.check_definitions();
// Check import blocks.
// checker.check_import_blocks();
checker.checks
}

43
src/check_imports.rs Normal file
View File

@@ -0,0 +1,43 @@
//! Lint rules based on import analysis.
use nohash_hasher::IntSet;
use rustpython_parser::ast::Suite;
use crate::ast::visitor::Visitor;
use crate::autofix::fixer;
use crate::checks::Check;
use crate::isort;
use crate::isort::track::ImportTracker;
use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
fn check_import_blocks(
tracker: ImportTracker,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: &fixer::Mode,
) -> Vec<Check> {
let mut checks = vec![];
for block in tracker.into_iter() {
if !block.is_empty() {
if let Some(check) = isort::plugins::check_imports(block, locator, settings, autofix) {
checks.push(check);
}
}
}
checks
}
pub fn check_imports(
python_ast: &Suite,
locator: &SourceCodeLocator,
exclusions: &IntSet<usize>,
settings: &Settings,
autofix: &fixer::Mode,
) -> Vec<Check> {
let mut tracker = ImportTracker::new(exclusions);
for stmt in python_ast {
tracker.visit_stmt(stmt);
}
check_import_blocks(tracker, locator, settings, autofix)
}

View File

@@ -1,7 +1,6 @@
//! Lint rules based on checking raw physical lines.
use std::collections::BTreeMap;
use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::ast::Location;
@@ -36,7 +35,7 @@ fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
pub fn check_lines(
checks: &mut Vec<Check>,
contents: &str,
noqa_line_for: &[usize],
noqa_line_for: &IntMap<usize, usize>,
settings: &Settings,
autofix: &fixer::Mode,
) {
@@ -44,18 +43,23 @@ pub fn check_lines(
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
let enforce_noqa = settings.enabled.contains(&CheckCode::M001);
let mut noqa_directives: BTreeMap<usize, (Directive, Vec<&str>)> = BTreeMap::new();
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
let mut line_checks = vec![];
let mut ignored = vec![];
checks.sort_by_key(|check| check.location);
let mut checks_iter = checks.iter().enumerate().peekable();
if let Some((_index, check)) = checks_iter.peek() {
assert!(check.location.row() >= 1);
}
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(lineno)
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
@@ -90,26 +94,25 @@ pub fn check_lines(
}
// Remove any ignored checks.
// TODO(charlie): Only validate checks for the current line.
for (index, check) in checks.iter().enumerate() {
if check.location.row() == lineno + 1 {
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
while let Some((index, check)) =
checks_iter.next_if(|(_index, check)| check.location.row() == lineno + 1)
{
let noqa = noqa_directives
.entry(noqa_lineno)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
match noqa {
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
ignored.push(index)
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
(Directive::None, _) => {}
match noqa {
(Directive::All(..), matches) => {
matches.push(check.kind.code().as_ref());
ignored.push(index)
}
(Directive::Codes(_, _, codes), matches) => {
if codes.contains(&check.kind.code().as_ref()) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
(Directive::None, _) => {}
}
}
@@ -153,7 +156,7 @@ pub fn check_lines(
if let Some(line) = lines.last() {
let lineno = lines.len() - 1;
let noqa_lineno = noqa_line_for
.get(lineno)
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
@@ -257,6 +260,8 @@ pub fn check_lines(
#[cfg(test)]
mod tests {
use nohash_hasher::IntMap;
use super::check_lines;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
@@ -265,7 +270,7 @@ mod tests {
#[test]
fn e501_non_ascii_char() {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let noqa_line_for: Vec<usize> = vec![1];
let noqa_line_for: IntMap<usize, usize> = Default::default();
let check_with_max_line_length = |line_length: usize| {
let mut checks: Vec<Check> = vec![];
check_lines(

View File

@@ -84,6 +84,8 @@ pub enum CheckCode {
B006,
B007,
B008,
B009,
B010,
B011,
B013,
B014,
@@ -130,6 +132,17 @@ pub enum CheckCode {
ANN205,
ANN206,
ANN401,
// flake8-2020
YTT101,
YTT102,
YTT103,
YTT201,
YTT202,
YTT203,
YTT204,
YTT301,
YTT302,
YTT303,
// pyupgrade
U001,
U002,
@@ -203,6 +216,8 @@ pub enum CheckCode {
N816,
N817,
N818,
// isort
I001,
// Ruff
RUF001,
RUF002,
@@ -215,6 +230,7 @@ pub enum CheckCode {
pub enum CheckCategory {
Pyflakes,
Pycodestyle,
Isort,
Pydocstyle,
Pyupgrade,
PEP8Naming,
@@ -224,6 +240,7 @@ pub enum CheckCategory {
Flake8Print,
Flake8Quotes,
Flake8Annotations,
Flake82020,
Ruff,
Meta,
}
@@ -233,12 +250,14 @@ impl CheckCategory {
match self {
CheckCategory::Pycodestyle => "pycodestyle",
CheckCategory::Pyflakes => "Pyflakes",
CheckCategory::Isort => "isort",
CheckCategory::Flake8Builtins => "flake8-builtins",
CheckCategory::Flake8Bugbear => "flake8-bugbear",
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
CheckCategory::Flake8Annotations => "flake8-annotations",
CheckCategory::Flake82020 => "flake8-2020",
CheckCategory::Pyupgrade => "pyupgrade",
CheckCategory::Pydocstyle => "pydocstyle",
CheckCategory::PEP8Naming => "pep8-naming",
@@ -251,6 +270,7 @@ impl CheckCategory {
match self {
CheckCategory::Pycodestyle => Some("https://pypi.org/project/pycodestyle/2.9.1/"),
CheckCategory::Pyflakes => Some("https://pypi.org/project/pyflakes/2.5.0/"),
CheckCategory::Isort => Some("https://pypi.org/project/isort/5.10.1/"),
CheckCategory::Flake8Builtins => {
Some("https://pypi.org/project/flake8-builtins/2.0.1/")
}
@@ -265,6 +285,7 @@ impl CheckCategory {
CheckCategory::Flake8Annotations => {
Some("https://pypi.org/project/flake8-annotations/2.9.1/")
}
CheckCategory::Flake82020 => Some("https://pypi.org/project/flake8-2020/1.7.0/"),
CheckCategory::Pyupgrade => Some("https://pypi.org/project/pyupgrade/3.2.0/"),
CheckCategory::Pydocstyle => Some("https://pypi.org/project/pydocstyle/6.1.1/"),
CheckCategory::PEP8Naming => Some("https://pypi.org/project/pep8-naming/0.13.2/"),
@@ -280,6 +301,7 @@ pub enum LintSource {
FileSystem,
Lines,
Tokens,
Imports,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -349,6 +371,8 @@ pub enum CheckKind {
MutableArgumentDefault,
UnusedLoopControlVariable(String),
FunctionCallArgumentDefault,
GetAttrWithConstant,
SetAttrWithConstant,
DoNotAssertFalse,
RedundantTupleInExceptionHandler(String),
DuplicateHandlerException(Vec<String>),
@@ -395,6 +419,17 @@ pub enum CheckKind {
MissingReturnTypeStaticMethod(String),
MissingReturnTypeClassMethod(String),
DynamicallyTypedExpression(String),
// flake8-2020
SysVersionSlice3Referenced,
SysVersion2Referenced,
SysVersionCmpStr3,
SysVersionInfo0Eq3Referenced,
SixPY3Referenced,
SysVersionInfo1CmpInt,
SysVersionInfoMinorCmpInt,
SysVersion0Referenced,
SysVersionCmpStr10,
SysVersionSlice1Referenced,
// pyupgrade
TypeOfPrimitive(Primitive),
UnnecessaryAbspath,
@@ -468,6 +503,8 @@ pub enum CheckKind {
MixedCaseVariableInGlobalScope(String),
CamelcaseImportedAsAcronym(String, String),
ErrorSuffixOnExceptionName(String),
// isort
UnsortedImports,
// Ruff
AmbiguousUnicodeCharacterString(char, char),
AmbiguousUnicodeCharacterDocstring(char, char),
@@ -493,6 +530,7 @@ impl CheckCode {
| CheckCode::RUF002
| CheckCode::RUF003 => &LintSource::Tokens,
CheckCode::E902 => &LintSource::FileSystem,
CheckCode::I001 => &LintSource::Imports,
_ => &LintSource::AST,
}
}
@@ -561,6 +599,8 @@ impl CheckCode {
CheckCode::B006 => CheckKind::MutableArgumentDefault,
CheckCode::B007 => CheckKind::UnusedLoopControlVariable("i".to_string()),
CheckCode::B008 => CheckKind::FunctionCallArgumentDefault,
CheckCode::B009 => CheckKind::GetAttrWithConstant,
CheckCode::B010 => CheckKind::SetAttrWithConstant,
CheckCode::B011 => CheckKind::DoNotAssertFalse,
CheckCode::B013 => {
CheckKind::RedundantTupleInExceptionHandler("ValueError".to_string())
@@ -622,6 +662,17 @@ impl CheckCode {
CheckCode::ANN205 => CheckKind::MissingReturnTypeStaticMethod("...".to_string()),
CheckCode::ANN206 => CheckKind::MissingReturnTypeClassMethod("...".to_string()),
CheckCode::ANN401 => CheckKind::DynamicallyTypedExpression("...".to_string()),
// flake8-2020
CheckCode::YTT101 => CheckKind::SysVersionSlice3Referenced,
CheckCode::YTT102 => CheckKind::SysVersion2Referenced,
CheckCode::YTT103 => CheckKind::SysVersionCmpStr3,
CheckCode::YTT201 => CheckKind::SysVersionInfo0Eq3Referenced,
CheckCode::YTT202 => CheckKind::SixPY3Referenced,
CheckCode::YTT203 => CheckKind::SysVersionInfo1CmpInt,
CheckCode::YTT204 => CheckKind::SysVersionInfoMinorCmpInt,
CheckCode::YTT301 => CheckKind::SysVersion0Referenced,
CheckCode::YTT302 => CheckKind::SysVersionCmpStr10,
CheckCode::YTT303 => CheckKind::SysVersionSlice1Referenced,
// pyupgrade
CheckCode::U001 => CheckKind::UselessMetaclassType,
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
@@ -714,6 +765,8 @@ impl CheckCode {
CheckKind::CamelcaseImportedAsAcronym("...".to_string(), "...".to_string())
}
CheckCode::N818 => CheckKind::ErrorSuffixOnExceptionName("...".to_string()),
// isort
CheckCode::I001 => CheckKind::UnsortedImports,
// Ruff
CheckCode::RUF001 => CheckKind::AmbiguousUnicodeCharacterString('𝐁', 'B'),
CheckCode::RUF002 => CheckKind::AmbiguousUnicodeCharacterDocstring('𝐁', 'B'),
@@ -779,6 +832,8 @@ impl CheckCode {
CheckCode::B006 => CheckCategory::Flake8Bugbear,
CheckCode::B007 => CheckCategory::Flake8Bugbear,
CheckCode::B008 => CheckCategory::Flake8Bugbear,
CheckCode::B009 => CheckCategory::Flake8Bugbear,
CheckCode::B010 => CheckCategory::Flake8Bugbear,
CheckCode::B011 => CheckCategory::Flake8Bugbear,
CheckCode::B013 => CheckCategory::Flake8Bugbear,
CheckCode::B014 => CheckCategory::Flake8Bugbear,
@@ -821,6 +876,16 @@ impl CheckCode {
CheckCode::ANN205 => CheckCategory::Flake8Annotations,
CheckCode::ANN206 => CheckCategory::Flake8Annotations,
CheckCode::ANN401 => CheckCategory::Flake8Annotations,
CheckCode::YTT101 => CheckCategory::Flake82020,
CheckCode::YTT102 => CheckCategory::Flake82020,
CheckCode::YTT103 => CheckCategory::Flake82020,
CheckCode::YTT201 => CheckCategory::Flake82020,
CheckCode::YTT202 => CheckCategory::Flake82020,
CheckCode::YTT203 => CheckCategory::Flake82020,
CheckCode::YTT204 => CheckCategory::Flake82020,
CheckCode::YTT301 => CheckCategory::Flake82020,
CheckCode::YTT302 => CheckCategory::Flake82020,
CheckCode::YTT303 => CheckCategory::Flake82020,
CheckCode::U001 => CheckCategory::Pyupgrade,
CheckCode::U002 => CheckCategory::Pyupgrade,
CheckCode::U003 => CheckCategory::Pyupgrade,
@@ -891,6 +956,7 @@ impl CheckCode {
CheckCode::N816 => CheckCategory::PEP8Naming,
CheckCode::N817 => CheckCategory::PEP8Naming,
CheckCode::N818 => CheckCategory::PEP8Naming,
CheckCode::I001 => CheckCategory::Isort,
CheckCode::RUF001 => CheckCategory::Ruff,
CheckCode::RUF002 => CheckCategory::Ruff,
CheckCode::RUF003 => CheckCategory::Ruff,
@@ -961,6 +1027,8 @@ impl CheckKind {
CheckKind::MutableArgumentDefault => &CheckCode::B006,
CheckKind::UnusedLoopControlVariable(_) => &CheckCode::B007,
CheckKind::FunctionCallArgumentDefault => &CheckCode::B008,
CheckKind::GetAttrWithConstant => &CheckCode::B009,
CheckKind::SetAttrWithConstant => &CheckCode::B010,
CheckKind::DoNotAssertFalse => &CheckCode::B011,
CheckKind::RedundantTupleInExceptionHandler(_) => &CheckCode::B013,
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
@@ -1007,6 +1075,17 @@ impl CheckKind {
CheckKind::MissingReturnTypeStaticMethod(_) => &CheckCode::ANN205,
CheckKind::MissingReturnTypeClassMethod(_) => &CheckCode::ANN206,
CheckKind::DynamicallyTypedExpression(_) => &CheckCode::ANN401,
// flake8-2020
CheckKind::SysVersionSlice3Referenced => &CheckCode::YTT101,
CheckKind::SysVersion2Referenced => &CheckCode::YTT102,
CheckKind::SysVersionCmpStr3 => &CheckCode::YTT103,
CheckKind::SysVersionInfo0Eq3Referenced => &CheckCode::YTT201,
CheckKind::SixPY3Referenced => &CheckCode::YTT202,
CheckKind::SysVersionInfo1CmpInt => &CheckCode::YTT203,
CheckKind::SysVersionInfoMinorCmpInt => &CheckCode::YTT204,
CheckKind::SysVersion0Referenced => &CheckCode::YTT301,
CheckKind::SysVersionCmpStr10 => &CheckCode::YTT302,
CheckKind::SysVersionSlice1Referenced => &CheckCode::YTT303,
// pyupgrade
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
@@ -1080,6 +1159,8 @@ impl CheckKind {
CheckKind::MixedCaseVariableInGlobalScope(..) => &CheckCode::N816,
CheckKind::CamelcaseImportedAsAcronym(..) => &CheckCode::N817,
CheckKind::ErrorSuffixOnExceptionName(..) => &CheckCode::N818,
// isort
CheckKind::UnsortedImports => &CheckCode::I001,
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(..) => &CheckCode::RUF001,
CheckKind::AmbiguousUnicodeCharacterDocstring(..) => &CheckCode::RUF002,
@@ -1264,6 +1345,14 @@ impl CheckKind {
CheckKind::FunctionCallArgumentDefault => {
"Do not perform function calls in argument defaults.".to_string()
}
CheckKind::GetAttrWithConstant => "Do not call `getattr` with a constant attribute \
value, it is not any safer than normal property \
access."
.to_string(),
CheckKind::SetAttrWithConstant => "Do not call `setattr` with a constant attribute \
value, it is not any safer than normal property \
access."
.to_string(),
CheckKind::DoNotAssertFalse => "Do not `assert False` (`python -O` removes these \
calls), raise `AssertionError()`"
.to_string(),
@@ -1441,6 +1530,38 @@ impl CheckKind {
CheckKind::DynamicallyTypedExpression(name) => {
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
// flake8-2020
CheckKind::SysVersionSlice3Referenced => {
"`sys.version[:3]` referenced (python3.10), use `sys.version_info`".to_string()
}
CheckKind::SysVersion2Referenced => {
"`sys.version[2]` referenced (python3.10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionCmpStr3 => {
"`sys.version` compared to string (python3.10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionInfo0Eq3Referenced => {
"`sys.version_info[0] == 3` referenced (python4), use `>=`".to_string()
}
CheckKind::SixPY3Referenced => {
"`six.PY3` referenced (python4), use `not six.PY2`".to_string()
}
CheckKind::SysVersionInfo1CmpInt => "`sys.version_info[1]` compared to integer \
(python4), compare `sys.version_info` to tuple"
.to_string(),
CheckKind::SysVersionInfoMinorCmpInt => "`sys.version_info.minor` compared to integer \
(python4), compare `sys.version_info` to \
tuple"
.to_string(),
CheckKind::SysVersion0Referenced => {
"`sys.version[0]` referenced (python10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionCmpStr10 => {
"`sys.version` compared to string (python10), use `sys.version_info`".to_string()
}
CheckKind::SysVersionSlice1Referenced => {
"`sys.version[:1]` referenced (python10), use `sys.version_info`".to_string()
}
// pyupgrade
CheckKind::TypeOfPrimitive(primitive) => {
format!("Use `{}` instead of `type(...)`", primitive.builtin())
@@ -1635,6 +1756,8 @@ impl CheckKind {
CheckKind::PEP3120UnnecessaryCodingComment => {
"utf-8 encoding declaration is unnecessary".to_string()
}
// isort
CheckKind::UnsortedImports => "Import block is un-sorted or un-formatted".to_string(),
// Ruff
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant) => {
format!(
@@ -1709,6 +1832,7 @@ impl CheckKind {
| CheckKind::DeprecatedUnittestAlias(_, _)
| CheckKind::DoNotAssertFalse
| CheckKind::DuplicateHandlerException(_)
| CheckKind::GetAttrWithConstant
| CheckKind::IsLiteral
| CheckKind::NewLineAfterLastParagraph
| CheckKind::NewLineAfterSectionName(_)
@@ -1740,12 +1864,13 @@ impl CheckKind {
| CheckKind::UnnecessaryGeneratorSet
| CheckKind::UnnecessaryLRUCacheParams
| CheckKind::UnnecessaryListCall
| CheckKind::UnnecessaryListComprehensionSet
| CheckKind::UnnecessaryListComprehensionDict
| CheckKind::UnnecessaryListComprehensionSet
| CheckKind::UnnecessaryLiteralDict(_)
| CheckKind::UnnecessaryLiteralSet(_)
| CheckKind::UnnecessaryLiteralWithinListCall(_)
| CheckKind::UnnecessaryLiteralWithinTupleCall(_)
| CheckKind::UnsortedImports
| CheckKind::UnusedImport(_, false)
| CheckKind::UnusedLoopControlVariable(_)
| CheckKind::UnusedNOQA(_)

View File

@@ -43,7 +43,9 @@ pub enum CheckCodePrefix {
B006,
B007,
B008,
B009,
B01,
B010,
B011,
B013,
B014,
@@ -202,6 +204,10 @@ pub enum CheckCodePrefix {
F9,
F90,
F901,
I,
I0,
I00,
I001,
M,
M0,
M00,
@@ -265,6 +271,23 @@ pub enum CheckCodePrefix {
W6,
W60,
W605,
YTT,
YTT1,
YTT10,
YTT101,
YTT102,
YTT103,
YTT2,
YTT20,
YTT201,
YTT202,
YTT203,
YTT204,
YTT3,
YTT30,
YTT301,
YTT302,
YTT303,
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
@@ -336,6 +359,8 @@ impl CheckCodePrefix {
CheckCode::B006,
CheckCode::B007,
CheckCode::B008,
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B013,
CheckCode::B014,
@@ -354,6 +379,8 @@ impl CheckCodePrefix {
CheckCode::B006,
CheckCode::B007,
CheckCode::B008,
CheckCode::B009,
CheckCode::B010,
CheckCode::B011,
CheckCode::B013,
CheckCode::B014,
@@ -372,6 +399,7 @@ impl CheckCodePrefix {
CheckCode::B006,
CheckCode::B007,
CheckCode::B008,
CheckCode::B009,
],
CheckCodePrefix::B002 => vec![CheckCode::B002],
CheckCodePrefix::B003 => vec![CheckCode::B003],
@@ -380,7 +408,9 @@ impl CheckCodePrefix {
CheckCodePrefix::B006 => vec![CheckCode::B006],
CheckCodePrefix::B007 => vec![CheckCode::B007],
CheckCodePrefix::B008 => vec![CheckCode::B008],
CheckCodePrefix::B009 => vec![CheckCode::B009],
CheckCodePrefix::B01 => vec![
CheckCode::B010,
CheckCode::B011,
CheckCode::B013,
CheckCode::B014,
@@ -389,6 +419,7 @@ impl CheckCodePrefix {
CheckCode::B017,
CheckCode::B018,
],
CheckCodePrefix::B010 => vec![CheckCode::B010],
CheckCodePrefix::B011 => vec![CheckCode::B011],
CheckCodePrefix::B013 => vec![CheckCode::B013],
CheckCodePrefix::B014 => vec![CheckCode::B014],
@@ -847,6 +878,10 @@ impl CheckCodePrefix {
CheckCodePrefix::F9 => vec![CheckCode::F901],
CheckCodePrefix::F90 => vec![CheckCode::F901],
CheckCodePrefix::F901 => vec![CheckCode::F901],
CheckCodePrefix::I => vec![CheckCode::I001],
CheckCodePrefix::I0 => vec![CheckCode::I001],
CheckCodePrefix::I00 => vec![CheckCode::I001],
CheckCodePrefix::I001 => vec![CheckCode::I001],
CheckCodePrefix::M => vec![CheckCode::M001],
CheckCodePrefix::M0 => vec![CheckCode::M001],
CheckCodePrefix::M00 => vec![CheckCode::M001],
@@ -1008,6 +1043,44 @@ impl CheckCodePrefix {
CheckCodePrefix::W6 => vec![CheckCode::W605],
CheckCodePrefix::W60 => vec![CheckCode::W605],
CheckCodePrefix::W605 => vec![CheckCode::W605],
CheckCodePrefix::YTT => vec![
CheckCode::YTT101,
CheckCode::YTT102,
CheckCode::YTT103,
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
CheckCode::YTT301,
CheckCode::YTT302,
CheckCode::YTT303,
],
CheckCodePrefix::YTT1 => vec![CheckCode::YTT101, CheckCode::YTT102, CheckCode::YTT103],
CheckCodePrefix::YTT10 => vec![CheckCode::YTT101, CheckCode::YTT102, CheckCode::YTT103],
CheckCodePrefix::YTT101 => vec![CheckCode::YTT101],
CheckCodePrefix::YTT102 => vec![CheckCode::YTT102],
CheckCodePrefix::YTT103 => vec![CheckCode::YTT103],
CheckCodePrefix::YTT2 => vec![
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
],
CheckCodePrefix::YTT20 => vec![
CheckCode::YTT201,
CheckCode::YTT202,
CheckCode::YTT203,
CheckCode::YTT204,
],
CheckCodePrefix::YTT201 => vec![CheckCode::YTT201],
CheckCodePrefix::YTT202 => vec![CheckCode::YTT202],
CheckCodePrefix::YTT203 => vec![CheckCode::YTT203],
CheckCodePrefix::YTT204 => vec![CheckCode::YTT204],
CheckCodePrefix::YTT3 => vec![CheckCode::YTT301, CheckCode::YTT302, CheckCode::YTT303],
CheckCodePrefix::YTT30 => vec![CheckCode::YTT301, CheckCode::YTT302, CheckCode::YTT303],
CheckCodePrefix::YTT301 => vec![CheckCode::YTT301],
CheckCodePrefix::YTT302 => vec![CheckCode::YTT302],
CheckCodePrefix::YTT303 => vec![CheckCode::YTT303],
}
}
}
@@ -1051,7 +1124,9 @@ impl CheckCodePrefix {
CheckCodePrefix::B006 => PrefixSpecificity::Explicit,
CheckCodePrefix::B007 => PrefixSpecificity::Explicit,
CheckCodePrefix::B008 => PrefixSpecificity::Explicit,
CheckCodePrefix::B009 => PrefixSpecificity::Explicit,
CheckCodePrefix::B01 => PrefixSpecificity::Tens,
CheckCodePrefix::B010 => PrefixSpecificity::Explicit,
CheckCodePrefix::B011 => PrefixSpecificity::Explicit,
CheckCodePrefix::B013 => PrefixSpecificity::Explicit,
CheckCodePrefix::B014 => PrefixSpecificity::Explicit,
@@ -1210,6 +1285,10 @@ impl CheckCodePrefix {
CheckCodePrefix::F9 => PrefixSpecificity::Hundreds,
CheckCodePrefix::F90 => PrefixSpecificity::Tens,
CheckCodePrefix::F901 => PrefixSpecificity::Explicit,
CheckCodePrefix::I => PrefixSpecificity::Category,
CheckCodePrefix::I0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::I00 => PrefixSpecificity::Tens,
CheckCodePrefix::I001 => PrefixSpecificity::Explicit,
CheckCodePrefix::M => PrefixSpecificity::Category,
CheckCodePrefix::M0 => PrefixSpecificity::Hundreds,
CheckCodePrefix::M00 => PrefixSpecificity::Tens,
@@ -1273,6 +1352,23 @@ impl CheckCodePrefix {
CheckCodePrefix::W6 => PrefixSpecificity::Hundreds,
CheckCodePrefix::W60 => PrefixSpecificity::Tens,
CheckCodePrefix::W605 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT => PrefixSpecificity::Category,
CheckCodePrefix::YTT1 => PrefixSpecificity::Hundreds,
CheckCodePrefix::YTT10 => PrefixSpecificity::Tens,
CheckCodePrefix::YTT101 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT102 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT103 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT2 => PrefixSpecificity::Hundreds,
CheckCodePrefix::YTT20 => PrefixSpecificity::Tens,
CheckCodePrefix::YTT201 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT202 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT203 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT204 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT3 => PrefixSpecificity::Hundreds,
CheckCodePrefix::YTT30 => PrefixSpecificity::Tens,
CheckCodePrefix::YTT301 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT302 => PrefixSpecificity::Explicit,
CheckCodePrefix::YTT303 => PrefixSpecificity::Explicit,
}
}
}

206
src/directives.rs Normal file
View File

@@ -0,0 +1,206 @@
//! Extract `# noqa` and `# isort: skip` directives from tokenized source.
use bitflags::bitflags;
use nohash_hasher::{IntMap, IntSet};
use rustpython_ast::Location;
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::types::Range;
use crate::checks::LintSource;
use crate::{Settings, SourceCodeLocator};
bitflags! {
pub struct Flags: u32 {
const NOQA = 0b00000001;
const ISORT = 0b00000010;
}
}
impl Flags {
pub fn from_settings(settings: &Settings) -> Self {
if settings
.enabled
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::Imports))
{
Flags::NOQA | Flags::ISORT
} else {
Flags::NOQA
}
}
}
pub struct Directives {
pub noqa_line_for: IntMap<usize, usize>,
pub isort_exclusions: IntSet<usize>,
}
pub fn extract_directives(
lxr: &[LexResult],
locator: &SourceCodeLocator,
flags: &Flags,
) -> Directives {
Directives {
noqa_line_for: if flags.contains(Flags::NOQA) {
extract_noqa_line_for(lxr)
} else {
Default::default()
},
isort_exclusions: if flags.contains(Flags::ISORT) {
extract_isort_exclusions(lxr, locator)
} else {
Default::default()
},
}
}
/// Extract a mapping from logical line to noqa line.
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::EndOfFile) {
break;
}
// For multi-line strings, we expect `noqa` directives on the last line of the
// string.
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
for i in start.row()..end.row() {
noqa_line_for.insert(i, end.row());
}
}
}
noqa_line_for
}
/// Extract a set of lines over which to disable isort.
pub fn extract_isort_exclusions(lxr: &[LexResult], locator: &SourceCodeLocator) -> IntSet<usize> {
let mut exclusions: IntSet<usize> = IntSet::default();
let mut off: Option<&Location> = None;
for (start, tok, end) in lxr.iter().flatten() {
// 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);
}
}
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);
}
}
} else if matches!(tok, Tok::EndOfFile) {
if let Some(start) = off {
for row in start.row() + 1..=end.row() {
exclusions.insert(row);
}
}
break;
}
}
exclusions
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use nohash_hasher::IntMap;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::directives::extract_noqa_line_for;
#[test]
fn extraction() -> Result<()> {
let empty: IntMap<usize, usize> = Default::default();
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"
x = 1
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = '''abc
def
ghi
'''
y = 2
z = x + 1",
)
.collect();
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(1, 4), (2, 4), (3, 4)])
);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = '''abc
def
ghi
'''
z = 2",
)
.collect();
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = '''abc
def
ghi
'''",
)
.collect();
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
);
Ok(())
}
}

View File

@@ -1,4 +1,4 @@
use rustpython_ast::{Expr, Location};
use rustpython_ast::{Located, Location};
use crate::ast::types::Range;
use crate::check_ast::Checker;
@@ -24,9 +24,9 @@ pub fn leading_space(line: &str) -> String {
.collect()
}
/// Extract the leading indentation from a docstring.
pub fn indentation<'a>(checker: &'a Checker, docstring: &Expr) -> String {
let range = Range::from_located(docstring);
/// Extract the leading indentation from a line.
pub fn indentation<'a, T>(checker: &'a Checker, located: &Located<T>) -> String {
let range = Range::from_located(located);
checker
.locator
.slice_source_code_range(&Range {

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

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

192
src/flake8_2020/plugins.rs Normal file
View File

@@ -0,0 +1,192 @@
use num_bigint::BigInt;
use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
use crate::ast::helpers::match_name_or_attr_from_module;
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckCode, CheckKind};
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
match_name_or_attr_from_module(expr, target, "sys", checker.from_imports.get("sys"))
}
/// YTT101, YTT102, YTT301, YTT303
pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
if is_sys(checker, value, "version") {
match &slice.node {
ExprKind::Slice {
lower: None,
upper: Some(upper),
step: None,
..
} => {
if let ExprKind::Constant {
value: Constant::Int(i),
..
} = &upper.node
{
if *i == BigInt::from(1)
&& checker.settings.enabled.contains(&CheckCode::YTT303)
{
checker.add_check(Check::new(
CheckKind::SysVersionSlice1Referenced,
Range::from_located(value),
));
} else if *i == BigInt::from(3)
&& checker.settings.enabled.contains(&CheckCode::YTT101)
{
checker.add_check(Check::new(
CheckKind::SysVersionSlice3Referenced,
Range::from_located(value),
));
}
}
}
ExprKind::Constant {
value: Constant::Int(i),
..
} => {
if *i == BigInt::from(2) && checker.settings.enabled.contains(&CheckCode::YTT102) {
checker.add_check(Check::new(
CheckKind::SysVersion2Referenced,
Range::from_located(value),
));
} else if *i == BigInt::from(0)
&& checker.settings.enabled.contains(&CheckCode::YTT301)
{
checker.add_check(Check::new(
CheckKind::SysVersion0Referenced,
Range::from_located(value),
));
}
}
_ => {}
}
}
}
/// YTT103, YTT201, YTT203, YTT204, YTT302
pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &[Expr]) {
match &left.node {
ExprKind::Subscript { value, slice, .. } if is_sys(checker, value, "version_info") => {
if let ExprKind::Constant {
value: Constant::Int(i),
..
} = &slice.node
{
if *i == BigInt::from(0) {
if let (
[Cmpop::Eq | Cmpop::NotEq],
[Located {
node:
ExprKind::Constant {
value: Constant::Int(n),
..
},
..
}],
) = (ops, comparators)
{
if *n == BigInt::from(3)
&& checker.settings.enabled.contains(&CheckCode::YTT201)
{
checker.add_check(Check::new(
CheckKind::SysVersionInfo0Eq3Referenced,
Range::from_located(left),
));
}
}
} else if *i == BigInt::from(1) {
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Located {
node:
ExprKind::Constant {
value: Constant::Int(_),
..
},
..
}],
) = (ops, comparators)
{
if checker.settings.enabled.contains(&CheckCode::YTT203) {
checker.add_check(Check::new(
CheckKind::SysVersionInfo1CmpInt,
Range::from_located(left),
));
}
}
}
}
}
ExprKind::Attribute { value, attr, .. }
if is_sys(checker, value, "version_info") && attr == "minor" =>
{
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Located {
node:
ExprKind::Constant {
value: Constant::Int(_),
..
},
..
}],
) = (ops, comparators)
{
if checker.settings.enabled.contains(&CheckCode::YTT204) {
checker.add_check(Check::new(
CheckKind::SysVersionInfoMinorCmpInt,
Range::from_located(left),
));
}
}
}
_ => {}
}
if is_sys(checker, left, "version") {
if let (
[Cmpop::Lt | Cmpop::LtE | Cmpop::Gt | Cmpop::GtE],
[Located {
node:
ExprKind::Constant {
value: Constant::Str(s),
..
},
..
}],
) = (ops, comparators)
{
if s.len() == 1 {
if checker.settings.enabled.contains(&CheckCode::YTT302) {
checker.add_check(Check::new(
CheckKind::SysVersionCmpStr10,
Range::from_located(left),
));
}
} else if checker.settings.enabled.contains(&CheckCode::YTT103) {
checker.add_check(Check::new(
CheckKind::SysVersionCmpStr3,
Range::from_located(left),
));
}
}
}
}
/// YTT202
pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if match_name_or_attr_from_module(expr, "PY3", "six", checker.from_imports.get("six"))
&& checker.settings.enabled.contains(&CheckCode::YTT202)
{
checker.add_check(Check::new(
CheckKind::SixPY3Referenced,
Range::from_located(expr),
));
}
}

View File

@@ -6,32 +6,15 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::linter::tokenize;
use crate::{flake8_annotations, fs, linter, noqa, Settings, SourceCodeLocator};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(
path,
&contents,
tokens,
&locator,
&noqa_line_for,
settings,
autofix,
)
}
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::{flake8_annotations, Settings};
#[test]
fn defaults() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"),
&Settings {
..Settings::for_rules(vec![
@@ -57,7 +40,7 @@ mod tests {
#[test]
fn suppress_dummy_args() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/suppress_dummy_args.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
@@ -83,7 +66,7 @@ mod tests {
#[test]
fn mypy_init_return() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/mypy_init_return.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
@@ -109,7 +92,7 @@ mod tests {
#[test]
fn suppress_none_returning() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/suppress_none_returning.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {
@@ -135,7 +118,7 @@ mod tests {
#[test]
fn allow_star_arg_any() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_annotations/allow_star_arg_any.py"),
&Settings {
flake8_annotations: flake8_annotations::settings::Settings {

View File

@@ -0,0 +1,5 @@
use once_cell::sync::Lazy;
use regex::Regex;
pub static IDENTIFIER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());

View File

@@ -1 +1,2 @@
mod constants;
pub mod plugins;

View File

@@ -8,6 +8,7 @@ use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_bugbear::plugins::mutable_argument_default::is_mutable_func;
// TODO(charlie): Verify imports for each of the imported members.
const IMMUTABLE_FUNCS: [&str; 11] = [
"tuple",
"frozenset",

View File

@@ -0,0 +1,53 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind};
use crate::ast::types::Range;
use crate::autofix::Fix;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::code_gen::SourceGenerator;
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
fn attribute(value: &Expr, attr: &str) -> Expr {
Expr::new(
Default::default(),
Default::default(),
ExprKind::Attribute {
value: Box::new(value.clone()),
attr: attr.to_string(),
ctx: ExprContext::Load,
},
)
}
pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "getattr" {
if let [obj, arg] = args {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &arg.node
{
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
let mut check =
Check::new(CheckKind::GetAttrWithConstant, Range::from_located(expr));
if checker.patch() {
let mut generator = SourceGenerator::new();
if let Ok(()) = generator.unparse_expr(&attribute(obj, value), 0) {
if let Ok(content) = generator.generate() {
check.amend(Fix::replacement(
content,
expr.location,
expr.end_location.unwrap(),
));
}
}
}
checker.add_check(check);
}
}
}
}
}
}

View File

@@ -4,8 +4,10 @@ pub use assignment_to_os_environ::assignment_to_os_environ;
pub use cannot_raise_literal::cannot_raise_literal;
pub use duplicate_exceptions::{duplicate_exceptions, duplicate_handler_exceptions};
pub use function_call_argument_default::function_call_argument_default;
pub use getattr_with_constant::getattr_with_constant;
pub use mutable_argument_default::mutable_argument_default;
pub use redundant_tuple_in_exception_handler::redundant_tuple_in_exception_handler;
pub use setattr_with_constant::setattr_with_constant;
pub use star_arg_unpacking_after_keyword_arg::star_arg_unpacking_after_keyword_arg;
pub use strip_with_multi_characters::strip_with_multi_characters;
pub use unary_prefix_increment::unary_prefix_increment;
@@ -20,8 +22,10 @@ mod assignment_to_os_environ;
mod cannot_raise_literal;
mod duplicate_exceptions;
mod function_call_argument_default;
mod getattr_with_constant;
mod mutable_argument_default;
mod redundant_tuple_in_exception_handler;
mod setattr_with_constant;
mod star_arg_unpacking_after_keyword_arg;
mod strip_with_multi_characters;
mod unary_prefix_increment;

View File

@@ -4,6 +4,7 @@ use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
// TODO(charlie): Verify imports for each of the imported members.
pub fn is_mutable_func(expr: &Expr) -> bool {
match &expr.node {
ExprKind::Name { id, .. }

View File

@@ -0,0 +1,29 @@
use rustpython_ast::{Constant, Expr, ExprKind};
use crate::ast::types::Range;
use crate::check_ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_bugbear::constants::IDENTIFIER_REGEX;
use crate::python::keyword::KWLIST;
/// B010
pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
if let ExprKind::Name { id, .. } = &func.node {
if id == "setattr" {
if let [_, arg, _] = args {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &arg.node
{
if IDENTIFIER_REGEX.is_match(value) && !KWLIST.contains(&value.as_str()) {
checker.add_check(Check::new(
CheckKind::SetAttrWithConstant,
Range::from_located(expr),
));
}
}
}
}
}
}

View File

@@ -6,30 +6,13 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::checks::CheckCode;
use crate::flake8_quotes::settings::Quote;
use crate::linter::tokenize;
use crate::{flake8_quotes, fs, linter, noqa, Settings, SourceCodeLocator};
fn check_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(
path,
&contents,
tokens,
&locator,
&noqa_line_for,
settings,
autofix,
)
}
use crate::linter::test_path;
use crate::{flake8_quotes, Settings};
#[test_case(Path::new("doubles.py"))]
#[test_case(Path::new("doubles_escaped.py"))]
@@ -38,7 +21,7 @@ mod tests {
#[test_case(Path::new("doubles_wrapped.py"))]
fn doubles(path: &Path) -> Result<()> {
let snapshot = format!("doubles_{}", path.to_string_lossy());
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
@@ -70,7 +53,7 @@ mod tests {
#[test_case(Path::new("singles_wrapped.py"))]
fn singles(path: &Path) -> Result<()> {
let snapshot = format!("singles_{}", path.to_string_lossy());
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
@@ -107,7 +90,7 @@ mod tests {
#[test_case(Path::new("docstring_singles_function.py"))]
fn double_docstring(path: &Path) -> Result<()> {
let snapshot = format!("double_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),
@@ -144,7 +127,7 @@ mod tests {
#[test_case(Path::new("docstring_singles_function.py"))]
fn single_docstring(path: &Path) -> Result<()> {
let snapshot = format!("single_docstring_{}", path.to_string_lossy());
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_quotes")
.join(path)
.as_path(),

70
src/isort/categorize.rs Normal file
View File

@@ -0,0 +1,70 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::path::PathBuf;
use once_cell::sync::Lazy;
use crate::python::sys::KNOWN_STANDARD_LIBRARY;
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone)]
pub enum ImportType {
Future,
StandardLibrary,
ThirdParty,
FirstParty,
LocalFolder,
}
pub fn categorize(
module_base: &str,
level: &Option<usize>,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> ImportType {
if level.map(|level| level > 0).unwrap_or(false) {
ImportType::LocalFolder
} else if known_first_party.contains(module_base) {
ImportType::FirstParty
} else if known_third_party.contains(module_base) {
ImportType::ThirdParty
} else if extra_standard_library.contains(module_base) {
ImportType::StandardLibrary
} else if let Some(import_type) = STATIC_CLASSIFICATIONS.get(module_base) {
import_type.clone()
} else if KNOWN_STANDARD_LIBRARY.contains(module_base) {
ImportType::StandardLibrary
} else if find_local(src, module_base) {
ImportType::FirstParty
} else {
ImportType::ThirdParty
}
}
static STATIC_CLASSIFICATIONS: Lazy<BTreeMap<&'static str, ImportType>> = Lazy::new(|| {
BTreeMap::from([
("__future__", ImportType::Future),
("__main__", ImportType::FirstParty),
// Force `disutils` to be considered third-party.
("disutils", ImportType::ThirdParty),
// Relative imports (e.g., `from . import module`).
("", ImportType::FirstParty),
])
});
fn find_local(paths: &[PathBuf], base: &str) -> bool {
for path in paths {
if let Ok(metadata) = fs::metadata(path.join(base)) {
if metadata.is_dir() {
return true;
}
}
if let Ok(metadata) = fs::metadata(path.join(format!("{base}.py"))) {
if metadata.is_file() {
return true;
}
}
}
false
}

272
src/isort/mod.rs Normal file
View File

@@ -0,0 +1,272 @@
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::{Stmt, StmtKind};
use crate::isort::categorize::{categorize, ImportType};
use crate::isort::sorting::{member_key, module_key};
use crate::isort::types::{AliasData, ImportBlock, ImportFromData, Importable, OrderedImportBlock};
mod categorize;
pub mod plugins;
pub mod settings;
mod sorting;
pub mod track;
mod types;
// Hard-code four-space indentation for the imports themselves, to match Black.
const INDENT: &str = " ";
fn normalize_imports<'a>(imports: &'a [&'a Stmt]) -> ImportBlock<'a> {
let mut block: ImportBlock = Default::default();
for import in imports {
match &import.node {
StmtKind::Import { names } => {
for name in names {
block.import.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
}
}
StmtKind::ImportFrom {
module,
names,
level,
} => {
let targets = block
.import_from
.entry(ImportFromData { module, level })
.or_default();
for name in names {
targets.insert(AliasData {
name: &name.node.name,
asname: &name.node.asname,
});
}
}
_ => unreachable!("Expected StmtKind::Import | StmtKind::ImportFrom"),
}
}
block
}
fn categorize_imports<'a>(
block: ImportBlock<'a>,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> BTreeMap<ImportType, ImportBlock<'a>> {
let mut block_by_type: BTreeMap<ImportType, ImportBlock> = Default::default();
// Categorize `StmtKind::Import`.
for alias in block.import {
let import_type = categorize(
&alias.module_base(),
&None,
src,
known_first_party,
known_third_party,
extra_standard_library,
);
block_by_type
.entry(import_type)
.or_default()
.import
.insert(alias);
}
// Categorize `StmtKind::ImportFrom`.
for (import_from, aliases) in block.import_from {
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
.insert(import_from, aliases);
}
block_by_type
}
fn sort_imports(block: ImportBlock) -> OrderedImportBlock {
let mut ordered: OrderedImportBlock = Default::default();
// Sort `StmtKind::Import`.
for import in block
.import
.into_iter()
.sorted_by_cached_key(|alias| module_key(alias.name))
{
ordered.import.push(import);
}
// Sort `StmtKind::ImportFrom`.
for (import_from, aliases) in
block
.import_from
.into_iter()
.sorted_by_cached_key(|(import_from, _)| {
import_from.module.as_ref().map(|module| module_key(module))
})
{
ordered.import_from.push((
import_from,
aliases
.into_iter()
.sorted_by_cached_key(|alias| member_key(alias.name))
.collect(),
));
}
ordered
}
pub fn format_imports(
block: Vec<&Stmt>,
line_length: &usize,
src: &[PathBuf],
known_first_party: &BTreeSet<String>,
known_third_party: &BTreeSet<String>,
extra_standard_library: &BTreeSet<String>,
) -> String {
// Normalize imports (i.e., deduplicate, aggregate `from` imports).
let block = normalize_imports(&block);
// Categorize by type (e.g., first-party vs. third-party).
let block_by_type = categorize_imports(
block,
src,
known_first_party,
known_third_party,
extra_standard_library,
);
// Generate replacement source code.
let mut output = RopeBuilder::new();
let mut first_block = true;
for import_block in block_by_type.into_values() {
let import_block = sort_imports(import_block);
// Add a blank line between every section.
if !first_block {
output.append("\n");
} else {
first_block = false;
}
// Format `StmtKind::Import` statements.
for AliasData { name, asname } in import_block.import.iter() {
if let Some(asname) = asname {
output.append(&format!("import {} as {}\n", name, asname));
} else {
output.append(&format!("import {}\n", name));
}
}
// Format `StmtKind::ImportFrom` statements.
for (import_from, aliases) in import_block.import_from.iter() {
let prelude: String = format!("from {} import ", import_from.module_name());
let members: Vec<String> = aliases
.iter()
.map(|AliasData { name, asname }| {
if let Some(asname) = asname {
format!("{} as {}", name, asname)
} else {
name.to_string()
}
})
.collect();
// Can we fit the import on a single line?
let expected_len: usize =
// `from base import `
prelude.len()
// `member( as alias)?`
+ members.iter().map(|part| part.len()).sum::<usize>()
// `, `
+ 2 * (members.len() - 1);
if expected_len <= *line_length {
// `from base import `
output.append(&prelude);
// `member( as alias)?(, )?`
for (index, part) in members.into_iter().enumerate() {
if index > 0 {
output.append(", ");
}
output.append(&part);
}
// `\n`
output.append("\n");
} else {
// `from base import (\n`
output.append(&prelude);
output.append("(");
output.append("\n");
// ` member( as alias)?,\n`
for part in members {
output.append(INDENT);
output.append(&part);
output.append(",");
output.append("\n");
}
// `)\n`
output.append(")");
output.append("\n");
}
}
}
output.finish().to_string()
}
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::Settings;
#[test_case(Path::new("combine_import_froms.py"))]
#[test_case(Path::new("deduplicate_imports.py"))]
#[test_case(Path::new("fit_line_length.py"))]
#[test_case(Path::new("import_from_after_import.py"))]
#[test_case(Path::new("leading_prefix.py"))]
#[test_case(Path::new("no_reorder_within_section.py"))]
#[test_case(Path::new("order_by_type.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"))]
#[test_case(Path::new("separate_future_imports.py"))]
#[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("trailing_suffix.py"))]
fn isort(path: &Path) -> Result<()> {
let snapshot = format!("{}", path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures/isort")
.join(path)
.as_path(),
&Settings {
src: vec![Path::new("resources/test/fixtures/isort").to_path_buf()],
..Settings::for_rule(CheckCode::I001)
},
&fixer::Mode::Generate,
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}
}

116
src/isort/plugins.rs Normal file
View File

@@ -0,0 +1,116 @@
use rustpython_ast::{Location, Stmt};
use textwrap::{dedent, indent};
use crate::ast::types::Range;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckKind;
use crate::docstrings::helpers::leading_space;
use crate::isort::format_imports;
use crate::{Check, Settings, SourceCodeLocator};
fn extract_range(body: &[&Stmt]) -> Range {
let location = body.first().unwrap().location;
let end_location = body.last().unwrap().end_location.unwrap();
Range {
location,
end_location,
}
}
fn extract_indentation(body: &[&Stmt], locator: &SourceCodeLocator) -> String {
let location = body.first().unwrap().location;
let range = Range {
location: Location::new(location.row(), 0),
end_location: location,
};
let existing = locator.slice_source_code_range(&range);
leading_space(&existing)
}
fn match_leading_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
let location = body.first().unwrap().location;
let range = Range {
location: Location::new(location.row(), 0),
end_location: location,
};
let prefix = locator.slice_source_code_range(&range);
prefix.chars().any(|char| !char.is_whitespace())
}
fn match_trailing_content(body: &[&Stmt], locator: &SourceCodeLocator) -> bool {
let end_location = body.last().unwrap().end_location.unwrap();
let range = Range {
location: end_location,
end_location: Location::new(end_location.row() + 1, 0),
};
let suffix = locator.slice_source_code_range(&range);
suffix.chars().any(|char| !char.is_whitespace())
}
/// I001
pub fn check_imports(
body: Vec<&Stmt>,
locator: &SourceCodeLocator,
settings: &Settings,
autofix: &fixer::Mode,
) -> Option<Check> {
let range = extract_range(&body);
let indentation = extract_indentation(&body, locator);
// Special-cases: there's leading or trailing content in the import block.
let has_leading_content = match_leading_content(&body, locator);
let has_trailing_content = match_trailing_content(&body, locator);
// Generate the sorted import block.
let expected = format_imports(
body,
&settings.line_length,
&settings.src,
&settings.isort.known_first_party,
&settings.isort.known_third_party,
&settings.isort.extra_standard_library,
);
if has_leading_content || has_trailing_content {
let mut check = Check::new(CheckKind::UnsortedImports, range);
if autofix.patch() {
let mut content = String::new();
if has_leading_content {
content.push('\n');
}
content.push_str(&indent(&expected, &indentation));
check.amend(Fix::replacement(
content,
// Preserve leading prefix (but put the imports on a new line).
if has_leading_content {
range.location
} else {
Location::new(range.location.row(), 0)
},
// TODO(charlie): Preserve trailing suffixes. Right now, we strip them.
Location::new(range.end_location.row() + 1, 0),
));
}
Some(check)
} else {
// 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),
};
let actual = dedent(&locator.slice_source_code_range(&range));
if actual != expected {
let mut check = Check::new(CheckKind::UnsortedImports, range);
if autofix.patch() {
check.amend(Fix::replacement(
indent(&expected, &indentation),
range.location,
range.end_location,
));
}
Some(check)
} else {
None
}
}
}

32
src/isort/settings.rs Normal file
View File

@@ -0,0 +1,32 @@
//! Settings for the `isort` plugin.
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct Options {
pub known_first_party: Option<Vec<String>>,
pub known_third_party: Option<Vec<String>>,
pub extra_standard_library: Option<Vec<String>>,
}
#[derive(Debug, Hash, Default)]
pub struct Settings {
pub known_first_party: BTreeSet<String>,
pub known_third_party: BTreeSet<String>,
pub extra_standard_library: BTreeSet<String>,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
known_first_party: BTreeSet::from_iter(options.known_first_party.unwrap_or_default()),
known_third_party: BTreeSet::from_iter(options.known_third_party.unwrap_or_default()),
extra_standard_library: BTreeSet::from_iter(
options.extra_standard_library.unwrap_or_default(),
),
}
}
}

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 6
column: 0
fix:
patch:
content: "from collections import (\n AsyncIterable,\n Awaitable,\n ChainMap,\n Collection,\n MutableMapping,\n MutableSequence,\n)\n"
location:
row: 1
column: 0
end_location:
row: 6
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 5
column: 0
fix:
patch:
content: "import os\nimport os as os1\nimport os as os2\n"
location:
row: 1
column: 0
end_location:
row: 5
column: 0
applied: false

View File

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

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 3
column: 0
fix:
patch:
content: "import os\nfrom collections import Collection\n"
location:
row: 1
column: 0
end_location:
row: 3
column: 0
applied: false

View File

@@ -0,0 +1,39 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 7
end_location:
row: 2
column: 9
fix:
patch:
content: "\nimport os\nimport sys\n"
location:
row: 1
column: 7
end_location:
row: 3
column: 0
applied: false
- kind: UnsortedImports
location:
row: 5
column: 11
end_location:
row: 6
column: 13
fix:
patch:
content: "\n import os\n import sys\n"
location:
row: 5
column: 11
end_location:
row: 7
column: 0
applied: false

View File

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

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 13
column: 0
fix:
patch:
content: "import glob\nimport os\nimport shutil\nimport tempfile\nimport time\nfrom subprocess import PIPE, STDOUT, Popen\n\nimport BAR\nimport bar\nimport FOO\nimport foo\nimport StringIO\nfrom module import BASIC, CONSTANT, Apple, Class, function\n"
location:
row: 1
column: 0
end_location:
row: 13
column: 0
applied: false

View File

@@ -0,0 +1,39 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 2
column: 0
end_location:
row: 4
column: 0
fix:
patch:
content: " import os\n import sys\n"
location:
row: 2
column: 0
end_location:
row: 4
column: 0
applied: false
- kind: UnsortedImports
location:
row: 5
column: 0
end_location:
row: 7
column: 0
fix:
patch:
content: " import os\n import sys\n"
location:
row: 5
column: 0
end_location:
row: 7
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 3
column: 0
fix:
patch:
content: "import os\nimport sys\n"
location:
row: 1
column: 0
end_location:
row: 3
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 6
column: 0
fix:
patch:
content: "import os\nimport sys\n\nimport numpy as np\n\nimport leading_prefix\nfrom leading_prefix import Class\n"
location:
row: 1
column: 0
end_location:
row: 6
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 4
column: 0
fix:
patch:
content: "from __future__ import annotations\n\nimport os\nimport sys\n"
location:
row: 1
column: 0
end_location:
row: 4
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 5
column: 0
fix:
patch:
content: "import os\nimport sys\n\nimport leading_prefix\n\nfrom . import leading_prefix\n"
location:
row: 1
column: 0
end_location:
row: 5
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 5
column: 0
fix:
patch:
content: "import os\nimport sys\n\nimport numpy as np\nimport pandas as pd\n"
location:
row: 1
column: 0
end_location:
row: 5
column: 0
applied: false

View File

@@ -0,0 +1,22 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 9
column: 0
end_location:
row: 11
column: 0
fix:
patch:
content: "import abc\nimport collections\n"
location:
row: 9
column: 0
end_location:
row: 11
column: 0
applied: false

View File

@@ -0,0 +1,39 @@
---
source: src/isort/mod.rs
expression: checks
---
- kind: UnsortedImports
location:
row: 1
column: 0
end_location:
row: 2
column: 9
fix:
patch:
content: "import os\nimport sys\n"
location:
row: 1
column: 0
end_location:
row: 3
column: 0
applied: false
- kind: UnsortedImports
location:
row: 5
column: 4
end_location:
row: 6
column: 13
fix:
patch:
content: " import os\n import sys\n"
location:
row: 5
column: 0
end_location:
row: 7
column: 0
applied: false

34
src/isort/sorting.rs Normal file
View File

@@ -0,0 +1,34 @@
/// See: https://github.com/PyCQA/isort/blob/12cc5fbd67eebf92eb2213b03c07b138ae1fb448/isort/sorting.py#L13
use crate::python::string;
#[derive(PartialOrd, Ord, PartialEq, Eq)]
pub enum Prefix {
Constants,
Classes,
Variables,
}
pub fn module_key(module_name: &str) -> String {
module_name.to_lowercase()
}
pub fn member_key(member_name: &str) -> (Prefix, String) {
(
if member_name.len() > 1 && string::is_upper(member_name) {
// Ex) `CONSTANT`
Prefix::Constants
} else if member_name
.chars()
.next()
.map(|char| char.is_uppercase())
.unwrap_or(false)
{
// Ex) `Class`
Prefix::Classes
} else {
// Ex) `variable`
Prefix::Variables
},
member_name.to_lowercase(),
)
}

211
src/isort/track.rs Normal file
View File

@@ -0,0 +1,211 @@
use nohash_hasher::IntSet;
use rustpython_ast::{
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
ExcepthandlerKind, Expr, ExprContext, Keyword, MatchCase, Operator, Pattern, Stmt, StmtKind,
Unaryop, Withitem,
};
use crate::ast::visitor::Visitor;
#[derive(Debug)]
pub struct ImportTracker<'a> {
exclusions: &'a IntSet<usize>,
blocks: Vec<Vec<&'a Stmt>>,
}
impl<'a> ImportTracker<'a> {
pub fn new(exclusions: &'a IntSet<usize>) -> Self {
Self {
exclusions,
blocks: vec![vec![]],
}
}
fn track_import(&mut self, stmt: &'a Stmt) {
let index = self.blocks.len() - 1;
self.blocks[index].push(stmt);
}
fn finalize(&mut self) {
let index = self.blocks.len() - 1;
if !self.blocks[index].is_empty() {
self.blocks.push(vec![]);
}
}
pub fn into_iter(self) -> impl IntoIterator<Item = Vec<&'a Stmt>> {
self.blocks.into_iter()
}
}
impl<'a, 'b> Visitor<'b> for ImportTracker<'a>
where
'b: 'a,
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Track imports.
if matches!(
stmt.node,
StmtKind::Import { .. } | StmtKind::ImportFrom { .. }
) && !self.exclusions.contains(&stmt.location.row())
{
self.track_import(stmt);
} else {
self.finalize();
}
// Track scope.
match &stmt.node {
StmtKind::FunctionDef { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::AsyncFunctionDef { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::ClassDef { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::For { body, orelse, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
for stmt in orelse {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::AsyncFor { body, orelse, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
for stmt in orelse {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::While { body, orelse, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
for stmt in orelse {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::If { body, orelse, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
for stmt in orelse {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::With { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::AsyncWith { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
}
StmtKind::Match { cases, .. } => {
for match_case in cases {
self.visit_match_case(match_case);
}
}
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => {
for excepthandler in handlers {
self.visit_excepthandler(excepthandler)
}
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
for stmt in orelse {
self.visit_stmt(stmt);
}
self.finalize();
for stmt in finalbody {
self.visit_stmt(stmt);
}
self.finalize();
}
_ => {}
}
}
fn visit_annotation(&mut self, _: &'b Expr) {}
fn visit_expr(&mut self, _: &'b Expr) {}
fn visit_constant(&mut self, _: &'b Constant) {}
fn visit_expr_context(&mut self, _: &'b ExprContext) {}
fn visit_boolop(&mut self, _: &'b Boolop) {}
fn visit_operator(&mut self, _: &'b Operator) {}
fn visit_unaryop(&mut self, _: &'b Unaryop) {}
fn visit_cmpop(&mut self, _: &'b Cmpop) {}
fn visit_comprehension(&mut self, _: &'b Comprehension) {}
fn visit_excepthandler(&mut self, excepthandler: &'b Excepthandler) {
let ExcepthandlerKind::ExceptHandler { body, .. } = &excepthandler.node;
for stmt in body {
self.visit_stmt(stmt);
}
self.finalize();
}
fn visit_arguments(&mut self, _: &'b Arguments) {}
fn visit_arg(&mut self, _: &'b Arg) {}
fn visit_keyword(&mut self, _: &'b Keyword) {}
fn visit_alias(&mut self, _: &'b Alias) {}
fn visit_withitem(&mut self, _: &'b Withitem) {}
fn visit_match_case(&mut self, match_case: &'b MatchCase) {
for stmt in &match_case.body {
self.visit_stmt(stmt);
}
self.finalize();
}
fn visit_pattern(&mut self, _: &'b Pattern) {}
}

63
src/isort/types.rs Normal file
View File

@@ -0,0 +1,63 @@
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct ImportFromData<'a> {
pub module: &'a Option<String>,
pub level: &'a Option<usize>,
}
#[derive(Debug, Hash, Ord, PartialOrd, Eq, PartialEq)]
pub struct AliasData<'a> {
pub name: &'a str,
pub asname: &'a Option<String>,
}
pub trait Importable {
fn module_name(&self) -> String;
fn module_base(&self) -> String;
}
impl Importable for AliasData<'_> {
fn module_name(&self) -> String {
self.name.to_string()
}
fn module_base(&self) -> String {
self.module_name().split('.').next().unwrap().to_string()
}
}
impl Importable for ImportFromData<'_> {
fn module_name(&self) -> String {
let mut module_name = String::new();
if let Some(level) = self.level {
if level > &0 {
module_name.push_str(&".".repeat(*level));
}
}
if let Some(module) = self.module {
module_name.push_str(module);
}
module_name
}
fn module_base(&self) -> String {
self.module_name().split('.').next().unwrap().to_string()
}
}
#[derive(Debug, Default)]
pub struct ImportBlock<'a> {
// Map from (module, level) to `AliasData`.
pub import_from: BTreeMap<ImportFromData<'a>, BTreeSet<AliasData<'a>>>,
// Set of (name, asname).
pub import: BTreeSet<AliasData<'a>>,
}
#[derive(Debug, Default)]
pub struct OrderedImportBlock<'a> {
// Map from (module, level) to `AliasData`.
pub import_from: Vec<(ImportFromData<'a>, Vec<AliasData<'a>>)>,
// Set of (name, asname).
pub import: Vec<AliasData<'a>>,
}

View File

@@ -17,6 +17,7 @@ mod ast;
pub mod autofix;
pub mod cache;
pub mod check_ast;
mod check_imports;
mod check_lines;
mod check_tokens;
pub mod checks;
@@ -24,7 +25,9 @@ pub mod checks_gen;
pub mod cli;
pub mod code_gen;
mod cst;
mod directives;
mod docstrings;
mod flake8_2020;
pub mod flake8_annotations;
mod flake8_bugbear;
mod flake8_builtins;
@@ -32,6 +35,7 @@ mod flake8_comprehensions;
mod flake8_print;
pub mod flake8_quotes;
pub mod fs;
mod isort;
mod lex;
pub mod linter;
pub mod logging;
@@ -72,8 +76,12 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
// Initialize the SourceCodeLocator (which computes offsets lazily).
let locator = SourceCodeLocator::new(contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
&locator,
&directives::Flags::from_settings(&settings),
);
// Generate checks.
let checks = check_path(
@@ -81,7 +89,7 @@ pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Check>> {
contents,
tokens,
&locator,
&noqa_line_for,
&directives,
&settings,
&if autofix { Mode::Generate } else { Mode::None },
)?;

View File

@@ -16,15 +16,17 @@ use crate::ast::types::Range;
use crate::autofix::fixer;
use crate::autofix::fixer::fix_file;
use crate::check_ast::check_ast;
use crate::check_imports::check_imports;
use crate::check_lines::check_lines;
use crate::check_tokens::check_tokens;
use crate::checks::{Check, CheckCode, CheckKind, LintSource};
use crate::code_gen::SourceGenerator;
use crate::directives::Directives;
use crate::message::Message;
use crate::noqa::add_noqa;
use crate::settings::Settings;
use crate::source_code_locator::SourceCodeLocator;
use crate::{cache, fs, noqa};
use crate::{cache, directives, fs};
/// Collect tokens up to and including the first error.
pub(crate) fn tokenize(contents: &str) -> Vec<LexResult> {
@@ -55,7 +57,7 @@ pub(crate) fn check_path(
contents: &str,
tokens: Vec<LexResult>,
locator: &SourceCodeLocator,
noqa_line_for: &[usize],
directives: &Directives,
settings: &Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
@@ -63,23 +65,38 @@ pub(crate) fn check_path(
let mut checks: Vec<Check> = vec![];
// Run the token-based checks.
if settings
let use_tokens = settings
.enabled
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::Tokens))
{
.any(|check_code| matches!(check_code.lint_source(), LintSource::Tokens));
if use_tokens {
check_tokens(&mut checks, locator, &tokens, settings, autofix);
}
// Run the AST-based checks.
if settings
let use_ast = settings
.enabled
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST))
{
.any(|check_code| matches!(check_code.lint_source(), LintSource::AST));
let use_imports = settings
.enabled
.iter()
.any(|check_code| matches!(check_code.lint_source(), LintSource::Imports));
if use_ast || use_imports {
match parse_program_tokens(tokens, "<filename>") {
Ok(python_ast) => {
checks.extend(check_ast(&python_ast, locator, settings, autofix, path))
if use_ast {
checks.extend(check_ast(&python_ast, locator, settings, autofix, path));
}
if use_imports {
checks.extend(check_imports(
&python_ast,
locator,
&directives.isort_exclusions,
settings,
autofix,
));
}
}
Err(parse_error) => {
if settings.enabled.contains(&CheckCode::E999) {
@@ -96,7 +113,13 @@ pub(crate) fn check_path(
}
// Run the lines-based checks.
check_lines(&mut checks, contents, noqa_line_for, settings, autofix);
check_lines(
&mut checks,
contents,
&directives.noqa_line_for,
settings,
autofix,
);
// Create path ignores.
if !checks.is_empty() && !settings.per_file_ignores.is_empty() {
@@ -124,8 +147,12 @@ pub fn lint_stdin(
// Initialize the SourceCodeLocator (which computes offsets lazily).
let locator = SourceCodeLocator::new(stdin);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
&locator,
&directives::Flags::from_settings(settings),
);
// Generate checks.
let mut checks = check_path(
@@ -133,7 +160,7 @@ pub fn lint_stdin(
stdin,
tokens,
&locator,
&noqa_line_for,
&directives,
settings,
autofix,
)?;
@@ -178,8 +205,12 @@ pub fn lint_path(
// Initialize the SourceCodeLocator (which computes offsets lazily).
let locator = SourceCodeLocator::new(&contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Determine the noqa and isort exclusions.
let directives = directives::extract_directives(
&tokens,
&locator,
&directives::Flags::from_settings(settings),
);
// Generate checks.
let mut checks = check_path(
@@ -187,7 +218,7 @@ pub fn lint_path(
&contents,
tokens,
&locator,
&noqa_line_for,
&directives,
settings,
autofix,
)?;
@@ -220,8 +251,12 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
// Initialize the SourceCodeLocator (which computes offsets lazily).
let locator = SourceCodeLocator::new(&contents);
// Determine the noqa line for every line in the source.
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
&locator,
&directives::Flags::from_settings(settings),
);
// Generate checks.
let checks = check_path(
@@ -229,12 +264,12 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
&contents,
tokens,
&locator,
&noqa_line_for,
&directives,
settings,
&fixer::Mode::None,
)?;
add_noqa(&checks, &contents, &noqa_line_for, path)
add_noqa(&checks, &contents, &directives.noqa_line_for, path)
}
pub fn autoformat_path(path: &Path) -> Result<()> {
@@ -253,6 +288,27 @@ pub fn autoformat_path(path: &Path) -> Result<()> {
Ok(())
}
#[cfg(test)]
pub fn test_path(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let directives = directives::extract_directives(
&tokens,
&locator,
&directives::Flags::from_settings(settings),
);
check_path(
path,
&contents,
tokens,
&locator,
&directives,
settings,
autofix,
)
}
#[cfg(test)]
mod tests {
use std::convert::AsRef;
@@ -260,34 +316,12 @@ mod tests {
use anyhow::Result;
use regex::Regex;
use rustpython_parser::lexer::LexResult;
use test_case::test_case;
use crate::autofix::fixer;
use crate::checks::{Check, CheckCode};
use crate::linter::tokenize;
use crate::source_code_locator::SourceCodeLocator;
use crate::{fs, linter, noqa, settings};
fn check_path(
path: &Path,
settings: &settings::Settings,
autofix: &fixer::Mode,
) -> Result<Vec<Check>> {
let contents = fs::read_file(path)?;
let tokens: Vec<LexResult> = tokenize(&contents);
let locator = SourceCodeLocator::new(&contents);
let noqa_line_for = noqa::extract_noqa_line_for(&tokens);
linter::check_path(
path,
&contents,
tokens,
&locator,
&noqa_line_for,
settings,
autofix,
)
}
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
#[test_case(CheckCode::A002, Path::new("A002.py"); "A002")]
@@ -299,6 +333,8 @@ mod tests {
#[test_case(CheckCode::B006, Path::new("B006_B008.py"); "B006")]
#[test_case(CheckCode::B007, Path::new("B007.py"); "B007")]
#[test_case(CheckCode::B008, Path::new("B006_B008.py"); "B008")]
#[test_case(CheckCode::B009, Path::new("B009_B010.py"); "B009")]
#[test_case(CheckCode::B010, Path::new("B009_B010.py"); "B010")]
#[test_case(CheckCode::B011, Path::new("B011.py"); "B011")]
#[test_case(CheckCode::B013, Path::new("B013.py"); "B013")]
#[test_case(CheckCode::B014, Path::new("B014.py"); "B014")]
@@ -457,9 +493,19 @@ mod tests {
#[test_case(CheckCode::RUF001, Path::new("RUF001.py"); "RUF001")]
#[test_case(CheckCode::RUF002, Path::new("RUF002.py"); "RUF002")]
#[test_case(CheckCode::RUF003, Path::new("RUF003.py"); "RUF003")]
#[test_case(CheckCode::YTT101, Path::new("YTT101.py"); "YTT101")]
#[test_case(CheckCode::YTT102, Path::new("YTT102.py"); "YTT102")]
#[test_case(CheckCode::YTT103, Path::new("YTT103.py"); "YTT103")]
#[test_case(CheckCode::YTT201, Path::new("YTT201.py"); "YTT201")]
#[test_case(CheckCode::YTT202, Path::new("YTT202.py"); "YTT202")]
#[test_case(CheckCode::YTT203, Path::new("YTT203.py"); "YTT203")]
#[test_case(CheckCode::YTT204, Path::new("YTT204.py"); "YTT204")]
#[test_case(CheckCode::YTT301, Path::new("YTT301.py"); "YTT301")]
#[test_case(CheckCode::YTT302, Path::new("YTT302.py"); "YTT302")]
#[test_case(CheckCode::YTT303, Path::new("YTT303.py"); "YTT303")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures").join(path).as_path(),
&settings::Settings::for_rule(check_code.clone()),
&fixer::Mode::Generate,
@@ -471,7 +517,7 @@ mod tests {
#[test]
fn f841_dummy_variable_rgx() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/F841.py"),
&settings::Settings {
dummy_variable_rgx: Regex::new(r"^z$").unwrap(),
@@ -486,7 +532,7 @@ mod tests {
#[test]
fn m001() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/M001.py"),
&settings::Settings::for_rules(vec![CheckCode::M001, CheckCode::E501, CheckCode::F841]),
&fixer::Mode::Generate,
@@ -498,7 +544,7 @@ mod tests {
#[test]
fn init() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/__init__.py"),
&settings::Settings::for_rules(vec![CheckCode::F821, CheckCode::F822]),
&fixer::Mode::Generate,
@@ -510,7 +556,7 @@ mod tests {
#[test]
fn future_annotations() -> Result<()> {
let mut checks = check_path(
let mut checks = test_path(
Path::new("./resources/test/fixtures/future_annotations.py"),
&settings::Settings::for_rules(vec![CheckCode::F401, CheckCode::F821]),
&fixer::Mode::Generate,

View File

@@ -3,14 +3,14 @@ use std::fs;
use std::path::Path;
use anyhow::Result;
use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_parser::lexer::{LexResult, Tok};
use crate::checks::{Check, CheckCode};
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
Regex::new(r"(?P<noqa>\s*# noqa(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)")
.expect("Invalid regex")
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").expect("Invalid regex"));
@@ -43,30 +43,21 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
}
}
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> Vec<usize> {
let mut noqa_line_for: Vec<usize> = vec![];
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::EndOfFile) {
break;
}
// For multi-line strings, we expect `noqa` directives on the last line of the
// string. By definition, we can't have multiple multi-line strings on
// the same line, so we don't need to verify that we haven't already
// traversed past the current line.
if matches!(tok, Tok::String { .. }) && end.row() > start.row() {
for i in (noqa_line_for.len())..(start.row() - 1) {
noqa_line_for.push(i + 1);
}
noqa_line_for.extend(vec![end.row(); (end.row() + 1) - start.row()]);
}
}
noqa_line_for
pub fn add_noqa(
checks: &[Check],
contents: &str,
noqa_line_for: &IntMap<usize, usize>,
path: &Path,
) -> Result<usize> {
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for)?;
fs::write(path, output)?;
Ok(count)
}
fn add_noqa_inner(
checks: &[Check],
contents: &str,
noqa_line_for: &[usize],
noqa_line_for: &IntMap<usize, usize>,
) -> Result<(usize, String)> {
let lines: Vec<&str> = contents.lines().collect();
let mut matches_by_line: BTreeMap<usize, BTreeSet<&CheckCode>> = BTreeMap::new();
@@ -82,7 +73,7 @@ fn add_noqa_inner(
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let noqa_lineno = noqa_line_for
.get(lineno)
.get(&lineno)
.map(|lineno| lineno - 1)
.unwrap_or(lineno);
@@ -120,108 +111,20 @@ fn add_noqa_inner(
Ok((count, output))
}
pub fn add_noqa(
checks: &[Check],
contents: &str,
noqa_line_for: &[usize],
path: &Path,
) -> Result<usize> {
let (count, output) = add_noqa_inner(checks, contents, noqa_line_for)?;
fs::write(path, output)?;
Ok(count)
}
#[cfg(test)]
mod tests {
use anyhow::Result;
use rustpython_parser::ast::Location;
use rustpython_parser::lexer;
use rustpython_parser::lexer::LexResult;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::noqa::{add_noqa_inner, extract_noqa_line_for};
#[test]
fn extraction() -> Result<()> {
let empty: Vec<usize> = Default::default();
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"
x = 1
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = 2
z = x + 1
",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), empty);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = '''abc
def
ghi
'''
y = 2
z = x + 1",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), vec![4, 4, 4, 4]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = '''abc
def
ghi
'''
z = 2",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 5, 5, 5, 5]);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = '''abc
def
ghi
'''",
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), vec![1, 5, 5, 5, 5]);
Ok(())
}
use crate::noqa::add_noqa_inner;
#[test]
fn modification() -> Result<()> {
let checks = vec![];
let contents = "x = 1";
let noqa_line_for = vec![1];
let noqa_line_for = Default::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 0);
assert_eq!(output.trim(), contents.trim());
@@ -234,7 +137,7 @@ ghi
},
)];
let contents = "x = 1";
let noqa_line_for = vec![1];
let noqa_line_for = Default::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: F841".trim());
@@ -256,7 +159,7 @@ ghi
),
];
let contents = "x = 1 # noqa: E741";
let noqa_line_for = vec![1];
let noqa_line_for = Default::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());
@@ -278,7 +181,7 @@ ghi
),
];
let contents = "x = 1 # noqa";
let noqa_line_for = vec![1];
let noqa_line_for = Default::default();
let (count, output) = add_noqa_inner(&checks, contents, &noqa_line_for)?;
assert_eq!(count, 1);
assert_eq!(output.trim(), "x = 1 # noqa: E741, F841".trim());

View File

@@ -5,6 +5,7 @@ use crate::checks::{Check, CheckKind};
use crate::pep8_naming::helpers;
use crate::pep8_naming::helpers::FunctionType;
use crate::pep8_naming::settings::Settings;
use crate::python::string;
/// N801
pub fn invalid_class_name(class_def: &Stmt, name: &str) -> Option<Check> {
@@ -133,7 +134,7 @@ pub fn constant_imported_as_non_constant(
name: &str,
asname: &str,
) -> Option<Check> {
if helpers::is_upper(name) && !helpers::is_upper(asname) {
if string::is_upper(name) && !string::is_upper(asname) {
return Some(Check::new(
CheckKind::ConstantImportedAsNonConstant(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -148,7 +149,7 @@ pub fn lowercase_imported_as_non_lowercase(
name: &str,
asname: &str,
) -> Option<Check> {
if !helpers::is_upper(name) && helpers::is_lower(name) && asname.to_lowercase() != asname {
if !string::is_upper(name) && string::is_lower(name) && asname.to_lowercase() != asname {
return Some(Check::new(
CheckKind::LowercaseImportedAsNonLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -163,7 +164,7 @@ pub fn camelcase_imported_as_lowercase(
name: &str,
asname: &str,
) -> Option<Check> {
if helpers::is_camelcase(name) && helpers::is_lower(asname) {
if helpers::is_camelcase(name) && string::is_lower(asname) {
return Some(Check::new(
CheckKind::CamelcaseImportedAsLowercase(name.to_string(), asname.to_string()),
Range::from_located(import_from),
@@ -179,8 +180,8 @@ pub fn camelcase_imported_as_constant(
asname: &str,
) -> Option<Check> {
if helpers::is_camelcase(name)
&& !helpers::is_lower(asname)
&& helpers::is_upper(asname)
&& !string::is_lower(asname)
&& string::is_upper(asname)
&& !helpers::is_acronym(name, asname)
{
return Some(Check::new(
@@ -230,8 +231,8 @@ pub fn camelcase_imported_as_acronym(
asname: &str,
) -> Option<Check> {
if helpers::is_camelcase(name)
&& !helpers::is_lower(asname)
&& helpers::is_upper(asname)
&& !string::is_lower(asname)
&& string::is_upper(asname)
&& helpers::is_acronym(name, asname)
{
return Some(Check::new(

View File

@@ -4,6 +4,7 @@ use rustpython_ast::{Expr, ExprKind};
use crate::ast::helpers::match_name_or_attr;
use crate::ast::types::{Scope, ScopeKind};
use crate::pep8_naming::settings::Settings;
use crate::python::string::{is_lower, is_upper};
const CLASS_METHODS: [&str; 3] = ["__new__", "__init_subclass__", "__class_getitem__"];
const METACLASS_BASES: [&str; 2] = ["type", "ABCMeta"];
@@ -59,30 +60,6 @@ pub fn function_type(
}
}
pub fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
pub fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if !cased && c.is_uppercase() {
cased = true;
}
}
cased
}
pub fn is_camelcase(name: &str) -> bool {
!is_lower(name) && !is_upper(name) && !name.contains('_')
}
@@ -103,31 +80,7 @@ pub fn is_acronym(name: &str, asname: &str) -> bool {
#[cfg(test)]
mod tests {
use crate::pep8_naming::helpers::{
is_acronym, is_camelcase, is_lower, is_mixed_case, is_upper,
};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
use crate::pep8_naming::helpers::{is_acronym, is_camelcase, is_mixed_case};
#[test]
fn test_is_camelcase() -> () {

7
src/python/keyword.rs Normal file
View File

@@ -0,0 +1,7 @@
// See: https://github.com/python/cpython/blob/9d692841691590c25e6cf5b2250a594d3bf54825/Lib/keyword.py#L18
pub const KWLIST: [&str; 35] = [
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", "continue",
"def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import",
"in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while",
"with", "yield",
];

View File

@@ -1,3 +1,6 @@
pub mod builtins;
pub mod future;
pub mod keyword;
pub mod string;
pub mod sys;
pub mod typing;

50
src/python/string.rs Normal file
View File

@@ -0,0 +1,50 @@
pub fn is_lower(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_uppercase() {
return false;
} else if !cased && c.is_lowercase() {
cased = true;
}
}
cased
}
pub fn is_upper(s: &str) -> bool {
let mut cased = false;
for c in s.chars() {
if c.is_lowercase() {
return false;
} else if !cased && c.is_uppercase() {
cased = true;
}
}
cased
}
#[cfg(test)]
mod tests {
use crate::python::string::{is_lower, is_upper};
#[test]
fn test_is_lower() -> () {
assert!(is_lower("abc"));
assert!(is_lower("a_b_c"));
assert!(is_lower("a2c"));
assert!(!is_lower("aBc"));
assert!(!is_lower("ABC"));
assert!(!is_lower(""));
assert!(!is_lower("_"));
}
#[test]
fn test_is_upper() -> () {
assert!(is_upper("ABC"));
assert!(is_upper("A_B_C"));
assert!(is_upper("A2C"));
assert!(!is_upper("aBc"));
assert!(!is_upper("abc"));
assert!(!is_upper(""));
assert!(!is_upper("_"));
}
}

228
src/python/sys.rs Normal file
View File

@@ -0,0 +1,228 @@
use std::collections::BTreeSet;
use once_cell::sync::Lazy;
// See: https://pycqa.github.io/isort/docs/configuration/options.html#known-standard-library
pub static KNOWN_STANDARD_LIBRARY: Lazy<BTreeSet<&'static str>> = Lazy::new(|| {
BTreeSet::from([
"_ast",
"_dummy_thread",
"_thread",
"abc",
"aifc",
"argparse",
"array",
"ast",
"asynchat",
"asyncio",
"asyncore",
"atexit",
"audioop",
"base64",
"bdb",
"binascii",
"binhex",
"bisect",
"builtins",
"bz2",
"cProfile",
"calendar",
"cgi",
"cgitb",
"chunk",
"cmath",
"cmd",
"code",
"codecs",
"codeop",
"collections",
"colorsys",
"compileall",
"concurrent",
"configparser",
"contextlib",
"contextvars",
"copy",
"copyreg",
"crypt",
"csv",
"ctypes",
"curses",
"dataclasses",
"datetime",
"dbm",
"decimal",
"difflib",
"dis",
"distutils",
"doctest",
"dummy_threading",
"email",
"encodings",
"ensurepip",
"enum",
"errno",
"faulthandler",
"fcntl",
"filecmp",
"fileinput",
"fnmatch",
"formatter",
"fpectl",
"fractions",
"ftplib",
"functools",
"gc",
"getopt",
"getpass",
"gettext",
"glob",
"graphlib",
"grp",
"gzip",
"hashlib",
"heapq",
"hmac",
"html",
"http",
"imaplib",
"imghdr",
"imp",
"importlib",
"inspect",
"io",
"ipaddress",
"itertools",
"json",
"keyword",
"lib2to3",
"linecache",
"locale",
"logging",
"lzma",
"macpath",
"mailbox",
"mailcap",
"marshal",
"math",
"mimetypes",
"mmap",
"modulefinder",
"msilib",
"msvcrt",
"multiprocessing",
"netrc",
"nis",
"nntplib",
"ntpath",
"numbers",
"operator",
"optparse",
"os",
"ossaudiodev",
"parser",
"pathlib",
"pdb",
"pickle",
"pickletools",
"pipes",
"pkgutil",
"platform",
"plistlib",
"poplib",
"posix",
"posixpath",
"pprint",
"profile",
"pstats",
"pty",
"pwd",
"py_compile",
"pyclbr",
"pydoc",
"queue",
"quopri",
"random",
"re",
"readline",
"reprlib",
"resource",
"rlcompleter",
"runpy",
"sched",
"secrets",
"select",
"selectors",
"shelve",
"shlex",
"shutil",
"signal",
"site",
"smtpd",
"smtplib",
"sndhdr",
"socket",
"socketserver",
"spwd",
"sqlite3",
"sre",
"sre_compile",
"sre_constants",
"sre_parse",
"ssl",
"stat",
"statistics",
"string",
"stringprep",
"struct",
"subprocess",
"sunau",
"symbol",
"symtable",
"sys",
"sysconfig",
"syslog",
"tabnanny",
"tarfile",
"telnetlib",
"tempfile",
"termios",
"test",
"textwrap",
"threading",
"time",
"timeit",
"tkinter",
"token",
"tokenize",
"trace",
"traceback",
"tracemalloc",
"tty",
"turtle",
"turtledemo",
"types",
"typing",
"unicodedata",
"unittest",
"urllib",
"uu",
"uuid",
"venv",
"warnings",
"wave",
"weakref",
"webbrowser",
"winreg",
"winsound",
"wsgiref",
"xdrlib",
"xml",
"xmlrpc",
"zipapp",
"zipfile",
"zipimport",
"zlib",
"zoneinfo",
])
});

View File

@@ -2,16 +2,17 @@
//! command-line options. Structure mirrors the user-facing representation of
//! the various parameters.
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use once_cell::sync::Lazy;
use path_absolutize::path_dedot;
use regex::Regex;
use crate::checks_gen::CheckCodePrefix;
use crate::settings::pyproject::load_options;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{flake8_annotations, flake8_quotes, pep8_naming};
use crate::{flake8_annotations, flake8_quotes, fs, isort, pep8_naming};
#[derive(Debug)]
pub struct Configuration {
@@ -25,10 +26,12 @@ pub struct Configuration {
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub select: Vec<CheckCodePrefix>,
pub src: Vec<PathBuf>,
pub target_version: PythonVersion,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
pub flake8_quotes: flake8_quotes::settings::Settings,
pub isort: isort::settings::Settings,
pub pep8_naming: pep8_naming::settings::Settings,
}
@@ -71,6 +74,25 @@ impl Configuration {
.map_err(|e| anyhow!("Invalid dummy-variable-rgx value: {e}"))?,
None => DEFAULT_DUMMY_VARIABLE_RGX.clone(),
},
src: options
.src
.map(|src| {
src.iter()
.map(|path| {
let path = Path::new(path);
match project_root {
Some(project_root) => fs::normalize_path_to(path, project_root),
None => fs::normalize_path(path),
}
})
.collect()
})
.unwrap_or_else(|| {
vec![match project_root {
Some(project_root) => project_root.clone(),
None => path_dedot::CWD.clone(),
}]
}),
target_version: options.target_version.unwrap_or(PythonVersion::Py310),
exclude: options
.exclude
@@ -115,6 +137,10 @@ impl Configuration {
.flake8_quotes
.map(flake8_quotes::settings::Settings::from_options)
.unwrap_or_default(),
isort: options
.isort
.map(isort::settings::Settings::from_options)
.unwrap_or_default(),
pep8_naming: options
.pep8_naming
.map(pep8_naming::settings::Settings::from_options)

View File

@@ -4,14 +4,16 @@
use std::collections::BTreeSet;
use std::hash::{Hash, Hasher};
use std::path::PathBuf;
use path_absolutize::path_dedot;
use regex::Regex;
use crate::checks::CheckCode;
use crate::checks_gen::{CheckCodePrefix, PrefixSpecificity};
use crate::settings::configuration::Configuration;
use crate::settings::types::{FilePattern, PerFileIgnore, PythonVersion};
use crate::{flake8_annotations, flake8_quotes, pep8_naming};
use crate::{flake8_annotations, flake8_quotes, isort, pep8_naming};
pub mod configuration;
pub mod options;
@@ -27,10 +29,12 @@ pub struct Settings {
pub extend_exclude: Vec<FilePattern>,
pub line_length: usize,
pub per_file_ignores: Vec<PerFileIgnore>,
pub src: Vec<PathBuf>,
pub target_version: PythonVersion,
// Plugins
pub flake8_annotations: flake8_annotations::settings::Settings,
pub flake8_quotes: flake8_quotes::settings::Settings,
pub isort: isort::settings::Settings,
pub pep8_naming: pep8_naming::settings::Settings,
}
@@ -48,9 +52,11 @@ impl Settings {
extend_exclude: config.extend_exclude,
flake8_annotations: config.flake8_annotations,
flake8_quotes: config.flake8_quotes,
isort: config.isort,
line_length: config.line_length,
pep8_naming: config.pep8_naming,
per_file_ignores: config.per_file_ignores,
src: config.src,
target_version: config.target_version,
}
}
@@ -63,9 +69,11 @@ impl Settings {
extend_exclude: Default::default(),
line_length: 88,
per_file_ignores: Default::default(),
src: vec![path_dedot::CWD.clone()],
target_version: PythonVersion::Py310,
flake8_annotations: Default::default(),
flake8_quotes: Default::default(),
isort: Default::default(),
pep8_naming: Default::default(),
}
}
@@ -78,9 +86,11 @@ impl Settings {
extend_exclude: Default::default(),
line_length: 88,
per_file_ignores: Default::default(),
src: vec![path_dedot::CWD.clone()],
target_version: PythonVersion::Py310,
flake8_annotations: Default::default(),
flake8_quotes: Default::default(),
isort: Default::default(),
pep8_naming: Default::default(),
}
}
@@ -101,6 +111,7 @@ impl Hash for Settings {
// Add plugin properties in alphabetical order.
self.flake8_annotations.hash(state);
self.flake8_quotes.hash(state);
self.isort.hash(state);
self.pep8_naming.hash(state);
}
}

View File

@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::checks_gen::CheckCodePrefix;
use crate::settings::types::PythonVersion;
use crate::{flake8_annotations, flake8_quotes, pep8_naming};
use crate::{flake8_annotations, flake8_quotes, isort, pep8_naming};
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
@@ -21,9 +21,11 @@ pub struct Options {
pub line_length: Option<usize>,
pub per_file_ignores: Option<BTreeMap<String, Vec<CheckCodePrefix>>>,
pub select: Option<Vec<CheckCodePrefix>>,
pub src: Option<Vec<String>>,
pub target_version: Option<PythonVersion>,
// Plugins
pub flake8_annotations: Option<flake8_annotations::settings::Options>,
pub flake8_quotes: Option<flake8_quotes::settings::Options>,
pub isort: Option<isort::settings::Options>,
pub pep8_naming: Option<pep8_naming::settings::Options>,
}

View File

@@ -143,9 +143,11 @@ mod tests {
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
src: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
})
})
@@ -172,9 +174,11 @@ line-length = 79
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
src: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
})
})
@@ -201,9 +205,11 @@ exclude = ["foo.py"]
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
src: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
})
})
@@ -230,9 +236,11 @@ select = ["E501"]
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
src: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
})
})
@@ -260,9 +268,11 @@ ignore = ["E501"]
extend_ignore: None,
per_file_ignores: None,
dummy_variable_rgx: None,
src: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: None,
isort: None,
pep8_naming: None,
})
})
@@ -323,9 +333,9 @@ other-attribute = 1
fix: None,
exclude: None,
extend_exclude: Some(vec![
"excluded.py".to_string(),
"excluded_file.py".to_string(),
"migrations".to_string(),
"directory/also_excluded.py".to_string(),
"with_excluded_file/other_excluded_file.py".to_string(),
]),
select: None,
extend_select: None,
@@ -336,6 +346,7 @@ other-attribute = 1
vec![CheckCodePrefix::F401]
),])),
dummy_variable_rgx: None,
src: None,
target_version: None,
flake8_annotations: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
@@ -344,6 +355,7 @@ other-attribute = 1
docstring_quotes: Some(Quote::Double),
avoid_escape: Some(true),
}),
isort: None,
pep8_naming: Some(pep8_naming::settings::Options {
ignore_names: Some(vec![
"setUp".to_string(),

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