Compare commits
257 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1b711d9c6 | ||
|
|
35f593846e | ||
|
|
c384fa513b | ||
|
|
022ff64d29 | ||
|
|
5a06fb28fd | ||
|
|
46750a3e17 | ||
|
|
9cc902b802 | ||
|
|
c2a36ebd1e | ||
|
|
34ca225393 | ||
|
|
38c30905e6 | ||
|
|
2774194b03 | ||
|
|
71ebd39f35 | ||
|
|
a2f78ba2c7 | ||
|
|
b51a080a44 | ||
|
|
6a1d7d8a1c | ||
|
|
10b250ee57 | ||
|
|
30b1b1e15a | ||
|
|
aafe7c0c39 | ||
|
|
f060248656 | ||
|
|
bbe0220c72 | ||
|
|
129e2b6ad3 | ||
|
|
73e744b1d0 | ||
|
|
50a3fc5a67 | ||
|
|
de499f0258 | ||
|
|
e1abe37c6a | ||
|
|
7fe5945541 | ||
|
|
806f3fd4f6 | ||
|
|
2bba643dd2 | ||
|
|
54090bd7ac | ||
|
|
c62727db42 | ||
|
|
d0e1612507 | ||
|
|
5ccd907398 | ||
|
|
346610c2e3 | ||
|
|
307fa26515 | ||
|
|
136d412edd | ||
|
|
d9edec0ac9 | ||
|
|
473675fffb | ||
|
|
95dfc61315 | ||
|
|
dd496c7b52 | ||
|
|
78aafb4b34 | ||
|
|
99c66d513a | ||
|
|
04ade6a2f3 | ||
|
|
4cf2682cda | ||
|
|
e3a7357187 | ||
|
|
b60768debb | ||
|
|
25e476639f | ||
|
|
4645788205 | ||
|
|
0b9eda8836 | ||
|
|
ad23d6acee | ||
|
|
e3d1d01a1f | ||
|
|
6dfdd21a7c | ||
|
|
f17d3b3c44 | ||
|
|
da6b913317 | ||
|
|
bd34850f98 | ||
|
|
d5b33cdb40 | ||
|
|
3fb4cf7009 | ||
|
|
6e19539e28 | ||
|
|
4eac7a07f5 | ||
|
|
5141285c8e | ||
|
|
82cc139d2d | ||
|
|
df438ba051 | ||
|
|
a70624cd47 | ||
|
|
b307afc00c | ||
|
|
3d5bc1f51f | ||
|
|
aba01745f5 | ||
|
|
1eeeffab66 | ||
|
|
9b564c9cf4 | ||
|
|
5bf8b13644 | ||
|
|
f80d5e70dd | ||
|
|
44897b2a5b | ||
|
|
d1bcc919a2 | ||
|
|
03e1397427 | ||
|
|
fdb32330a9 | ||
|
|
4e6ae33a3a | ||
|
|
295ff8eb1a | ||
|
|
2449771d2f | ||
|
|
406491a3a2 | ||
|
|
bfae262359 | ||
|
|
af894f290f | ||
|
|
c901742244 | ||
|
|
7e4faf4b69 | ||
|
|
31a0b20271 | ||
|
|
0966bf2c66 | ||
|
|
64d8e25528 | ||
|
|
b049cced04 | ||
|
|
bc335f839e | ||
|
|
4819e19ba2 | ||
|
|
622b8adb79 | ||
|
|
558d9fcbe3 | ||
|
|
83f18193c2 | ||
|
|
46e6a1b3be | ||
|
|
4d0d433af9 | ||
|
|
11f7532e72 | ||
|
|
417764d309 | ||
|
|
1e36c109c6 | ||
|
|
3960016d55 | ||
|
|
75d669fa86 | ||
|
|
20989e12ba | ||
|
|
5a1b6c32eb | ||
|
|
46bdcb9080 | ||
|
|
d16a7252af | ||
|
|
ca6551eb37 | ||
|
|
43a4f5749e | ||
|
|
6fef4db433 | ||
|
|
7470d6832f | ||
|
|
63ba0bfeef | ||
|
|
91666fcaf6 | ||
|
|
643e27221d | ||
|
|
c7349b69c1 | ||
|
|
7f84753f3c | ||
|
|
e2ec62cf33 | ||
|
|
1d5592d937 | ||
|
|
886def13bd | ||
|
|
949e4d4077 | ||
|
|
c8cb2eead2 | ||
|
|
02ae494a0e | ||
|
|
dce86e065b | ||
|
|
d77979429c | ||
|
|
a3a15d2eb2 | ||
|
|
5af95428ff | ||
|
|
6338cad4e6 | ||
|
|
485881877f | ||
|
|
b8f517c70e | ||
|
|
9f601c2abd | ||
|
|
c0ce0b0c48 | ||
|
|
e5b16973a9 | ||
|
|
de9ceb2fe1 | ||
|
|
38b19b78b7 | ||
|
|
7043e15b57 | ||
|
|
9594079235 | ||
|
|
732f208e47 | ||
|
|
32e62d9209 | ||
|
|
d9e4b0cdc1 | ||
|
|
36fcfad56a | ||
|
|
65d29d9734 | ||
|
|
1e171ce0e8 | ||
|
|
2bdc500c61 | ||
|
|
f453e429b6 | ||
|
|
73874f4788 | ||
|
|
8846dcdf6a | ||
|
|
d827e6e36a | ||
|
|
71d9b2ac5f | ||
|
|
401b53cc45 | ||
|
|
aa9c1e255c | ||
|
|
f7fc702b2c | ||
|
|
50ca0d7d0a | ||
|
|
65e0284698 | ||
|
|
e4f571ea61 | ||
|
|
4ed88dd245 | ||
|
|
09b926fd59 | ||
|
|
a4869e4974 | ||
|
|
f53c4fc221 | ||
|
|
3892a49a97 | ||
|
|
27cc7e236c | ||
|
|
fa0954fe47 | ||
|
|
a0b50d7ebc | ||
|
|
afe7a04211 | ||
|
|
14806c62ca | ||
|
|
0d0c8730fa | ||
|
|
cf6a23b83c | ||
|
|
9e0daac561 | ||
|
|
f2fd7335ce | ||
|
|
b8f878df5e | ||
|
|
9bdb922c75 | ||
|
|
edecc1bba6 | ||
|
|
8e903153f6 | ||
|
|
3937885f37 | ||
|
|
24de97d951 | ||
|
|
06e5b3e457 | ||
|
|
68a0e6dc19 | ||
|
|
9d4a4478f7 | ||
|
|
6bbf3f46c4 | ||
|
|
4ac4e8c991 | ||
|
|
0091a3ae5f | ||
|
|
17b3109a8b | ||
|
|
71520213c1 | ||
|
|
f24e7a0052 | ||
|
|
507e9f7ec3 | ||
|
|
592c53c8bf | ||
|
|
a2df89dedd | ||
|
|
b8f12d2e79 | ||
|
|
67b1d0463a | ||
|
|
d008a181ec | ||
|
|
6d612a428a | ||
|
|
c0cb73ab16 | ||
|
|
2e1eb84cbf | ||
|
|
b03a8728b5 | ||
|
|
1dd3350a30 | ||
|
|
bda34945a5 | ||
|
|
85dcaa8d3c | ||
|
|
4ac74ed0ad | ||
|
|
b7e2a4b9a9 | ||
|
|
53a7758248 | ||
|
|
2ba767957d | ||
|
|
08152787e1 | ||
|
|
5f77b420cd | ||
|
|
90f9e60517 | ||
|
|
320737f6e4 | ||
|
|
dfba1416b2 | ||
|
|
2ca3f35bd1 | ||
|
|
b1c40d5fa7 | ||
|
|
a129e27b3e | ||
|
|
ad7daa008e | ||
|
|
062d7081a0 | ||
|
|
40c1e7e005 | ||
|
|
3cbd05ddff | ||
|
|
9414090617 | ||
|
|
825777edc1 | ||
|
|
4e0807e908 | ||
|
|
546be5692a | ||
|
|
43e1f20b28 | ||
|
|
97388cefda | ||
|
|
63ce579989 | ||
|
|
5f4a62aa40 | ||
|
|
02ab52b3e2 | ||
|
|
549732b1da | ||
|
|
c4565fe0f5 | ||
|
|
81ae3bfc94 | ||
|
|
62e6feadc7 | ||
|
|
18a26e8f0b | ||
|
|
2371de3895 | ||
|
|
e3c8f61340 | ||
|
|
f6628ae100 | ||
|
|
989ed9c10b | ||
|
|
8698c06c36 | ||
|
|
dfd8a4158d | ||
|
|
c247730bf5 | ||
|
|
024472d578 | ||
|
|
7d69a153e8 | ||
|
|
4fc68e0310 | ||
|
|
6a24351202 | ||
|
|
d7f95ac6b6 | ||
|
|
11234ea555 | ||
|
|
b536159541 | ||
|
|
7c17785eac | ||
|
|
f1acd28f08 | ||
|
|
c61ff9a947 | ||
|
|
2c64cf3149 | ||
|
|
fc5f34c76f | ||
|
|
a8f4faa6e4 | ||
|
|
2ac5c830c1 | ||
|
|
994f12050d | ||
|
|
fad4e4c51d | ||
|
|
c0042a3ca4 | ||
|
|
5deb63a05f | ||
|
|
5e9ea8bda2 | ||
|
|
55d1f34bae | ||
|
|
59b518a54a | ||
|
|
74ecdc73ac | ||
|
|
1ad6be7196 | ||
|
|
b44d6c2c44 | ||
|
|
2749660b1f | ||
|
|
c1eeae90f1 | ||
|
|
27025055ee | ||
|
|
e306fe0765 | ||
|
|
5ffb9c08d5 | ||
|
|
1a8940f015 |
14
.editorconfig
Normal file
14
.editorconfig
Normal file
@@ -0,0 +1,14 @@
|
||||
# Check http://editorconfig.org for more information
|
||||
# This is the main config file for this project:
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
indent_size = 2
|
||||
|
||||
[*.{rs,py}]
|
||||
indent_size = 4
|
||||
28
.github/workflows/release.yaml
vendored
28
.github/workflows/release.yaml
vendored
@@ -1,6 +1,8 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
create:
|
||||
tags:
|
||||
- v*
|
||||
@@ -14,7 +16,7 @@ env:
|
||||
PYTHON_VERSION: "3.7" # to build abi3 wheels
|
||||
|
||||
jobs:
|
||||
macos:
|
||||
macos-x86_64:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -37,6 +39,25 @@ jobs:
|
||||
- name: Install built wheel - x86_64
|
||||
run: |
|
||||
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
|
||||
- name: Upload wheels
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: wheels
|
||||
path: dist
|
||||
macos-universal:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
architecture: x64
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
default: true
|
||||
- name: Build wheels - universal2
|
||||
uses: messense/maturin-action@v1
|
||||
with:
|
||||
@@ -127,7 +148,7 @@ jobs:
|
||||
with:
|
||||
target: ${{ matrix.target }}
|
||||
manylinux: auto
|
||||
args: --release --out dist
|
||||
args: --no-default-features --release --out dist
|
||||
maturin-version: "v0.13.0"
|
||||
- uses: uraimo/run-on-arch-action@v2.0.5
|
||||
if: matrix.target != 'ppc64'
|
||||
@@ -259,7 +280,8 @@ jobs:
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- macos
|
||||
- macos-universal
|
||||
- macos-x86_64
|
||||
- windows
|
||||
- linux
|
||||
- linux-cross
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff
|
||||
rev: v0.0.26
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.40
|
||||
hooks:
|
||||
- id: lint
|
||||
|
||||
- repo: https://github.com/abravalheri/validate-pyproject
|
||||
rev: v0.10.1
|
||||
hooks:
|
||||
- id: validate-pyproject
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
- id: lint
|
||||
name: ruff lint
|
||||
description: Run ruff to lint Python files.
|
||||
entry: ruff
|
||||
language: python
|
||||
types_or: [python]
|
||||
pass_filenames: true
|
||||
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
charlie.r.marsh@gmail.com.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
||||
111
CONTRIBUTING.md
Normal file
111
CONTRIBUTING.md
Normal file
@@ -0,0 +1,111 @@
|
||||
# Contributing to ruff
|
||||
|
||||
Welcome! We're happy to have you here. Thank you in advance for your contribution to ruff.
|
||||
|
||||
## The basics
|
||||
|
||||
ruff welcomes contributions in the form of Pull Requests. For small changes (e.g., bug fixes), feel
|
||||
free to submit a PR. For larger changes (e.g., new lint rules, new functionality, new configuration
|
||||
options), consider submitting an [Issue](https://github.com/charliermarsh/ruff/issues) outlining
|
||||
your proposed change.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
ruff is written in Rust (1.63.0). You'll need to install the
|
||||
[Rust toolchain](https://www.rust-lang.org/tools/install) for development.
|
||||
|
||||
You'll also need [Insta](https://insta.rs/docs/) to update snapshot tests:
|
||||
|
||||
```shell
|
||||
cargo install cargo-insta
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
After cloning the repository, run ruff locally with:
|
||||
|
||||
```shell
|
||||
cargo run resources/test/fixtures --no-cache
|
||||
```
|
||||
|
||||
Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes
|
||||
both the lint and test validation checks:
|
||||
|
||||
```shell
|
||||
cargo fmt # Auto-formatting...
|
||||
cargo clippy # Linting...
|
||||
cargo test # Testing...
|
||||
```
|
||||
|
||||
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
|
||||
will save you time and expedite the merge process.
|
||||
|
||||
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
|
||||
prior to merging.
|
||||
|
||||
### Example: Adding a new lint rule
|
||||
|
||||
There are three phases to adding a new lint rule:
|
||||
|
||||
1. Define the rule in `src/checks.rs`.
|
||||
2. Define the _logic_ for triggering the rule in `src/check_ast.rs` (for AST-based checks)
|
||||
or `src/check_lines.rs` (for text-based checks).
|
||||
3. Add a test fixture.
|
||||
|
||||
To define the rule, open up `src/checks.rs`. You'll need to define both a `CheckCode` and
|
||||
`CheckKind`. As an example, you can grep for `E402` and `ModuleImportNotAtTopOfFile`, and follow the
|
||||
pattern implemented therein.
|
||||
|
||||
To trigger the rule, you'll likely want to augment the logic in `src/check_ast.rs`, which defines
|
||||
the Python AST visitor, responsible for iterating over the abstract syntax tree and collecting
|
||||
lint-rule violations as it goes. Grep for the `Check::new` invocations to understand how other,
|
||||
similar rules are implemented.
|
||||
|
||||
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
|
||||
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
|
||||
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run ruff locally
|
||||
with (e.g.) `cargo run resources/test/fixtures/E402.py`. Once you're satisfied with the output,
|
||||
codify the behavior as a snapshot test by adding a new function to the `mod tests` section of
|
||||
`src/linter.rs`, like so:
|
||||
|
||||
```rust
|
||||
#[test]
|
||||
fn e402() -> Result<()> {
|
||||
let mut checks = check_path(
|
||||
Path::new("./resources/test/fixtures/E402.py"),
|
||||
&settings::Settings::for_rule(CheckCode::E402),
|
||||
&fixer::Mode::Generate,
|
||||
)?;
|
||||
checks.sort_by_key(|check| check.location);
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Then, run `cargo test`. Your test will fail, but you'll be prompted to follow-up with
|
||||
`cargo insta review`. Accept the generated snapshot, then commit the snapshot file alongside the
|
||||
rest of your changes.
|
||||
|
||||
### Example: Adding a new configuration option
|
||||
|
||||
ruff's user-facing settings live in two places: first, the command-line options defined with
|
||||
[clap](https://docs.rs/clap/latest/clap/) via the `Cli` struct in `src/main.rs`; and second, the
|
||||
`Config` struct defined `src/pyproject.rs`, which is responsible for extracting user-defined
|
||||
settings from a `pyproject.toml` file.
|
||||
|
||||
Ultimately, these two sources of configuration are merged into the `Settings` struct defined
|
||||
in `src/settings.rs`, which is then threaded through the codebase.
|
||||
|
||||
To add a new configuration option, you'll likely want to _both_ add a CLI option to `src/main.rs`
|
||||
_and_ a `pyproject.toml` parameter to `src/pyproject.rs`. If you want to pattern-match against an
|
||||
existing example, grep for `dummy_variable_rgx`, which defines a regular expression to match against
|
||||
acceptable unused variables (e.g., `_`).
|
||||
|
||||
## Release process
|
||||
|
||||
As of now, ruff has an ad hoc release process: releases are cut with high frequency via GitHub
|
||||
Actions, which automatically generates the appropriate wheels across architectures and publishes
|
||||
them to [PyPI](https://pypi.org/project/ruff/).
|
||||
|
||||
ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
|
||||
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
|
||||
580
Cargo.lock
generated
580
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
29
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.26"
|
||||
version = "0.0.67"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
@@ -11,7 +11,7 @@ anyhow = { version = "1.0.60" }
|
||||
bincode = { version = "1.3.3" }
|
||||
cacache = { version = "10.0.1" }
|
||||
chrono = { version = "0.4.21" }
|
||||
clap = { version = "3.2.16", features = ["derive"] }
|
||||
clap = { version = "4.0.1", features = ["derive"] }
|
||||
clearscreen = { version = "1.0.10" }
|
||||
colored = { version = "2.0.0" }
|
||||
common-path = { version = "1.0.0" }
|
||||
@@ -19,20 +19,41 @@ dirs = { version = "4.0.0" }
|
||||
fern = { version = "0.6.1" }
|
||||
filetime = { version = "0.2.17" }
|
||||
glob = { version = "0.3.0" }
|
||||
itertools = { version = "0.10.5" }
|
||||
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "32a044c127668df44582f85699358e67803b0d73" }
|
||||
log = { version = "0.4.17" }
|
||||
notify = { version = "4.0.17" }
|
||||
once_cell = { version = "1.13.1" }
|
||||
path-absolutize = { version = "3.0.13", features = ["once_cell_cache"] }
|
||||
rayon = { version = "1.5.3" }
|
||||
regex = { version = "1.6.0" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "ff6112d7c70729be76eaad8c9e5b8ef24d13de99" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
rustpython-common = { git = "https://github.com/charliermarsh/RustPython.git", rev = "778ae2aeb521d0438d2a91bd11238bb5c2bf9d4f" }
|
||||
serde = { version = "1.0.143", features = ["derive"] }
|
||||
serde_json = { version = "1.0.83" }
|
||||
toml = { version = "0.5.9" }
|
||||
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"] }
|
||||
update-informer = { version = "0.5.0", default_features = false, features = ["pypi"], optional = true }
|
||||
walkdir = { version = "2.3.2" }
|
||||
strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||
strum_macros = "0.24.3"
|
||||
num-bigint = "0.4.3"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = { version = "1.19.1", features = ["yaml"] }
|
||||
|
||||
[features]
|
||||
default = ["update-informer"]
|
||||
update-informer = ["dep:update-informer"]
|
||||
|
||||
[profile.release]
|
||||
panic = "abort"
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.insta]
|
||||
opt-level = 3
|
||||
|
||||
[profile.dev.package.similar]
|
||||
opt-level = 3
|
||||
|
||||
402
README.md
402
README.md
@@ -1,7 +1,9 @@
|
||||
# ruff
|
||||
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://pypi.python.org/pypi/ruff)
|
||||
[](https://github.com/charliermarsh/ruff/actions)
|
||||
[](https://badge.fury.io/py/ruff)
|
||||
|
||||
An extremely fast Python linter, written in Rust.
|
||||
|
||||
@@ -18,11 +20,9 @@ An extremely fast Python linter, written in Rust.
|
||||
- 🤝 Python 3.10 compatibility
|
||||
- 🛠️ `pyproject.toml` support
|
||||
- 📦 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired cache support
|
||||
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#caching)-inspired `--fix` support
|
||||
- 🔧 [ESLint](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix)-inspired `--fix` support
|
||||
- 👀 [TypeScript](https://www.typescriptlang.org/docs/handbook/configuring-watch.html)-inspired `--watch` support
|
||||
|
||||
_ruff is a proof-of-concept and not yet intended for production use. It supports only a small subset
|
||||
of the Flake8 rules, and may crash on your codebase._
|
||||
- ⚖️ [Near-complete parity](#Parity-with-Flake8) with the built-in Flake8 rule set
|
||||
|
||||
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
|
||||
|
||||
@@ -52,14 +52,14 @@ You can run ruff in `--watch` mode to automatically re-run on-change:
|
||||
ruff path/to/code/ --watch
|
||||
```
|
||||
|
||||
ruff also works with [Pre-Commit](https://pre-commit.com) (requires Cargo on system):
|
||||
ruff also works with [pre-commit](https://pre-commit.com):
|
||||
|
||||
```yaml
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff
|
||||
rev: v0.0.26
|
||||
hooks:
|
||||
- id: lint
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.48
|
||||
hooks:
|
||||
- id: lint
|
||||
```
|
||||
|
||||
## Configuration
|
||||
@@ -80,64 +80,268 @@ select = [
|
||||
Alternatively, on the command-line:
|
||||
|
||||
```shell
|
||||
ruff path/to/code/ --select F401 F403
|
||||
ruff path/to/code/ --select F401 --select F403
|
||||
```
|
||||
|
||||
See `ruff --help` for more:
|
||||
|
||||
```shell
|
||||
ruff (v0.0.26)
|
||||
An extremely fast Python linter.
|
||||
ruff: An extremely fast Python linter.
|
||||
|
||||
USAGE:
|
||||
ruff [OPTIONS] <FILES>...
|
||||
Usage: ruff [OPTIONS] <FILES>...
|
||||
|
||||
ARGS:
|
||||
<FILES>...
|
||||
Arguments:
|
||||
<FILES>...
|
||||
|
||||
OPTIONS:
|
||||
-e, --exit-zero Exit with status code "0", even upon detecting errors
|
||||
-f, --fix Attempt to automatically fix lint errors
|
||||
-h, --help Print help information
|
||||
--ignore <IGNORE>... Comma-separated list of error codes to ignore
|
||||
-n, --no-cache Disable cache reads
|
||||
-q, --quiet Disable all logging (but still exit with status code "1" upon
|
||||
detecting errors)
|
||||
--select <SELECT>... Comma-separated list of error codes to enable
|
||||
-v, --verbose Enable verbose logging
|
||||
-w, --watch Run in watch mode by re-running whenever files change
|
||||
Options:
|
||||
-v, --verbose
|
||||
Enable verbose logging
|
||||
-q, --quiet
|
||||
Disable all logging (but still exit with status code "1" upon detecting errors)
|
||||
-e, --exit-zero
|
||||
Exit with status code "0", even upon detecting errors
|
||||
-w, --watch
|
||||
Run in watch mode by re-running whenever files change
|
||||
-f, --fix
|
||||
Attempt to automatically fix lint errors
|
||||
-n, --no-cache
|
||||
Disable cache reads
|
||||
--select <SELECT>
|
||||
List of error codes to enable
|
||||
--extend-select <EXTEND_SELECT>
|
||||
Like --select, but adds additional error codes on top of the selected ones
|
||||
--ignore <IGNORE>
|
||||
List of error codes to ignore
|
||||
--extend-ignore <EXTEND_IGNORE>
|
||||
Like --ignore, but adds additional error codes on top of the ignored ones
|
||||
--exclude <EXCLUDE>
|
||||
List of paths, used to exclude files and/or directories from checks
|
||||
--extend-exclude <EXTEND_EXCLUDE>
|
||||
Like --exclude, but adds additional files and directories on top of the excluded ones
|
||||
--per-file-ignores <PER_FILE_IGNORES>
|
||||
List of mappings from file pattern to code to exclude
|
||||
--format <FORMAT>
|
||||
Output serialization format for error messages [default: text] [possible values: text, json]
|
||||
--show-files
|
||||
See the files ruff will be run against with the current settings
|
||||
--show-settings
|
||||
See ruff's settings
|
||||
--add-noqa
|
||||
Enable automatic additions of noqa directives to failing lines
|
||||
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
|
||||
Regular expression matching the name of dummy variables
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported
|
||||
-h, --help
|
||||
Print help information
|
||||
-V, --version
|
||||
Print version information
|
||||
```
|
||||
|
||||
### Excluding files
|
||||
|
||||
Exclusions are based on globs, and can be either:
|
||||
|
||||
- Single-path patterns, like `.mypy_cache` (to exclude any directory named `.mypy_cache` in the
|
||||
tree), `foo.py` (to exclude any file named `foo.py`), or `foo_*.py` (to exclude any file matching
|
||||
`foo_*.py` ).
|
||||
- Relative patterns, like `directory/foo.py` (to exclude that specific file) or `directory/*.py`
|
||||
(to exclude any Python files in `directory`). Note that these paths are relative to the
|
||||
project root (e.g., the directory containing your `pyproject.toml`).
|
||||
|
||||
### Ignoring errors
|
||||
|
||||
To omit a lint check entirely, add it to the "ignore" list via `--ignore` or `--extend-ignore`,
|
||||
either on the command-line or in your `project.toml` file.
|
||||
|
||||
To ignore an error in-line, ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
|
||||
To ignore an individual error, add `# noqa: {code}` to the end of the line, like so:
|
||||
|
||||
```python
|
||||
# Ignore F841.
|
||||
x = 1 # noqa: F841
|
||||
|
||||
# Ignore E741 and F841.
|
||||
i = 1 # noqa: E741, F841
|
||||
|
||||
# Ignore _all_ errors.
|
||||
x = 1 # noqa
|
||||
```
|
||||
|
||||
Note that, for multi-line strings, the `noqa` directive should come at the end of the string, and
|
||||
will apply to the entire body, like so:
|
||||
|
||||
```python
|
||||
"""Lorem ipsum dolor sit amet.
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
""" # noqa: E501
|
||||
```
|
||||
|
||||
ruff supports several (experimental) 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.**
|
||||
|
||||
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.**
|
||||
|
||||
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.**
|
||||
|
||||
### Compatibility with Black
|
||||
|
||||
ruff is intended to be compatible with [Black](https://github.com/psf/black), and should be
|
||||
compatible out-of-the-box as long as the `line-length` setting is consistent between the two.
|
||||
ruff is compatible with [Black](https://github.com/psf/black) out-of-the-box, as long as
|
||||
the `line-length` setting is consistent between the two.
|
||||
|
||||
As a project, ruff is designed to be used alongside Black and, as such, will defer implementing
|
||||
lint rules that are obviated by Black (e.g., stylistic rules).
|
||||
stylistic lint rules that are obviated by autoformatting.
|
||||
|
||||
### Parity with Flake8
|
||||
|
||||
ruff's goal is to achieve feature parity with Flake8 when used (1) without plugins, (2) alongside
|
||||
Black, and (3) on Python 3 code.
|
||||
|
||||
**Under those conditions, ruff implements 44 out of 60 rules.** (ruff is missing: 14 rules related
|
||||
to string `.format` calls, 1 rule related to docstring parsing, and 1 rule related to redefined
|
||||
variables.)
|
||||
|
||||
ruff also implements some of the most popular Flake8 plugins natively, including:
|
||||
|
||||
- [`flake8-builtins`](https://pypi.org/project/flake8-builtins/)
|
||||
- [`flake8-super`](https://pypi.org/project/flake8-super/)
|
||||
- [`flake8-print`](https://pypi.org/project/flake8-print/)
|
||||
- [`flake8-comprehensions`](https://pypi.org/project/flake8-comprehensions/) (11/16)
|
||||
- [`flake8-bugbear`](https://pypi.org/project/flake8-bugbear/) (3/32)
|
||||
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (partial)
|
||||
|
||||
Beyond rule-set parity, ruff suffers from the following limitations vis-à-vis Flake8:
|
||||
|
||||
1. ruff does not yet support a few Python 3.9 and 3.10 language features, including structural
|
||||
pattern matching and parenthesized context managers.
|
||||
2. Flake8 has a plugin architecture and supports writing custom lint rules.
|
||||
|
||||
## Rules
|
||||
|
||||
| Code | Name | Message |
|
||||
| ---- | ----- | ------- |
|
||||
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file |
|
||||
| E501 | LineTooLong | Line too long |
|
||||
| F401 | UnusedImport | `...` imported but unused |
|
||||
| F403 | ImportStarUsage | Unable to detect undefined names |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` |
|
||||
| F704 | YieldOutsideFunction | a `yield` or `yield from` statement outside of a function/method |
|
||||
| F706 | ReturnOutsideFunction | a `return` statement outside of a function/method |
|
||||
| F707 | DefaultExceptNotLast | an `except:` block as not the last exception handler |
|
||||
| F821 | UndefinedName | Undefined name `...` |
|
||||
| F822 | UndefinedExport | Undefined name `...` in `__all__` |
|
||||
| F823 | UndefinedLocal | Local variable `...` referenced before assignment |
|
||||
| F831 | DuplicateArgumentName | Duplicate argument name in function definition |
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` |
|
||||
| R001 | UselessObjectInheritance | Class `...` inherits from object |
|
||||
| R002 | NoAssertEquals | `assertEquals` is deprecated, use `assertEqual` instead |
|
||||
The ✅ emoji indicates a rule is enabled by default.
|
||||
|
||||
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
|
||||
|
||||
| Code | Name | Message | | |
|
||||
| ---- | ---- | ------- | --- | --- |
|
||||
| E402 | ModuleImportNotAtTopOfFile | Module level import not at top of file | ✅ | |
|
||||
| E501 | LineTooLong | Line too long (89 > 88 characters) | ✅ | |
|
||||
| E711 | NoneComparison | Comparison to `None` should be `cond is None` | ✅ | |
|
||||
| E712 | TrueFalseComparison | Comparison to `True` should be `cond is True` | ✅ | |
|
||||
| E713 | NotInTest | Test for membership should be `not in` | ✅ | |
|
||||
| E714 | NotIsTest | Test for object identity should be `is not` | ✅ | |
|
||||
| E721 | TypeComparison | Do not compare types, use `isinstance()` | ✅ | |
|
||||
| E722 | DoNotUseBareExcept | Do not use bare `except` | ✅ | |
|
||||
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | ✅ | |
|
||||
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | ✅ | |
|
||||
| E742 | AmbiguousClassName | Ambiguous class name: `...` | ✅ | |
|
||||
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | ✅ | |
|
||||
| E902 | IOError | IOError: `...` | ✅ | |
|
||||
| E999 | SyntaxError | SyntaxError: `...` | ✅ | |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | ✅ | |
|
||||
| F401 | UnusedImport | `...` imported but unused | ✅ | 🛠 |
|
||||
| F402 | ImportShadowedByLoopVar | Import `...` from line 1 shadowed by loop variable | ✅ | |
|
||||
| F403 | ImportStarUsed | `from ... import *` used; unable to detect undefined names | ✅ | |
|
||||
| F404 | LateFutureImport | `from __future__` imports must occur at the beginning of the file | ✅ | |
|
||||
| F405 | ImportStarUsage | `...` may be undefined, or defined from star imports: `...` | ✅ | |
|
||||
| F406 | ImportStarNotPermitted | `from ... import *` only allowed at module level | ✅ | |
|
||||
| F407 | FutureFeatureNotDefined | Future feature `...` is not defined | ✅ | |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders | ✅ | |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | ✅ | |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | ✅ | |
|
||||
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | ✅ | |
|
||||
| F622 | TwoStarredExpressions | Two starred expressions in assignment | ✅ | |
|
||||
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | ✅ | |
|
||||
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | ✅ | |
|
||||
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | ✅ | |
|
||||
| F634 | IfTuple | If test is a tuple, which is always `True` | ✅ | |
|
||||
| F701 | BreakOutsideLoop | `break` outside loop | ✅ | |
|
||||
| F702 | ContinueOutsideLoop | `continue` not properly in loop | ✅ | |
|
||||
| F704 | YieldOutsideFunction | `yield` or `yield from` statement outside of a function/method | ✅ | |
|
||||
| F706 | ReturnOutsideFunction | `return` statement outside of a function/method | ✅ | |
|
||||
| F707 | DefaultExceptNotLast | An `except:` block as not the last exception handler | ✅ | |
|
||||
| F722 | ForwardAnnotationSyntaxError | Syntax error in forward annotation: `...` | ✅ | |
|
||||
| F821 | UndefinedName | Undefined name `...` | ✅ | |
|
||||
| F822 | UndefinedExport | Undefined name `...` in `__all__` | ✅ | |
|
||||
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | ✅ | |
|
||||
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | ✅ | |
|
||||
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | ✅ | |
|
||||
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | ✅ | |
|
||||
| A001 | BuiltinVariableShadowing | Variable `...` is shadowing a python builtin | | |
|
||||
| A002 | BuiltinArgumentShadowing | Argument `...` is shadowing a python builtin | | |
|
||||
| A003 | BuiltinAttributeShadowing | Class attribute `...` is shadowing a python builtin | | |
|
||||
| B011 | DoNotAssertFalse | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | | 🛠 |
|
||||
| B014 | DuplicateHandlerException | Exception handler with duplicate exception `Exception` | | |
|
||||
| B025 | DuplicateTryBlockException | try-except block with duplicate exception `Exception` | | |
|
||||
| C400 | UnnecessaryGeneratorList | Unnecessary generator - rewrite as a list comprehension | | |
|
||||
| C401 | UnnecessaryGeneratorSet | Unnecessary generator - rewrite as a set comprehension | | |
|
||||
| C402 | UnnecessaryGeneratorDict | Unnecessary generator - rewrite as a dict comprehension | | |
|
||||
| C403 | UnnecessaryListComprehensionSet | Unnecessary list comprehension - rewrite as a set comprehension | | |
|
||||
| C404 | UnnecessaryListComprehensionDict | Unnecessary list comprehension - rewrite as a dict comprehension | | |
|
||||
| C405 | UnnecessaryLiteralSet | Unnecessary <list/tuple> literal - rewrite as a set literal | | |
|
||||
| C406 | UnnecessaryLiteralDict | Unnecessary <list/tuple> literal - rewrite as a dict literal | | |
|
||||
| C408 | UnnecessaryCollectionCall | Unnecessary <dict/list/tuple> call - rewrite as a literal | | |
|
||||
| C409 | UnnecessaryLiteralWithinTupleCall | Unnecessary <list/tuple> literal passed to tuple() - remove the outer call to tuple() | | |
|
||||
| C410 | UnnecessaryLiteralWithinListCall | Unnecessary <list/tuple> literal passed to list() - rewrite as a list literal | | |
|
||||
| C415 | UnnecessarySubscriptReversal | Unnecessary subscript reversal of iterable within <reversed/set/sorted>() | | |
|
||||
| T201 | PrintFound | `print` found | | 🛠 |
|
||||
| T203 | PPrintFound | `pprint` found | | 🛠 |
|
||||
| U001 | UselessMetaclassType | `__metaclass__ = type` is implied | | 🛠 |
|
||||
| U002 | UnnecessaryAbspath | `abspath(__file__)` is unnecessary in Python 3.9 and later | | 🛠 |
|
||||
| U003 | TypeOfPrimitive | Use `str` instead of `type(...)` | | 🛠 |
|
||||
| U004 | UselessObjectInheritance | Class `...` inherits from object | | 🛠 |
|
||||
| U005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` instead | | 🛠 |
|
||||
| U006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | | 🛠 |
|
||||
| U007 | UsePEP604Annotation | Use `X \| Y` for type annotations | | 🛠 |
|
||||
| U008 | SuperCallWithParameters | Use `super()` instead of `super(__class__, self)` | | 🛠 |
|
||||
| M001 | UnusedNOQA | Unused `noqa` directive | | 🛠 |
|
||||
|
||||
## Integrations
|
||||
|
||||
### PyCharm
|
||||
|
||||
ruff can be installed as an [External Tool](https://www.jetbrains.com/help/pycharm/configuring-third-party-tools.html)
|
||||
in PyCharm. Open the Preferences pane, then navigate to "Tools", then "External Tools". From there,
|
||||
add a new tool with the following configuration:
|
||||
|
||||

|
||||
|
||||
ruff should then appear as a runnable action:
|
||||
|
||||

|
||||
|
||||
### GitHub Actions
|
||||
|
||||
GitHub Actions has everything you need to run ruff out-of-the-box:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.10"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install ruff
|
||||
- name: Run ruff
|
||||
run: ruff .
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
@@ -171,9 +375,9 @@ git clone --branch 3.10 https://github.com/python/cpython.git resources/test/cpy
|
||||
Add this `pyproject.toml` to the CPython directory:
|
||||
|
||||
```toml
|
||||
[tool.linter]
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
exclude = [
|
||||
extend-exclude = [
|
||||
"Lib/lib2to3/tests/data/bom.py",
|
||||
"Lib/lib2to3/tests/data/crlf.py",
|
||||
"Lib/lib2to3/tests/data/different_encoding.py",
|
||||
@@ -204,17 +408,21 @@ Next, to benchmark the release build:
|
||||
```shell
|
||||
cargo build --release
|
||||
|
||||
hyperfine --ignore-failure --warmup 1 \
|
||||
hyperfine --ignore-failure --warmup 10 --runs 100 \
|
||||
"./target/release/ruff ./resources/test/cpython/ --no-cache" \
|
||||
"./target/release/ruff ./resources/test/cpython/"
|
||||
|
||||
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
|
||||
Time (mean ± σ): 353.6 ms ± 7.6 ms [User: 2868.8 ms, System: 171.5 ms]
|
||||
Range (min … max): 344.4 ms … 367.3 ms 10 runs
|
||||
Time (mean ± σ): 297.4 ms ± 4.9 ms [User: 2460.0 ms, System: 67.2 ms]
|
||||
Range (min … max): 287.7 ms … 312.1 ms 100 runs
|
||||
|
||||
Warning: Ignoring non-zero exit code.
|
||||
|
||||
Benchmark 2: ./target/release/ruff ./resources/test/cpython/
|
||||
Time (mean ± σ): 59.6 ms ± 2.5 ms [User: 36.4 ms, System: 345.6 ms]
|
||||
Range (min … max): 55.9 ms … 67.0 ms 48 runs
|
||||
Time (mean ± σ): 79.6 ms ± 7.3 ms [User: 59.7 ms, System: 356.1 ms]
|
||||
Range (min … max): 62.4 ms … 111.2 ms 100 runs
|
||||
|
||||
Warning: Ignoring non-zero exit code.
|
||||
```
|
||||
|
||||
To benchmark against the ecosystem's existing tools:
|
||||
@@ -226,11 +434,8 @@ hyperfine --ignore-failure --warmup 5 \
|
||||
"pyflakes resources/test/cpython" \
|
||||
"autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython" \
|
||||
"pycodestyle resources/test/cpython" \
|
||||
"pycodestyle --select E501 resources/test/cpython" \
|
||||
"flake8 resources/test/cpython" \
|
||||
"flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython" \
|
||||
"python -m scripts.run_flake8 resources/test/cpython" \
|
||||
"python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501"
|
||||
"python -m scripts.run_flake8 resources/test/cpython"
|
||||
```
|
||||
|
||||
In order, these evaluate:
|
||||
@@ -240,68 +445,65 @@ In order, these evaluate:
|
||||
- PyFlakes
|
||||
- autoflake
|
||||
- pycodestyle
|
||||
- pycodestyle, limited to the checks supported by ruff
|
||||
- Flake8
|
||||
- Flake8, limited to the checks supported by ruff
|
||||
- Flake8, with a hack to enable multiprocessing on macOS
|
||||
- Flake8, with a hack to enable multiprocessing on macOS, limited to the checks supported by ruff
|
||||
|
||||
(You can `poetry install` from `./scripts` to create a working environment for the above.)
|
||||
|
||||
```shell
|
||||
Benchmark 1: ./target/release/ruff ./resources/test/cpython/ --no-cache
|
||||
Time (mean ± σ): 469.3 ms ± 16.3 ms [User: 2663.0 ms, System: 972.5 ms]
|
||||
Range (min … max): 445.2 ms … 494.8 ms 10 runs
|
||||
Time (mean ± σ): 297.9 ms ± 7.0 ms [User: 2436.6 ms, System: 65.9 ms]
|
||||
Range (min … max): 289.9 ms … 314.6 ms 10 runs
|
||||
|
||||
Warning: Ignoring non-zero exit code.
|
||||
|
||||
Benchmark 2: pylint --recursive=y resources/test/cpython/
|
||||
Time (mean ± σ): 27.211 s ± 0.097 s [User: 26.405 s, System: 0.799 s]
|
||||
Range (min … max): 27.056 s … 27.349 s 10 runs
|
||||
Time (mean ± σ): 37.634 s ± 0.225 s [User: 36.728 s, System: 0.853 s]
|
||||
Range (min … max): 37.201 s … 38.106 s 10 runs
|
||||
|
||||
Warning: Ignoring non-zero exit code.
|
||||
|
||||
Benchmark 3: pyflakes resources/test/cpython
|
||||
Time (mean ± σ): 27.309 s ± 0.033 s [User: 27.137 s, System: 0.169 s]
|
||||
Range (min … max): 27.267 s … 27.372 s 10 runs
|
||||
Time (mean ± σ): 40.950 s ± 0.449 s [User: 40.688 s, System: 0.229 s]
|
||||
Range (min … max): 40.348 s … 41.671 s 10 runs
|
||||
|
||||
Warning: Ignoring non-zero exit code.
|
||||
|
||||
Benchmark 4: autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython
|
||||
Time (mean ± σ): 8.027 s ± 0.024 s [User: 74.255 s, System: 0.953 s]
|
||||
Range (min … max): 7.969 s … 8.052 s 10 runs
|
||||
Time (mean ± σ): 11.562 s ± 0.160 s [User: 107.022 s, System: 1.143 s]
|
||||
Range (min … max): 11.417 s … 11.917 s 10 runs
|
||||
|
||||
Benchmark 5: pycodestyle resources/test/cpython
|
||||
Time (mean ± σ): 41.666 s ± 0.266 s [User: 41.531 s, System: 0.132 s]
|
||||
Range (min … max): 41.295 s … 41.980 s 10 runs
|
||||
Time (mean ± σ): 67.428 s ± 0.985 s [User: 67.199 s, System: 0.203 s]
|
||||
Range (min … max): 65.313 s … 68.496 s 10 runs
|
||||
|
||||
Benchmark 6: pycodestyle --select E501 resources/test/cpython
|
||||
Time (mean ± σ): 14.547 s ± 0.077 s [User: 14.466 s, System: 0.079 s]
|
||||
Range (min … max): 14.429 s … 14.695 s 10 runs
|
||||
Warning: Ignoring non-zero exit code.
|
||||
|
||||
Benchmark 7: flake8 resources/test/cpython
|
||||
Time (mean ± σ): 75.700 s ± 0.152 s [User: 75.254 s, System: 0.440 s]
|
||||
Range (min … max): 75.513 s … 76.014 s 10 runs
|
||||
Benchmark 6: flake8 resources/test/cpython
|
||||
Time (mean ± σ): 116.099 s ± 1.178 s [User: 115.217 s, System: 0.845 s]
|
||||
Range (min … max): 114.180 s … 117.724 s 10 runs
|
||||
|
||||
Benchmark 8: flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython
|
||||
Time (mean ± σ): 75.122 s ± 0.532 s [User: 74.677 s, System: 0.440 s]
|
||||
Range (min … max): 74.130 s … 75.606 s 10 runs
|
||||
Warning: Ignoring non-zero exit code.
|
||||
|
||||
Benchmark 9: python -m scripts.run_flake8 resources/test/cpython
|
||||
Time (mean ± σ): 12.794 s ± 0.147 s [User: 90.792 s, System: 0.738 s]
|
||||
Range (min … max): 12.606 s … 13.030 s 10 runs
|
||||
|
||||
Benchmark 10: python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501
|
||||
Time (mean ± σ): 12.487 s ± 0.118 s [User: 90.052 s, System: 0.714 s]
|
||||
Range (min … max): 12.265 s … 12.665 s 10 runs
|
||||
Benchmark 7: python -m scripts.run_flake8 resources/test/cpython
|
||||
Time (mean ± σ): 20.477 s ± 0.349 s [User: 142.372 s, System: 1.504 s]
|
||||
Range (min … max): 20.107 s … 21.183 s 10 runs
|
||||
|
||||
Summary
|
||||
'./target/release/ruff ./resources/test/cpython/ --no-cache' ran
|
||||
17.10 ± 0.60 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
|
||||
26.60 ± 0.96 times faster than 'python -m scripts.run_flake8 resources/test/cpython --select=F831,F541,F634,F403,F706,F901,E501'
|
||||
27.26 ± 1.00 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
|
||||
30.99 ± 1.09 times faster than 'pycodestyle --select E501 resources/test/cpython'
|
||||
57.98 ± 2.03 times faster than 'pylint --recursive=y resources/test/cpython/'
|
||||
58.19 ± 2.02 times faster than 'pyflakes resources/test/cpython'
|
||||
88.77 ± 3.14 times faster than 'pycodestyle resources/test/cpython'
|
||||
160.06 ± 5.68 times faster than 'flake8 --select=F831,F541,F634,F403,F706,F901,E501 resources/test/cpython'
|
||||
161.29 ± 5.61 times faster than 'flake8 resources/test/cpython'
|
||||
38.81 ± 1.05 times faster than 'autoflake --recursive --expand-star-imports --remove-all-unused-imports --remove-unused-variables --remove-duplicate-keys resources/test/cpython'
|
||||
68.74 ± 1.99 times faster than 'python -m scripts.run_flake8 resources/test/cpython'
|
||||
126.33 ± 3.05 times faster than 'pylint --recursive=y resources/test/cpython/'
|
||||
137.46 ± 3.55 times faster than 'pyflakes resources/test/cpython'
|
||||
226.35 ± 6.23 times faster than 'pycodestyle resources/test/cpython'
|
||||
389.73 ± 9.92 times faster than 'flake8 resources/test/cpython'
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome and hugely appreciated. To get started, check out the
|
||||
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md).
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
/// Generate a Markdown-compatible table of supported lint rules.
|
||||
use ruff::checks::CheckKind;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use ruff::checks::{CheckCode, DEFAULT_CHECK_CODES};
|
||||
|
||||
fn main() {
|
||||
let mut check_kinds: Vec<CheckKind> = vec![
|
||||
CheckKind::AssertTuple,
|
||||
CheckKind::DefaultExceptNotLast,
|
||||
CheckKind::DuplicateArgumentName,
|
||||
CheckKind::FStringMissingPlaceholders,
|
||||
CheckKind::IfTuple,
|
||||
CheckKind::ImportStarUsage,
|
||||
CheckKind::LineTooLong,
|
||||
CheckKind::ModuleImportNotAtTopOfFile,
|
||||
CheckKind::NoAssertEquals,
|
||||
CheckKind::RaiseNotImplemented,
|
||||
CheckKind::ReturnOutsideFunction,
|
||||
CheckKind::UndefinedExport("...".to_string()),
|
||||
CheckKind::UndefinedLocal("...".to_string()),
|
||||
CheckKind::UndefinedName("...".to_string()),
|
||||
CheckKind::UnusedImport("...".to_string()),
|
||||
CheckKind::UnusedVariable("...".to_string()),
|
||||
CheckKind::UselessObjectInheritance("...".to_string()),
|
||||
CheckKind::YieldOutsideFunction,
|
||||
];
|
||||
check_kinds.sort_by_key(|check_kind| check_kind.code());
|
||||
|
||||
println!("| Code | Name | Message |");
|
||||
println!("| ---- | ----- | ------- |");
|
||||
for check_kind in check_kinds {
|
||||
println!("| Code | Name | Message | | |");
|
||||
println!("| ---- | ---- | ------- | --- | --- |");
|
||||
for check_code in CheckCode::iter() {
|
||||
let check_kind = check_code.kind();
|
||||
let default_token = if DEFAULT_CHECK_CODES.contains(&check_code) {
|
||||
"✅"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
|
||||
println!(
|
||||
"| {} | {} | {} |",
|
||||
check_kind.code().as_str(),
|
||||
check_kind.name(),
|
||||
check_kind.body()
|
||||
"| {} | {} | {} | {} | {} |",
|
||||
check_kind.code().as_ref(),
|
||||
check_kind.as_ref(),
|
||||
check_kind.body().replace("|", r"\|"),
|
||||
default_token,
|
||||
fix_token
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
26
examples/generate_source_code.rs
Normal file
26
examples/generate_source_code.rs
Normal file
@@ -0,0 +1,26 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Parser;
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use ruff::code_gen::SourceGenerator;
|
||||
use ruff::fs;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
#[arg(required = true)]
|
||||
file: PathBuf,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let cli = Cli::parse();
|
||||
|
||||
let contents = fs::read_file(&cli.file)?;
|
||||
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
|
||||
let mut generator = SourceGenerator::new();
|
||||
generator.unparse_suite(&python_ast)?;
|
||||
println!("{}", generator.generate()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,14 +2,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueHint};
|
||||
use clap::Parser;
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use ruff::fs;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
|
||||
#[arg(required = true)]
|
||||
file: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueHint};
|
||||
use clap::Parser;
|
||||
use rustpython_parser::lexer;
|
||||
|
||||
use ruff::fs;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
#[clap(parse(from_os_str), value_hint = ValueHint::FilePath, required = true)]
|
||||
#[arg(required = true)]
|
||||
file: PathBuf,
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Operating System :: OS Independent",
|
||||
"Programming Language :: Python",
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Programming Language :: Python :: 3.7",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
@@ -19,10 +18,12 @@ classifiers = [
|
||||
]
|
||||
author = "Charlie Marsh"
|
||||
author_email = "charlie.r.marsh@gmail.com"
|
||||
url = "https://github.com/charliermarsh/ruff"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
requires-python = ">=3.7"
|
||||
|
||||
[project.urls]
|
||||
repository = "https://github.com/charliermarsh/ruff"
|
||||
|
||||
[build-system]
|
||||
requires = ["maturin>=0.13,<0.14"]
|
||||
build-backend = "maturin"
|
||||
|
||||
27
resources/test/fixtures/A001.py
vendored
Normal file
27
resources/test/fixtures/A001.py
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import some as sum
|
||||
from some import other as int
|
||||
|
||||
print = 1
|
||||
copyright: 'annotation' = 2
|
||||
(complex := 3)
|
||||
float = object = 4
|
||||
min, max = 5, 6
|
||||
|
||||
def bytes():
|
||||
pass
|
||||
|
||||
class slice:
|
||||
pass
|
||||
|
||||
try:
|
||||
...
|
||||
except ImportError as ValueError:
|
||||
...
|
||||
|
||||
for memoryview, *bytearray in []:
|
||||
pass
|
||||
|
||||
with open('file') as str, open('file2') as (all, any):
|
||||
pass
|
||||
|
||||
[0 for sum in ()]
|
||||
9
resources/test/fixtures/A002.py
vendored
Normal file
9
resources/test/fixtures/A002.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
def func1(str, /, type, *complex, Exception, **getattr):
|
||||
pass
|
||||
|
||||
|
||||
async def func2(bytes):
|
||||
pass
|
||||
|
||||
|
||||
map([], lambda float: ...)
|
||||
8
resources/test/fixtures/A003.py
vendored
Normal file
8
resources/test/fixtures/A003.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
class MyClass:
|
||||
ImportError = 4
|
||||
|
||||
def __init__(self):
|
||||
self.float = 5 # is fine
|
||||
|
||||
def str(self):
|
||||
pass
|
||||
10
resources/test/fixtures/B011.py
vendored
Normal file
10
resources/test/fixtures/B011.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Should emit:
|
||||
B011 - on line 8
|
||||
B011 - on line 10
|
||||
"""
|
||||
|
||||
assert 1 != 2
|
||||
assert False
|
||||
assert 1 != 2, "message"
|
||||
assert False, "message"
|
||||
76
resources/test/fixtures/B014.py
vendored
Normal file
76
resources/test/fixtures/B014.py
vendored
Normal file
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Should emit:
|
||||
B014 - on lines 11, 17, 28, 42, 49, 56, and 74.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import re
|
||||
|
||||
try:
|
||||
pass
|
||||
except (Exception, TypeError):
|
||||
# TypeError is a subclass of Exception, so it doesn't add anything
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except (OSError, OSError) as err:
|
||||
# Duplicate exception types are useless
|
||||
pass
|
||||
|
||||
|
||||
class MyError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyError, MyError):
|
||||
# Detect duplicate non-builtin errors
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyError, Exception) as e:
|
||||
# Don't assume that we're all subclasses of Exception
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyError, BaseException) as e:
|
||||
# But we *can* assume that everything is a subclass of BaseException
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (re.error, re.error):
|
||||
# Duplicate exception types as attributes
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (IOError, EnvironmentError, OSError):
|
||||
# Detect if a primary exception and any its aliases are present.
|
||||
#
|
||||
# Since Python 3.3, IOError, EnvironmentError, WindowsError, mmap.error,
|
||||
# socket.error and select.error are aliases of OSError. See PEP 3151 for
|
||||
# more info.
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (MyException, NotImplemented):
|
||||
# NotImplemented is not an exception, let's not crash on it.
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
pass
|
||||
except (ValueError, binascii.Error):
|
||||
# binascii.Error is a subclass of ValueError.
|
||||
pass
|
||||
38
resources/test/fixtures/B025.py
vendored
Normal file
38
resources/test/fixtures/B025.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Should emit:
|
||||
B025 - on lines 15, 22, 31
|
||||
"""
|
||||
|
||||
import pickle
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except ValueError:
|
||||
a = 2
|
||||
finally:
|
||||
a = 3
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except ValueError:
|
||||
a = 2
|
||||
except ValueError:
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except pickle.PickleError:
|
||||
a = 2
|
||||
except ValueError:
|
||||
a = 2
|
||||
except pickle.PickleError:
|
||||
a = 2
|
||||
|
||||
try:
|
||||
a = 1
|
||||
except (ValueError, TypeError):
|
||||
a = 2
|
||||
except ValueError:
|
||||
a = 2
|
||||
except (OSError, TypeError):
|
||||
a = 2
|
||||
1
resources/test/fixtures/C400.py
vendored
Normal file
1
resources/test/fixtures/C400.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
x = list(x for x in range(3))
|
||||
1
resources/test/fixtures/C401.py
vendored
Normal file
1
resources/test/fixtures/C401.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
x = set(x for x in range(3))
|
||||
1
resources/test/fixtures/C402.py
vendored
Normal file
1
resources/test/fixtures/C402.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
d = dict((x, x) for x in range(3))
|
||||
1
resources/test/fixtures/C403.py
vendored
Normal file
1
resources/test/fixtures/C403.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
s = set([x for x in range(3)])
|
||||
1
resources/test/fixtures/C404.py
vendored
Normal file
1
resources/test/fixtures/C404.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
d = dict([(i, i) for i in range(3)])
|
||||
5
resources/test/fixtures/C405.py
vendored
Normal file
5
resources/test/fixtures/C405.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
s1 = set([1, 2])
|
||||
s2 = set((1, 2))
|
||||
s3 = set([])
|
||||
s4 = set(())
|
||||
s5 = set()
|
||||
5
resources/test/fixtures/C406.py
vendored
Normal file
5
resources/test/fixtures/C406.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
d1 = dict([(1, 2)])
|
||||
d2 = dict(((1, 2),))
|
||||
d3 = dict([])
|
||||
d4 = dict(())
|
||||
d5 = dict()
|
||||
5
resources/test/fixtures/C408.py
vendored
Normal file
5
resources/test/fixtures/C408.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
t = tuple()
|
||||
l = list()
|
||||
d1 = dict()
|
||||
d2 = dict(a=1)
|
||||
d3 = dict(**d2)
|
||||
3
resources/test/fixtures/C409.py
vendored
Normal file
3
resources/test/fixtures/C409.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
t1 = tuple([1, 2])
|
||||
t2 = tuple((1, 2))
|
||||
t3 = tuple([])
|
||||
4
resources/test/fixtures/C410.py
vendored
Normal file
4
resources/test/fixtures/C410.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
l1 = list([1, 2])
|
||||
l2 = list((1, 2))
|
||||
l3 = list([])
|
||||
l4 = list(())
|
||||
9
resources/test/fixtures/C415.py
vendored
Normal file
9
resources/test/fixtures/C415.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
lst = [2, 1, 3]
|
||||
a = set(lst[::-1])
|
||||
b = reversed(lst[::-1])
|
||||
c = sorted(lst[::-1])
|
||||
d = sorted(lst[::-1], reverse=True)
|
||||
e = set(lst[2:-1])
|
||||
f = set(lst[:1:-1])
|
||||
g = set(lst[::1])
|
||||
h = set(lst[::-2])
|
||||
4
resources/test/fixtures/E402.py
vendored
4
resources/test/fixtures/E402.py
vendored
@@ -1,4 +1,8 @@
|
||||
"""Top-level docstring."""
|
||||
|
||||
__all__ = ["y"]
|
||||
__version__: str = "0.1.0"
|
||||
|
||||
import a
|
||||
|
||||
try:
|
||||
|
||||
10
resources/test/fixtures/E501.py
vendored
10
resources/test/fixtures/E501.py
vendored
@@ -4,3 +4,13 @@
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
"""
|
||||
|
||||
_ = """Lorem ipsum dolor sit amet.
|
||||
|
||||
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
""" # noqa: E501
|
||||
|
||||
_ = "---------------------------------------------------------------------------AAAAAAA"
|
||||
_ = "---------------------------------------------------------------------------亜亜亜亜亜亜亜"
|
||||
|
||||
50
resources/test/fixtures/E711.py
vendored
Normal file
50
resources/test/fixtures/E711.py
vendored
Normal file
@@ -0,0 +1,50 @@
|
||||
#: E711
|
||||
if res == None:
|
||||
pass
|
||||
#: E711
|
||||
if res != None:
|
||||
pass
|
||||
#: E711
|
||||
if None == res:
|
||||
pass
|
||||
#: E711
|
||||
if None != res:
|
||||
pass
|
||||
#: E711
|
||||
if res[1] == None:
|
||||
pass
|
||||
#: E711
|
||||
if res[1] != None:
|
||||
pass
|
||||
#: E711
|
||||
if None != res[1]:
|
||||
pass
|
||||
#: E711
|
||||
if None == res[1]:
|
||||
pass
|
||||
|
||||
#: Okay
|
||||
if x not in y:
|
||||
pass
|
||||
|
||||
if not (X in Y or X is Z):
|
||||
pass
|
||||
|
||||
if not (X in Y):
|
||||
pass
|
||||
|
||||
if x is not y:
|
||||
pass
|
||||
|
||||
if X is not Y is not Z:
|
||||
pass
|
||||
|
||||
if TrueElement.get_element(True) == TrueElement.get_element(False):
|
||||
pass
|
||||
|
||||
if (True) == TrueElement or x == TrueElement:
|
||||
pass
|
||||
|
||||
assert (not foo) in bar
|
||||
assert {"x": not foo} in bar
|
||||
assert [42, not foo] in bar
|
||||
46
resources/test/fixtures/E712.py
vendored
Normal file
46
resources/test/fixtures/E712.py
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
#: E712
|
||||
if res == True:
|
||||
pass
|
||||
#: E712
|
||||
if res != False:
|
||||
pass
|
||||
#: E712
|
||||
if True != res:
|
||||
pass
|
||||
#: E712
|
||||
if False == res:
|
||||
pass
|
||||
#: E712
|
||||
if res[1] == True:
|
||||
pass
|
||||
#: E712
|
||||
if res[1] != False:
|
||||
pass
|
||||
#: E712
|
||||
var = 1 if cond == True else -1 if cond == False else cond
|
||||
#: E712
|
||||
if (True) == TrueElement or x == TrueElement:
|
||||
pass
|
||||
|
||||
#: Okay
|
||||
if x not in y:
|
||||
pass
|
||||
|
||||
if not (X in Y or X is Z):
|
||||
pass
|
||||
|
||||
if not (X in Y):
|
||||
pass
|
||||
|
||||
if x is not y:
|
||||
pass
|
||||
|
||||
if X is not Y is not Z:
|
||||
pass
|
||||
|
||||
if TrueElement.get_element(True) == TrueElement.get_element(False):
|
||||
pass
|
||||
|
||||
assert (not foo) in bar
|
||||
assert {"x": not foo} in bar
|
||||
assert [42, not foo] in bar
|
||||
38
resources/test/fixtures/E713.py
vendored
Normal file
38
resources/test/fixtures/E713.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
#: E713
|
||||
if not X in Y:
|
||||
pass
|
||||
#: E713
|
||||
if not X.B in Y:
|
||||
pass
|
||||
#: E713
|
||||
if not X in Y and Z == "zero":
|
||||
pass
|
||||
#: E713
|
||||
if X == "zero" or not Y in Z:
|
||||
pass
|
||||
#: E713
|
||||
if not (X in Y):
|
||||
pass
|
||||
|
||||
#: Okay
|
||||
if x not in y:
|
||||
pass
|
||||
|
||||
if not (X in Y or X is Z):
|
||||
pass
|
||||
|
||||
if x is not y:
|
||||
pass
|
||||
|
||||
if X is not Y is not Z:
|
||||
pass
|
||||
|
||||
if TrueElement.get_element(True) == TrueElement.get_element(False):
|
||||
pass
|
||||
|
||||
if (True) == TrueElement or x == TrueElement:
|
||||
pass
|
||||
|
||||
assert (not foo) in bar
|
||||
assert {"x": not foo} in bar
|
||||
assert [42, not foo] in bar
|
||||
38
resources/test/fixtures/E714.py
vendored
Normal file
38
resources/test/fixtures/E714.py
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
#: E714
|
||||
if not X is Y:
|
||||
pass
|
||||
#: E714
|
||||
if not X.B is Y:
|
||||
pass
|
||||
#: E714
|
||||
if not X is Y is not Z:
|
||||
pass
|
||||
|
||||
#: Okay
|
||||
if not X is not Y:
|
||||
pass
|
||||
|
||||
if x not in y:
|
||||
pass
|
||||
|
||||
if not (X in Y or X is Z):
|
||||
pass
|
||||
|
||||
if not (X in Y):
|
||||
pass
|
||||
|
||||
if x is not y:
|
||||
pass
|
||||
|
||||
if X is not Y is not Z:
|
||||
pass
|
||||
|
||||
if TrueElement.get_element(True) == TrueElement.get_element(False):
|
||||
pass
|
||||
|
||||
if (True) == TrueElement or x == TrueElement:
|
||||
pass
|
||||
|
||||
assert (not foo) in bar
|
||||
assert {"x": not foo} in bar
|
||||
assert [42, not foo] in bar
|
||||
54
resources/test/fixtures/E721.py
vendored
Normal file
54
resources/test/fixtures/E721.py
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
#: E721
|
||||
if type(res) == type(42):
|
||||
pass
|
||||
#: E721
|
||||
if type(res) != type(""):
|
||||
pass
|
||||
#: E721
|
||||
import types
|
||||
|
||||
if res == types.IntType:
|
||||
pass
|
||||
#: E721
|
||||
import types
|
||||
|
||||
if type(res) is not types.ListType:
|
||||
pass
|
||||
#: E721
|
||||
assert type(res) == type(False) or type(res) == type(None)
|
||||
#: E721
|
||||
assert type(res) == type([])
|
||||
#: E721
|
||||
assert type(res) == type(())
|
||||
#: E721
|
||||
assert type(res) == type((0,))
|
||||
#: E721
|
||||
assert type(res) == type((0))
|
||||
#: E721
|
||||
assert type(res) != type((1,))
|
||||
#: E721
|
||||
assert type(res) is type((1,))
|
||||
#: E721
|
||||
assert type(res) is not type((1,))
|
||||
#: E211 E721
|
||||
assert type(res) == type(
|
||||
[
|
||||
2,
|
||||
]
|
||||
)
|
||||
#: E201 E201 E202 E721
|
||||
assert type(res) == type(())
|
||||
#: E201 E202 E721
|
||||
assert type(res) == type((0,))
|
||||
|
||||
#: Okay
|
||||
import types
|
||||
|
||||
if isinstance(res, int):
|
||||
pass
|
||||
if isinstance(res, str):
|
||||
pass
|
||||
if isinstance(res, types.MethodType):
|
||||
pass
|
||||
if type(a) != type(b) or type(a) == type(ccc):
|
||||
pass
|
||||
33
resources/test/fixtures/E722.py
vendored
Normal file
33
resources/test/fixtures/E722.py
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
#: E722
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
#: E722
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
#: E722
|
||||
try:
|
||||
pass
|
||||
except:
|
||||
pass
|
||||
#: Okay
|
||||
fake_code = """"
|
||||
try:
|
||||
do_something()
|
||||
except:
|
||||
pass
|
||||
"""
|
||||
try:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
#: Okay
|
||||
from . import compute_type
|
||||
|
||||
if compute_type(foo) == 5:
|
||||
pass
|
||||
21
resources/test/fixtures/E731.py
vendored
Normal file
21
resources/test/fixtures/E731.py
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
#: E731
|
||||
f = lambda x: 2 * x
|
||||
#: E731
|
||||
f = lambda x: 2 * x
|
||||
#: E731
|
||||
while False:
|
||||
this = lambda y, z: 2 * x
|
||||
|
||||
|
||||
f = object()
|
||||
#: E731
|
||||
f.method = lambda: "Method"
|
||||
|
||||
f = {}
|
||||
#: E731
|
||||
f["a"] = lambda x: x ** 2
|
||||
|
||||
f = []
|
||||
f.append(lambda x: x ** 2)
|
||||
|
||||
lambda: "no-op"
|
||||
75
resources/test/fixtures/E741.py
vendored
Normal file
75
resources/test/fixtures/E741.py
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
from contextlib import contextmanager
|
||||
|
||||
l = 0
|
||||
I = 0
|
||||
O = 0
|
||||
l: int = 0
|
||||
|
||||
a, l = 0, 1
|
||||
[a, l] = 0, 1
|
||||
a, *l = 0, 1, 2
|
||||
a = l = 0
|
||||
|
||||
o = 0
|
||||
i = 0
|
||||
|
||||
for l in range(3):
|
||||
pass
|
||||
|
||||
|
||||
for a, l in zip(range(3), range(3)):
|
||||
pass
|
||||
|
||||
|
||||
def f1():
|
||||
global l
|
||||
l = 0
|
||||
|
||||
|
||||
def f2():
|
||||
l = 0
|
||||
|
||||
def f3():
|
||||
nonlocal l
|
||||
l = 1
|
||||
|
||||
f3()
|
||||
return l
|
||||
|
||||
|
||||
def f4(l, /, I):
|
||||
return l, I, O
|
||||
|
||||
|
||||
def f5(l=0, *, I=1):
|
||||
return l, I
|
||||
|
||||
|
||||
def f6(*l, **I):
|
||||
return l, I
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ctx1():
|
||||
yield 0
|
||||
|
||||
|
||||
with ctx1() as l:
|
||||
pass
|
||||
|
||||
|
||||
@contextmanager
|
||||
def ctx2():
|
||||
yield 0, 1
|
||||
|
||||
|
||||
with ctx2() as (a, l):
|
||||
pass
|
||||
|
||||
try:
|
||||
pass
|
||||
except ValueError as l:
|
||||
pass
|
||||
|
||||
if (l := 5) > 0:
|
||||
pass
|
||||
14
resources/test/fixtures/E742.py
vendored
Normal file
14
resources/test/fixtures/E742.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
class l:
|
||||
pass
|
||||
|
||||
|
||||
class I:
|
||||
pass
|
||||
|
||||
|
||||
class O:
|
||||
pass
|
||||
|
||||
|
||||
class X:
|
||||
pass
|
||||
15
resources/test/fixtures/E743.py
vendored
Normal file
15
resources/test/fixtures/E743.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
def l():
|
||||
pass
|
||||
|
||||
|
||||
def I():
|
||||
pass
|
||||
|
||||
|
||||
class X:
|
||||
def O(self):
|
||||
pass
|
||||
|
||||
|
||||
def x():
|
||||
pass
|
||||
2
resources/test/fixtures/E999.py
vendored
Normal file
2
resources/test/fixtures/E999.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
def x():
|
||||
|
||||
25
resources/test/fixtures/F401.py
vendored
25
resources/test/fixtures/F401.py
vendored
@@ -1,25 +0,0 @@
|
||||
from __future__ import all_feature_names
|
||||
import os
|
||||
import functools
|
||||
from collections import (
|
||||
Counter,
|
||||
OrderedDict,
|
||||
namedtuple,
|
||||
)
|
||||
import multiprocessing.pool
|
||||
import multiprocessing.process
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
|
||||
from blah import ClassA, ClassB, ClassC
|
||||
|
||||
|
||||
class X:
|
||||
def a(self) -> "namedtuple":
|
||||
x = os.environ["1"]
|
||||
y = Counter()
|
||||
z = multiprocessing.pool.ThreadPool()
|
||||
|
||||
|
||||
__all__ = ["ClassA"] + ["ClassB"]
|
||||
__all__ += ["ClassC"]
|
||||
88
resources/test/fixtures/F401_0.py
vendored
Normal file
88
resources/test/fixtures/F401_0.py
vendored
Normal file
@@ -0,0 +1,88 @@
|
||||
from __future__ import all_feature_names
|
||||
import functools, os
|
||||
from datetime import datetime
|
||||
from collections import (
|
||||
Counter,
|
||||
OrderedDict,
|
||||
namedtuple,
|
||||
)
|
||||
import multiprocessing.pool
|
||||
import multiprocessing.process
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
NamedTuple,
|
||||
Dict,
|
||||
Type,
|
||||
TypeVar,
|
||||
List,
|
||||
Set,
|
||||
Union,
|
||||
cast,
|
||||
)
|
||||
from dataclasses import MISSING, field
|
||||
|
||||
from blah import ClassA, ClassB, ClassC
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Fruit, Nut, Vegetable
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import shelve
|
||||
import importlib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
"""Hello, world!"""
|
||||
import pathlib
|
||||
|
||||
z = 1
|
||||
|
||||
|
||||
class X:
|
||||
datetime: datetime
|
||||
foo: Type["NamedTuple"]
|
||||
|
||||
def a(self) -> "namedtuple":
|
||||
x = os.environ["1"]
|
||||
y = Counter()
|
||||
z = multiprocessing.pool.ThreadPool()
|
||||
|
||||
def b(self) -> None:
|
||||
import pickle
|
||||
|
||||
|
||||
__all__ = ["ClassA"] + ["ClassB"]
|
||||
__all__ += ["ClassC"]
|
||||
|
||||
X = TypeVar("X")
|
||||
Y = TypeVar("Y", bound="Dict")
|
||||
Z = TypeVar("Z", "List", "Set")
|
||||
|
||||
a = list["Fruit"]
|
||||
b = Union["Nut", None]
|
||||
c = cast("Vegetable", b)
|
||||
|
||||
Field = lambda default=MISSING: field(default=default)
|
||||
|
||||
|
||||
# Test: access a sub-importation via an alias.
|
||||
import pyarrow as pa
|
||||
import pyarrow.csv
|
||||
|
||||
print(pa.csv.read_csv("test.csv"))
|
||||
|
||||
|
||||
# Test: referencing an import via TypeAlias.
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
CustomInt: TypeAlias = "np.int8 | np.int16"
|
||||
5
resources/test/fixtures/F401_1.py
vendored
Normal file
5
resources/test/fixtures/F401_1.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Access a sub-importation via an alias."""
|
||||
import pyarrow as pa
|
||||
import pyarrow.csv
|
||||
|
||||
print(pa.csv.read_csv("test.csv"))
|
||||
12
resources/test/fixtures/F401_2.py
vendored
Normal file
12
resources/test/fixtures/F401_2.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Test: referencing an import via TypeAlias."""
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
CustomInt: TypeAlias = "np.int8 | np.int16"
|
||||
14
resources/test/fixtures/F401_3.py
vendored
Normal file
14
resources/test/fixtures/F401_3.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Test: referencing an import via TypeAlias (with future annotations)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
CustomInt: TypeAlias = np.int8 | np.int16
|
||||
14
resources/test/fixtures/F401_4.py
vendored
Normal file
14
resources/test/fixtures/F401_4.py
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Test: referencing an import via TypeAlias (with future annotations and quotes)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from typing import TypeAlias
|
||||
else:
|
||||
from typing_extensions import TypeAlias
|
||||
|
||||
|
||||
CustomInt: TypeAlias = "np.int8 | np.int16"
|
||||
9
resources/test/fixtures/F402.py
vendored
Normal file
9
resources/test/fixtures/F402.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import os
|
||||
import os.path as path
|
||||
|
||||
|
||||
for os in range(3):
|
||||
pass
|
||||
|
||||
for path in range(3):
|
||||
pass
|
||||
7
resources/test/fixtures/F404.py
vendored
Normal file
7
resources/test/fixtures/F404.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import print_function
|
||||
"""Docstring"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
from __future__ import print_function
|
||||
11
resources/test/fixtures/F405.py
vendored
Normal file
11
resources/test/fixtures/F405.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
from mymodule import *
|
||||
|
||||
|
||||
def print_name():
|
||||
print(name)
|
||||
|
||||
|
||||
def print_name(name):
|
||||
print(name)
|
||||
|
||||
__all__ = ['a']
|
||||
9
resources/test/fixtures/F406.py
vendored
Normal file
9
resources/test/fixtures/F406.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
from F634 import *
|
||||
|
||||
|
||||
def f():
|
||||
from F634 import *
|
||||
|
||||
|
||||
class F:
|
||||
from F634 import *
|
||||
2
resources/test/fixtures/F407.py
vendored
Normal file
2
resources/test/fixtures/F407.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from __future__ import print_function
|
||||
from __future__ import non_existent_feature
|
||||
12
resources/test/fixtures/F601.py
vendored
Normal file
12
resources/test/fixtures/F601.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
x = {
|
||||
"a": 1,
|
||||
"a": 2,
|
||||
"b": 3,
|
||||
("a", "b"): 3,
|
||||
("a", "b"): 4,
|
||||
1.0: 2,
|
||||
1: 0,
|
||||
1: 3,
|
||||
b"123": 1,
|
||||
b"123": 4,
|
||||
}
|
||||
7
resources/test/fixtures/F602.py
vendored
Normal file
7
resources/test/fixtures/F602.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
a = 1
|
||||
b = 2
|
||||
x = {
|
||||
a: 1,
|
||||
a: 2,
|
||||
b: 3,
|
||||
}
|
||||
3
resources/test/fixtures/F622.py
vendored
Normal file
3
resources/test/fixtures/F622.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
*a, *b, c = (1, 2, 3)
|
||||
*a, b, c = (1, 2, 3)
|
||||
a, b, *c = (1, 2, 3)
|
||||
5
resources/test/fixtures/F632.py
vendored
Normal file
5
resources/test/fixtures/F632.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
if x is "abc":
|
||||
pass
|
||||
|
||||
if 123 is not y:
|
||||
pass
|
||||
4
resources/test/fixtures/F633.py
vendored
Normal file
4
resources/test/fixtures/F633.py
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
from __future__ import print_function
|
||||
import sys
|
||||
|
||||
print >> sys.stderr, "Hello"
|
||||
23
resources/test/fixtures/F701.py
vendored
Normal file
23
resources/test/fixtures/F701.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
for i in range(10):
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
i = 0
|
||||
while i < 10:
|
||||
i += 1
|
||||
break
|
||||
|
||||
|
||||
def f():
|
||||
for i in range(10):
|
||||
break
|
||||
|
||||
break
|
||||
|
||||
|
||||
class Foo:
|
||||
break
|
||||
|
||||
|
||||
break
|
||||
23
resources/test/fixtures/F702.py
vendored
Normal file
23
resources/test/fixtures/F702.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
for i in range(10):
|
||||
continue
|
||||
else:
|
||||
continue
|
||||
|
||||
i = 0
|
||||
while i < 10:
|
||||
i += 1
|
||||
continue
|
||||
|
||||
|
||||
def f():
|
||||
for i in range(10):
|
||||
continue
|
||||
|
||||
continue
|
||||
|
||||
|
||||
class Foo:
|
||||
continue
|
||||
|
||||
|
||||
continue
|
||||
10
resources/test/fixtures/F722.py
vendored
Normal file
10
resources/test/fixtures/F722.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
class A:
|
||||
pass
|
||||
|
||||
|
||||
def f() -> "A":
|
||||
pass
|
||||
|
||||
|
||||
def g() -> "///":
|
||||
pass
|
||||
91
resources/test/fixtures/F821.py
vendored
91
resources/test/fixtures/F821.py
vendored
@@ -33,14 +33,101 @@ def ternary_optarg(prec, exp_range, itr):
|
||||
class Foo:
|
||||
CLASS_VAR = 1
|
||||
REFERENCES_CLASS_VAR = {"CLASS_VAR": CLASS_VAR}
|
||||
ANNOTATED_CLASS_VAR: int = 2
|
||||
|
||||
|
||||
from typing import Literal
|
||||
|
||||
|
||||
class Class:
|
||||
copy_on_model_validation: Literal["none", "deep", "shallow"]
|
||||
post_init_call: Literal["before_validation", "after_validation"]
|
||||
|
||||
def __init__(self):
|
||||
# TODO(charlie): This should be recognized as a defined variable.
|
||||
Class # noqa: F821
|
||||
Class
|
||||
|
||||
|
||||
try:
|
||||
x = 1 / 0
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
|
||||
y: int = 1
|
||||
|
||||
x: "Bar" = 1
|
||||
|
||||
[first] = ["yup"]
|
||||
|
||||
|
||||
from typing import List, TypedDict
|
||||
|
||||
|
||||
class Item(TypedDict):
|
||||
nodes: List[TypedDict("Node", {"name": str})]
|
||||
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class Ticket:
|
||||
class Status(Enum):
|
||||
OPEN = "OPEN"
|
||||
CLOSED = "CLOSED"
|
||||
|
||||
def set_status(self, status: Status):
|
||||
self.status = status
|
||||
|
||||
|
||||
def update_tomato():
|
||||
print(TOMATO)
|
||||
TOMATO = "cherry tomato"
|
||||
|
||||
|
||||
A = f'{B}'
|
||||
A = (
|
||||
f'B'
|
||||
f'{B}'
|
||||
)
|
||||
|
||||
|
||||
from typing import Annotated, Literal
|
||||
|
||||
|
||||
def arbitrary_callable() -> None:
|
||||
...
|
||||
|
||||
|
||||
class PEP593Test:
|
||||
field: Annotated[
|
||||
int,
|
||||
"base64",
|
||||
arbitrary_callable(),
|
||||
123,
|
||||
(1, 2, 3),
|
||||
]
|
||||
field_with_stringified_type: Annotated[
|
||||
"PEP593Test",
|
||||
123,
|
||||
]
|
||||
field_with_undefined_stringified_type: Annotated[
|
||||
"PEP593Test123",
|
||||
123,
|
||||
]
|
||||
field_with_nested_subscript: Annotated[
|
||||
dict[Literal["foo"], str],
|
||||
123,
|
||||
]
|
||||
field_with_undefined_nested_subscript: Annotated[
|
||||
dict["foo", "bar"], # Expected to fail as undefined.
|
||||
123,
|
||||
]
|
||||
|
||||
|
||||
def in_ipython_notebook() -> bool:
|
||||
try:
|
||||
# autoimported by notebooks
|
||||
get_ipython() # type: ignore[name-defined]
|
||||
except NameError:
|
||||
return False # not in notebook
|
||||
return True
|
||||
|
||||
23
resources/test/fixtures/F841.py
vendored
23
resources/test/fixtures/F841.py
vendored
@@ -10,7 +10,28 @@ except ValueError as e:
|
||||
print(e)
|
||||
|
||||
|
||||
def f():
|
||||
def f1():
|
||||
x = 1
|
||||
y = 2
|
||||
z = x + y
|
||||
|
||||
|
||||
def f2():
|
||||
foo = (1, 2)
|
||||
(a, b) = (1, 2)
|
||||
|
||||
bar = (1, 2)
|
||||
(c, d) = bar
|
||||
|
||||
(x, y) = baz = bar
|
||||
|
||||
|
||||
def f3():
|
||||
locals()
|
||||
x = 1
|
||||
|
||||
|
||||
def f4():
|
||||
_ = 1
|
||||
__ = 1
|
||||
_discarded = 1
|
||||
|
||||
60
resources/test/fixtures/M001.py
vendored
Normal file
60
resources/test/fixtures/M001.py
vendored
Normal file
@@ -0,0 +1,60 @@
|
||||
def f() -> None:
|
||||
# Valid
|
||||
a = 1 # noqa
|
||||
|
||||
# Valid
|
||||
b = 2 # noqa: F841
|
||||
|
||||
# Invalid
|
||||
c = 1 # noqa
|
||||
print(c)
|
||||
|
||||
# Invalid
|
||||
d = 1 # noqa: E501
|
||||
|
||||
# Invalid
|
||||
d = 1 # noqa: F841, E501
|
||||
|
||||
# Invalid (and unimplemented)
|
||||
d = 1 # noqa: F841, W191
|
||||
|
||||
|
||||
# Valid
|
||||
_ = """Lorem ipsum dolor sit amet.
|
||||
|
||||
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
""" # noqa: E501
|
||||
|
||||
# Valid
|
||||
_ = """Lorem ipsum dolor sit amet.
|
||||
|
||||
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
""" # noqa
|
||||
|
||||
# Invalid
|
||||
_ = """Lorem ipsum dolor sit amet.
|
||||
|
||||
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
|
||||
""" # noqa: E501, F841
|
||||
|
||||
# Invalid
|
||||
_ = """Lorem ipsum dolor sit amet.
|
||||
|
||||
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
|
||||
""" # noqa: E501
|
||||
|
||||
# Invalid
|
||||
_ = """Lorem ipsum dolor sit amet.
|
||||
|
||||
https://github.com/PyCQA/pycodestyle/pull/258/files#diff-841c622497a8033d10152bfdfb15b20b92437ecdea21a260944ea86b77b51533
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
|
||||
""" # noqa
|
||||
3
resources/test/fixtures/R002.py
vendored
3
resources/test/fixtures/R002.py
vendored
@@ -1,3 +0,0 @@
|
||||
self.assertEquals (1, 2)
|
||||
self.assertEquals(1, 2)
|
||||
self.assertEqual(3, 4)
|
||||
1
resources/test/fixtures/T201.py
vendored
Normal file
1
resources/test/fixtures/T201.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
print("Hello, world!") # T201
|
||||
10
resources/test/fixtures/T203.py
vendored
Normal file
10
resources/test/fixtures/T203.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
from pprint import pprint
|
||||
|
||||
pprint("Hello, world!") # T203
|
||||
|
||||
|
||||
import pprint
|
||||
|
||||
pprint.pprint("Hello, world!") # T203
|
||||
|
||||
pprint.pformat("Hello, world!")
|
||||
13
resources/test/fixtures/U001.py
vendored
Normal file
13
resources/test/fixtures/U001.py
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
class A:
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
class B:
|
||||
__metaclass__ = type
|
||||
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class C(metaclass=type):
|
||||
pass
|
||||
15
resources/test/fixtures/U002.py
vendored
Normal file
15
resources/test/fixtures/U002.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
from os.path import abspath
|
||||
|
||||
x = abspath(__file__)
|
||||
|
||||
|
||||
import os
|
||||
|
||||
|
||||
y = os.path.abspath(__file__)
|
||||
|
||||
|
||||
from os import path
|
||||
|
||||
|
||||
z = path.abspath(__file__)
|
||||
5
resources/test/fixtures/U003.py
vendored
Normal file
5
resources/test/fixtures/U003.py
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
type('')
|
||||
type(b'')
|
||||
type(0)
|
||||
type(0.)
|
||||
type(0j)
|
||||
10
resources/test/fixtures/U005.py
vendored
Normal file
10
resources/test/fixtures/U005.py
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import unittest
|
||||
|
||||
|
||||
class Suite(unittest.TestCase):
|
||||
def test(self) -> None:
|
||||
self.assertEquals (1, 2)
|
||||
self.assertEquals(1, 2)
|
||||
self.assertEqual(3, 4)
|
||||
self.failUnlessAlmostEqual(1, 1.1)
|
||||
self.assertNotRegexpMatches("a", "b")
|
||||
12
resources/test/fixtures/U006.py
vendored
Normal file
12
resources/test/fixtures/U006.py
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
from typing import List
|
||||
|
||||
|
||||
def f(x: List[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
def f(x: typing.List[str]) -> None:
|
||||
...
|
||||
40
resources/test/fixtures/U007.py
vendored
Normal file
40
resources/test/fixtures/U007.py
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def f(x: Optional[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
def f(x: typing.Optional[str]) -> None:
|
||||
...
|
||||
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
def f(x: Union[str, int, Union[float, bytes]]) -> None:
|
||||
...
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
def f(x: typing.Union[str, int]) -> None:
|
||||
...
|
||||
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
def f(x: "Union[str, int, Union[float, bytes]]") -> None:
|
||||
...
|
||||
|
||||
|
||||
import typing
|
||||
|
||||
|
||||
def f(x: "typing.Union[str, int]") -> None:
|
||||
...
|
||||
65
resources/test/fixtures/U008.py
vendored
Normal file
65
resources/test/fixtures/U008.py
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
class Parent:
|
||||
def method(self):
|
||||
pass
|
||||
|
||||
def wrong(self):
|
||||
pass
|
||||
|
||||
|
||||
class Child(Parent):
|
||||
def method(self):
|
||||
parent = super() # ok
|
||||
super().method() # ok
|
||||
Parent.method(self) # ok
|
||||
Parent.super(1, 2) # ok
|
||||
|
||||
def wrong(self):
|
||||
parent = super(Child, self) # wrong
|
||||
super(Child, self).method # wrong
|
||||
super(
|
||||
Child,
|
||||
self,
|
||||
).method() # wrong
|
||||
|
||||
|
||||
class BaseClass:
|
||||
def f(self):
|
||||
print("f")
|
||||
|
||||
|
||||
def defined_outside(self):
|
||||
super(MyClass, self).f() # CANNOT use super()
|
||||
|
||||
|
||||
class MyClass(BaseClass):
|
||||
def normal(self):
|
||||
super(MyClass, self).f() # can use super()
|
||||
super().f()
|
||||
|
||||
def different_argument(self, other):
|
||||
super(MyClass, other).f() # CANNOT use super()
|
||||
|
||||
def comprehension_scope(self):
|
||||
[super(MyClass, self).f() for x in [1]] # CANNOT use super()
|
||||
|
||||
def inner_functions(self):
|
||||
def outer_argument():
|
||||
super(MyClass, self).f() # CANNOT use super()
|
||||
|
||||
def inner_argument(self):
|
||||
super(MyClass, self).f() # can use super()
|
||||
super().f()
|
||||
|
||||
outer_argument()
|
||||
inner_argument(self)
|
||||
|
||||
def inner_class(self):
|
||||
class InnerClass:
|
||||
super(MyClass, self).f() # CANNOT use super()
|
||||
|
||||
def method(inner_self):
|
||||
super(MyClass, self).f() # CANNOT use super()
|
||||
|
||||
InnerClass().method()
|
||||
|
||||
defined_outside = defined_outside
|
||||
2
resources/test/fixtures/W292_0.py
vendored
Normal file
2
resources/test/fixtures/W292_0.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
def fn() -> None:
|
||||
pass
|
||||
2
resources/test/fixtures/W292_1.py
vendored
Normal file
2
resources/test/fixtures/W292_1.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
def fn() -> None:
|
||||
pass # noqa: W292
|
||||
2
resources/test/fixtures/W292_2.py
vendored
Normal file
2
resources/test/fixtures/W292_2.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
def fn() -> None:
|
||||
pass
|
||||
3
resources/test/fixtures/__init__.py
vendored
3
resources/test/fixtures/__init__.py
vendored
@@ -0,0 +1,3 @@
|
||||
print(__path__)
|
||||
|
||||
__all__ = ["a", "b", "c"]
|
||||
|
||||
9
resources/test/fixtures/directory/also_excluded.py
vendored
Normal file
9
resources/test/fixtures/directory/also_excluded.py
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
a = "abc"
|
||||
b = f"ghi{'jkl'}"
|
||||
|
||||
c = f"def"
|
||||
d = f"def" + "ghi"
|
||||
e = (
|
||||
f"def" +
|
||||
"ghi"
|
||||
)
|
||||
30
resources/test/fixtures/future_annotations.py
vendored
Normal file
30
resources/test/fixtures/future_annotations.py
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from models import (
|
||||
Fruit,
|
||||
Nut,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Foo:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
@classmethod
|
||||
def a(cls) -> Foo:
|
||||
return cls(x=0, y=0)
|
||||
|
||||
@classmethod
|
||||
def b(cls) -> "Foo":
|
||||
return cls(x=0, y=0)
|
||||
|
||||
@classmethod
|
||||
def c(cls) -> Bar:
|
||||
return cls(x=0, y=0)
|
||||
|
||||
@classmethod
|
||||
def d(cls) -> Fruit:
|
||||
return cls(x=0, y=0)
|
||||
24
resources/test/fixtures/pyproject.toml
vendored
24
resources/test/fixtures/pyproject.toml
vendored
@@ -1,23 +1,7 @@
|
||||
[tool.ruff]
|
||||
line-length = 88
|
||||
exclude = ["excluded.py", "**/migrations"]
|
||||
select = [
|
||||
"E402",
|
||||
"E501",
|
||||
"F401",
|
||||
"F403",
|
||||
"F541",
|
||||
"F631",
|
||||
"F634",
|
||||
"F704",
|
||||
"F706",
|
||||
"F707",
|
||||
"F821",
|
||||
"F822",
|
||||
"F823",
|
||||
"F831",
|
||||
"F841",
|
||||
"F901",
|
||||
"R001",
|
||||
"R002",
|
||||
extend-exclude = [
|
||||
"excluded.py",
|
||||
"migrations",
|
||||
"directory/also_excluded.py",
|
||||
]
|
||||
|
||||
6
src/ast.rs
Normal file
6
src/ast.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod checks;
|
||||
pub mod helpers;
|
||||
pub mod operations;
|
||||
pub mod relocate;
|
||||
pub mod types;
|
||||
pub mod visitor;
|
||||
1158
src/ast/checks.rs
Normal file
1158
src/ast/checks.rs
Normal file
File diff suppressed because it is too large
Load Diff
133
src/ast/helpers.rs
Normal file
133
src/ast/helpers.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprKind, StmtKind};
|
||||
|
||||
use crate::python::typing;
|
||||
|
||||
fn compose_call_path_inner<'a>(expr: &'a Expr, parts: &mut Vec<&'a str>) {
|
||||
match &expr.node {
|
||||
ExprKind::Call { func, .. } => {
|
||||
compose_call_path_inner(func, parts);
|
||||
}
|
||||
ExprKind::Attribute { value, attr, .. } => {
|
||||
compose_call_path_inner(value, parts);
|
||||
parts.push(attr);
|
||||
}
|
||||
ExprKind::Name { id, .. } => {
|
||||
parts.push(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose_call_path(expr: &Expr) -> Option<String> {
|
||||
let mut segments = vec![];
|
||||
compose_call_path_inner(expr, &mut segments);
|
||||
if segments.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(segments.join("."))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_name_or_attr(expr: &Expr, target: &str) -> bool {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { attr, .. } => target == attr,
|
||||
ExprKind::Name { id, .. } => target == id,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SubscriptKind {
|
||||
AnnotatedSubscript,
|
||||
PEP593AnnotatedSubscript,
|
||||
}
|
||||
|
||||
pub fn match_annotated_subscript(expr: &Expr) -> Option<SubscriptKind> {
|
||||
match &expr.node {
|
||||
ExprKind::Attribute { attr, .. } => {
|
||||
if typing::is_annotated_subscript(attr) {
|
||||
Some(SubscriptKind::AnnotatedSubscript)
|
||||
} else if typing::is_pep593_annotated_subscript(attr) {
|
||||
Some(SubscriptKind::PEP593AnnotatedSubscript)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
ExprKind::Name { id, .. } => {
|
||||
if typing::is_annotated_subscript(id) {
|
||||
Some(SubscriptKind::AnnotatedSubscript)
|
||||
} else if typing::is_pep593_annotated_subscript(id) {
|
||||
Some(SubscriptKind::PEP593AnnotatedSubscript)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
|
||||
|
||||
pub fn is_assignment_to_a_dunder(node: &StmtKind) -> bool {
|
||||
// Check whether it's an assignment to a dunder, with or without a type annotation.
|
||||
// This is what pycodestyle (as of 2.9.1) does.
|
||||
match node {
|
||||
StmtKind::Assign {
|
||||
targets,
|
||||
value: _,
|
||||
type_comment: _,
|
||||
} => {
|
||||
if targets.len() != 1 {
|
||||
return false;
|
||||
}
|
||||
match &targets[0].node {
|
||||
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
StmtKind::AnnAssign {
|
||||
target,
|
||||
annotation: _,
|
||||
value: _,
|
||||
simple: _,
|
||||
} => match &target.node {
|
||||
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the names of all handled exceptions.
|
||||
pub fn extract_handler_names(handlers: &[Excepthandler]) -> Vec<String> {
|
||||
let mut handler_names = vec![];
|
||||
for handler in handlers {
|
||||
match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { type_, .. } => {
|
||||
if let Some(type_) = type_ {
|
||||
if let ExprKind::Tuple { elts, .. } = &type_.node {
|
||||
for type_ in elts {
|
||||
if let Some(name) = compose_call_path(type_) {
|
||||
handler_names.push(name);
|
||||
}
|
||||
}
|
||||
} else if let Some(name) = compose_call_path(type_) {
|
||||
handler_names.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
handler_names
|
||||
}
|
||||
|
||||
/// Returns `true` if a call is an argumented `super` invocation.
|
||||
pub fn is_super_call_with_arguments(func: &Expr, args: &Vec<Expr>) -> bool {
|
||||
// Check: is this a `super` call?
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
id == "super" && !args.is_empty()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -1,56 +1,6 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use rustpython_parser::ast::{Constant, Expr, ExprKind, Location, Stmt, StmtKind};
|
||||
|
||||
fn id() -> usize {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub enum ScopeKind {
|
||||
Class,
|
||||
Function,
|
||||
Generator,
|
||||
Module,
|
||||
}
|
||||
|
||||
pub struct Scope {
|
||||
pub id: usize,
|
||||
pub kind: ScopeKind,
|
||||
pub values: BTreeMap<String, Binding>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn new(kind: ScopeKind) -> Self {
|
||||
Scope {
|
||||
id: id(),
|
||||
kind,
|
||||
values: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BindingKind {
|
||||
Argument,
|
||||
Assignment,
|
||||
Builtin,
|
||||
ClassDefinition,
|
||||
Definition,
|
||||
Export(Vec<String>),
|
||||
FutureImportation,
|
||||
Importation(String),
|
||||
StarImportation,
|
||||
SubmoduleImportation(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Binding {
|
||||
pub kind: BindingKind,
|
||||
pub location: Location,
|
||||
pub used: Option<usize>,
|
||||
}
|
||||
use crate::ast::types::{BindingKind, Range, Scope};
|
||||
|
||||
/// Extract the names bound to a given __all__ assignment.
|
||||
pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
@@ -72,7 +22,7 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
if let StmtKind::AugAssign { .. } = &stmt.node {
|
||||
if let Some(binding) = scope.values.get("__all__") {
|
||||
if let BindingKind::Export(existing) = &binding.kind {
|
||||
names.extend(existing.clone());
|
||||
names.extend_from_slice(existing);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +66,58 @@ pub fn extract_all_names(stmt: &Stmt, scope: &Scope) -> Vec<String> {
|
||||
names
|
||||
}
|
||||
|
||||
/// Check if a node is parent of a conditional branch.
|
||||
pub fn on_conditional_branch(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
|
||||
for index in parent_stack.iter().rev() {
|
||||
let parent = parents[*index];
|
||||
if matches!(parent.node, StmtKind::If { .. } | StmtKind::While { .. }) {
|
||||
return true;
|
||||
}
|
||||
if let StmtKind::Expr { value } = &parent.node {
|
||||
if matches!(value.node, ExprKind::IfExp { .. }) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a node is in a nested block.
|
||||
pub fn in_nested_block(parent_stack: &[usize], parents: &[&Stmt]) -> bool {
|
||||
for index in parent_stack.iter().rev() {
|
||||
let parent = parents[*index];
|
||||
if matches!(
|
||||
parent.node,
|
||||
StmtKind::Try { .. } | StmtKind::If { .. } | StmtKind::With { .. }
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a node represents an unpacking assignment.
|
||||
pub fn is_unpacking_assignment(stmt: &Stmt) -> bool {
|
||||
if let StmtKind::Assign { targets, value, .. } = &stmt.node {
|
||||
if !targets.iter().any(|child| {
|
||||
matches!(
|
||||
child.node,
|
||||
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
|
||||
)
|
||||
}) {
|
||||
return false;
|
||||
}
|
||||
match &value.node {
|
||||
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. } => return false,
|
||||
_ => {}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Struct used to efficiently slice source code at (row, column) Locations.
|
||||
pub struct SourceCodeLocator<'a> {
|
||||
content: &'a str,
|
||||
@@ -132,7 +134,7 @@ impl<'a> SourceCodeLocator<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slice_source_code(&mut self, location: &Location) -> &'a str {
|
||||
pub fn slice_source_code_at(&mut self, location: &Location) -> &'a str {
|
||||
if !self.initialized {
|
||||
let mut offset = 0;
|
||||
for i in self.content.lines() {
|
||||
@@ -145,4 +147,19 @@ impl<'a> SourceCodeLocator<'a> {
|
||||
let offset = self.offsets[location.row() - 1] + location.column() - 1;
|
||||
&self.content[offset..]
|
||||
}
|
||||
|
||||
pub fn slice_source_code_range(&mut self, range: &Range) -> &'a str {
|
||||
if !self.initialized {
|
||||
let mut offset = 0;
|
||||
for i in self.content.lines() {
|
||||
self.offsets.push(offset);
|
||||
offset += i.len();
|
||||
offset += 1;
|
||||
}
|
||||
self.initialized = true;
|
||||
}
|
||||
let start = self.offsets[range.location.row() - 1] + range.location.column() - 1;
|
||||
let end = self.offsets[range.end_location.row() - 1] + range.end_location.column() - 1;
|
||||
&self.content[start..end]
|
||||
}
|
||||
}
|
||||
141
src/ast/relocate.rs
Normal file
141
src/ast/relocate.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use rustpython_parser::ast::{Expr, ExprKind, Keyword};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
|
||||
fn relocate_keyword(keyword: &mut Keyword, location: Range) {
|
||||
keyword.location = location.location;
|
||||
keyword.end_location = location.end_location;
|
||||
relocate_expr(&mut keyword.node.value, location);
|
||||
}
|
||||
|
||||
/// Change an expression's location (recursively) to match a desired, fixed location.
|
||||
pub fn relocate_expr(expr: &mut Expr, location: Range) {
|
||||
expr.location = location.location;
|
||||
expr.end_location = location.end_location;
|
||||
match &mut expr.node {
|
||||
ExprKind::BoolOp { values, .. } => {
|
||||
for expr in values {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::NamedExpr { target, value } => {
|
||||
relocate_expr(target, location);
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::BinOp { left, right, .. } => {
|
||||
relocate_expr(left, location);
|
||||
relocate_expr(right, location);
|
||||
}
|
||||
ExprKind::UnaryOp { operand, .. } => {
|
||||
relocate_expr(operand, location);
|
||||
}
|
||||
ExprKind::Lambda { body, .. } => {
|
||||
relocate_expr(body, location);
|
||||
}
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
relocate_expr(test, location);
|
||||
relocate_expr(body, location);
|
||||
relocate_expr(orelse, location);
|
||||
}
|
||||
ExprKind::Dict { keys, values } => {
|
||||
for expr in keys {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
for expr in values {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Set { elts } => {
|
||||
for expr in elts {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::ListComp { elt, .. } => {
|
||||
relocate_expr(elt, location);
|
||||
}
|
||||
ExprKind::SetComp { elt, .. } => {
|
||||
relocate_expr(elt, location);
|
||||
}
|
||||
ExprKind::DictComp { key, value, .. } => {
|
||||
relocate_expr(key, location);
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::GeneratorExp { elt, .. } => {
|
||||
relocate_expr(elt, location);
|
||||
}
|
||||
ExprKind::Await { value } => relocate_expr(value, location),
|
||||
ExprKind::Yield { value } => {
|
||||
if let Some(expr) = value {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::YieldFrom { value } => relocate_expr(value, location),
|
||||
ExprKind::Compare {
|
||||
left, comparators, ..
|
||||
} => {
|
||||
relocate_expr(left, location);
|
||||
for expr in comparators {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
relocate_expr(func, location);
|
||||
for expr in args {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
for keyword in keywords {
|
||||
relocate_keyword(keyword, location);
|
||||
}
|
||||
}
|
||||
ExprKind::FormattedValue {
|
||||
value, format_spec, ..
|
||||
} => {
|
||||
relocate_expr(value, location);
|
||||
if let Some(expr) = format_spec {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::JoinedStr { values } => {
|
||||
for expr in values {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Constant { .. } => {}
|
||||
ExprKind::Attribute { value, .. } => {
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::Subscript { value, slice, .. } => {
|
||||
relocate_expr(value, location);
|
||||
relocate_expr(slice, location);
|
||||
}
|
||||
ExprKind::Starred { value, .. } => {
|
||||
relocate_expr(value, location);
|
||||
}
|
||||
ExprKind::Name { .. } => {}
|
||||
ExprKind::List { elts, .. } => {
|
||||
for expr in elts {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Tuple { elts, .. } => {
|
||||
for expr in elts {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
ExprKind::Slice { lower, upper, step } => {
|
||||
if let Some(expr) = lower {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
if let Some(expr) = upper {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
if let Some(expr) = step {
|
||||
relocate_expr(expr, location);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/ast/types.rs
Normal file
99
src/ast/types.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use rustpython_parser::ast::{Located, Location};
|
||||
|
||||
fn id() -> usize {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(1);
|
||||
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Range {
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
}
|
||||
|
||||
impl Range {
|
||||
pub fn from_located<T>(located: &Located<T>) -> Self {
|
||||
Range {
|
||||
location: located.location,
|
||||
end_location: located.end_location,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct FunctionScope {
|
||||
pub uses_locals: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ScopeKind {
|
||||
Class,
|
||||
Function(FunctionScope),
|
||||
Generator,
|
||||
Module,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Scope {
|
||||
pub id: usize,
|
||||
pub kind: ScopeKind,
|
||||
pub import_starred: bool,
|
||||
pub values: BTreeMap<String, Binding>,
|
||||
}
|
||||
|
||||
impl Scope {
|
||||
pub fn new(kind: ScopeKind) -> Self {
|
||||
Scope {
|
||||
id: id(),
|
||||
kind,
|
||||
import_starred: false,
|
||||
values: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BindingContext {
|
||||
pub defined_by: usize,
|
||||
pub defined_in: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum BindingKind {
|
||||
Annotation,
|
||||
Argument,
|
||||
Assignment,
|
||||
Binding,
|
||||
LoopVar,
|
||||
Builtin,
|
||||
ClassDefinition,
|
||||
Definition,
|
||||
Export(Vec<String>),
|
||||
FutureImportation,
|
||||
StarImportation,
|
||||
Importation(String, String, BindingContext),
|
||||
FromImportation(String, String, BindingContext),
|
||||
SubmoduleImportation(String, String, BindingContext),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Binding {
|
||||
pub kind: BindingKind,
|
||||
pub range: Range,
|
||||
/// Tuple of (scope index, range) indicating the scope and range at which the binding was
|
||||
/// last used.
|
||||
pub used: Option<(usize, Range)>,
|
||||
}
|
||||
|
||||
pub trait CheckLocator {
|
||||
fn locate_check(&self, default: Range) -> Range;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ImportKind {
|
||||
Import,
|
||||
ImportFrom,
|
||||
}
|
||||
@@ -1,105 +1,145 @@
|
||||
use crate::ast::helpers::match_name_or_attr;
|
||||
use rustpython_parser::ast::{
|
||||
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
|
||||
ExcepthandlerKind, Expr, ExprContext, ExprKind, Keyword, MatchCase, Operator, Pattern,
|
||||
PatternKind, Stmt, StmtKind, Unaryop, Withitem,
|
||||
};
|
||||
|
||||
pub trait Visitor {
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
pub trait Visitor<'a> {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
walk_stmt(self, stmt);
|
||||
}
|
||||
fn visit_annotation(&mut self, expr: &Expr) {
|
||||
fn visit_annotation(&mut self, expr: &'a Expr) {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
fn visit_expr(&mut self, expr: &Expr, _parent: Option<&Stmt>) {
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
walk_expr(self, expr);
|
||||
}
|
||||
fn visit_constant(&mut self, constant: &Constant) {
|
||||
fn visit_constant(&mut self, constant: &'a Constant) {
|
||||
walk_constant(self, constant);
|
||||
}
|
||||
fn visit_expr_context(&mut self, expr_content: &ExprContext) {
|
||||
fn visit_expr_context(&mut self, expr_content: &'a ExprContext) {
|
||||
walk_expr_context(self, expr_content);
|
||||
}
|
||||
fn visit_boolop(&mut self, boolop: &Boolop) {
|
||||
fn visit_boolop(&mut self, boolop: &'a Boolop) {
|
||||
walk_boolop(self, boolop);
|
||||
}
|
||||
fn visit_operator(&mut self, operator: &Operator) {
|
||||
fn visit_operator(&mut self, operator: &'a Operator) {
|
||||
walk_operator(self, operator);
|
||||
}
|
||||
fn visit_unaryop(&mut self, unaryop: &Unaryop) {
|
||||
fn visit_unaryop(&mut self, unaryop: &'a Unaryop) {
|
||||
walk_unaryop(self, unaryop);
|
||||
}
|
||||
fn visit_cmpop(&mut self, cmpop: &Cmpop) {
|
||||
fn visit_cmpop(&mut self, cmpop: &'a Cmpop) {
|
||||
walk_cmpop(self, cmpop);
|
||||
}
|
||||
fn visit_comprehension(&mut self, comprehension: &Comprehension) {
|
||||
fn visit_comprehension(&mut self, comprehension: &'a Comprehension) {
|
||||
walk_comprehension(self, comprehension);
|
||||
}
|
||||
fn visit_excepthandler(&mut self, excepthandler: &Excepthandler) {
|
||||
fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) {
|
||||
walk_excepthandler(self, excepthandler);
|
||||
}
|
||||
fn visit_arguments(&mut self, arguments: &Arguments) {
|
||||
fn visit_arguments(&mut self, arguments: &'a Arguments) {
|
||||
walk_arguments(self, arguments);
|
||||
}
|
||||
fn visit_arg(&mut self, arg: &Arg) {
|
||||
fn visit_arg(&mut self, arg: &'a Arg) {
|
||||
walk_arg(self, arg);
|
||||
}
|
||||
fn visit_keyword(&mut self, keyword: &Keyword) {
|
||||
fn visit_keyword(&mut self, keyword: &'a Keyword) {
|
||||
walk_keyword(self, keyword);
|
||||
}
|
||||
fn visit_alias(&mut self, alias: &Alias) {
|
||||
fn visit_alias(&mut self, alias: &'a Alias) {
|
||||
walk_alias(self, alias);
|
||||
}
|
||||
fn visit_withitem(&mut self, withitem: &Withitem) {
|
||||
fn visit_withitem(&mut self, withitem: &'a Withitem) {
|
||||
walk_withitem(self, withitem);
|
||||
}
|
||||
fn visit_match_case(&mut self, match_case: &MatchCase) {
|
||||
fn visit_match_case(&mut self, match_case: &'a MatchCase) {
|
||||
walk_match_case(self, match_case);
|
||||
}
|
||||
fn visit_pattern(&mut self, pattern: &Pattern) {
|
||||
fn visit_pattern(&mut self, pattern: &'a Pattern) {
|
||||
walk_pattern(self, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
pub fn walk_stmt<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, stmt: &'a Stmt) {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { args, body, .. } => {
|
||||
StmtKind::FunctionDef {
|
||||
args,
|
||||
body,
|
||||
decorator_list,
|
||||
returns,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_arguments(args);
|
||||
for expr in decorator_list {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for expr in returns {
|
||||
visitor.visit_annotation(expr);
|
||||
}
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::AsyncFunctionDef { args, body, .. } => {
|
||||
StmtKind::AsyncFunctionDef {
|
||||
args,
|
||||
body,
|
||||
decorator_list,
|
||||
returns,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_arguments(args);
|
||||
for expr in decorator_list {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for expr in returns {
|
||||
visitor.visit_annotation(expr);
|
||||
}
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::ClassDef { body, .. } => {
|
||||
StmtKind::ClassDef {
|
||||
bases,
|
||||
keywords,
|
||||
body,
|
||||
decorator_list,
|
||||
..
|
||||
} => {
|
||||
for expr in bases {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for keyword in keywords {
|
||||
visitor.visit_keyword(keyword);
|
||||
}
|
||||
for expr in decorator_list {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::Return { value } => {
|
||||
if let Some(expr) = value {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::Delete { targets } => {
|
||||
for expr in targets {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::Assign { targets, value, .. } => {
|
||||
visitor.visit_expr(value);
|
||||
for expr in targets {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_expr(value, Some(stmt))
|
||||
}
|
||||
StmtKind::AugAssign { target, op, value } => {
|
||||
visitor.visit_expr(target, Some(stmt));
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_operator(op);
|
||||
visitor.visit_expr(value, Some(stmt));
|
||||
visitor.visit_expr(value);
|
||||
}
|
||||
StmtKind::AnnAssign {
|
||||
target,
|
||||
@@ -107,11 +147,15 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
value,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(target, Some(stmt));
|
||||
visitor.visit_annotation(annotation);
|
||||
if let Some(expr) = value {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
if match_name_or_attr(annotation, "TypeAlias") {
|
||||
visitor.visit_annotation(expr);
|
||||
} else {
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
visitor.visit_expr(target);
|
||||
}
|
||||
StmtKind::For {
|
||||
target,
|
||||
@@ -120,13 +164,13 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
orelse,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(target, Some(stmt));
|
||||
visitor.visit_expr(iter, Some(stmt));
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_expr(iter);
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
for stmt in orelse {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::AsyncFor {
|
||||
@@ -136,31 +180,31 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
orelse,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(target, Some(stmt));
|
||||
visitor.visit_expr(iter, Some(stmt));
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_expr(iter);
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
for stmt in orelse {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::While { test, body, orelse } => {
|
||||
visitor.visit_expr(test, Some(stmt));
|
||||
visitor.visit_expr(test);
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
for stmt in orelse {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::If { test, body, orelse } => {
|
||||
visitor.visit_expr(test, Some(stmt));
|
||||
visitor.visit_expr(test);
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
for stmt in orelse {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::With { items, body, .. } => {
|
||||
@@ -168,7 +212,7 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
visitor.visit_withitem(withitem);
|
||||
}
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::AsyncWith { items, body, .. } => {
|
||||
@@ -176,22 +220,22 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
visitor.visit_withitem(withitem);
|
||||
}
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::Match { subject, cases } => {
|
||||
// TODO(charlie): Handle `cases`.
|
||||
visitor.visit_expr(subject, Some(stmt));
|
||||
visitor.visit_expr(subject);
|
||||
for match_case in cases {
|
||||
visitor.visit_match_case(match_case);
|
||||
}
|
||||
}
|
||||
StmtKind::Raise { exc, cause } => {
|
||||
if let Some(expr) = exc {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
visitor.visit_expr(expr);
|
||||
};
|
||||
if let Some(expr) = cause {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
visitor.visit_expr(expr);
|
||||
};
|
||||
}
|
||||
StmtKind::Try {
|
||||
@@ -201,22 +245,22 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
finalbody,
|
||||
} => {
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
for excepthandler in handlers {
|
||||
visitor.visit_excepthandler(excepthandler)
|
||||
}
|
||||
for stmt in orelse {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
for stmt in finalbody {
|
||||
visitor.visit_stmt(stmt)
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
StmtKind::Assert { test, msg } => {
|
||||
visitor.visit_expr(test, None);
|
||||
visitor.visit_expr(test);
|
||||
if let Some(expr) = msg {
|
||||
visitor.visit_expr(expr, Some(stmt))
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
StmtKind::Import { names } => {
|
||||
@@ -231,67 +275,67 @@ pub fn walk_stmt<V: Visitor + ?Sized>(visitor: &mut V, stmt: &Stmt) {
|
||||
}
|
||||
StmtKind::Global { .. } => {}
|
||||
StmtKind::Nonlocal { .. } => {}
|
||||
StmtKind::Expr { value } => visitor.visit_expr(value, Some(stmt)),
|
||||
StmtKind::Expr { value } => visitor.visit_expr(value),
|
||||
StmtKind::Pass => {}
|
||||
StmtKind::Break => {}
|
||||
StmtKind::Continue => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
|
||||
pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
|
||||
match &expr.node {
|
||||
ExprKind::BoolOp { op, values } => {
|
||||
visitor.visit_boolop(op);
|
||||
for expr in values {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::NamedExpr { target, value } => {
|
||||
visitor.visit_expr(target, None);
|
||||
visitor.visit_expr(value, None);
|
||||
visitor.visit_expr(target);
|
||||
visitor.visit_expr(value);
|
||||
}
|
||||
ExprKind::BinOp { left, op, right } => {
|
||||
visitor.visit_expr(left, None);
|
||||
visitor.visit_expr(left);
|
||||
visitor.visit_operator(op);
|
||||
visitor.visit_expr(right, None);
|
||||
visitor.visit_expr(right);
|
||||
}
|
||||
ExprKind::UnaryOp { op, operand } => {
|
||||
visitor.visit_unaryop(op);
|
||||
visitor.visit_expr(operand, None);
|
||||
visitor.visit_expr(operand);
|
||||
}
|
||||
ExprKind::Lambda { args, body } => {
|
||||
visitor.visit_arguments(args);
|
||||
visitor.visit_expr(body, None);
|
||||
visitor.visit_expr(body);
|
||||
}
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
visitor.visit_expr(test, None);
|
||||
visitor.visit_expr(body, None);
|
||||
visitor.visit_expr(orelse, None);
|
||||
visitor.visit_expr(test);
|
||||
visitor.visit_expr(body);
|
||||
visitor.visit_expr(orelse);
|
||||
}
|
||||
ExprKind::Dict { keys, values } => {
|
||||
for expr in keys {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for expr in values {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Set { elts } => {
|
||||
for expr in elts {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::ListComp { elt, generators } => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension)
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(elt, None);
|
||||
visitor.visit_expr(elt);
|
||||
}
|
||||
ExprKind::SetComp { elt, generators } => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension)
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(elt, None);
|
||||
visitor.visit_expr(elt);
|
||||
}
|
||||
ExprKind::DictComp {
|
||||
key,
|
||||
@@ -299,35 +343,35 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
|
||||
generators,
|
||||
} => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension)
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(key, None);
|
||||
visitor.visit_expr(value, None);
|
||||
visitor.visit_expr(key);
|
||||
visitor.visit_expr(value);
|
||||
}
|
||||
ExprKind::GeneratorExp { elt, generators } => {
|
||||
for comprehension in generators {
|
||||
visitor.visit_comprehension(comprehension)
|
||||
visitor.visit_comprehension(comprehension);
|
||||
}
|
||||
visitor.visit_expr(elt, None);
|
||||
visitor.visit_expr(elt);
|
||||
}
|
||||
ExprKind::Await { value } => visitor.visit_expr(value, None),
|
||||
ExprKind::Await { value } => visitor.visit_expr(value),
|
||||
ExprKind::Yield { value } => {
|
||||
if let Some(expr) = value {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::YieldFrom { value } => visitor.visit_expr(value, None),
|
||||
ExprKind::YieldFrom { value } => visitor.visit_expr(value),
|
||||
ExprKind::Compare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
} => {
|
||||
visitor.visit_expr(left, None);
|
||||
visitor.visit_expr(left);
|
||||
for cmpop in ops {
|
||||
visitor.visit_cmpop(cmpop);
|
||||
}
|
||||
for expr in comparators {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Call {
|
||||
@@ -335,9 +379,9 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
visitor.visit_expr(func, None);
|
||||
visitor.visit_expr(func);
|
||||
for expr in args {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for keyword in keywords {
|
||||
visitor.visit_keyword(keyword);
|
||||
@@ -346,28 +390,28 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
|
||||
ExprKind::FormattedValue {
|
||||
value, format_spec, ..
|
||||
} => {
|
||||
visitor.visit_expr(value, None);
|
||||
visitor.visit_expr(value);
|
||||
if let Some(expr) = format_spec {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::JoinedStr { values } => {
|
||||
for expr in values {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
ExprKind::Constant { value, .. } => visitor.visit_constant(value),
|
||||
ExprKind::Attribute { value, ctx, .. } => {
|
||||
visitor.visit_expr(value, None);
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Subscript { value, slice, ctx } => {
|
||||
visitor.visit_expr(value, None);
|
||||
visitor.visit_expr(slice, None);
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr(slice);
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Starred { value, ctx } => {
|
||||
visitor.visit_expr(value, None);
|
||||
visitor.visit_expr(value);
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Name { ctx, .. } => {
|
||||
@@ -375,31 +419,31 @@ pub fn walk_expr<V: Visitor + ?Sized>(visitor: &mut V, expr: &Expr) {
|
||||
}
|
||||
ExprKind::List { elts, ctx } => {
|
||||
for expr in elts {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Tuple { elts, ctx } => {
|
||||
for expr in elts {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
visitor.visit_expr_context(ctx);
|
||||
}
|
||||
ExprKind::Slice { lower, upper, step } => {
|
||||
if let Some(expr) = lower {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(expr) = upper {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(expr) = step {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant) {
|
||||
pub fn walk_constant<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, constant: &'a Constant) {
|
||||
if let Constant::Tuple(constants) = constant {
|
||||
for constant in constants {
|
||||
visitor.visit_constant(constant)
|
||||
@@ -407,19 +451,25 @@ pub fn walk_constant<V: Visitor + ?Sized>(visitor: &mut V, constant: &Constant)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_comprehension<V: Visitor + ?Sized>(visitor: &mut V, comprehension: &Comprehension) {
|
||||
visitor.visit_expr(&comprehension.target, None);
|
||||
visitor.visit_expr(&comprehension.iter, None);
|
||||
pub fn walk_comprehension<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
comprehension: &'a Comprehension,
|
||||
) {
|
||||
visitor.visit_expr(&comprehension.target);
|
||||
visitor.visit_expr(&comprehension.iter);
|
||||
for expr in &comprehension.ifs {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &Excepthandler) {
|
||||
pub fn walk_excepthandler<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
excepthandler: &'a Excepthandler,
|
||||
) {
|
||||
match &excepthandler.node {
|
||||
ExcepthandlerKind::ExceptHandler { type_, body, .. } => {
|
||||
if let Some(expr) = type_ {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for stmt in body {
|
||||
visitor.visit_stmt(stmt);
|
||||
@@ -428,7 +478,7 @@ pub fn walk_excepthandler<V: Visitor + ?Sized>(visitor: &mut V, excepthandler: &
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Arguments) {
|
||||
pub fn walk_arguments<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arguments: &'a Arguments) {
|
||||
for arg in &arguments.posonlyargs {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
@@ -436,61 +486,61 @@ pub fn walk_arguments<V: Visitor + ?Sized>(visitor: &mut V, arguments: &Argument
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
if let Some(arg) = &arguments.vararg {
|
||||
visitor.visit_arg(arg)
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for arg in &arguments.kwonlyargs {
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for expr in &arguments.kw_defaults {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
if let Some(arg) = &arguments.kwarg {
|
||||
visitor.visit_arg(arg)
|
||||
visitor.visit_arg(arg);
|
||||
}
|
||||
for expr in &arguments.defaults {
|
||||
visitor.visit_expr(expr, None)
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_arg<V: Visitor + ?Sized>(visitor: &mut V, arg: &Arg) {
|
||||
pub fn walk_arg<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, arg: &'a Arg) {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
visitor.visit_annotation(expr)
|
||||
visitor.visit_annotation(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_keyword<V: Visitor + ?Sized>(visitor: &mut V, keyword: &Keyword) {
|
||||
visitor.visit_expr(&keyword.node.value, None);
|
||||
pub fn walk_keyword<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, keyword: &'a Keyword) {
|
||||
visitor.visit_expr(&keyword.node.value);
|
||||
}
|
||||
|
||||
pub fn walk_withitem<V: Visitor + ?Sized>(visitor: &mut V, withitem: &Withitem) {
|
||||
visitor.visit_expr(&withitem.context_expr, None);
|
||||
pub fn walk_withitem<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, withitem: &'a Withitem) {
|
||||
visitor.visit_expr(&withitem.context_expr);
|
||||
if let Some(expr) = &withitem.optional_vars {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_match_case<V: Visitor + ?Sized>(visitor: &mut V, match_case: &MatchCase) {
|
||||
pub fn walk_match_case<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, match_case: &'a MatchCase) {
|
||||
visitor.visit_pattern(&match_case.pattern);
|
||||
if let Some(expr) = &match_case.guard {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for stmt in &match_case.body {
|
||||
visitor.visit_stmt(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
|
||||
pub fn walk_pattern<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, pattern: &'a Pattern) {
|
||||
match &pattern.node {
|
||||
PatternKind::MatchValue { value } => visitor.visit_expr(value, None),
|
||||
PatternKind::MatchValue { value } => visitor.visit_expr(value),
|
||||
PatternKind::MatchSingleton { value } => visitor.visit_constant(value),
|
||||
PatternKind::MatchSequence { patterns } => {
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern)
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
PatternKind::MatchMapping { keys, patterns, .. } => {
|
||||
for expr in keys {
|
||||
visitor.visit_expr(expr, None);
|
||||
visitor.visit_expr(expr);
|
||||
}
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
@@ -502,7 +552,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
|
||||
kwd_patterns,
|
||||
..
|
||||
} => {
|
||||
visitor.visit_expr(cls, None);
|
||||
visitor.visit_expr(cls);
|
||||
for pattern in patterns {
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
@@ -514,7 +564,7 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
|
||||
PatternKind::MatchStar { .. } => {}
|
||||
PatternKind::MatchAs { pattern, .. } => {
|
||||
if let Some(pattern) = pattern {
|
||||
visitor.visit_pattern(pattern)
|
||||
visitor.visit_pattern(pattern);
|
||||
}
|
||||
}
|
||||
PatternKind::MatchOr { patterns } => {
|
||||
@@ -527,24 +577,28 @@ pub fn walk_pattern<V: Visitor + ?Sized>(visitor: &mut V, pattern: &Pattern) {
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[inline(always)]
|
||||
pub fn walk_expr_context<V: Visitor + ?Sized>(visitor: &mut V, expr_context: &ExprContext) {}
|
||||
pub fn walk_expr_context<'a, V: Visitor<'a> + ?Sized>(
|
||||
visitor: &mut V,
|
||||
expr_context: &'a ExprContext,
|
||||
) {
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[inline(always)]
|
||||
pub fn walk_boolop<V: Visitor + ?Sized>(visitor: &mut V, boolop: &Boolop) {}
|
||||
pub fn walk_boolop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, boolop: &'a Boolop) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[inline(always)]
|
||||
pub fn walk_operator<V: Visitor + ?Sized>(visitor: &mut V, operator: &Operator) {}
|
||||
pub fn walk_operator<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, operator: &'a Operator) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[inline(always)]
|
||||
pub fn walk_unaryop<V: Visitor + ?Sized>(visitor: &mut V, unaryop: &Unaryop) {}
|
||||
pub fn walk_unaryop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, unaryop: &'a Unaryop) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[inline(always)]
|
||||
pub fn walk_cmpop<V: Visitor + ?Sized>(visitor: &mut V, cmpop: &Cmpop) {}
|
||||
pub fn walk_cmpop<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, cmpop: &'a Cmpop) {}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
#[inline(always)]
|
||||
pub fn walk_alias<V: Visitor + ?Sized>(visitor: &mut V, alias: &Alias) {}
|
||||
pub fn walk_alias<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, alias: &'a Alias) {}
|
||||
218
src/autofix.rs
218
src/autofix.rs
@@ -1,216 +1,2 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
Apply,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Mode::Apply,
|
||||
false => Mode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = apply_fixes(
|
||||
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
|
||||
contents,
|
||||
);
|
||||
|
||||
fs::write(path, output).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
let mut output = "".to_string();
|
||||
let mut last_pos: Location = Location::new(0, 0);
|
||||
|
||||
for fix in fixes {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.start {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.start.row() > last_pos.row() {
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.start.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.start.row() - 1][..fix.start.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.start.column() - 1],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
}
|
||||
|
||||
last_pos = fix.end;
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
let mut fixes = vec![];
|
||||
let actual = apply_fixes(fixes.iter_mut(), "");
|
||||
let expected = "";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
start: Location::new(1, 9),
|
||||
end: Location::new(1, 15),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A(Bar):
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 17),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 17),
|
||||
end: Location::new(1, 24),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object, object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
start: Location::new(1, 8),
|
||||
end: Location::new(1, 16),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
start: Location::new(1, 10),
|
||||
end: Location::new(1, 12),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
pub mod fixer;
|
||||
pub mod fixes;
|
||||
|
||||
222
src/autofix/fixer.rs
Normal file
222
src/autofix/fixer.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, Fix};
|
||||
|
||||
#[derive(Hash)]
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
Apply,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Mode::Apply,
|
||||
false => Mode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file(checks: &mut [Check], contents: &str, path: &Path) -> Result<()> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let output = apply_fixes(
|
||||
checks.iter_mut().filter_map(|check| check.fix.as_mut()),
|
||||
contents,
|
||||
);
|
||||
|
||||
fs::write(path, output).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(fixes: impl Iterator<Item = &'a mut Fix>, contents: &str) -> String {
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
|
||||
let mut output = "".to_string();
|
||||
let mut last_pos: Location = Location::new(0, 0);
|
||||
|
||||
for fix in fixes.sorted_by_key(|fix| fix.location) {
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied, skip it.
|
||||
if last_pos > fix.location {
|
||||
continue;
|
||||
}
|
||||
|
||||
if fix.location.row() > last_pos.row() {
|
||||
if last_pos.row() > 0 || last_pos.column() > 0 {
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
for line in &lines[last_pos.row()..fix.location.row() - 1] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
output.push_str(&lines[fix.location.row() - 1][..fix.location.column() - 1]);
|
||||
output.push_str(&fix.content);
|
||||
} else {
|
||||
output.push_str(
|
||||
&lines[last_pos.row() - 1][last_pos.column() - 1..fix.location.column() - 1],
|
||||
);
|
||||
output.push_str(&fix.content);
|
||||
}
|
||||
|
||||
last_pos = fix.end_location;
|
||||
fix.applied = true;
|
||||
}
|
||||
|
||||
if last_pos.row() > 0
|
||||
&& (last_pos.row() - 1) < lines.len()
|
||||
&& (last_pos.row() > 0 || last_pos.column() > 0)
|
||||
{
|
||||
output.push_str(&lines[last_pos.row() - 1][last_pos.column() - 1..]);
|
||||
output.push('\n');
|
||||
}
|
||||
if last_pos.row() < lines.len() {
|
||||
for line in &lines[last_pos.row()..] {
|
||||
output.push_str(line);
|
||||
output.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer::apply_fixes;
|
||||
use crate::checks::Fix;
|
||||
|
||||
#[test]
|
||||
fn empty_file() -> Result<()> {
|
||||
let mut fixes = vec![];
|
||||
let actual = apply_fixes(fixes.iter_mut(), "");
|
||||
let expected = "";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 15),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A(Bar):
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() -> Result<()> {
|
||||
let mut fixes = vec![Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
applied: false,
|
||||
}];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_double_removal() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 17),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 17),
|
||||
end_location: Location::new(1, 24),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object, object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() -> Result<()> {
|
||||
let mut fixes = vec![
|
||||
Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 16),
|
||||
applied: false,
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 10),
|
||||
end_location: Location::new(1, 12),
|
||||
applied: false,
|
||||
},
|
||||
];
|
||||
let actual = apply_fixes(
|
||||
fixes.iter_mut(),
|
||||
"class A(object):
|
||||
...
|
||||
",
|
||||
);
|
||||
|
||||
let expected = "class A:
|
||||
...
|
||||
";
|
||||
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
392
src/autofix/fixes.rs
Normal file
392
src/autofix/fixes.rs
Normal file
@@ -0,0 +1,392 @@
|
||||
use anyhow::Result;
|
||||
use itertools::Itertools;
|
||||
use libcst_native::ImportNames::Aliases;
|
||||
use libcst_native::NameOrAttribute::N;
|
||||
use libcst_native::{Codegen, Expression, SmallStatement, Statement};
|
||||
use rustpython_parser::ast::{ExcepthandlerKind, Expr, Keyword, Location, Stmt, StmtKind};
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::token::Tok;
|
||||
|
||||
use crate::ast::operations::SourceCodeLocator;
|
||||
use crate::ast::types::Range;
|
||||
use crate::checks::Fix;
|
||||
|
||||
/// Convert a location within a file (relative to `base`) to an absolute position.
|
||||
fn to_absolute(relative: &Location, base: &Location) -> Location {
|
||||
if relative.row() == 1 {
|
||||
Location::new(
|
||||
relative.row() + base.row() - 1,
|
||||
relative.column() + base.column() - 1,
|
||||
)
|
||||
} else {
|
||||
Location::new(relative.row() + base.row() - 1, relative.column())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a fix to remove a base from a ClassDef statement.
|
||||
pub fn remove_class_def_base(
|
||||
locator: &mut SourceCodeLocator,
|
||||
stmt_at: &Location,
|
||||
expr_at: Location,
|
||||
bases: &[Expr],
|
||||
keywords: &[Keyword],
|
||||
) -> Option<Fix> {
|
||||
let content = locator.slice_source_code_at(stmt_at);
|
||||
|
||||
// Case 1: `object` is the only base.
|
||||
if bases.len() == 1 && keywords.is_empty() {
|
||||
let mut fix_start = None;
|
||||
let mut fix_end = None;
|
||||
let mut count: usize = 0;
|
||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||
if matches!(tok, Tok::Lpar) {
|
||||
if count == 0 {
|
||||
fix_start = Some(to_absolute(&start, stmt_at));
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
|
||||
if matches!(tok, Tok::Rpar) {
|
||||
count -= 1;
|
||||
if count == 0 {
|
||||
fix_end = Some(to_absolute(&end, stmt_at));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Some(Fix {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
applied: false,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
|
||||
if bases
|
||||
.iter()
|
||||
.map(|node| node.location)
|
||||
.chain(keywords.iter().map(|node| node.location))
|
||||
.any(|location| location > expr_at)
|
||||
{
|
||||
// Case 2: `object` is _not_ the last node.
|
||||
let mut fix_start: Option<Location> = None;
|
||||
let mut fix_end: Option<Location> = None;
|
||||
let mut seen_comma = false;
|
||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||
let start = to_absolute(&start, stmt_at);
|
||||
if seen_comma {
|
||||
if matches!(tok, Tok::Newline) {
|
||||
fix_end = Some(end);
|
||||
} else {
|
||||
fix_end = Some(start);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if start == expr_at {
|
||||
fix_start = Some(start);
|
||||
}
|
||||
if fix_start.is_some() && matches!(tok, Tok::Comma) {
|
||||
seen_comma = true;
|
||||
}
|
||||
}
|
||||
|
||||
match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Some(Fix {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
applied: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
// Case 3: `object` is the last node, so we have to find the last token that isn't a comma.
|
||||
let mut fix_start: Option<Location> = None;
|
||||
let mut fix_end: Option<Location> = None;
|
||||
for (start, tok, end) in lexer::make_tokenizer(content).flatten() {
|
||||
let start = to_absolute(&start, stmt_at);
|
||||
let end = to_absolute(&end, stmt_at);
|
||||
if start == expr_at {
|
||||
fix_end = Some(end);
|
||||
break;
|
||||
}
|
||||
if matches!(tok, Tok::Comma) {
|
||||
fix_start = Some(start);
|
||||
}
|
||||
}
|
||||
|
||||
match (fix_start, fix_end) {
|
||||
(Some(start), Some(end)) => Some(Fix {
|
||||
content: "".to_string(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
applied: false,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_super_arguments(locator: &mut SourceCodeLocator, expr: &Expr) -> Option<Fix> {
|
||||
let range = Range::from_located(expr);
|
||||
let contents = locator.slice_source_code_range(&range);
|
||||
|
||||
let mut tree = match libcst_native::parse_module(contents, None) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||
if let Some(SmallStatement::Expr(body)) = body.body.first_mut() {
|
||||
if let Expression::Call(body) = &mut body.value {
|
||||
body.args = vec![];
|
||||
body.whitespace_before_args = Default::default();
|
||||
body.whitespace_after_func = Default::default();
|
||||
|
||||
let mut state = Default::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
return Some(Fix {
|
||||
content: state.to_string(),
|
||||
location: range.location,
|
||||
end_location: range.end_location,
|
||||
applied: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Determine if a body contains only a single statement, taking into account deleted.
|
||||
fn has_single_child(body: &[Stmt], deleted: &[&Stmt]) -> bool {
|
||||
body.iter().filter(|child| !deleted.contains(child)).count() == 1
|
||||
}
|
||||
|
||||
/// Determine if a child is the only statement in its body.
|
||||
fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool> {
|
||||
match &parent.node {
|
||||
StmtKind::FunctionDef { body, .. }
|
||||
| StmtKind::AsyncFunctionDef { body, .. }
|
||||
| StmtKind::ClassDef { body, .. }
|
||||
| StmtKind::With { body, .. }
|
||||
| StmtKind::AsyncWith { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::For { body, orelse, .. }
|
||||
| StmtKind::AsyncFor { body, orelse, .. }
|
||||
| StmtKind::While { body, orelse, .. }
|
||||
| StmtKind::If { body, orelse, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
StmtKind::Try {
|
||||
body,
|
||||
handlers,
|
||||
orelse,
|
||||
finalbody,
|
||||
} => {
|
||||
if body.iter().contains(child) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else if orelse.iter().contains(child) {
|
||||
Ok(has_single_child(orelse, deleted))
|
||||
} else if finalbody.iter().contains(child) {
|
||||
Ok(has_single_child(finalbody, deleted))
|
||||
} else if let Some(body) = handlers.iter().find_map(|handler| match &handler.node {
|
||||
ExcepthandlerKind::ExceptHandler { body, .. } => {
|
||||
if body.iter().contains(child) {
|
||||
Some(body)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}) {
|
||||
Ok(has_single_child(body, deleted))
|
||||
} else {
|
||||
Err(anyhow::anyhow!("Unable to find child in parent body."))
|
||||
}
|
||||
}
|
||||
_ => Err(anyhow::anyhow!("Unable to find child in parent body.")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_stmt(stmt: &Stmt, parent: Option<&Stmt>, deleted: &[&Stmt]) -> Result<Fix> {
|
||||
if parent
|
||||
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||
.map_or(Ok(None), |v| v.map(Some))?
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// If removing this node would lead to an invalid syntax tree, replace
|
||||
// it with a `pass`.
|
||||
Ok(Fix {
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
content: "pass".to_string(),
|
||||
applied: false,
|
||||
})
|
||||
} else {
|
||||
// Otherwise, nuke the entire line.
|
||||
// TODO(charlie): This logic assumes that there are no multi-statement physical lines.
|
||||
Ok(Fix {
|
||||
location: Location::new(stmt.location.row(), 1),
|
||||
end_location: Location::new(stmt.end_location.row() + 1, 1),
|
||||
content: "".to_string(),
|
||||
applied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a Fix to remove any unused imports from an `import` statement.
|
||||
pub fn remove_unused_imports(
|
||||
locator: &mut SourceCodeLocator,
|
||||
full_names: &[&str],
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
) -> Result<Fix> {
|
||||
let mut tree = match libcst_native::parse_module(
|
||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||
None,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||
};
|
||||
|
||||
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||
};
|
||||
let body = if let Some(SmallStatement::Import(body)) = body.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Expected node to be: SmallStatement::ImportFrom."
|
||||
));
|
||||
};
|
||||
let aliases = &mut body.names;
|
||||
|
||||
// Preserve the trailing comma (or not) from the last entry.
|
||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||
|
||||
// Identify unused imports from within the `import from`.
|
||||
let mut removable = vec![];
|
||||
for (index, alias) in aliases.iter().enumerate() {
|
||||
if let N(import_name) = &alias.name {
|
||||
if full_names.contains(&import_name.value) {
|
||||
removable.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(charlie): This is quadratic.
|
||||
for index in removable.iter().rev() {
|
||||
aliases.remove(*index);
|
||||
}
|
||||
|
||||
if let Some(alias) = aliases.last_mut() {
|
||||
alias.comma = trailing_comma;
|
||||
}
|
||||
|
||||
if aliases.is_empty() {
|
||||
remove_stmt(stmt, parent, deleted)
|
||||
} else {
|
||||
let mut state = Default::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
Ok(Fix {
|
||||
content: state.to_string(),
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
applied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a Fix to remove any unused imports from an `import from` statement.
|
||||
pub fn remove_unused_import_froms(
|
||||
locator: &mut SourceCodeLocator,
|
||||
full_names: &[&str],
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
) -> Result<Fix> {
|
||||
let mut tree = match libcst_native::parse_module(
|
||||
locator.slice_source_code_range(&Range::from_located(stmt)),
|
||||
None,
|
||||
) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return Err(anyhow::anyhow!("Failed to extract CST from source.")),
|
||||
};
|
||||
|
||||
let body = if let Some(Statement::Simple(body)) = tree.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Statement::Simple."));
|
||||
};
|
||||
let body = if let Some(SmallStatement::ImportFrom(body)) = body.body.first_mut() {
|
||||
body
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Expected node to be: SmallStatement::ImportFrom."
|
||||
));
|
||||
};
|
||||
let aliases = if let Aliases(aliases) = &mut body.names {
|
||||
aliases
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Expected node to be: Aliases."));
|
||||
};
|
||||
|
||||
// Preserve the trailing comma (or not) from the last entry.
|
||||
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
|
||||
|
||||
// Identify unused imports from within the `import from`.
|
||||
let mut removable = vec![];
|
||||
for (index, alias) in aliases.iter().enumerate() {
|
||||
if let N(name) = &alias.name {
|
||||
let import_name = if let Some(N(module_name)) = &body.module {
|
||||
format!("{}.{}", module_name.value, name.value)
|
||||
} else {
|
||||
name.value.to_string()
|
||||
};
|
||||
if full_names.contains(&import_name.as_str()) {
|
||||
removable.push(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(charlie): This is quadratic.
|
||||
for index in removable.iter().rev() {
|
||||
aliases.remove(*index);
|
||||
}
|
||||
|
||||
if let Some(alias) = aliases.last_mut() {
|
||||
alias.comma = trailing_comma;
|
||||
}
|
||||
|
||||
if aliases.is_empty() {
|
||||
remove_stmt(stmt, parent, deleted)
|
||||
} else {
|
||||
let mut state = Default::default();
|
||||
tree.codegen(&mut state);
|
||||
|
||||
Ok(Fix {
|
||||
content: state.to_string(),
|
||||
location: stmt.location,
|
||||
end_location: stmt.end_location,
|
||||
applied: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
27
src/cache.rs
27
src/cache.rs
@@ -1,14 +1,17 @@
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs::Metadata;
|
||||
use std::fs::{create_dir_all, File, Metadata};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::autofix;
|
||||
use anyhow::Result;
|
||||
use cacache::Error::EntryNotFound;
|
||||
use filetime::FileTime;
|
||||
use log::error;
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::message::Message;
|
||||
use crate::settings::Settings;
|
||||
|
||||
@@ -62,7 +65,7 @@ impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
match value {
|
||||
true => Mode::ReadWrite,
|
||||
false => Mode::WriteOnly,
|
||||
false => Mode::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,23 +74,33 @@ fn cache_dir() -> &'static str {
|
||||
"./.ruff_cache"
|
||||
}
|
||||
|
||||
fn cache_key(path: &Path, settings: &Settings, autofix: &autofix::Mode) -> String {
|
||||
fn cache_key(path: &Path, settings: &Settings, autofix: &fixer::Mode) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
settings.hash(&mut hasher);
|
||||
autofix.hash(&mut hasher);
|
||||
format!(
|
||||
"{}@{}@{}",
|
||||
path.canonicalize().unwrap().to_string_lossy(),
|
||||
path.absolutize().unwrap().to_string_lossy(),
|
||||
VERSION,
|
||||
hasher.finish()
|
||||
)
|
||||
}
|
||||
|
||||
pub fn init() -> Result<()> {
|
||||
let gitignore_path = Path::new(cache_dir()).join(".gitignore");
|
||||
if gitignore_path.exists() {
|
||||
return Ok(());
|
||||
}
|
||||
create_dir_all(cache_dir())?;
|
||||
let mut file = File::create(gitignore_path)?;
|
||||
file.write_all(b"*").map_err(|e| e.into())
|
||||
}
|
||||
|
||||
pub fn get(
|
||||
path: &Path,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: &autofix::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
mode: &Mode,
|
||||
) -> Option<Vec<Message>> {
|
||||
if !mode.allow_read() {
|
||||
@@ -116,7 +129,7 @@ pub fn set(
|
||||
path: &Path,
|
||||
metadata: &Metadata,
|
||||
settings: &Settings,
|
||||
autofix: &autofix::Mode,
|
||||
autofix: &fixer::Mode,
|
||||
messages: &[Message],
|
||||
mode: &Mode,
|
||||
) {
|
||||
|
||||
2007
src/check_ast.rs
2007
src/check_ast.rs
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,17 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::checks::{Check, CheckKind};
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode, CheckKind, Fix};
|
||||
use crate::noqa;
|
||||
use crate::noqa::Directive;
|
||||
use crate::settings::Settings;
|
||||
|
||||
/// Whether the given line is too long and should be reported.
|
||||
fn should_enforce_line_length(line: &str, limit: usize) -> bool {
|
||||
if line.len() > limit {
|
||||
fn should_enforce_line_length(line: &str, length: usize, limit: usize) -> bool {
|
||||
if length > limit {
|
||||
let mut chunks = line.split_whitespace();
|
||||
if let (Some(first), Some(_)) = (chunks.next(), chunks.next()) {
|
||||
// Do not enforce the line length for commented lines with a single word
|
||||
@@ -19,34 +25,244 @@ fn should_enforce_line_length(line: &str, limit: usize) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_lines(checks: &mut Vec<Check>, contents: &str, settings: &Settings) {
|
||||
let enforce_line_too_long = settings.select.contains(CheckKind::LineTooLong.code());
|
||||
pub fn check_lines(
|
||||
checks: &mut Vec<Check>,
|
||||
contents: &str,
|
||||
noqa_line_for: &[usize],
|
||||
settings: &Settings,
|
||||
autofix: &fixer::Mode,
|
||||
) {
|
||||
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 line_checks = vec![];
|
||||
let mut ignored = vec![];
|
||||
for (row, line) in contents.lines().enumerate() {
|
||||
|
||||
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)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
if enforce_noqa {
|
||||
noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
}
|
||||
|
||||
// Remove any ignored checks.
|
||||
// TODO(charlie): Only validate checks for the current line.
|
||||
for (index, check) in checks.iter().enumerate() {
|
||||
if check.location.row() == row + 1 && check.is_inline_ignored(line) {
|
||||
ignored.push(index);
|
||||
if 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, _) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce line length.
|
||||
if enforce_line_too_long && should_enforce_line_length(line, settings.line_length) {
|
||||
let check = Check::new(
|
||||
CheckKind::LineTooLong,
|
||||
Location::new(row + 1, settings.line_length + 1),
|
||||
);
|
||||
if !check.is_inline_ignored(line) {
|
||||
line_checks.push(check);
|
||||
if enforce_line_too_long {
|
||||
let line_length = line.chars().count();
|
||||
if should_enforce_line_length(line, line_length, settings.line_length) {
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
|
||||
let check = Check::new(
|
||||
CheckKind::LineTooLong(line_length, settings.line_length),
|
||||
Range {
|
||||
location: Location::new(lineno + 1, 1),
|
||||
end_location: Location::new(lineno + 1, line_length + 1),
|
||||
},
|
||||
);
|
||||
|
||||
match noqa {
|
||||
(Directive::All(_, _), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
}
|
||||
(Directive::Codes(_, _, codes), matches) => {
|
||||
if codes.contains(&check.kind.code().as_ref()) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
} else {
|
||||
line_checks.push(check);
|
||||
}
|
||||
}
|
||||
(Directive::None, _) => line_checks.push(check),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce newlines at end of files.
|
||||
if settings.enabled.contains(&CheckCode::W292) && !contents.ends_with('\n') {
|
||||
// Note: if `lines.last()` is `None`, then `contents` is empty (and so we don't want to
|
||||
// raise W292 anyway).
|
||||
if let Some(line) = lines.last() {
|
||||
let lineno = lines.len() - 1;
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(lineno)
|
||||
.map(|lineno| lineno - 1)
|
||||
.unwrap_or(lineno);
|
||||
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno]), vec![]));
|
||||
|
||||
let check = Check::new(
|
||||
CheckKind::NoNewLineAtEndOfFile,
|
||||
Range {
|
||||
location: Location::new(lines.len(), line.len() + 1),
|
||||
end_location: Location::new(lines.len(), line.len() + 1),
|
||||
},
|
||||
);
|
||||
|
||||
match noqa {
|
||||
(Directive::All(_, _), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
}
|
||||
(Directive::Codes(_, _, codes), matches) => {
|
||||
if codes.contains(&check.kind.code().as_ref()) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
} else {
|
||||
line_checks.push(check);
|
||||
}
|
||||
}
|
||||
(Directive::None, _) => line_checks.push(check),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce that the noqa directive was actually used.
|
||||
if enforce_noqa {
|
||||
for (row, (directive, matches)) in noqa_directives {
|
||||
match directive {
|
||||
Directive::All(start, end) => {
|
||||
if matches.is_empty() {
|
||||
let mut check = Check::new(
|
||||
CheckKind::UnusedNOQA(None),
|
||||
Range {
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
check.amend(Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
}
|
||||
line_checks.push(check);
|
||||
}
|
||||
}
|
||||
Directive::Codes(start, end, codes) => {
|
||||
let mut invalid_codes = vec![];
|
||||
let mut valid_codes = vec![];
|
||||
for code in codes {
|
||||
if !matches.contains(&code) {
|
||||
invalid_codes.push(code.to_string());
|
||||
} else {
|
||||
valid_codes.push(code.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !invalid_codes.is_empty() {
|
||||
let mut check = Check::new(
|
||||
CheckKind::UnusedNOQA(Some(invalid_codes)),
|
||||
Range {
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(row + 1, end + 1),
|
||||
},
|
||||
);
|
||||
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
|
||||
if valid_codes.is_empty() {
|
||||
check.amend(Fix {
|
||||
content: "".to_string(),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
} else {
|
||||
check.amend(Fix {
|
||||
content: format!(" # noqa: {}", valid_codes.join(", ")),
|
||||
location: Location::new(row + 1, start + 1),
|
||||
end_location: Location::new(
|
||||
row + 1,
|
||||
lines[row].chars().count() + 1,
|
||||
),
|
||||
applied: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
line_checks.push(check);
|
||||
}
|
||||
}
|
||||
Directive::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ignored.sort();
|
||||
for index in ignored.iter().rev() {
|
||||
checks.swap_remove(*index);
|
||||
}
|
||||
checks.extend(line_checks);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::autofix::fixer;
|
||||
use crate::checks::{Check, CheckCode};
|
||||
use crate::settings;
|
||||
|
||||
use super::check_lines;
|
||||
|
||||
#[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 check_with_max_line_length = |line_length: usize| {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
check_lines(
|
||||
&mut checks,
|
||||
line,
|
||||
&noqa_line_for,
|
||||
&settings::Settings {
|
||||
line_length,
|
||||
..settings::Settings::for_rule(CheckCode::E501)
|
||||
},
|
||||
&fixer::Mode::Generate,
|
||||
);
|
||||
return checks;
|
||||
};
|
||||
assert!(!check_with_max_line_length(6).is_empty());
|
||||
assert!(check_with_max_line_length(7).is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
805
src/checks.rs
805
src/checks.rs
@@ -1,269 +1,728 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use itertools::Itertools;
|
||||
use rustpython_parser::ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use strum_macros::{AsRefStr, EnumIter, EnumString};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Hash, PartialOrd, Ord)]
|
||||
use crate::ast::checks::Primitive;
|
||||
use crate::ast::types::Range;
|
||||
|
||||
pub const DEFAULT_CHECK_CODES: [CheckCode; 43] = [
|
||||
// pycodestyle errors
|
||||
CheckCode::E402,
|
||||
CheckCode::E501,
|
||||
CheckCode::E711,
|
||||
CheckCode::E712,
|
||||
CheckCode::E713,
|
||||
CheckCode::E714,
|
||||
CheckCode::E721,
|
||||
CheckCode::E722,
|
||||
CheckCode::E731,
|
||||
CheckCode::E741,
|
||||
CheckCode::E742,
|
||||
CheckCode::E743,
|
||||
CheckCode::E902,
|
||||
CheckCode::E999,
|
||||
// pycodestyle warnings
|
||||
CheckCode::W292,
|
||||
// pyflakes
|
||||
CheckCode::F401,
|
||||
CheckCode::F402,
|
||||
CheckCode::F403,
|
||||
CheckCode::F404,
|
||||
CheckCode::F405,
|
||||
CheckCode::F406,
|
||||
CheckCode::F407,
|
||||
CheckCode::F541,
|
||||
CheckCode::F601,
|
||||
CheckCode::F602,
|
||||
CheckCode::F621,
|
||||
CheckCode::F622,
|
||||
CheckCode::F631,
|
||||
CheckCode::F632,
|
||||
CheckCode::F633,
|
||||
CheckCode::F634,
|
||||
CheckCode::F701,
|
||||
CheckCode::F702,
|
||||
CheckCode::F704,
|
||||
CheckCode::F706,
|
||||
CheckCode::F707,
|
||||
CheckCode::F722,
|
||||
CheckCode::F821,
|
||||
CheckCode::F822,
|
||||
CheckCode::F823,
|
||||
CheckCode::F831,
|
||||
CheckCode::F841,
|
||||
CheckCode::F901,
|
||||
];
|
||||
|
||||
#[derive(
|
||||
AsRefStr,
|
||||
EnumIter,
|
||||
EnumString,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Clone,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
)]
|
||||
pub enum CheckCode {
|
||||
// pycodestyle errors
|
||||
E402,
|
||||
E501,
|
||||
E711,
|
||||
E712,
|
||||
E713,
|
||||
E714,
|
||||
E721,
|
||||
E722,
|
||||
E731,
|
||||
E741,
|
||||
E742,
|
||||
E743,
|
||||
E902,
|
||||
E999,
|
||||
// pycodestyle warnings
|
||||
W292,
|
||||
// pyflakes
|
||||
F401,
|
||||
F402,
|
||||
F403,
|
||||
F404,
|
||||
F405,
|
||||
F406,
|
||||
F407,
|
||||
F541,
|
||||
F601,
|
||||
F602,
|
||||
F621,
|
||||
F622,
|
||||
F631,
|
||||
F632,
|
||||
F633,
|
||||
F634,
|
||||
F701,
|
||||
F702,
|
||||
F704,
|
||||
F706,
|
||||
F707,
|
||||
F722,
|
||||
F821,
|
||||
F822,
|
||||
F823,
|
||||
F831,
|
||||
F841,
|
||||
F901,
|
||||
R001,
|
||||
R002,
|
||||
}
|
||||
|
||||
impl FromStr for CheckCode {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
match s {
|
||||
"E402" => Ok(CheckCode::E402),
|
||||
"E501" => Ok(CheckCode::E501),
|
||||
"F401" => Ok(CheckCode::F401),
|
||||
"F403" => Ok(CheckCode::F403),
|
||||
"F541" => Ok(CheckCode::F541),
|
||||
"F631" => Ok(CheckCode::F631),
|
||||
"F634" => Ok(CheckCode::F634),
|
||||
"F704" => Ok(CheckCode::F704),
|
||||
"F706" => Ok(CheckCode::F706),
|
||||
"F707" => Ok(CheckCode::F707),
|
||||
"F821" => Ok(CheckCode::F821),
|
||||
"F822" => Ok(CheckCode::F822),
|
||||
"F823" => Ok(CheckCode::F823),
|
||||
"F831" => Ok(CheckCode::F831),
|
||||
"F841" => Ok(CheckCode::F841),
|
||||
"F901" => Ok(CheckCode::F901),
|
||||
"R001" => Ok(CheckCode::R001),
|
||||
"R002" => Ok(CheckCode::R002),
|
||||
_ => Err(anyhow::anyhow!("Unknown check code: {s}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckCode {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
CheckCode::E402 => "E402",
|
||||
CheckCode::E501 => "E501",
|
||||
CheckCode::F401 => "F401",
|
||||
CheckCode::F403 => "F403",
|
||||
CheckCode::F541 => "F541",
|
||||
CheckCode::F631 => "F631",
|
||||
CheckCode::F634 => "F634",
|
||||
CheckCode::F704 => "F704",
|
||||
CheckCode::F706 => "F706",
|
||||
CheckCode::F707 => "F707",
|
||||
CheckCode::F821 => "F821",
|
||||
CheckCode::F822 => "F822",
|
||||
CheckCode::F823 => "F823",
|
||||
CheckCode::F831 => "F831",
|
||||
CheckCode::F841 => "F841",
|
||||
CheckCode::F901 => "F901",
|
||||
CheckCode::R001 => "R001",
|
||||
CheckCode::R002 => "R002",
|
||||
}
|
||||
}
|
||||
|
||||
/// The source for the check (either the AST, or the physical lines).
|
||||
pub fn lint_source(&self) -> &'static LintSource {
|
||||
match self {
|
||||
CheckCode::E402 => &LintSource::AST,
|
||||
CheckCode::E501 => &LintSource::Lines,
|
||||
CheckCode::F401 => &LintSource::AST,
|
||||
CheckCode::F403 => &LintSource::AST,
|
||||
CheckCode::F541 => &LintSource::AST,
|
||||
CheckCode::F631 => &LintSource::AST,
|
||||
CheckCode::F634 => &LintSource::AST,
|
||||
CheckCode::F704 => &LintSource::AST,
|
||||
CheckCode::F706 => &LintSource::AST,
|
||||
CheckCode::F707 => &LintSource::AST,
|
||||
CheckCode::F821 => &LintSource::AST,
|
||||
CheckCode::F822 => &LintSource::AST,
|
||||
CheckCode::F823 => &LintSource::AST,
|
||||
CheckCode::F831 => &LintSource::AST,
|
||||
CheckCode::F841 => &LintSource::AST,
|
||||
CheckCode::F901 => &LintSource::AST,
|
||||
CheckCode::R001 => &LintSource::AST,
|
||||
CheckCode::R002 => &LintSource::AST,
|
||||
}
|
||||
}
|
||||
// flake8-builtins
|
||||
A001,
|
||||
A002,
|
||||
A003,
|
||||
// flake8-bugbear
|
||||
B011,
|
||||
B014,
|
||||
B025,
|
||||
// flake8-comprehensions
|
||||
C400,
|
||||
C401,
|
||||
C402,
|
||||
C403,
|
||||
C404,
|
||||
C405,
|
||||
C406,
|
||||
C408,
|
||||
C409,
|
||||
C410,
|
||||
C415,
|
||||
// flake8-print
|
||||
T201,
|
||||
T203,
|
||||
// pyupgrade
|
||||
U001,
|
||||
U002,
|
||||
U003,
|
||||
U004,
|
||||
U005,
|
||||
U006,
|
||||
U007,
|
||||
U008,
|
||||
// Meta
|
||||
M001,
|
||||
}
|
||||
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum LintSource {
|
||||
AST,
|
||||
Lines,
|
||||
FileSystem,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum RejectedCmpop {
|
||||
Eq,
|
||||
NotEq,
|
||||
}
|
||||
|
||||
#[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum CheckKind {
|
||||
// pycodestyle errors
|
||||
AmbiguousClassName(String),
|
||||
AmbiguousFunctionName(String),
|
||||
AmbiguousVariableName(String),
|
||||
AssertTuple,
|
||||
BreakOutsideLoop,
|
||||
ContinueOutsideLoop,
|
||||
DefaultExceptNotLast,
|
||||
DoNotAssignLambda,
|
||||
DoNotUseBareExcept,
|
||||
DuplicateArgumentName,
|
||||
ExpressionsInStarAssignment,
|
||||
FStringMissingPlaceholders,
|
||||
ForwardAnnotationSyntaxError(String),
|
||||
FutureFeatureNotDefined(String),
|
||||
IOError(String),
|
||||
IfTuple,
|
||||
ImportStarUsage,
|
||||
LineTooLong,
|
||||
ImportShadowedByLoopVar(String, usize),
|
||||
ImportStarNotPermitted(String),
|
||||
ImportStarUsage(String, Vec<String>),
|
||||
ImportStarUsed(String),
|
||||
InvalidPrintSyntax,
|
||||
IsLiteral,
|
||||
LateFutureImport,
|
||||
LineTooLong(usize, usize),
|
||||
ModuleImportNotAtTopOfFile,
|
||||
NoAssertEquals,
|
||||
MultiValueRepeatedKeyLiteral,
|
||||
MultiValueRepeatedKeyVariable(String),
|
||||
NoneComparison(RejectedCmpop),
|
||||
NotInTest,
|
||||
NotIsTest,
|
||||
RaiseNotImplemented,
|
||||
ReturnOutsideFunction,
|
||||
SyntaxError(String),
|
||||
TrueFalseComparison(bool, RejectedCmpop),
|
||||
TwoStarredExpressions,
|
||||
TypeComparison,
|
||||
UndefinedExport(String),
|
||||
UndefinedLocal(String),
|
||||
UndefinedName(String),
|
||||
UnusedImport(String),
|
||||
UnusedImport(Vec<String>),
|
||||
UnusedVariable(String),
|
||||
UselessObjectInheritance(String),
|
||||
YieldOutsideFunction,
|
||||
// pycodestyle warnings
|
||||
NoNewLineAtEndOfFile,
|
||||
// flake8-builtin
|
||||
BuiltinVariableShadowing(String),
|
||||
BuiltinArgumentShadowing(String),
|
||||
BuiltinAttributeShadowing(String),
|
||||
// flake8-bugbear
|
||||
DoNotAssertFalse,
|
||||
DuplicateHandlerException(String),
|
||||
DuplicateTryBlockException(String),
|
||||
// flake8-comprehensions
|
||||
UnnecessaryGeneratorList,
|
||||
UnnecessaryGeneratorSet,
|
||||
UnnecessaryGeneratorDict,
|
||||
UnnecessaryListComprehensionSet,
|
||||
UnnecessaryListComprehensionDict,
|
||||
UnnecessaryLiteralSet(String),
|
||||
UnnecessaryLiteralDict(String),
|
||||
UnnecessaryCollectionCall(String),
|
||||
UnnecessaryLiteralWithinTupleCall(String),
|
||||
UnnecessaryLiteralWithinListCall(String),
|
||||
UnnecessarySubscriptReversal(String),
|
||||
// flake8-print
|
||||
PrintFound,
|
||||
PPrintFound,
|
||||
// pyupgrade
|
||||
TypeOfPrimitive(Primitive),
|
||||
UnnecessaryAbspath,
|
||||
UselessMetaclassType,
|
||||
DeprecatedUnittestAlias(String, String),
|
||||
UselessObjectInheritance(String),
|
||||
UsePEP585Annotation(String),
|
||||
UsePEP604Annotation,
|
||||
SuperCallWithParameters,
|
||||
// Meta
|
||||
UnusedNOQA(Option<Vec<String>>),
|
||||
}
|
||||
|
||||
impl CheckKind {
|
||||
/// The name of the check.
|
||||
pub fn name(&self) -> &'static str {
|
||||
impl CheckCode {
|
||||
/// The source for the check (either the AST, the filesystem, or the physical lines).
|
||||
pub fn lint_source(&self) -> &'static LintSource {
|
||||
match self {
|
||||
CheckKind::AssertTuple => "AssertTuple",
|
||||
CheckKind::DefaultExceptNotLast => "DefaultExceptNotLast",
|
||||
CheckKind::DuplicateArgumentName => "DuplicateArgumentName",
|
||||
CheckKind::FStringMissingPlaceholders => "FStringMissingPlaceholders",
|
||||
CheckKind::IfTuple => "IfTuple",
|
||||
CheckKind::ImportStarUsage => "ImportStarUsage",
|
||||
CheckKind::LineTooLong => "LineTooLong",
|
||||
CheckKind::ModuleImportNotAtTopOfFile => "ModuleImportNotAtTopOfFile",
|
||||
CheckKind::NoAssertEquals => "NoAssertEquals",
|
||||
CheckKind::RaiseNotImplemented => "RaiseNotImplemented",
|
||||
CheckKind::ReturnOutsideFunction => "ReturnOutsideFunction",
|
||||
CheckKind::UndefinedExport(_) => "UndefinedExport",
|
||||
CheckKind::UndefinedLocal(_) => "UndefinedLocal",
|
||||
CheckKind::UndefinedName(_) => "UndefinedName",
|
||||
CheckKind::UnusedImport(_) => "UnusedImport",
|
||||
CheckKind::UnusedVariable(_) => "UnusedVariable",
|
||||
CheckKind::UselessObjectInheritance(_) => "UselessObjectInheritance",
|
||||
CheckKind::YieldOutsideFunction => "YieldOutsideFunction",
|
||||
CheckCode::E501 | CheckCode::M001 => &LintSource::Lines,
|
||||
CheckCode::E902 => &LintSource::FileSystem,
|
||||
_ => &LintSource::AST,
|
||||
}
|
||||
}
|
||||
|
||||
/// A placeholder representation of the CheckKind for the check.
|
||||
pub fn kind(&self) -> CheckKind {
|
||||
match self {
|
||||
// pycodestyle errors
|
||||
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
|
||||
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
|
||||
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
|
||||
CheckCode::E712 => CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
|
||||
CheckCode::E713 => CheckKind::NotInTest,
|
||||
CheckCode::E714 => CheckKind::NotIsTest,
|
||||
CheckCode::E721 => CheckKind::TypeComparison,
|
||||
CheckCode::E722 => CheckKind::DoNotUseBareExcept,
|
||||
CheckCode::E731 => CheckKind::DoNotAssignLambda,
|
||||
CheckCode::E741 => CheckKind::AmbiguousVariableName("...".to_string()),
|
||||
CheckCode::E742 => CheckKind::AmbiguousClassName("...".to_string()),
|
||||
CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()),
|
||||
CheckCode::E902 => CheckKind::IOError("IOError: `...`".to_string()),
|
||||
CheckCode::E999 => CheckKind::SyntaxError("`...`".to_string()),
|
||||
// pycodestyle warnings
|
||||
CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile,
|
||||
// pyflakes
|
||||
CheckCode::F401 => CheckKind::UnusedImport(vec!["...".to_string()]),
|
||||
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
|
||||
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
|
||||
CheckCode::F404 => CheckKind::LateFutureImport,
|
||||
CheckCode::F405 => {
|
||||
CheckKind::ImportStarUsage("...".to_string(), vec!["...".to_string()])
|
||||
}
|
||||
CheckCode::F406 => CheckKind::ImportStarNotPermitted("...".to_string()),
|
||||
CheckCode::F407 => CheckKind::FutureFeatureNotDefined("...".to_string()),
|
||||
CheckCode::F541 => CheckKind::FStringMissingPlaceholders,
|
||||
CheckCode::F601 => CheckKind::MultiValueRepeatedKeyLiteral,
|
||||
CheckCode::F602 => CheckKind::MultiValueRepeatedKeyVariable("...".to_string()),
|
||||
CheckCode::F621 => CheckKind::ExpressionsInStarAssignment,
|
||||
CheckCode::F622 => CheckKind::TwoStarredExpressions,
|
||||
CheckCode::F631 => CheckKind::AssertTuple,
|
||||
CheckCode::F632 => CheckKind::IsLiteral,
|
||||
CheckCode::F633 => CheckKind::InvalidPrintSyntax,
|
||||
CheckCode::F634 => CheckKind::IfTuple,
|
||||
CheckCode::F701 => CheckKind::BreakOutsideLoop,
|
||||
CheckCode::F702 => CheckKind::ContinueOutsideLoop,
|
||||
CheckCode::F704 => CheckKind::YieldOutsideFunction,
|
||||
CheckCode::F706 => CheckKind::ReturnOutsideFunction,
|
||||
CheckCode::F707 => CheckKind::DefaultExceptNotLast,
|
||||
CheckCode::F722 => CheckKind::ForwardAnnotationSyntaxError("...".to_string()),
|
||||
CheckCode::F821 => CheckKind::UndefinedName("...".to_string()),
|
||||
CheckCode::F822 => CheckKind::UndefinedExport("...".to_string()),
|
||||
CheckCode::F823 => CheckKind::UndefinedLocal("...".to_string()),
|
||||
CheckCode::F831 => CheckKind::DuplicateArgumentName,
|
||||
CheckCode::F841 => CheckKind::UnusedVariable("...".to_string()),
|
||||
CheckCode::F901 => CheckKind::RaiseNotImplemented,
|
||||
// flake8-builtins
|
||||
CheckCode::A001 => CheckKind::BuiltinVariableShadowing("...".to_string()),
|
||||
CheckCode::A002 => CheckKind::BuiltinArgumentShadowing("...".to_string()),
|
||||
CheckCode::A003 => CheckKind::BuiltinAttributeShadowing("...".to_string()),
|
||||
// flake8-bugbear
|
||||
CheckCode::B011 => CheckKind::DoNotAssertFalse,
|
||||
CheckCode::B014 => CheckKind::DuplicateHandlerException("Exception".to_string()),
|
||||
CheckCode::B025 => CheckKind::DuplicateTryBlockException("Exception".to_string()),
|
||||
// flake8-comprehensions
|
||||
CheckCode::C400 => CheckKind::UnnecessaryGeneratorList,
|
||||
CheckCode::C401 => CheckKind::UnnecessaryGeneratorSet,
|
||||
CheckCode::C402 => CheckKind::UnnecessaryGeneratorDict,
|
||||
CheckCode::C403 => CheckKind::UnnecessaryListComprehensionSet,
|
||||
CheckCode::C404 => CheckKind::UnnecessaryListComprehensionDict,
|
||||
CheckCode::C405 => CheckKind::UnnecessaryLiteralSet("<list/tuple>".to_string()),
|
||||
CheckCode::C406 => CheckKind::UnnecessaryLiteralDict("<list/tuple>".to_string()),
|
||||
CheckCode::C408 => {
|
||||
CheckKind::UnnecessaryCollectionCall("<dict/list/tuple>".to_string())
|
||||
}
|
||||
CheckCode::C409 => {
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall("<list/tuple>".to_string())
|
||||
}
|
||||
CheckCode::C410 => {
|
||||
CheckKind::UnnecessaryLiteralWithinListCall("<list/tuple>".to_string())
|
||||
}
|
||||
CheckCode::C415 => {
|
||||
CheckKind::UnnecessarySubscriptReversal("<reversed/set/sorted>".to_string())
|
||||
}
|
||||
// flake8-print
|
||||
CheckCode::T201 => CheckKind::PrintFound,
|
||||
CheckCode::T203 => CheckKind::PPrintFound,
|
||||
// pyupgrade
|
||||
CheckCode::U001 => CheckKind::UselessMetaclassType,
|
||||
CheckCode::U002 => CheckKind::UnnecessaryAbspath,
|
||||
CheckCode::U003 => CheckKind::TypeOfPrimitive(Primitive::Str),
|
||||
CheckCode::U004 => CheckKind::UselessObjectInheritance("...".to_string()),
|
||||
CheckCode::U005 => CheckKind::DeprecatedUnittestAlias(
|
||||
"assertEquals".to_string(),
|
||||
"assertEqual".to_string(),
|
||||
),
|
||||
CheckCode::U006 => CheckKind::UsePEP585Annotation("List".to_string()),
|
||||
CheckCode::U007 => CheckKind::UsePEP604Annotation,
|
||||
CheckCode::U008 => CheckKind::SuperCallWithParameters,
|
||||
// Meta
|
||||
CheckCode::M001 => CheckKind::UnusedNOQA(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CheckKind {
|
||||
/// A four-letter shorthand code for the check.
|
||||
pub fn code(&self) -> &'static CheckCode {
|
||||
match self {
|
||||
// pycodestyle errors
|
||||
CheckKind::AmbiguousClassName(_) => &CheckCode::E742,
|
||||
CheckKind::AmbiguousFunctionName(_) => &CheckCode::E743,
|
||||
CheckKind::AmbiguousVariableName(_) => &CheckCode::E741,
|
||||
CheckKind::AssertTuple => &CheckCode::F631,
|
||||
CheckKind::BreakOutsideLoop => &CheckCode::F701,
|
||||
CheckKind::ContinueOutsideLoop => &CheckCode::F702,
|
||||
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
|
||||
CheckKind::DoNotAssignLambda => &CheckCode::E731,
|
||||
CheckKind::DoNotUseBareExcept => &CheckCode::E722,
|
||||
CheckKind::DuplicateArgumentName => &CheckCode::F831,
|
||||
CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
|
||||
CheckKind::ForwardAnnotationSyntaxError(_) => &CheckCode::F722,
|
||||
CheckKind::FutureFeatureNotDefined(_) => &CheckCode::F407,
|
||||
CheckKind::IOError(_) => &CheckCode::E902,
|
||||
CheckKind::IfTuple => &CheckCode::F634,
|
||||
CheckKind::ImportStarUsage => &CheckCode::F403,
|
||||
CheckKind::LineTooLong => &CheckCode::E501,
|
||||
CheckKind::ImportShadowedByLoopVar(_, _) => &CheckCode::F402,
|
||||
CheckKind::ImportStarNotPermitted(_) => &CheckCode::F406,
|
||||
CheckKind::ImportStarUsage(_, _) => &CheckCode::F405,
|
||||
CheckKind::ImportStarUsed(_) => &CheckCode::F403,
|
||||
CheckKind::InvalidPrintSyntax => &CheckCode::F633,
|
||||
CheckKind::IsLiteral => &CheckCode::F632,
|
||||
CheckKind::LateFutureImport => &CheckCode::F404,
|
||||
CheckKind::LineTooLong(_, _) => &CheckCode::E501,
|
||||
CheckKind::ModuleImportNotAtTopOfFile => &CheckCode::E402,
|
||||
CheckKind::NoAssertEquals => &CheckCode::R002,
|
||||
CheckKind::MultiValueRepeatedKeyLiteral => &CheckCode::F601,
|
||||
CheckKind::MultiValueRepeatedKeyVariable(_) => &CheckCode::F602,
|
||||
CheckKind::NoneComparison(_) => &CheckCode::E711,
|
||||
CheckKind::NotInTest => &CheckCode::E713,
|
||||
CheckKind::NotIsTest => &CheckCode::E714,
|
||||
CheckKind::RaiseNotImplemented => &CheckCode::F901,
|
||||
CheckKind::ReturnOutsideFunction => &CheckCode::F706,
|
||||
CheckKind::SyntaxError(_) => &CheckCode::E999,
|
||||
CheckKind::ExpressionsInStarAssignment => &CheckCode::F621,
|
||||
CheckKind::TrueFalseComparison(_, _) => &CheckCode::E712,
|
||||
CheckKind::TwoStarredExpressions => &CheckCode::F622,
|
||||
CheckKind::TypeComparison => &CheckCode::E721,
|
||||
CheckKind::UndefinedExport(_) => &CheckCode::F822,
|
||||
CheckKind::UndefinedLocal(_) => &CheckCode::F823,
|
||||
CheckKind::UndefinedName(_) => &CheckCode::F821,
|
||||
CheckKind::UnusedImport(_) => &CheckCode::F401,
|
||||
CheckKind::UnusedVariable(_) => &CheckCode::F841,
|
||||
CheckKind::UselessObjectInheritance(_) => &CheckCode::R001,
|
||||
CheckKind::YieldOutsideFunction => &CheckCode::F704,
|
||||
// pycodestyle warnings
|
||||
CheckKind::NoNewLineAtEndOfFile => &CheckCode::W292,
|
||||
// flake8-builtins
|
||||
CheckKind::BuiltinVariableShadowing(_) => &CheckCode::A001,
|
||||
CheckKind::BuiltinArgumentShadowing(_) => &CheckCode::A002,
|
||||
CheckKind::BuiltinAttributeShadowing(_) => &CheckCode::A003,
|
||||
// flake8-bugbear
|
||||
CheckKind::DoNotAssertFalse => &CheckCode::B011,
|
||||
CheckKind::DuplicateHandlerException(_) => &CheckCode::B014,
|
||||
CheckKind::DuplicateTryBlockException(_) => &CheckCode::B025,
|
||||
// flake8-comprehensions
|
||||
CheckKind::UnnecessaryGeneratorList => &CheckCode::C400,
|
||||
CheckKind::UnnecessaryGeneratorSet => &CheckCode::C401,
|
||||
CheckKind::UnnecessaryGeneratorDict => &CheckCode::C402,
|
||||
CheckKind::UnnecessaryListComprehensionSet => &CheckCode::C403,
|
||||
CheckKind::UnnecessaryListComprehensionDict => &CheckCode::C404,
|
||||
CheckKind::UnnecessaryLiteralSet(_) => &CheckCode::C405,
|
||||
CheckKind::UnnecessaryLiteralDict(_) => &CheckCode::C406,
|
||||
CheckKind::UnnecessaryCollectionCall(_) => &CheckCode::C408,
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall(..) => &CheckCode::C409,
|
||||
CheckKind::UnnecessaryLiteralWithinListCall(..) => &CheckCode::C410,
|
||||
CheckKind::UnnecessarySubscriptReversal(_) => &CheckCode::C415,
|
||||
// flake8-print
|
||||
CheckKind::PrintFound => &CheckCode::T201,
|
||||
CheckKind::PPrintFound => &CheckCode::T203,
|
||||
// pyupgrade
|
||||
CheckKind::TypeOfPrimitive(_) => &CheckCode::U003,
|
||||
CheckKind::UnnecessaryAbspath => &CheckCode::U002,
|
||||
CheckKind::UselessMetaclassType => &CheckCode::U001,
|
||||
CheckKind::DeprecatedUnittestAlias(_, _) => &CheckCode::U005,
|
||||
CheckKind::UsePEP585Annotation(_) => &CheckCode::U006,
|
||||
CheckKind::UsePEP604Annotation => &CheckCode::U007,
|
||||
CheckKind::UselessObjectInheritance(_) => &CheckCode::U004,
|
||||
CheckKind::SuperCallWithParameters => &CheckCode::U008,
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(_) => &CheckCode::M001,
|
||||
}
|
||||
}
|
||||
|
||||
/// The body text for the check.
|
||||
pub fn body(&self) -> String {
|
||||
match self {
|
||||
// pycodestyle errors
|
||||
CheckKind::AmbiguousClassName(name) => {
|
||||
format!("Ambiguous class name: `{}`", name)
|
||||
}
|
||||
CheckKind::AmbiguousFunctionName(name) => {
|
||||
format!("Ambiguous function name: `{}`", name)
|
||||
}
|
||||
CheckKind::AmbiguousVariableName(name) => {
|
||||
format!("Ambiguous variable name: `{}`", name)
|
||||
}
|
||||
CheckKind::AssertTuple => {
|
||||
"Assert test is a non-empty tuple, which is always `True`".to_string()
|
||||
}
|
||||
CheckKind::BreakOutsideLoop => "`break` outside loop".to_string(),
|
||||
CheckKind::ContinueOutsideLoop => "`continue` not properly in loop".to_string(),
|
||||
CheckKind::DefaultExceptNotLast => {
|
||||
"an `except:` block as not the last exception handler".to_string()
|
||||
"An `except:` block as not the last exception handler".to_string()
|
||||
}
|
||||
CheckKind::DoNotAssignLambda => {
|
||||
"Do not assign a lambda expression, use a def".to_string()
|
||||
}
|
||||
CheckKind::DoNotUseBareExcept => "Do not use bare `except`".to_string(),
|
||||
CheckKind::DuplicateArgumentName => {
|
||||
"Duplicate argument name in function definition".to_string()
|
||||
}
|
||||
CheckKind::ForwardAnnotationSyntaxError(body) => {
|
||||
format!("Syntax error in forward annotation: `{body}`")
|
||||
}
|
||||
CheckKind::FStringMissingPlaceholders => {
|
||||
"f-string without any placeholders".to_string()
|
||||
}
|
||||
CheckKind::FutureFeatureNotDefined(name) => {
|
||||
format!("Future feature `{name}` is not defined")
|
||||
}
|
||||
CheckKind::IOError(message) => message.clone(),
|
||||
CheckKind::IfTuple => "If test is a tuple, which is always `True`".to_string(),
|
||||
CheckKind::ImportStarUsage => "Unable to detect undefined names".to_string(),
|
||||
CheckKind::LineTooLong => "Line too long".to_string(),
|
||||
CheckKind::InvalidPrintSyntax => {
|
||||
"Use of `>>` is invalid with `print` function".to_string()
|
||||
}
|
||||
CheckKind::ImportShadowedByLoopVar(name, line) => {
|
||||
format!("Import `{name}` from line {line} shadowed by loop variable")
|
||||
}
|
||||
CheckKind::ImportStarNotPermitted(name) => {
|
||||
format!("`from {name} import *` only allowed at module level")
|
||||
}
|
||||
CheckKind::ImportStarUsed(name) => {
|
||||
format!("`from {name} import *` used; unable to detect undefined names")
|
||||
}
|
||||
CheckKind::ImportStarUsage(name, sources) => {
|
||||
let sources = sources
|
||||
.iter()
|
||||
.map(|source| format!("`{}`", source))
|
||||
.join(", ");
|
||||
format!("`{name}` may be undefined, or defined from star imports: {sources}")
|
||||
}
|
||||
CheckKind::IsLiteral => "Use `==` and `!=` to compare constant literals".to_string(),
|
||||
CheckKind::LateFutureImport => {
|
||||
"`from __future__` imports must occur at the beginning of the file".to_string()
|
||||
}
|
||||
CheckKind::LineTooLong(length, limit) => {
|
||||
format!("Line too long ({length} > {limit} characters)")
|
||||
}
|
||||
CheckKind::ModuleImportNotAtTopOfFile => {
|
||||
"Module level import not at top of file".to_string()
|
||||
}
|
||||
CheckKind::NoAssertEquals => {
|
||||
"`assertEquals` is deprecated, use `assertEqual` instead".to_string()
|
||||
CheckKind::MultiValueRepeatedKeyLiteral => {
|
||||
"Dictionary key literal repeated".to_string()
|
||||
}
|
||||
CheckKind::MultiValueRepeatedKeyVariable(name) => {
|
||||
format!("Dictionary key `{name}` repeated")
|
||||
}
|
||||
CheckKind::NoneComparison(op) => match op {
|
||||
RejectedCmpop::Eq => "Comparison to `None` should be `cond is None`".to_string(),
|
||||
RejectedCmpop::NotEq => {
|
||||
"Comparison to `None` should be `cond is not None`".to_string()
|
||||
}
|
||||
},
|
||||
CheckKind::NotInTest => "Test for membership should be `not in`".to_string(),
|
||||
CheckKind::NotIsTest => "Test for object identity should be `is not`".to_string(),
|
||||
CheckKind::RaiseNotImplemented => {
|
||||
"`raise NotImplemented` should be `raise NotImplementedError`".to_string()
|
||||
}
|
||||
CheckKind::ReturnOutsideFunction => {
|
||||
"a `return` statement outside of a function/method".to_string()
|
||||
"`return` statement outside of a function/method".to_string()
|
||||
}
|
||||
CheckKind::SyntaxError(message) => format!("SyntaxError: {message}"),
|
||||
CheckKind::ExpressionsInStarAssignment => {
|
||||
"Too many expressions in star-unpacking assignment".to_string()
|
||||
}
|
||||
CheckKind::TrueFalseComparison(value, op) => match *value {
|
||||
true => match op {
|
||||
RejectedCmpop::Eq => {
|
||||
"Comparison to `True` should be `cond is True`".to_string()
|
||||
}
|
||||
RejectedCmpop::NotEq => {
|
||||
"Comparison to `True` should be `cond is not True`".to_string()
|
||||
}
|
||||
},
|
||||
false => match op {
|
||||
RejectedCmpop::Eq => {
|
||||
"Comparison to `False` should be `cond is False`".to_string()
|
||||
}
|
||||
RejectedCmpop::NotEq => {
|
||||
"Comparison to `False` should be `cond is not False`".to_string()
|
||||
}
|
||||
},
|
||||
},
|
||||
CheckKind::TwoStarredExpressions => "Two starred expressions in assignment".to_string(),
|
||||
CheckKind::TypeComparison => "Do not compare types, use `isinstance()`".to_string(),
|
||||
CheckKind::UndefinedExport(name) => {
|
||||
format!("Undefined name `{name}` in `__all__`")
|
||||
}
|
||||
CheckKind::UndefinedName(name) => {
|
||||
format!("Undefined name `{name}`")
|
||||
}
|
||||
CheckKind::UndefinedLocal(name) => {
|
||||
format!("Local variable `{name}` referenced before assignment")
|
||||
}
|
||||
CheckKind::UnusedImport(name) => format!("`{name}` imported but unused"),
|
||||
CheckKind::UndefinedName(name) => {
|
||||
format!("Undefined name `{name}`")
|
||||
}
|
||||
CheckKind::UnusedImport(names) => {
|
||||
let names = names.iter().map(|name| format!("`{name}`")).join(", ");
|
||||
format!("{names} imported but unused")
|
||||
}
|
||||
CheckKind::UnusedVariable(name) => {
|
||||
format!("Local variable `{name}` is assigned to but never used")
|
||||
}
|
||||
CheckKind::YieldOutsideFunction => {
|
||||
"`yield` or `yield from` statement outside of a function/method".to_string()
|
||||
}
|
||||
// pycodestyle warnings
|
||||
CheckKind::NoNewLineAtEndOfFile => "No newline at end of file".to_string(),
|
||||
// flake8-builtins
|
||||
CheckKind::BuiltinVariableShadowing(name) => {
|
||||
format!("Variable `{name}` is shadowing a python builtin")
|
||||
}
|
||||
CheckKind::BuiltinArgumentShadowing(name) => {
|
||||
format!("Argument `{name}` is shadowing a python builtin")
|
||||
}
|
||||
CheckKind::BuiltinAttributeShadowing(name) => {
|
||||
format!("Class attribute `{name}` is shadowing a python builtin")
|
||||
}
|
||||
// flake8-bugbear
|
||||
CheckKind::DoNotAssertFalse => {
|
||||
"Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`"
|
||||
.to_string()
|
||||
}
|
||||
CheckKind::DuplicateHandlerException(name) => {
|
||||
format!("Exception handler with duplicate exception `{name}`")
|
||||
}
|
||||
CheckKind::DuplicateTryBlockException(name) => {
|
||||
format!("try-except block with duplicate exception `{name}`")
|
||||
}
|
||||
// flake8-comprehensions
|
||||
CheckKind::UnnecessaryGeneratorList => {
|
||||
"Unnecessary generator - rewrite as a list comprehension".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryGeneratorSet => {
|
||||
"Unnecessary generator - rewrite as a set comprehension".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryGeneratorDict => {
|
||||
"Unnecessary generator - rewrite as a dict comprehension".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryListComprehensionSet => {
|
||||
"Unnecessary list comprehension - rewrite as a set comprehension".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryListComprehensionDict => {
|
||||
"Unnecessary list comprehension - rewrite as a dict comprehension".to_string()
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralSet(obj_type) => {
|
||||
format!("Unnecessary {obj_type} literal - rewrite as a set literal")
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralDict(obj_type) => {
|
||||
format!("Unnecessary {obj_type} literal - rewrite as a dict literal")
|
||||
}
|
||||
CheckKind::UnnecessaryCollectionCall(obj_type) => {
|
||||
format!("Unnecessary {obj_type} call - rewrite as a literal")
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralWithinTupleCall(literal) => {
|
||||
if literal == "list" {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to tuple() - rewrite as a tuple literal"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to tuple() - remove the outer call to tuple()"
|
||||
)
|
||||
}
|
||||
}
|
||||
CheckKind::UnnecessaryLiteralWithinListCall(literal) => {
|
||||
if literal == "list" {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to list() - remove the outer call to list()"
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"Unnecessary {literal} literal passed to list() - rewrite as a list literal"
|
||||
)
|
||||
}
|
||||
}
|
||||
CheckKind::UnnecessarySubscriptReversal(func) => {
|
||||
format!("Unnecessary subscript reversal of iterable within {func}()")
|
||||
}
|
||||
// flake8-print
|
||||
CheckKind::PrintFound => "`print` found".to_string(),
|
||||
CheckKind::PPrintFound => "`pprint` found".to_string(),
|
||||
// pyupgrade
|
||||
CheckKind::TypeOfPrimitive(primitive) => {
|
||||
format!("Use `{}` instead of `type(...)`", primitive.builtin())
|
||||
}
|
||||
CheckKind::UnnecessaryAbspath => {
|
||||
"`abspath(__file__)` is unnecessary in Python 3.9 and later".to_string()
|
||||
}
|
||||
CheckKind::UselessMetaclassType => "`__metaclass__ = type` is implied".to_string(),
|
||||
CheckKind::DeprecatedUnittestAlias(alias, target) => {
|
||||
format!("`{}` is deprecated, use `{}` instead", alias, target)
|
||||
}
|
||||
CheckKind::UselessObjectInheritance(name) => {
|
||||
format!("Class `{name}` inherits from object")
|
||||
}
|
||||
CheckKind::YieldOutsideFunction => {
|
||||
"a `yield` or `yield from` statement outside of a function/method".to_string()
|
||||
CheckKind::UsePEP585Annotation(name) => {
|
||||
format!(
|
||||
"Use `{}` instead of `{}` for type annotations",
|
||||
name.to_lowercase(),
|
||||
name,
|
||||
)
|
||||
}
|
||||
CheckKind::UsePEP604Annotation => "Use `X | Y` for type annotations".to_string(),
|
||||
CheckKind::SuperCallWithParameters => {
|
||||
"Use `super()` instead of `super(__class__, self)`".to_string()
|
||||
}
|
||||
// Meta
|
||||
CheckKind::UnusedNOQA(codes) => match codes {
|
||||
None => "Unused `noqa` directive".to_string(),
|
||||
Some(codes) => {
|
||||
let codes = codes
|
||||
.iter()
|
||||
.map(|code| {
|
||||
if CheckCode::from_str(code).is_ok() {
|
||||
code.to_string()
|
||||
} else {
|
||||
format!("{code} (not implemented)")
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
format!("Unused `noqa` directive for: {codes}")
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the check kind is (potentially) fixable.
|
||||
pub fn fixable(&self) -> bool {
|
||||
match self {
|
||||
CheckKind::AssertTuple => false,
|
||||
CheckKind::DefaultExceptNotLast => false,
|
||||
CheckKind::DuplicateArgumentName => false,
|
||||
CheckKind::FStringMissingPlaceholders => false,
|
||||
CheckKind::IfTuple => false,
|
||||
CheckKind::ImportStarUsage => false,
|
||||
CheckKind::LineTooLong => false,
|
||||
CheckKind::ModuleImportNotAtTopOfFile => false,
|
||||
CheckKind::NoAssertEquals => true,
|
||||
CheckKind::RaiseNotImplemented => false,
|
||||
CheckKind::ReturnOutsideFunction => false,
|
||||
CheckKind::UndefinedExport(_) => false,
|
||||
CheckKind::UndefinedLocal(_) => false,
|
||||
CheckKind::UndefinedName(_) => false,
|
||||
CheckKind::UnusedImport(_) => false,
|
||||
CheckKind::UnusedVariable(_) => false,
|
||||
CheckKind::UselessObjectInheritance(_) => true,
|
||||
CheckKind::YieldOutsideFunction => false,
|
||||
}
|
||||
matches!(
|
||||
self,
|
||||
CheckKind::DeprecatedUnittestAlias(_, _)
|
||||
| CheckKind::DoNotAssertFalse
|
||||
| CheckKind::PPrintFound
|
||||
| CheckKind::PrintFound
|
||||
| CheckKind::SuperCallWithParameters
|
||||
| CheckKind::TypeOfPrimitive(_)
|
||||
| CheckKind::UnnecessaryAbspath
|
||||
| CheckKind::UnusedImport(_)
|
||||
| CheckKind::UnusedNOQA(_)
|
||||
| CheckKind::UselessMetaclassType
|
||||
| CheckKind::UselessObjectInheritance(_)
|
||||
| CheckKind::UsePEP585Annotation(_)
|
||||
| CheckKind::UsePEP604Annotation
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct Fix {
|
||||
pub content: String,
|
||||
pub start: Location,
|
||||
pub end: Location,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
pub applied: bool,
|
||||
}
|
||||
|
||||
@@ -271,19 +730,16 @@ pub struct Fix {
|
||||
pub struct Check {
|
||||
pub kind: CheckKind,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
pub fix: Option<Fix>,
|
||||
}
|
||||
|
||||
static NO_QA_REGEX: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)# 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"));
|
||||
|
||||
impl Check {
|
||||
pub fn new(kind: CheckKind, location: Location) -> Self {
|
||||
pub fn new(kind: CheckKind, span: Range) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
location,
|
||||
location: span.location,
|
||||
end_location: span.end_location,
|
||||
fix: None,
|
||||
}
|
||||
}
|
||||
@@ -291,25 +747,26 @@ impl Check {
|
||||
pub fn amend(&mut self, fix: Fix) {
|
||||
self.fix = Some(fix);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_inline_ignored(&self, line: &str) -> bool {
|
||||
match NO_QA_REGEX.captures(line) {
|
||||
Some(caps) => match caps.name("codes") {
|
||||
Some(codes) => {
|
||||
for code in SPLIT_COMMA_REGEX
|
||||
.split(codes.as_str())
|
||||
.map(|code| code.trim())
|
||||
.filter(|code| !code.is_empty())
|
||||
{
|
||||
if code == self.kind.code().as_str() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
None => true,
|
||||
},
|
||||
None => false,
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::Result;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
|
||||
#[test]
|
||||
fn check_code_serialization() -> Result<()> {
|
||||
for check_code in CheckCode::iter() {
|
||||
assert!(
|
||||
CheckCode::from_str(check_code.as_ref()).is_ok(),
|
||||
"{:?} could not be round-trip serialized.",
|
||||
check_code
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
136
src/cli.rs
Normal file
136
src/cli.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::{command, Parser};
|
||||
use log::warn;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::checks::CheckCode;
|
||||
use crate::printer::SerializationFormat;
|
||||
use crate::pyproject::StrCheckCodePair;
|
||||
use crate::settings::PythonVersion;
|
||||
use crate::RawSettings;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, about = "ruff: An extremely fast Python linter.")]
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
#[arg(required = true)]
|
||||
pub files: Vec<PathBuf>,
|
||||
/// Enable verbose logging.
|
||||
#[arg(short, long)]
|
||||
pub verbose: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting errors).
|
||||
#[arg(short, long)]
|
||||
pub quiet: bool,
|
||||
/// Exit with status code "0", even upon detecting errors.
|
||||
#[arg(short, long)]
|
||||
pub exit_zero: bool,
|
||||
/// Run in watch mode by re-running whenever files change.
|
||||
#[arg(short, long)]
|
||||
pub watch: bool,
|
||||
/// Attempt to automatically fix lint errors.
|
||||
#[arg(short, long)]
|
||||
pub fix: bool,
|
||||
/// Disable cache reads.
|
||||
#[arg(short, long)]
|
||||
pub no_cache: bool,
|
||||
/// List of error codes to enable.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub select: Vec<CheckCode>,
|
||||
/// Like --select, but adds additional error codes on top of the selected ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_select: Vec<CheckCode>,
|
||||
/// List of error codes to ignore.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub ignore: Vec<CheckCode>,
|
||||
/// Like --ignore, but adds additional error codes on top of the ignored ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_ignore: Vec<CheckCode>,
|
||||
/// List of paths, used to exclude files and/or directories from checks.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub exclude: Vec<String>,
|
||||
/// Like --exclude, but adds additional files and directories on top of the excluded ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_exclude: Vec<String>,
|
||||
/// List of mappings from file pattern to code to exclude
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub per_file_ignores: Vec<StrCheckCodePair>,
|
||||
/// Output serialization format for error messages.
|
||||
#[arg(long, value_enum, default_value_t=SerializationFormat::Text)]
|
||||
pub format: SerializationFormat,
|
||||
/// See the files ruff will be run against with the current settings.
|
||||
#[arg(long)]
|
||||
pub show_files: bool,
|
||||
/// See ruff's settings.
|
||||
#[arg(long)]
|
||||
pub show_settings: bool,
|
||||
/// Enable automatic additions of noqa directives to failing lines.
|
||||
#[arg(long)]
|
||||
pub add_noqa: bool,
|
||||
/// Regular expression matching the name of dummy variables.
|
||||
#[arg(long)]
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
/// The minimum Python version that should be supported.
|
||||
#[arg(long)]
|
||||
pub target_version: Option<PythonVersion>,
|
||||
/// Round-trip auto-formatting.
|
||||
// TODO(charlie): This should be a sub-command.
|
||||
#[arg(long, hide = true)]
|
||||
pub autoformat: bool,
|
||||
}
|
||||
|
||||
pub enum Warnable {
|
||||
Select,
|
||||
ExtendSelect,
|
||||
}
|
||||
|
||||
impl fmt::Display for Warnable {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Warnable::Select => fmt.write_str("--select"),
|
||||
Warnable::ExtendSelect => fmt.write_str("--extend-select"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Warn the user if they attempt to enable a code that won't be respected.
|
||||
pub fn warn_on(
|
||||
flag: Warnable,
|
||||
codes: &Vec<CheckCode>,
|
||||
cli_ignore: &Vec<CheckCode>,
|
||||
cli_extend_ignore: &Vec<CheckCode>,
|
||||
pyproject_settings: &RawSettings,
|
||||
pyproject_path: &Option<PathBuf>,
|
||||
) {
|
||||
for code in codes {
|
||||
if !cli_ignore.is_empty() {
|
||||
if cli_ignore.contains(code) {
|
||||
warn!("{code:?} was passed to {flag}, but ignored via --ignore")
|
||||
}
|
||||
} else if pyproject_settings.ignore.contains(code) {
|
||||
if let Some(path) = pyproject_path {
|
||||
warn!(
|
||||
"{code:?} was passed to {flag}, but ignored by the `ignore` field in {}",
|
||||
path.to_string_lossy()
|
||||
)
|
||||
} else {
|
||||
warn!("{code:?} was passed to {flag}, but ignored by the default `ignore` field",)
|
||||
}
|
||||
}
|
||||
if !cli_extend_ignore.is_empty() {
|
||||
if cli_extend_ignore.contains(code) {
|
||||
warn!("{code:?} was passed to {flag}, but ignored via --extend-ignore")
|
||||
}
|
||||
} else if pyproject_settings.extend_ignore.contains(code) {
|
||||
if let Some(path) = pyproject_path {
|
||||
warn!(
|
||||
"{code:?} was passed to {flag}, but ignored by the `extend_ignore` field in {}",
|
||||
path.to_string_lossy()
|
||||
)
|
||||
} else {
|
||||
warn!("{code:?} was passed to {flag}, but ignored by the default `extend_ignore` field")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1039
src/code_gen.rs
Normal file
1039
src/code_gen.rs
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user