Compare commits
77 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aeb5df5fe | ||
|
|
7ffba7b552 | ||
|
|
06473bb1b5 | ||
|
|
bf5c048502 | ||
|
|
eaed08ae79 | ||
|
|
e0fdc4c5e8 | ||
|
|
590bec57f4 | ||
|
|
3110d342c7 | ||
|
|
39aae28eb4 | ||
|
|
dcccfe2591 | ||
|
|
38f5e8f423 | ||
|
|
74f14182ea | ||
|
|
bbc1e7804e | ||
|
|
c6320b29e4 | ||
|
|
1a90408e8c | ||
|
|
07134c50c8 | ||
|
|
b36d4a15b0 | ||
|
|
d8162ce79d | ||
|
|
e11ef54bda | ||
|
|
9a07b0623e | ||
|
|
f450e2e79d | ||
|
|
329946f162 | ||
|
|
588399e415 | ||
|
|
4523885268 | ||
|
|
de81b0cd38 | ||
|
|
4fce296e3f | ||
|
|
9d48d7bbd1 | ||
|
|
c56f263618 | ||
|
|
fb2382fbc3 | ||
|
|
c92a5a8704 | ||
|
|
d7cf3147b7 | ||
|
|
bf4d35c705 | ||
|
|
4e97e9c7cf | ||
|
|
a3fcc3b28d | ||
|
|
cfbd068dd5 | ||
|
|
8aed23fe0a | ||
|
|
c016c41c71 | ||
|
|
f1a5e53f06 | ||
|
|
1e94e0221f | ||
|
|
543865c96b | ||
|
|
b8e3f0bc13 | ||
|
|
643cedb200 | ||
|
|
91620c378a | ||
|
|
b732135795 | ||
|
|
9384a081f9 | ||
|
|
edab268d50 | ||
|
|
e4fad70a57 | ||
|
|
1a09fff991 | ||
|
|
b85105d2ec | ||
|
|
f7ac28a935 | ||
|
|
9532f342a6 | ||
|
|
0ee37aa0aa | ||
|
|
8a26c8b4e0 | ||
|
|
2cb59b0f45 | ||
|
|
2729f3d207 | ||
|
|
59155ce9f6 | ||
|
|
caf6c65de7 | ||
|
|
147d594b38 | ||
|
|
f18078a1eb | ||
|
|
fe4eb13601 | ||
|
|
161ab05533 | ||
|
|
2c537e24cc | ||
|
|
0fe349b5f8 | ||
|
|
09dc3c7225 | ||
|
|
498134b7ee | ||
|
|
0152814a00 | ||
|
|
0b3fab256b | ||
|
|
212ce4d331 | ||
|
|
491b1e4968 | ||
|
|
8b01b53d89 | ||
|
|
f9a5867d3e | ||
|
|
4149627f19 | ||
|
|
7d24146df7 | ||
|
|
1c6ef3666c | ||
|
|
16d933fcf5 | ||
|
|
a9cc56b2ac | ||
|
|
4de6c26ff9 |
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
@@ -4,7 +4,7 @@ Thank you for taking the time to report an issue! We're glad to have you involve
|
||||
If you're filing a bug report, please consider including the following information:
|
||||
|
||||
- A minimal code snippet that reproduces the bug.
|
||||
- The command you invoked (e.g., `ruff /path/to/file.py --fix`).
|
||||
- The command you invoked (e.g., `ruff /path/to/file.py --fix`), ideally including the `--isolated` flag.
|
||||
- The current Ruff settings (any relevant sections from your `pyproject.toml`).
|
||||
- The current Ruff version (`ruff --version`).
|
||||
-->
|
||||
|
||||
69
.github/workflows/ci.yaml
vendored
69
.github/workflows/ci.yaml
vendored
@@ -26,21 +26,9 @@ jobs:
|
||||
profile: minimal
|
||||
toolchain: nightly-2022-11-01
|
||||
override: true
|
||||
components: rustfmt
|
||||
- uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cargo
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- run: cargo build --all --release
|
||||
- run: ./target/release/ruff_dev generate-all
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: cargo build --all
|
||||
- run: ./target/debug/ruff_dev generate-all
|
||||
- run: git diff --quiet README.md || echo "::error file=README.md::This file is outdated. Run 'cargo +nightly dev generate-all'."
|
||||
- run: git diff --quiet ruff.schema.json || echo "::error file=ruff.schema.json::This file is outdated. Run 'cargo +nightly dev generate-all'."
|
||||
- run: git diff --exit-code -- README.md ruff.schema.json
|
||||
@@ -56,18 +44,6 @@ jobs:
|
||||
toolchain: nightly-2022-11-01
|
||||
override: true
|
||||
components: rustfmt
|
||||
- uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cargo
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- run: cargo fmt --all --check
|
||||
|
||||
cargo_clippy:
|
||||
@@ -82,18 +58,7 @@ jobs:
|
||||
override: true
|
||||
components: clippy
|
||||
target: wasm32-unknown-unknown
|
||||
- uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cargo
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::pedantic
|
||||
- run: cargo clippy --workspace --target wasm32-unknown-unknown --all-features -- -D warnings -W clippy::pedantic
|
||||
|
||||
@@ -107,18 +72,7 @@ jobs:
|
||||
profile: minimal
|
||||
toolchain: nightly-2022-11-01
|
||||
override: true
|
||||
- uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cargo
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- run: cargo install cargo-insta
|
||||
- run: pip install black[d]==22.12.0
|
||||
- name: Run tests
|
||||
@@ -167,22 +121,11 @@ jobs:
|
||||
profile: minimal
|
||||
toolchain: nightly-2022-11-01
|
||||
override: true
|
||||
- uses: Swatinem/rust-cache@v1
|
||||
- uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- run: pip install maturin
|
||||
- uses: actions/cache@v3
|
||||
env:
|
||||
cache-name: cache-cargo
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ env.cache-name }}-
|
||||
${{ runner.os }}-build-
|
||||
${{ runner.os }}-
|
||||
- run: maturin build -b bin
|
||||
|
||||
typos:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: v0.0.214
|
||||
rev: v0.0.220
|
||||
hooks:
|
||||
- id: ruff
|
||||
|
||||
|
||||
@@ -56,9 +56,9 @@ prior to merging.
|
||||
|
||||
There are four phases to adding a new lint rule:
|
||||
|
||||
1. Define the violation in `src/violations.rs` (e.g., `ModuleImportNotAtTopOfFile`).
|
||||
2. Map the violation to a code in `src/registry.rs` (e.g., `E402`).
|
||||
3. Define the _logic_ for triggering the violation in `src/checkers/ast.rs` (for AST-based checks),
|
||||
1. Define the violation struct in `src/violations.rs` (e.g., `ModuleImportNotAtTopOfFile`).
|
||||
2. Map the violation struct to a rule code in `src/registry.rs` (e.g., `E402`).
|
||||
3. Define the logic for triggering the violation in `src/checkers/ast.rs` (for AST-based checks),
|
||||
`src/checkers/tokens.rs` (for token-based checks), or `src/checkers/lines.rs` (for text-based checks).
|
||||
4. Add a test fixture.
|
||||
5. Update the generated files (documentation and generated code).
|
||||
@@ -74,15 +74,16 @@ collecting diagnostics as it goes.
|
||||
If you need to inspect the AST, you can run `cargo +nightly dev print-ast` with a Python file. 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/[plugin-name]`, named to match
|
||||
the code 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.
|
||||
To add a test fixture, create a file under `resources/test/fixtures/[origin]`, named to match
|
||||
the code you defined earlier (e.g., `resources/test/fixtures/pycodestyle/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 `cargo +nightly dev generate-all` to generate the code for your new fixture. Then run Ruff
|
||||
locally with (e.g.) `cargo run resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
|
||||
|
||||
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
|
||||
`test_case` macro in the relevant `src/[plugin-name]/mod.rs` file. Then, run `cargo test --all`.
|
||||
`test_case` macro in the relevant `src/[origin]/mod.rs` file. Then, run `cargo test --all`.
|
||||
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.
|
||||
|
||||
9
Cargo.lock
generated
9
Cargo.lock
generated
@@ -735,10 +735,11 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.214-dev.0"
|
||||
version = "0.0.220-dev.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.32",
|
||||
"colored",
|
||||
"configparser",
|
||||
"once_cell",
|
||||
"regex",
|
||||
@@ -1873,7 +1874,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
dependencies = [
|
||||
"annotate-snippets 0.9.1",
|
||||
"anyhow",
|
||||
@@ -1941,7 +1942,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap 4.0.32",
|
||||
@@ -1961,7 +1962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
|
||||
@@ -6,7 +6,7 @@ members = [
|
||||
|
||||
[package]
|
||||
name = "ruff"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
|
||||
edition = "2021"
|
||||
rust-version = "1.65.0"
|
||||
@@ -19,6 +19,7 @@ license = "MIT"
|
||||
[lib]
|
||||
name = "ruff"
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
||||
@@ -29,7 +30,7 @@ bitflags = { version = "1.3.2" }
|
||||
cachedir = { version = "0.3.0" }
|
||||
cfg-if = { version = "1.0.0" }
|
||||
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
|
||||
clap = { version = "4.0.1", features = ["derive"] }
|
||||
clap = { version = "4.0.1", features = ["derive", "env"] }
|
||||
clap_complete_command = { version = "0.4.0" }
|
||||
colored = { version = "2.0.0" }
|
||||
dirs = { version = "4.0.0" }
|
||||
@@ -51,7 +52,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
|
||||
quick-junit = { version = "0.3.2" }
|
||||
regex = { version = "1.6.0" }
|
||||
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
|
||||
ruff_macros = { version = "0.0.214", path = "ruff_macros" }
|
||||
ruff_macros = { version = "0.0.220", path = "ruff_macros" }
|
||||
rustc-hash = { version = "1.1.0" }
|
||||
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
|
||||
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "d532160333ffeb6dbeca2c2728c2391cd1e53b7f" }
|
||||
|
||||
375
README.md
375
README.md
@@ -46,7 +46,9 @@ imports, and more.
|
||||
|
||||
Ruff is extremely actively developed and used in major open-source projects like:
|
||||
|
||||
- [pandas](https://github.com/pandas-dev/pandas)
|
||||
- [FastAPI](https://github.com/tiangolo/fastapi)
|
||||
- [Apache Airflow](https://github.com/apache/airflow)
|
||||
- [Bokeh](https://github.com/bokeh/bokeh)
|
||||
- [Zulip](https://github.com/zulip/zulip)
|
||||
- [Pydantic](https://github.com/pydantic/pydantic)
|
||||
@@ -164,9 +166,9 @@ pacman -S ruff
|
||||
To run Ruff, try any of the following:
|
||||
|
||||
```shell
|
||||
ruff path/to/code/to/check.py # Run Ruff over `check.py`
|
||||
ruff path/to/code/ # Run Ruff over all files in `/path/to/code` (and any subdirectories)
|
||||
ruff path/to/code/*.py # Run Ruff over all `.py` files in `/path/to/code`
|
||||
ruff path/to/code/to/lint.py # Run Ruff over `lint.py`
|
||||
ruff path/to/code/ # Run Ruff over all files in `/path/to/code` (and any subdirectories)
|
||||
ruff path/to/code/*.py # Run Ruff over all `.py` files in `/path/to/code`
|
||||
```
|
||||
|
||||
You can run Ruff in `--watch` mode to automatically re-run on-change:
|
||||
@@ -180,7 +182,7 @@ Ruff also works with [pre-commit](https://pre-commit.com):
|
||||
```yaml
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: 'v0.0.214'
|
||||
rev: 'v0.0.220'
|
||||
hooks:
|
||||
- id: ruff
|
||||
# Respect `exclude` and `extend-exclude` settings.
|
||||
@@ -237,9 +239,9 @@ target-version = "py310"
|
||||
max-complexity = 10
|
||||
```
|
||||
|
||||
As an example, the following would configure Ruff to: (1) avoid checking for line-length
|
||||
violations (`E501`); (2) never remove unused imports (`F401`); and (3) ignore import-at-top-of-file
|
||||
errors (`E402`) in `__init__.py` files:
|
||||
As an example, the following would configure Ruff to: (1) avoid enforcing line-length violations
|
||||
(`E501`); (2) never remove unused imports (`F401`); and (3) ignore import-at-top-of-file violations
|
||||
(`E402`) in `__init__.py` files:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
@@ -269,16 +271,16 @@ select = ["E", "F", "Q"]
|
||||
docstring-quotes = "double"
|
||||
```
|
||||
|
||||
Ruff mirrors Flake8's error code system, in which each error code consists of a one-to-three letter
|
||||
prefix, followed by three digits (e.g., `F401`). The prefix indicates that "source" of the error
|
||||
code (e.g., `F` for Pyflakes, `E` for `pycodestyle`, `ANN` for `flake8-annotations`). The set of
|
||||
enabled errors is determined by the `select` and `ignore` options, which support both the full
|
||||
error code (e.g., `F401`) and the prefix (e.g., `F`).
|
||||
Ruff mirrors Flake8's rule code system, in which each rule code consists of a one-to-three letter
|
||||
prefix, followed by three digits (e.g., `F401`). The prefix indicates that "source" of the rule
|
||||
(e.g., `F` for Pyflakes, `E` for `pycodestyle`, `ANN` for `flake8-annotations`). The set of enabled
|
||||
rules is determined by the `select` and `ignore` options, which support both the full code (e.g.,
|
||||
`F401`) and the prefix (e.g., `F`).
|
||||
|
||||
As a special-case, Ruff also supports the `ALL` error code, which enables all error codes. Note that
|
||||
some of the `pydocstyle` error codes are conflicting (e.g., `D203` and `D211`) as they represent
|
||||
alternative docstring formats. Enabling `ALL` without further configuration may result in suboptimal
|
||||
behavior, especially for the `pydocstyle` plugin.
|
||||
As a special-case, Ruff also supports the `ALL` code, which enables all rules. Note that some of the
|
||||
`pydocstyle` rules conflict (e.g., `D203` and `D211`) as they represent alternative docstring
|
||||
formats. Enabling `ALL` without further configuration may result in suboptimal behavior, especially
|
||||
for the `pydocstyle` plugin.
|
||||
|
||||
As an alternative to `pyproject.toml`, Ruff will also respect a `ruff.toml` file, which implements
|
||||
an equivalent schema (though the `[tool.ruff]` hierarchy can be omitted). For example, the
|
||||
@@ -326,45 +328,47 @@ Options:
|
||||
-v, --verbose
|
||||
Enable verbose logging
|
||||
-q, --quiet
|
||||
Only log errors
|
||||
Print lint violations, but nothing else
|
||||
-s, --silent
|
||||
Disable all logging (but still exit with status code "1" upon detecting errors)
|
||||
Disable all logging (but still exit with status code "1" upon detecting lint violations)
|
||||
-e, --exit-zero
|
||||
Exit with status code "0", even upon detecting errors
|
||||
Exit with status code "0", even upon detecting lint violations
|
||||
-w, --watch
|
||||
Run in watch mode by re-running whenever files change
|
||||
--fix
|
||||
Attempt to automatically fix lint errors
|
||||
Attempt to automatically fix lint violations
|
||||
--fix-only
|
||||
Fix any fixable lint errors, but don't report on leftover violations. Implies `--fix`
|
||||
Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
|
||||
--diff
|
||||
Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
|
||||
-n, --no-cache
|
||||
Disable cache reads
|
||||
--select <SELECT>
|
||||
Comma-separated list of error codes to enable (or ALL, to enable all checks)
|
||||
--extend-select <EXTEND_SELECT>
|
||||
Like --select, but adds additional error codes on top of the selected ones
|
||||
--ignore <IGNORE>
|
||||
Comma-separated list of error codes to disable
|
||||
--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
|
||||
--fixable <FIXABLE>
|
||||
List of error codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
|
||||
--unfixable <UNFIXABLE>
|
||||
List of error codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
|
||||
--isolated
|
||||
Ignore all configuration files
|
||||
--select <RULE_CODE>
|
||||
Comma-separated list of rule codes to enable (or ALL, to enable all rules)
|
||||
--extend-select <RULE_CODE>
|
||||
Like --select, but adds additional rule codes on top of the selected ones
|
||||
--ignore <RULE_CODE>
|
||||
Comma-separated list of rule codes to disable
|
||||
--extend-ignore <RULE_CODE>
|
||||
Like --ignore, but adds additional rule codes on top of the ignored ones
|
||||
--exclude <FILE_PATTERN>
|
||||
List of paths, used to omit files and/or directories from analysis
|
||||
--extend-exclude <FILE_PATTERN>
|
||||
Like --exclude, but adds additional files and directories on top of those already excluded
|
||||
--fixable <RULE_CODE>
|
||||
List of rule codes to treat as eligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
|
||||
--unfixable <RULE_CODE>
|
||||
List of rule codes to treat as ineligible for autofix. Only applicable when autofix itself is enabled (e.g., via `--fix`)
|
||||
--per-file-ignores <PER_FILE_IGNORES>
|
||||
List of mappings from file pattern to code to exclude
|
||||
--format <FORMAT>
|
||||
Output serialization format for error messages [possible values: text, json, junit, grouped, github, gitlab]
|
||||
Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab]
|
||||
--stdin-filename <STDIN_FILENAME>
|
||||
The name of the file when passing it through stdin
|
||||
--cache-dir <CACHE_DIR>
|
||||
Path to the cache directory
|
||||
Path to the cache directory [env: RUFF_CACHE_DIR=]
|
||||
--show-source
|
||||
Show violations with source code
|
||||
--respect-gitignore
|
||||
@@ -378,7 +382,7 @@ Options:
|
||||
--target-version <TARGET_VERSION>
|
||||
The minimum Python version that should be supported
|
||||
--line-length <LINE_LENGTH>
|
||||
Set the line-length for length-associated checks and automatic formatting
|
||||
Set the line-length for length-associated rules and automatic formatting
|
||||
--max-complexity <MAX_COMPLEXITY>
|
||||
Maximum McCabe complexity allowed for a given function
|
||||
--add-noqa
|
||||
@@ -390,7 +394,7 @@ Options:
|
||||
--show-files
|
||||
See the files Ruff will be run against with the current settings
|
||||
--show-settings
|
||||
See the settings Ruff will use to check a given Python file
|
||||
See the settings Ruff will use to lint a given Python file
|
||||
-h, --help
|
||||
Print help information
|
||||
-V, --version
|
||||
@@ -447,16 +451,16 @@ in each directory's `pyproject.toml` file.
|
||||
By default, Ruff will also skip any files that are omitted via `.ignore`, `.gitignore`,
|
||||
`.git/info/exclude`, and global `gitignore` files (see: [`respect-gitignore`](#respect-gitignore)).
|
||||
|
||||
Files that are passed to `ruff` directly are always checked, regardless of the above criteria.
|
||||
For example, `ruff /path/to/excluded/file.py` will always check `file.py`.
|
||||
Files that are passed to `ruff` directly are always linted, regardless of the above criteria.
|
||||
For example, `ruff /path/to/excluded/file.py` will always lint `file.py`.
|
||||
|
||||
### Ignoring errors
|
||||
|
||||
To omit a lint check entirely, add it to the "ignore" list via [`ignore`](#ignore) or
|
||||
To omit a lint rule entirely, add it to the "ignore" list via [`ignore`](#ignore) or
|
||||
[`extend-ignore`](#extend-ignore), either on the command-line or in your `pyproject.toml` file.
|
||||
|
||||
To ignore an error inline, 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:
|
||||
To ignore a violation inline, Ruff uses a `noqa` system similar to [Flake8](https://flake8.pycqa.org/en/3.1.1/user/ignoring-errors.html).
|
||||
To ignore an individual violation, add `# noqa: {code}` to the end of the line, like so:
|
||||
|
||||
```python
|
||||
# Ignore F841.
|
||||
@@ -465,7 +469,7 @@ x = 1 # noqa: F841
|
||||
# Ignore E741 and F841.
|
||||
i = 1 # noqa: E741, F841
|
||||
|
||||
# Ignore _all_ errors.
|
||||
# Ignore _all_ violations.
|
||||
x = 1 # noqa
|
||||
```
|
||||
|
||||
@@ -479,9 +483,9 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
|
||||
""" # noqa: E501
|
||||
```
|
||||
|
||||
To ignore all errors across an entire file, Ruff supports Flake8's `# flake8: noqa` directive (or,
|
||||
equivalently, `# ruff: noqa`). Adding either of those directives to any part of a file will disable
|
||||
error reporting for the entire file.
|
||||
To ignore all violations across an entire file, Ruff supports Flake8's `# flake8: noqa` directive
|
||||
(or, equivalently, `# ruff: noqa`). Adding either of those directives to any part of a file will
|
||||
disable enforcement across the entire file.
|
||||
|
||||
For targeted exclusions across entire files (e.g., "Ignore all F841 violations in
|
||||
`/path/to/file.py`"), see the [`per-file-ignores`](#per-file-ignores) configuration setting.
|
||||
@@ -500,8 +504,8 @@ for more.
|
||||
|
||||
Ruff supports several workflows to aid in `noqa` management.
|
||||
|
||||
First, Ruff provides a special error code, `RUF100`, to enforce that your `noqa` directives are
|
||||
"valid", in that the errors they _say_ they ignore are actually being triggered on that line (and
|
||||
First, Ruff provides a special rule code, `RUF100`, to enforce that your `noqa` directives are
|
||||
"valid", in that the violations they _say_ they ignore are actually being triggered on that line (and
|
||||
thus suppressed). You can run `ruff /path/to/file.py --extend-select RUF100` to flag unused `noqa`
|
||||
directives.
|
||||
|
||||
@@ -511,13 +515,13 @@ You can run `ruff /path/to/file.py --extend-select RUF100 --fix` to automaticall
|
||||
|
||||
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.
|
||||
add `noqa` directives to all failing lines, with the appropriate rule codes.
|
||||
|
||||
## Supported Rules
|
||||
|
||||
Regardless of the rule's origin, Ruff re-implements every rule in Rust as a first-party feature.
|
||||
|
||||
By default, Ruff enables all `E` and `F` error codes, which correspond to those built-in to Flake8.
|
||||
By default, Ruff enables all `E` and `F` rule codes, which correspond to those built-in to Flake8.
|
||||
|
||||
The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` command-line option.
|
||||
|
||||
@@ -551,8 +555,8 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
|
||||
| F524 | StringDotFormatMissingArguments | '...'.format(...) is missing argument(s) for placeholder(s): ... | |
|
||||
| F525 | StringDotFormatMixingAutomatic | '...'.format(...) mixes automatic and manual numbering | |
|
||||
| F541 | FStringMissingPlaceholders | f-string without any placeholders | 🛠 |
|
||||
| F601 | MultiValueRepeatedKeyLiteral | Dictionary key literal repeated | |
|
||||
| F602 | MultiValueRepeatedKeyVariable | Dictionary key `...` repeated | |
|
||||
| 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` | |
|
||||
@@ -595,6 +599,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
|
||||
| E902 | IOError | IOError: `...` | |
|
||||
| E999 | SyntaxError | SyntaxError: `...` | |
|
||||
| W292 | NoNewLineAtEndOfFile | No newline at end of file | 🛠 |
|
||||
| W505 | DocLineTooLong | Doc line too long (89 > 88 characters) | |
|
||||
| W605 | InvalidEscapeSequence | Invalid escape sequence: '\c' | 🛠 |
|
||||
|
||||
### mccabe (C90)
|
||||
@@ -612,6 +617,7 @@ For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| I001 | UnsortedImports | Import block is un-sorted or un-formatted | 🛠 |
|
||||
| I002 | MissingRequiredImport | Missing required import: `from __future__ import ...` | 🛠 |
|
||||
|
||||
### pydocstyle (D)
|
||||
|
||||
@@ -699,6 +705,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
|
||||
| UP027 | RewriteListComprehension | Replace unpacked list comprehension with a generator expression | 🛠 |
|
||||
| UP028 | RewriteYieldFrom | Replace `yield` over `for` loop with `yield from` | 🛠 |
|
||||
| UP029 | UnnecessaryBuiltinImport | Unnecessary builtin import: `...` | 🛠 |
|
||||
| UP030 | FormatLiterals | Use implicit references for positional format fields | 🛠 |
|
||||
|
||||
### pep8-naming (N)
|
||||
|
||||
@@ -775,6 +782,9 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on
|
||||
| S324 | HashlibInsecureHashFunction | Probable use of insecure hash functions in `hashlib`: "..." | |
|
||||
| S501 | RequestWithNoCertValidation | Probable use of `...` call with `verify=False` disabling SSL certificate checks | |
|
||||
| S506 | UnsafeYAMLLoad | Probable use of unsafe `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
|
||||
| S508 | SnmpInsecureVersion | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
|
||||
| S509 | SnmpWeakCryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
|
||||
| S701 | Jinja2AutoescapeFalse | By default, jinja2 sets `autoescape` to `False`. Consider using `autoescape=True` or the `select_autoescape` function to mitigate XSS vulnerabilities. | |
|
||||
|
||||
### flake8-blind-except (BLE)
|
||||
|
||||
@@ -933,8 +943,8 @@ For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style
|
||||
| PT021 | FixtureFinalizerCallback | Use `yield` instead of `request.addfinalizer` | |
|
||||
| PT022 | UselessYieldFixture | No teardown in fixture `...`, use `return` instead of `yield` | 🛠 |
|
||||
| PT023 | IncorrectMarkParenthesesStyle | Use `@pytest.mark....` over `@pytest.mark....()` | 🛠 |
|
||||
| PT024 | UnnecessaryAsyncioMarkOnFixture | `pytest.mark.asyncio` is unnecessary for fixtures | |
|
||||
| PT025 | ErroneousUseFixturesOnFixture | `pytest.mark.usefixtures` has no effect on fixtures | |
|
||||
| PT024 | UnnecessaryAsyncioMarkOnFixture | `pytest.mark.asyncio` is unnecessary for fixtures | 🛠 |
|
||||
| PT025 | ErroneousUseFixturesOnFixture | `pytest.mark.usefixtures` has no effect on fixtures | 🛠 |
|
||||
| PT026 | UseFixturesWithoutParameters | Useless `pytest.mark.usefixtures` without parameters | 🛠 |
|
||||
|
||||
### flake8-quotes (Q)
|
||||
@@ -943,10 +953,10 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/3.3.1/) on
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | |
|
||||
| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | |
|
||||
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | |
|
||||
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | |
|
||||
| Q000 | BadQuotesInlineString | Single quotes found but double quotes preferred | 🛠 |
|
||||
| Q001 | BadQuotesMultilineString | Single quote multiline found but double quotes preferred | 🛠 |
|
||||
| Q002 | BadQuotesDocstring | Single quote docstring found but double quotes preferred | 🛠 |
|
||||
| Q003 | AvoidQuoteEscape | Change outer quotes to avoid escaping inner quotes | 🛠 |
|
||||
|
||||
### flake8-return (RET)
|
||||
|
||||
@@ -969,6 +979,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
|
||||
|
||||
| Code | Name | Message | Fix |
|
||||
| ---- | ---- | ------- | --- |
|
||||
| SIM115 | OpenFileWithContextHandler | Use context handler for opening files | |
|
||||
| SIM101 | DuplicateIsinstanceCall | Multiple `isinstance` calls for `...`, merge into a single call | 🛠 |
|
||||
| SIM102 | NestedIfStatements | Use a single `if` statement instead of nested `if` statements | |
|
||||
| SIM103 | ReturnBoolConditionDirectly | Return the condition `...` directly | 🛠 |
|
||||
@@ -978,6 +989,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
|
||||
| SIM109 | CompareWithTuple | Use `value in (..., ...)` instead of `value == ... or value == ...` | 🛠 |
|
||||
| SIM110 | ConvertLoopToAny | Use `return any(x for x in y)` instead of `for` loop | 🛠 |
|
||||
| SIM111 | ConvertLoopToAll | Use `return all(x for x in y)` instead of `for` loop | 🛠 |
|
||||
| SIM112 | UseCapitalEnvironmentVariables | Use capitalized environment variable `...` instead of `...` | 🛠 |
|
||||
| SIM117 | MultipleWithStatements | Use a single `with` statement with multiple contexts instead of nested `with` statements | |
|
||||
| SIM118 | KeyInDict | Use `key in dict` instead of `key in dict.keys()` | 🛠 |
|
||||
| SIM201 | NegateEqualOp | Use `left != right` instead of `not left == right` | 🛠 |
|
||||
@@ -991,6 +1003,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
|
||||
| SIM222 | OrTrue | Use `True` instead of `... or True` | 🛠 |
|
||||
| SIM223 | AndFalse | Use `False` instead of `... and False` | 🛠 |
|
||||
| SIM300 | YodaConditions | Yoda conditions are discouraged, use `left == right` instead | 🛠 |
|
||||
| SIM401 | DictGetWithDefault | Use `var = dict.get(key, "default")` instead of an `if` block | 🛠 |
|
||||
|
||||
### flake8-tidy-imports (TID)
|
||||
|
||||
@@ -1064,8 +1077,8 @@ For more, see [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) on GitH
|
||||
| ---- | ---- | ------- | --- |
|
||||
| PGH001 | NoEval | No builtin `eval()` allowed | |
|
||||
| PGH002 | DeprecatedLogWarn | `warn` is deprecated in favor of `warning` | |
|
||||
| PGH003 | BlanketTypeIgnore | Use specific error codes when ignoring type issues | |
|
||||
| PGH004 | BlanketNOQA | Use specific error codes when using `noqa` | |
|
||||
| PGH003 | BlanketTypeIgnore | Use specific rule codes when ignoring type issues | |
|
||||
| PGH004 | BlanketNOQA | Use specific rule codes when using `noqa` | |
|
||||
|
||||
### Pylint (PLC, PLE, PLR, PLW)
|
||||
|
||||
@@ -1391,7 +1404,7 @@ natively, including:
|
||||
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) ([#827](https://github.com/charliermarsh/ruff/issues/827))
|
||||
- [`yesqa`](https://github.com/asottile/yesqa)
|
||||
|
||||
Note that, in some cases, Ruff uses different error code prefixes than would be found in the
|
||||
Note that, in some cases, Ruff uses different rule codes and prefixes than would be found in the
|
||||
originating Flake8 plugins. For example, Ruff uses `TID252` to represent the `I252` rule from
|
||||
`flake8-tidy-imports`. This helps minimize conflicts across plugins and allows any individual plugin
|
||||
to be toggled on or off with a single (e.g.) `--select TID`, as opposed to `--select I2` (to avoid
|
||||
@@ -1416,9 +1429,9 @@ At time of writing, Pylint implements 409 total rules, while Ruff implements 224
|
||||
at least 60 overlap with the Pylint rule set. Subjectively, Pylint tends to implement more rules
|
||||
based on type inference (e.g., validating the number of arguments in a function call).
|
||||
|
||||
Like Flake8, Pylint supports plugins (called "checkers"), while Ruff implements all checks natively.
|
||||
Like Flake8, Pylint supports plugins (called "checkers"), while Ruff implements all rules natively.
|
||||
|
||||
Unlike Pylint, Ruff is capable of automatically fixing its own lint errors.
|
||||
Unlike Pylint, Ruff is capable of automatically fixing its own lint violations.
|
||||
|
||||
Pylint parity is being tracked in [#689](https://github.com/charliermarsh/ruff/issues/689).
|
||||
|
||||
@@ -1531,7 +1544,7 @@ For example, if you're coming from `flake8-docstrings`, and your originating con
|
||||
`--docstring-convention=numpy`, you'd instead set `convention = "numpy"` in your `pyproject.toml`,
|
||||
as above.
|
||||
|
||||
Alongside `convention`, you'll want to explicitly enable the `D` error code class, like so:
|
||||
Alongside `convention`, you'll want to explicitly enable the `D` rule code prefix, like so:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
@@ -1737,6 +1750,24 @@ allowed-confusables = ["−", "ρ", "∗"]
|
||||
|
||||
---
|
||||
|
||||
#### [`builtins`](#builtins)
|
||||
|
||||
A list of builtins to treat as defined references, in addition to the
|
||||
system builtins.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<String>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
builtins = ["_"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`cache-dir`](#cache-dir)
|
||||
|
||||
A path to the cache directory.
|
||||
@@ -1766,7 +1797,7 @@ cache-dir = "~/.cache/ruff"
|
||||
#### [`dummy-variable-rgx`](#dummy-variable-rgx)
|
||||
|
||||
A regular expression used to identify "dummy" variables, or those which
|
||||
should be ignored when evaluating (e.g.) unused-variable checks. The
|
||||
should be ignored when enforcing (e.g.) unused-variable rules. The
|
||||
default expression matches `_`, `__`, and `_var`, but not `_var_`.
|
||||
|
||||
**Default value**: `"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"`
|
||||
@@ -1797,6 +1828,8 @@ Exclusions are based on globs, and can be either:
|
||||
`directory`). Note that these paths are relative to the project root
|
||||
(e.g., the directory containing your `pyproject.toml`).
|
||||
|
||||
For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
|
||||
|
||||
Note that you'll typically want to use
|
||||
[`extend-exclude`](#extend-exclude) to modify the excluded paths.
|
||||
|
||||
@@ -1844,6 +1877,18 @@ line-length = 100
|
||||
A list of file patterns to omit from linting, in addition to those
|
||||
specified by `exclude`.
|
||||
|
||||
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`).
|
||||
|
||||
For more information on the glob syntax, refer to the [`globset` documentation](https://docs.rs/globset/latest/globset/#syntax).
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<FilePattern>`
|
||||
@@ -1860,18 +1905,18 @@ extend-exclude = ["tests", "src/bad.py"]
|
||||
|
||||
#### [`extend-ignore`](#extend-ignore)
|
||||
|
||||
A list of check code prefixes to ignore, in addition to those specified
|
||||
by `ignore`.
|
||||
A list of rule codes or prefixes to ignore, in addition to those
|
||||
specified by `ignore`.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<CheckCodePrefix>`
|
||||
**Type**: `Vec<RuleCodePrefix>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
# Skip unused variable checks (`F841`).
|
||||
# Skip unused variable rules (`F841`).
|
||||
extend-ignore = ["F841"]
|
||||
```
|
||||
|
||||
@@ -1879,12 +1924,12 @@ extend-ignore = ["F841"]
|
||||
|
||||
#### [`extend-select`](#extend-select)
|
||||
|
||||
A list of check code prefixes to enable, in addition to those specified
|
||||
by `select`.
|
||||
A list of rule codes or prefixes to enable, in addition to those
|
||||
specified by `select`.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<CheckCodePrefix>`
|
||||
**Type**: `Vec<RuleCodePrefix>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
@@ -1898,10 +1943,10 @@ extend-select = ["B", "Q"]
|
||||
|
||||
#### [`external`](#external)
|
||||
|
||||
A list of check codes that are unsupported by Ruff, but should be
|
||||
A list of rule codes that are unsupported by Ruff, but should be
|
||||
preserved when (e.g.) validating `# noqa` directives. Useful for
|
||||
retaining `# noqa` directives that cover plugins not yet implemented
|
||||
in Ruff.
|
||||
by Ruff.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
@@ -1955,17 +2000,17 @@ fix-only = true
|
||||
|
||||
#### [`fixable`](#fixable)
|
||||
|
||||
A list of check code prefixes to consider autofix-able.
|
||||
A list of rule codes or prefixes to consider autofixable.
|
||||
|
||||
**Default value**: `["A", "ANN", "ARG", "B", "BLE", "C", "D", "E", "ERA", "F", "FBT", "I", "ICN", "N", "PGH", "PLC", "PLE", "PLR", "PLW", "Q", "RET", "RUF", "S", "T", "TID", "UP", "W", "YTT"]`
|
||||
|
||||
**Type**: `Vec<CheckCodePrefix>`
|
||||
**Type**: `Vec<RuleCodePrefix>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
# Only allow autofix behavior for `E` and `F` checks.
|
||||
# Only allow autofix behavior for `E` and `F` rules.
|
||||
fixable = ["E", "F"]
|
||||
```
|
||||
|
||||
@@ -2021,23 +2066,23 @@ format = "grouped"
|
||||
|
||||
#### [`ignore`](#ignore)
|
||||
|
||||
A list of check code prefixes to ignore. Prefixes can specify exact
|
||||
checks (like `F841`), entire categories (like `F`), or anything in
|
||||
A list of rule codes or prefixes to ignore. Prefixes can specify exact
|
||||
rules (like `F841`), entire categories (like `F`), or anything in
|
||||
between.
|
||||
|
||||
When breaking ties between enabled and disabled checks (via `select` and
|
||||
When breaking ties between enabled and disabled rules (via `select` and
|
||||
`ignore`, respectively), more specific prefixes override less
|
||||
specific prefixes.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<CheckCodePrefix>`
|
||||
**Type**: `Vec<RuleCodePrefix>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
# Skip unused variable checks (`F841`).
|
||||
# Skip unused variable rules (`F841`).
|
||||
ignore = ["F841"]
|
||||
```
|
||||
|
||||
@@ -2085,12 +2130,12 @@ line-length = 120
|
||||
|
||||
#### [`per-file-ignores`](#per-file-ignores)
|
||||
|
||||
A list of mappings from file pattern to check code prefixes to exclude,
|
||||
when considering any matching files.
|
||||
A list of mappings from file pattern to rule codes or prefixes to
|
||||
exclude, when considering any matching files.
|
||||
|
||||
**Default value**: `{}`
|
||||
|
||||
**Type**: `HashMap<String, Vec<CheckCodePrefix>>`
|
||||
**Type**: `HashMap<String, Vec<RuleCodePrefix>>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
@@ -2144,17 +2189,17 @@ respect_gitignore = false
|
||||
|
||||
#### [`select`](#select)
|
||||
|
||||
A list of check code prefixes to enable. Prefixes can specify exact
|
||||
checks (like `F841`), entire categories (like `F`), or anything in
|
||||
A list of rule codes or prefixes to enable. Prefixes can specify exact
|
||||
rules (like `F841`), entire categories (like `F`), or anything in
|
||||
between.
|
||||
|
||||
When breaking ties between enabled and disabled checks (via `select` and
|
||||
When breaking ties between enabled and disabled rules (via `select` and
|
||||
`ignore`, respectively), more specific prefixes override less
|
||||
specific prefixes.
|
||||
|
||||
**Default value**: `["E", "F"]`
|
||||
|
||||
**Type**: `Vec<CheckCodePrefix>`
|
||||
**Type**: `Vec<RuleCodePrefix>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
@@ -2168,8 +2213,8 @@ select = ["E", "F", "B", "Q"]
|
||||
|
||||
#### [`show-source`](#show-source)
|
||||
|
||||
Whether to show source code snippets when reporting lint error
|
||||
violations (overridden by the `--show-source` command-line flag).
|
||||
Whether to show source code snippets when reporting lint violations
|
||||
(overridden by the `--show-source` command-line flag).
|
||||
|
||||
**Default value**: `false`
|
||||
|
||||
@@ -2252,7 +2297,7 @@ target-version = "py37"
|
||||
A list of task tags to recognize (e.g., "TODO", "FIXME", "XXX").
|
||||
|
||||
Comments starting with these tags will be ignored by commented-out code
|
||||
detection (`ERA`), and skipped by line-length checks (`E501`) if
|
||||
detection (`ERA`), and skipped by line-length rules (`E501`) if
|
||||
`ignore-overlong-task-comments` is set to `true`.
|
||||
|
||||
**Default value**: `["TODO", "FIXME", "XXX"]`
|
||||
@@ -2268,13 +2313,37 @@ task-tags = ["HACK"]
|
||||
|
||||
---
|
||||
|
||||
#### [`unfixable`](#unfixable)
|
||||
#### [`typing-modules`](#typing-modules)
|
||||
|
||||
A list of check code prefixes to consider un-autofix-able.
|
||||
A list of modules whose imports should be treated equivalently to
|
||||
members of the `typing` module.
|
||||
|
||||
This is useful for ensuring proper type annotation inference for
|
||||
projects that re-export `typing` and `typing_extensions` members
|
||||
from a compatibility module. If omitted, any members imported from
|
||||
modules apart from `typing` and `typing_extensions` will be treated
|
||||
as ordinary Python objects.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<CheckCodePrefix>`
|
||||
**Type**: `Vec<String>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
typing-modules = ["airflow.typing_compat"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`unfixable`](#unfixable)
|
||||
|
||||
A list of rule codes or prefixes to consider non-autofix-able.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<RuleCodePrefix>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
@@ -2291,7 +2360,7 @@ unfixable = ["F401"]
|
||||
Enable or disable automatic update checks (overridden by the
|
||||
`--update-check` and `--no-update-check` command-line flags).
|
||||
|
||||
**Default value**: `true`
|
||||
**Default value**: `false`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
@@ -2299,7 +2368,7 @@ Enable or disable automatic update checks (overridden by the
|
||||
|
||||
```toml
|
||||
[tool.ruff]
|
||||
update-check = false
|
||||
update-check = true
|
||||
```
|
||||
|
||||
---
|
||||
@@ -2344,7 +2413,7 @@ mypy-init-return = true
|
||||
|
||||
#### [`suppress-dummy-args`](#suppress-dummy-args)
|
||||
|
||||
Whether to suppress `ANN000`-level errors for arguments matching the
|
||||
Whether to suppress `ANN000`-level violations for arguments matching the
|
||||
"dummy" variable regex (like `_`).
|
||||
|
||||
**Default value**: `false`
|
||||
@@ -2362,8 +2431,8 @@ suppress-dummy-args = true
|
||||
|
||||
#### [`suppress-none-returning`](#suppress-none-returning)
|
||||
|
||||
Whether to suppress `ANN200`-level errors for functions that meet either
|
||||
of the following criteria:
|
||||
Whether to suppress `ANN200`-level violations for functions that meet
|
||||
either of the following criteria:
|
||||
|
||||
- Contain no `return` statement.
|
||||
- Explicit `return` statement(s) all return `None` (explicitly or
|
||||
@@ -2424,7 +2493,7 @@ extend-hardcoded-tmp-directory = ["/foo/bar"]
|
||||
#### [`extend-immutable-calls`](#extend-immutable-calls)
|
||||
|
||||
Additional callable functions to consider "immutable" when evaluating,
|
||||
e.g., `no-mutable-default-argument` checks (`B006`).
|
||||
e.g., the `no-mutable-default-argument` rule (`B006`).
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
@@ -2511,9 +2580,9 @@ will be added to the `aliases` mapping.
|
||||
|
||||
Boolean flag specifying whether `@pytest.fixture()` without parameters
|
||||
should have parentheses. If the option is set to `true` (the
|
||||
default), `@pytest.fixture()` is valid and `@pytest.fixture` is an
|
||||
error. If set to `false`, `@pytest.fixture` is valid and
|
||||
`@pytest.fixture()` is an error.
|
||||
default), `@pytest.fixture()` is valid and `@pytest.fixture` is
|
||||
invalid. If set to `false`, `@pytest.fixture` is valid and
|
||||
`@pytest.fixture()` is invalid.
|
||||
|
||||
**Default value**: `true`
|
||||
|
||||
@@ -2532,9 +2601,9 @@ fixture-parentheses = true
|
||||
|
||||
Boolean flag specifying whether `@pytest.mark.foo()` without parameters
|
||||
should have parentheses. If the option is set to `true` (the
|
||||
default), `@pytest.mark.foo()` is valid and `@pytest.mark.foo` is an
|
||||
error. If set to `false`, `@pytest.fixture` is valid and
|
||||
`@pytest.mark.foo()` is an error.
|
||||
default), `@pytest.mark.foo()` is valid and `@pytest.mark.foo` is
|
||||
invalid. If set to `false`, `@pytest.fixture` is valid and
|
||||
`@pytest.mark.foo()` is invalid.
|
||||
|
||||
**Default value**: `true`
|
||||
|
||||
@@ -2756,7 +2825,7 @@ ban-relative-imports = "all"
|
||||
#### [`banned-api`](#banned-api)
|
||||
|
||||
Specific modules or module members that may not be imported or accessed.
|
||||
Note that this check is only meant to flag accidental uses,
|
||||
Note that this rule is only meant to flag accidental uses,
|
||||
and can be circumvented via `eval` or `importlib`.
|
||||
|
||||
**Default value**: `{}`
|
||||
@@ -2848,6 +2917,25 @@ force-single-line = true
|
||||
|
||||
---
|
||||
|
||||
#### [`force-sort-within-sections`](#force-sort-within-sections)
|
||||
|
||||
Don't sort straight-style imports (like `import sys`) before from-style
|
||||
imports (like `from itertools import groupby`). Instead, sort the
|
||||
imports by module, independent of import style.
|
||||
|
||||
**Default value**: `false`
|
||||
|
||||
**Type**: `bool`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff.isort]
|
||||
force-sort-within-sections = true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`force-wrap-aliases`](#force-wrap-aliases)
|
||||
|
||||
Force `import from` statements with multiple members and at least one
|
||||
@@ -2935,6 +3023,47 @@ order-by-type = true
|
||||
|
||||
---
|
||||
|
||||
#### [`relative-imports-order`](#relative-imports-order)
|
||||
|
||||
Whether to place "closer" imports (fewer `.` characters, most local)
|
||||
before "further" imports (more `.` characters, least local), or vice
|
||||
versa.
|
||||
|
||||
The default ("furthest-to-closest") is equivalent to isort's
|
||||
`reverse-relative` default (`reverse-relative = false`); setting
|
||||
this to "closest-to-furthest" is equivalent to isort's `reverse-relative
|
||||
= true`.
|
||||
|
||||
**Default value**: `furthest-to-closest`
|
||||
|
||||
**Type**: `RelatveImportsOrder`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff.isort]
|
||||
relative-imports-order = "closest-to-furthest"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`required-imports`](#required-imports)
|
||||
|
||||
Add the specified import line to all files.
|
||||
|
||||
**Default value**: `[]`
|
||||
|
||||
**Type**: `Vec<String>`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff.isort]
|
||||
required-imports = ["from __future__ import annotations"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### [`single-line-exclusions`](#single-line-exclusions)
|
||||
|
||||
One or more modules to exclude from the single line rule.
|
||||
@@ -3057,7 +3186,7 @@ staticmethod-decorators = ["staticmethod", "stcmthd"]
|
||||
|
||||
#### [`ignore-overlong-task-comments`](#ignore-overlong-task-comments)
|
||||
|
||||
Whether or not line-length checks (`E501`) should be triggered for
|
||||
Whether or not line-length violations (`E501`) should be triggered for
|
||||
comments starting with `task-tags` (by default: ["TODO", "FIXME",
|
||||
and "XXX"]).
|
||||
|
||||
@@ -3074,6 +3203,24 @@ ignore-overlong-task-comments = true
|
||||
|
||||
---
|
||||
|
||||
#### [`max-doc-length`](#max-doc-length)
|
||||
|
||||
The maximum line length to allow for line-length violations within
|
||||
documentation (`W505`), including standalone comments.
|
||||
|
||||
**Default value**: `None`
|
||||
|
||||
**Type**: `usize`
|
||||
|
||||
**Example usage**:
|
||||
|
||||
```toml
|
||||
[tool.ruff.pycodestyle]
|
||||
max-doc-length = 88
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `pydocstyle`
|
||||
|
||||
#### [`convention`](#convention)
|
||||
@@ -3128,4 +3275,4 @@ MIT
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome and hugely appreciated. To get started, check out the
|
||||
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/.github/CONTRIBUTING.md).
|
||||
[contributing guidelines](https://github.com/charliermarsh/ruff/blob/main/CONTRIBUTING.md).
|
||||
|
||||
4
flake8_to_ruff/Cargo.lock
generated
4
flake8_to_ruff/Cargo.lock
generated
@@ -771,7 +771,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
|
||||
|
||||
[[package]]
|
||||
name = "flake8_to_ruff"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
@@ -1975,7 +1975,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bincode",
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
[package]
|
||||
name = "flake8-to-ruff"
|
||||
version = "0.0.214-dev.0"
|
||||
version = "0.0.220-dev.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "flake8_to_ruff"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.66" }
|
||||
clap = { version = "4.0.1", features = ["derive"] }
|
||||
colored = { version = "2.0.0" }
|
||||
configparser = { version = "3.0.2" }
|
||||
once_cell = { version = "1.16.0" }
|
||||
regex = { version = "1.6.0" }
|
||||
|
||||
@@ -84,7 +84,7 @@ flake8-to-ruff path/to/.flake8 --plugin flake8-builtins --plugin flake8-quotes
|
||||
1. Ruff only supports a subset of the Flake configuration options. `flake8-to-ruff` will warn on and
|
||||
ignore unsupported options in the `.flake8` file (or equivalent). (Similarly, Ruff has a few
|
||||
configuration options that don't exist in Flake8.)
|
||||
2. Ruff will omit any error codes that are unimplemented or unsupported by Ruff, including error
|
||||
2. Ruff will omit any rule codes that are unimplemented or unsupported by Ruff, including rule
|
||||
codes from unsupported plugins. (See the [Ruff README](https://github.com/charliermarsh/ruff#user-content-how-does-ruff-compare-to-flake8)
|
||||
for the complete list of supported plugins.)
|
||||
|
||||
|
||||
19
flake8_to_ruff/examples/jupyterhub.ini
Normal file
19
flake8_to_ruff/examples/jupyterhub.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
[flake8]
|
||||
# Ignore style and complexity
|
||||
# E: style errors
|
||||
# W: style warnings
|
||||
# C: complexity
|
||||
# D: docstring warnings (unused pydocstyle extension)
|
||||
# F841: local variable assigned but never used
|
||||
ignore = E, C, W, D, F841
|
||||
builtins = c, get_config
|
||||
exclude =
|
||||
.cache,
|
||||
.github,
|
||||
docs,
|
||||
jupyterhub/alembic*,
|
||||
onbuild,
|
||||
scripts,
|
||||
share,
|
||||
tools,
|
||||
setup.py
|
||||
@@ -1,18 +1,19 @@
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use anyhow::Result;
|
||||
use colored::Colorize;
|
||||
use ruff::flake8_pytest_style::types::{
|
||||
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
|
||||
};
|
||||
use ruff::flake8_quotes::settings::Quote;
|
||||
use ruff::flake8_tidy_imports::settings::Strictness;
|
||||
use ruff::pydocstyle::settings::Convention;
|
||||
use ruff::registry::CheckCodePrefix;
|
||||
use ruff::registry::RuleCodePrefix;
|
||||
use ruff::settings::options::Options;
|
||||
use ruff::settings::pyproject::Pyproject;
|
||||
use ruff::{
|
||||
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_pytest_style, flake8_quotes,
|
||||
flake8_tidy_imports, mccabe, pep8_naming, pydocstyle,
|
||||
flake8_tidy_imports, mccabe, pep8_naming, pydocstyle, warn_user,
|
||||
};
|
||||
|
||||
use crate::black::Black;
|
||||
@@ -29,8 +30,8 @@ pub fn convert(
|
||||
.get("flake8")
|
||||
.expect("Unable to find flake8 section in INI file");
|
||||
|
||||
// Extract all referenced check code prefixes, to power plugin inference.
|
||||
let mut referenced_codes: BTreeSet<CheckCodePrefix> = BTreeSet::default();
|
||||
// Extract all referenced rule code prefixes, to power plugin inference.
|
||||
let mut referenced_codes: BTreeSet<RuleCodePrefix> = BTreeSet::default();
|
||||
for (key, value) in flake8 {
|
||||
if let Some(value) = value {
|
||||
match key.as_str() {
|
||||
@@ -60,7 +61,7 @@ pub fn convert(
|
||||
}
|
||||
let from_codes = plugin::infer_plugins_from_codes(&referenced_codes);
|
||||
if !from_codes.is_empty() {
|
||||
eprintln!("Inferred plugins from referenced check codes: {from_codes:#?}");
|
||||
eprintln!("Inferred plugins from referenced codes: {from_codes:#?}");
|
||||
}
|
||||
from_options.into_iter().chain(from_codes).collect()
|
||||
});
|
||||
@@ -99,9 +100,14 @@ pub fn convert(
|
||||
if let Some(value) = value {
|
||||
match key.as_str() {
|
||||
// flake8
|
||||
"builtins" => {
|
||||
options.builtins = Some(parser::parse_strings(value.as_ref()));
|
||||
}
|
||||
"max-line-length" | "max_line_length" => match value.clone().parse::<usize>() {
|
||||
Ok(line_length) => options.line_length = Some(line_length),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
},
|
||||
"select" => {
|
||||
// No-op (handled above).
|
||||
@@ -130,7 +136,9 @@ pub fn convert(
|
||||
options.per_file_ignores =
|
||||
Some(parser::collect_per_file_ignores(per_file_ignores));
|
||||
}
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// flake8-bugbear
|
||||
@@ -142,46 +150,62 @@ pub fn convert(
|
||||
"suppress-none-returning" | "suppress_none_returning" => {
|
||||
match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_annotations.suppress_none_returning = Some(bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"suppress-dummy-args" | "suppress_dummy_args" => {
|
||||
match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_annotations.suppress_dummy_args = Some(bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"mypy-init-return" | "mypy_init_return" => {
|
||||
match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_annotations.mypy_init_return = Some(bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"allow-star-arg-any" | "allow_star_arg_any" => {
|
||||
match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_annotations.allow_star_arg_any = Some(bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// flake8-quotes
|
||||
"quotes" | "inline-quotes" | "inline_quotes" => match value.trim() {
|
||||
"'" | "single" => flake8_quotes.inline_quotes = Some(Quote::Single),
|
||||
"\"" | "double" => flake8_quotes.inline_quotes = Some(Quote::Double),
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
},
|
||||
"multiline-quotes" | "multiline_quotes" => match value.trim() {
|
||||
"'" | "single" => flake8_quotes.multiline_quotes = Some(Quote::Single),
|
||||
"\"" | "double" => flake8_quotes.multiline_quotes = Some(Quote::Double),
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
},
|
||||
"docstring-quotes" | "docstring_quotes" => match value.trim() {
|
||||
"'" | "single" => flake8_quotes.docstring_quotes = Some(Quote::Single),
|
||||
"\"" | "double" => flake8_quotes.docstring_quotes = Some(Quote::Double),
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
},
|
||||
"avoid-escape" | "avoid_escape" => match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_quotes.avoid_escape = Some(bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
},
|
||||
// pep8-naming
|
||||
"ignore-names" | "ignore_names" => {
|
||||
@@ -201,7 +225,9 @@ pub fn convert(
|
||||
"parents" => {
|
||||
flake8_tidy_imports.ban_relative_imports = Some(Strictness::Parents);
|
||||
}
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
},
|
||||
// flake8-docstrings
|
||||
"docstring-convention" => match value.trim() {
|
||||
@@ -209,12 +235,16 @@ pub fn convert(
|
||||
"numpy" => pydocstyle.convention = Some(Convention::Numpy),
|
||||
"pep257" => pydocstyle.convention = Some(Convention::Pep257),
|
||||
"all" => pydocstyle.convention = None,
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
},
|
||||
// mccabe
|
||||
"max-complexity" | "max_complexity" => match value.clone().parse::<usize>() {
|
||||
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
},
|
||||
// flake8-errmsg
|
||||
"errmsg-max-string-length" | "errmsg_max_string_length" => {
|
||||
@@ -222,14 +252,18 @@ pub fn convert(
|
||||
Ok(max_string_length) => {
|
||||
flake8_errmsg.max_string_length = Some(max_string_length);
|
||||
}
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// flake8-pytest-style
|
||||
"pytest-fixture-no-parentheses" | "pytest_fixture_no_parentheses " => {
|
||||
match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_pytest_style.fixture_parentheses = Some(!bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"pytest-parametrize-names-type" | "pytest_parametrize_names_type" => {
|
||||
@@ -246,7 +280,9 @@ pub fn convert(
|
||||
flake8_pytest_style.parametrize_names_type =
|
||||
Some(ParametrizeNameType::List);
|
||||
}
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"pytest-parametrize-values-type" | "pytest_parametrize_values_type" => {
|
||||
@@ -259,7 +295,9 @@ pub fn convert(
|
||||
flake8_pytest_style.parametrize_values_type =
|
||||
Some(ParametrizeValuesType::List);
|
||||
}
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"pytest-parametrize-values-row-type" | "pytest_parametrize_values_row_type" => {
|
||||
@@ -272,7 +310,9 @@ pub fn convert(
|
||||
flake8_pytest_style.parametrize_values_row_type =
|
||||
Some(ParametrizeValuesRowType::List);
|
||||
}
|
||||
_ => eprintln!("Unexpected '{key}' value: {value}"),
|
||||
_ => {
|
||||
warn_user!("Unexpected '{key}' value: {value}");
|
||||
}
|
||||
}
|
||||
}
|
||||
"pytest-raises-require-match-for" | "pytest_raises_require_match_for" => {
|
||||
@@ -282,11 +322,15 @@ pub fn convert(
|
||||
"pytest-mark-no-parentheses" | "pytest_mark_no_parentheses" => {
|
||||
match parser::parse_bool(value.as_ref()) {
|
||||
Ok(bool) => flake8_pytest_style.mark_parentheses = Some(!bool),
|
||||
Err(e) => eprintln!("Unable to parse '{key}' property: {e}"),
|
||||
Err(e) => {
|
||||
warn_user!("Unable to parse '{key}' property: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Unknown
|
||||
_ => eprintln!("Skipping unsupported property: {key}"),
|
||||
_ => {
|
||||
warn_user!("Skipping unsupported property: {}", key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,7 +389,7 @@ mod tests {
|
||||
|
||||
use anyhow::Result;
|
||||
use ruff::pydocstyle::settings::Convention;
|
||||
use ruff::registry::CheckCodePrefix;
|
||||
use ruff::registry::RuleCodePrefix;
|
||||
use ruff::settings::options::Options;
|
||||
use ruff::settings::pyproject::Pyproject;
|
||||
use ruff::{flake8_quotes, pydocstyle};
|
||||
@@ -362,6 +406,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -382,14 +427,15 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
@@ -425,6 +471,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -445,14 +492,15 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
@@ -488,6 +536,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -508,14 +557,15 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
@@ -551,6 +601,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -571,14 +622,15 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
@@ -614,6 +666,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -634,14 +687,15 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
@@ -685,6 +739,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -705,15 +760,16 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::D,
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::D,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
@@ -751,6 +807,7 @@ mod tests {
|
||||
)?;
|
||||
let expected = Pyproject::new(Options {
|
||||
allowed_confusables: None,
|
||||
builtins: None,
|
||||
cache_dir: None,
|
||||
dummy_variable_rgx: None,
|
||||
exclude: None,
|
||||
@@ -771,15 +828,16 @@ mod tests {
|
||||
required_version: None,
|
||||
respect_gitignore: None,
|
||||
select: Some(vec![
|
||||
CheckCodePrefix::E,
|
||||
CheckCodePrefix::F,
|
||||
CheckCodePrefix::Q,
|
||||
CheckCodePrefix::W,
|
||||
RuleCodePrefix::E,
|
||||
RuleCodePrefix::F,
|
||||
RuleCodePrefix::Q,
|
||||
RuleCodePrefix::W,
|
||||
]),
|
||||
show_source: None,
|
||||
src: None,
|
||||
target_version: None,
|
||||
unfixable: None,
|
||||
typing_modules: None,
|
||||
task_tags: None,
|
||||
update_check: None,
|
||||
flake8_annotations: None,
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use colored::Colorize;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use ruff::registry::{CheckCodePrefix, PREFIX_REDIRECTS};
|
||||
use ruff::registry::{RuleCodePrefix, PREFIX_REDIRECTS};
|
||||
use ruff::settings::types::PatternPrefixPair;
|
||||
use ruff::warn_user;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
|
||||
|
||||
/// Parse a comma-separated list of `CheckCodePrefix` values (e.g.,
|
||||
/// Parse a comma-separated list of `RuleCodePrefix` values (e.g.,
|
||||
/// "F401,E501").
|
||||
pub fn parse_prefix_codes(value: &str) -> Vec<CheckCodePrefix> {
|
||||
let mut codes: Vec<CheckCodePrefix> = vec![];
|
||||
pub fn parse_prefix_codes(value: &str) -> Vec<RuleCodePrefix> {
|
||||
let mut codes: Vec<RuleCodePrefix> = vec![];
|
||||
for code in COMMA_SEPARATED_LIST_RE.split(value) {
|
||||
let code = code.trim();
|
||||
if code.is_empty() {
|
||||
@@ -20,10 +22,10 @@ pub fn parse_prefix_codes(value: &str) -> Vec<CheckCodePrefix> {
|
||||
}
|
||||
if let Some(code) = PREFIX_REDIRECTS.get(code) {
|
||||
codes.push(code.clone());
|
||||
} else if let Ok(code) = CheckCodePrefix::from_str(code) {
|
||||
} else if let Ok(code) = RuleCodePrefix::from_str(code) {
|
||||
codes.push(code);
|
||||
} else {
|
||||
eprintln!("Unsupported prefix code: {code}");
|
||||
warn_user!("Unsupported prefix code: {code}");
|
||||
}
|
||||
}
|
||||
codes
|
||||
@@ -81,7 +83,8 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the list of `StrCheckCodePair` pairs for the current state.
|
||||
/// Generate the list of `StrRuleCodePair` pairs for the current
|
||||
/// state.
|
||||
fn parse(&self) -> Vec<PatternPrefixPair> {
|
||||
let mut codes: Vec<PatternPrefixPair> = vec![];
|
||||
for code in &self.codes {
|
||||
@@ -92,7 +95,7 @@ impl State {
|
||||
prefix: code.clone(),
|
||||
});
|
||||
}
|
||||
} else if let Ok(code) = CheckCodePrefix::from_str(code) {
|
||||
} else if let Ok(code) = RuleCodePrefix::from_str(code) {
|
||||
for filename in &self.filenames {
|
||||
codes.push(PatternPrefixPair {
|
||||
pattern: filename.clone(),
|
||||
@@ -100,7 +103,7 @@ impl State {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
eprintln!("Unsupported prefix code: {code}");
|
||||
warn_user!("Unsupported prefix code: {code}");
|
||||
}
|
||||
}
|
||||
codes
|
||||
@@ -186,8 +189,8 @@ pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair
|
||||
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
|
||||
pub fn collect_per_file_ignores(
|
||||
pairs: Vec<PatternPrefixPair>,
|
||||
) -> FxHashMap<String, Vec<CheckCodePrefix>> {
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<CheckCodePrefix>> = FxHashMap::default();
|
||||
) -> FxHashMap<String, Vec<RuleCodePrefix>> {
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<RuleCodePrefix>> = FxHashMap::default();
|
||||
for pair in pairs {
|
||||
per_file_ignores
|
||||
.entry(pair.pattern)
|
||||
@@ -200,7 +203,7 @@ pub fn collect_per_file_ignores(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use anyhow::Result;
|
||||
use ruff::registry::CheckCodePrefix;
|
||||
use ruff::registry::RuleCodePrefix;
|
||||
use ruff::settings::types::PatternPrefixPair;
|
||||
|
||||
use crate::parser::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
|
||||
@@ -208,27 +211,27 @@ mod tests {
|
||||
#[test]
|
||||
fn it_parses_prefix_codes() {
|
||||
let actual = parse_prefix_codes("");
|
||||
let expected: Vec<CheckCodePrefix> = vec![];
|
||||
let expected: Vec<RuleCodePrefix> = vec![];
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = parse_prefix_codes(" ");
|
||||
let expected: Vec<CheckCodePrefix> = vec![];
|
||||
let expected: Vec<RuleCodePrefix> = vec![];
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = parse_prefix_codes("F401");
|
||||
let expected = vec![CheckCodePrefix::F401];
|
||||
let expected = vec![RuleCodePrefix::F401];
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = parse_prefix_codes("F401,");
|
||||
let expected = vec![CheckCodePrefix::F401];
|
||||
let expected = vec![RuleCodePrefix::F401];
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = parse_prefix_codes("F401,E501");
|
||||
let expected = vec![CheckCodePrefix::F401, CheckCodePrefix::E501];
|
||||
let expected = vec![RuleCodePrefix::F401, RuleCodePrefix::E501];
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
let actual = parse_prefix_codes("F401, E501");
|
||||
let expected = vec![CheckCodePrefix::F401, CheckCodePrefix::E501];
|
||||
let expected = vec![RuleCodePrefix::F401, RuleCodePrefix::E501];
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
@@ -281,11 +284,11 @@ mod tests {
|
||||
let expected: Vec<PatternPrefixPair> = vec![
|
||||
PatternPrefixPair {
|
||||
pattern: "locust/test/*".to_string(),
|
||||
prefix: CheckCodePrefix::F841,
|
||||
prefix: RuleCodePrefix::F841,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "examples/*".to_string(),
|
||||
prefix: CheckCodePrefix::F841,
|
||||
prefix: RuleCodePrefix::F841,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual, expected);
|
||||
@@ -301,23 +304,23 @@ mod tests {
|
||||
let expected: Vec<PatternPrefixPair> = vec![
|
||||
PatternPrefixPair {
|
||||
pattern: "t/*".to_string(),
|
||||
prefix: CheckCodePrefix::D,
|
||||
prefix: RuleCodePrefix::D,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "setup.py".to_string(),
|
||||
prefix: CheckCodePrefix::D,
|
||||
prefix: RuleCodePrefix::D,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "examples/*".to_string(),
|
||||
prefix: CheckCodePrefix::D,
|
||||
prefix: RuleCodePrefix::D,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "docs/*".to_string(),
|
||||
prefix: CheckCodePrefix::D,
|
||||
prefix: RuleCodePrefix::D,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "extra/*".to_string(),
|
||||
prefix: CheckCodePrefix::D,
|
||||
prefix: RuleCodePrefix::D,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual, expected);
|
||||
@@ -339,47 +342,47 @@ mod tests {
|
||||
let expected: Vec<PatternPrefixPair> = vec![
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::E402,
|
||||
prefix: RuleCodePrefix::E402,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/core/downloader/handlers/http.py".to_string(),
|
||||
prefix: CheckCodePrefix::F401,
|
||||
prefix: RuleCodePrefix::F401,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/http/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::F401,
|
||||
prefix: RuleCodePrefix::F401,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/linkextractors/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::E402,
|
||||
prefix: RuleCodePrefix::E402,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/linkextractors/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::F401,
|
||||
prefix: RuleCodePrefix::F401,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/selector/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::F401,
|
||||
prefix: RuleCodePrefix::F401,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/spiders/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::E402,
|
||||
prefix: RuleCodePrefix::E402,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/spiders/__init__.py".to_string(),
|
||||
prefix: CheckCodePrefix::F401,
|
||||
prefix: RuleCodePrefix::F401,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/utils/url.py".to_string(),
|
||||
prefix: CheckCodePrefix::F403,
|
||||
prefix: RuleCodePrefix::F403,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "scrapy/utils/url.py".to_string(),
|
||||
prefix: CheckCodePrefix::F405,
|
||||
prefix: RuleCodePrefix::F405,
|
||||
},
|
||||
PatternPrefixPair {
|
||||
pattern: "tests/test_loader.py".to_string(),
|
||||
prefix: CheckCodePrefix::E741,
|
||||
prefix: RuleCodePrefix::E741,
|
||||
},
|
||||
];
|
||||
assert_eq!(actual, expected);
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use ruff::registry::CheckCodePrefix;
|
||||
use ruff::registry::RuleCodePrefix;
|
||||
|
||||
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum Plugin {
|
||||
@@ -97,53 +97,32 @@ impl fmt::Debug for Plugin {
|
||||
}
|
||||
|
||||
impl Plugin {
|
||||
pub fn prefix(&self) -> CheckCodePrefix {
|
||||
pub fn prefix(&self) -> RuleCodePrefix {
|
||||
match self {
|
||||
Plugin::Flake8Annotations => CheckCodePrefix::ANN,
|
||||
Plugin::Flake8Bandit => CheckCodePrefix::S,
|
||||
Plugin::Flake8Annotations => RuleCodePrefix::ANN,
|
||||
Plugin::Flake8Bandit => RuleCodePrefix::S,
|
||||
// TODO(charlie): Handle rename of `B` to `BLE`.
|
||||
Plugin::Flake8BlindExcept => CheckCodePrefix::BLE,
|
||||
Plugin::Flake8Bugbear => CheckCodePrefix::B,
|
||||
Plugin::Flake8Builtins => CheckCodePrefix::A,
|
||||
Plugin::Flake8Comprehensions => CheckCodePrefix::C4,
|
||||
Plugin::Flake8Datetimez => CheckCodePrefix::DTZ,
|
||||
Plugin::Flake8Debugger => CheckCodePrefix::T1,
|
||||
Plugin::Flake8Docstrings => CheckCodePrefix::D,
|
||||
Plugin::Flake8BlindExcept => RuleCodePrefix::BLE,
|
||||
Plugin::Flake8Bugbear => RuleCodePrefix::B,
|
||||
Plugin::Flake8Builtins => RuleCodePrefix::A,
|
||||
Plugin::Flake8Comprehensions => RuleCodePrefix::C4,
|
||||
Plugin::Flake8Datetimez => RuleCodePrefix::DTZ,
|
||||
Plugin::Flake8Debugger => RuleCodePrefix::T1,
|
||||
Plugin::Flake8Docstrings => RuleCodePrefix::D,
|
||||
// TODO(charlie): Handle rename of `E` to `ERA`.
|
||||
Plugin::Flake8Eradicate => CheckCodePrefix::ERA,
|
||||
Plugin::Flake8ErrMsg => CheckCodePrefix::EM,
|
||||
Plugin::Flake8ImplicitStrConcat => CheckCodePrefix::ISC,
|
||||
Plugin::Flake8Print => CheckCodePrefix::T2,
|
||||
Plugin::Flake8PytestStyle => CheckCodePrefix::PT,
|
||||
Plugin::Flake8Quotes => CheckCodePrefix::Q,
|
||||
Plugin::Flake8Return => CheckCodePrefix::RET,
|
||||
Plugin::Flake8Simplify => CheckCodePrefix::SIM,
|
||||
Plugin::Flake8TidyImports => CheckCodePrefix::TID25,
|
||||
Plugin::McCabe => CheckCodePrefix::C9,
|
||||
Plugin::PandasVet => CheckCodePrefix::PD,
|
||||
Plugin::PEP8Naming => CheckCodePrefix::N,
|
||||
Plugin::Pyupgrade => CheckCodePrefix::UP,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DocstringConvention {
|
||||
All,
|
||||
Pep257,
|
||||
Numpy,
|
||||
Google,
|
||||
}
|
||||
|
||||
impl FromStr for DocstringConvention {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(string: &str) -> Result<Self, Self::Err> {
|
||||
match string {
|
||||
"all" => Ok(DocstringConvention::All),
|
||||
"pep257" => Ok(DocstringConvention::Pep257),
|
||||
"numpy" => Ok(DocstringConvention::Numpy),
|
||||
"google" => Ok(DocstringConvention::Google),
|
||||
_ => Err(anyhow!("Unknown docstring convention: {string}")),
|
||||
Plugin::Flake8Eradicate => RuleCodePrefix::ERA,
|
||||
Plugin::Flake8ErrMsg => RuleCodePrefix::EM,
|
||||
Plugin::Flake8ImplicitStrConcat => RuleCodePrefix::ISC,
|
||||
Plugin::Flake8Print => RuleCodePrefix::T2,
|
||||
Plugin::Flake8PytestStyle => RuleCodePrefix::PT,
|
||||
Plugin::Flake8Quotes => RuleCodePrefix::Q,
|
||||
Plugin::Flake8Return => RuleCodePrefix::RET,
|
||||
Plugin::Flake8Simplify => RuleCodePrefix::SIM,
|
||||
Plugin::Flake8TidyImports => RuleCodePrefix::TID25,
|
||||
Plugin::McCabe => RuleCodePrefix::C9,
|
||||
Plugin::PandasVet => RuleCodePrefix::PD,
|
||||
Plugin::PEP8Naming => RuleCodePrefix::N,
|
||||
Plugin::Pyupgrade => RuleCodePrefix::UP,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +248,7 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
|
||||
///
|
||||
/// For example, if the user ignores `ANN101`, we should infer that
|
||||
/// `flake8-annotations` is active.
|
||||
pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin> {
|
||||
pub fn infer_plugins_from_codes(codes: &BTreeSet<RuleCodePrefix>) -> Vec<Plugin> {
|
||||
[
|
||||
Plugin::Flake8Annotations,
|
||||
Plugin::Flake8Bandit,
|
||||
@@ -307,9 +286,10 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Resolve the set of enabled `CheckCodePrefix` values for the given plugins.
|
||||
pub fn resolve_select(plugins: &[Plugin]) -> BTreeSet<CheckCodePrefix> {
|
||||
let mut select = BTreeSet::from([CheckCodePrefix::F, CheckCodePrefix::E, CheckCodePrefix::W]);
|
||||
/// Resolve the set of enabled `RuleCodePrefix` values for the given
|
||||
/// plugins.
|
||||
pub fn resolve_select(plugins: &[Plugin]) -> BTreeSet<RuleCodePrefix> {
|
||||
let mut select = BTreeSet::from([RuleCodePrefix::F, RuleCodePrefix::E, RuleCodePrefix::W]);
|
||||
select.extend(plugins.iter().map(Plugin::prefix));
|
||||
select
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { DEFAULT_PYTHON_SOURCE } from "../constants";
|
||||
import init, { check, Check, currentVersion, defaultSettings } from "../pkg";
|
||||
import init, {
|
||||
check,
|
||||
Diagnostic,
|
||||
currentVersion,
|
||||
defaultSettings,
|
||||
} from "../pkg";
|
||||
import { ErrorMessage } from "./ErrorMessage";
|
||||
import Header from "./Header";
|
||||
import { useTheme } from "./theme";
|
||||
@@ -18,7 +23,7 @@ export default function Editor() {
|
||||
const [edit, setEdit] = useState<number>(0);
|
||||
const [settingsSource, setSettingsSource] = useState<string | null>(null);
|
||||
const [pythonSource, setPythonSource] = useState<string | null>(null);
|
||||
const [checks, setChecks] = useState<Check[]>([]);
|
||||
const [diagnostics, setDiagnostics] = useState<Diagnostic[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [theme, setTheme] = useTheme();
|
||||
|
||||
@@ -32,25 +37,25 @@ export default function Editor() {
|
||||
}
|
||||
|
||||
let config: any;
|
||||
let checks: Check[];
|
||||
let diagnostics: Diagnostic[];
|
||||
|
||||
try {
|
||||
config = JSON.parse(settingsSource);
|
||||
} catch (e) {
|
||||
setChecks([]);
|
||||
setDiagnostics([]);
|
||||
setError((e as Error).message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
checks = check(pythonSource, config);
|
||||
diagnostics = check(pythonSource, config);
|
||||
} catch (e) {
|
||||
setError(e as string);
|
||||
return;
|
||||
}
|
||||
|
||||
setError(null);
|
||||
setChecks(checks);
|
||||
setDiagnostics(diagnostics);
|
||||
}, [initialized, settingsSource, pythonSource]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -122,7 +127,7 @@ export default function Editor() {
|
||||
visible={tab === "Source"}
|
||||
source={pythonSource}
|
||||
theme={theme}
|
||||
checks={checks}
|
||||
diagnostics={diagnostics}
|
||||
onChange={handlePythonSourceChange}
|
||||
/>
|
||||
<SettingsEditor
|
||||
|
||||
@@ -5,19 +5,19 @@
|
||||
import Editor, { useMonaco } from "@monaco-editor/react";
|
||||
import { MarkerSeverity, MarkerTag } from "monaco-editor";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { Check } from "../pkg";
|
||||
import { Diagnostic } from "../pkg";
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export default function SourceEditor({
|
||||
visible,
|
||||
source,
|
||||
theme,
|
||||
checks,
|
||||
diagnostics,
|
||||
onChange,
|
||||
}: {
|
||||
visible: boolean;
|
||||
source: string;
|
||||
checks: Check[];
|
||||
diagnostics: Diagnostic[];
|
||||
theme: Theme;
|
||||
onChange: (pythonSource: string) => void;
|
||||
}) {
|
||||
@@ -33,15 +33,15 @@ export default function SourceEditor({
|
||||
editor.setModelMarkers(
|
||||
model,
|
||||
"owner",
|
||||
checks.map((check) => ({
|
||||
startLineNumber: check.location.row,
|
||||
startColumn: check.location.column + 1,
|
||||
endLineNumber: check.end_location.row,
|
||||
endColumn: check.end_location.column + 1,
|
||||
message: `${check.code}: ${check.message}`,
|
||||
diagnostics.map((diagnostic) => ({
|
||||
startLineNumber: diagnostic.location.row,
|
||||
startColumn: diagnostic.location.column + 1,
|
||||
endLineNumber: diagnostic.end_location.row,
|
||||
endColumn: diagnostic.end_location.column + 1,
|
||||
message: `${diagnostic.code}: ${diagnostic.message}`,
|
||||
severity: MarkerSeverity.Error,
|
||||
tags:
|
||||
check.code === "F401" || check.code === "F841"
|
||||
diagnostic.code === "F401" || diagnostic.code === "F841"
|
||||
? [MarkerTag.Unnecessary]
|
||||
: [],
|
||||
})),
|
||||
@@ -52,7 +52,7 @@ export default function SourceEditor({
|
||||
{
|
||||
// @ts-expect-error: The type definition is wrong.
|
||||
provideCodeActions: function (model, position) {
|
||||
const actions = checks
|
||||
const actions = diagnostics
|
||||
.filter((check) => position.startLineNumber === check.location.row)
|
||||
.filter((check) => check.fix)
|
||||
.map((check) => ({
|
||||
@@ -89,7 +89,7 @@ export default function SourceEditor({
|
||||
return () => {
|
||||
codeActionProvider?.dispose();
|
||||
};
|
||||
}, [checks, monaco]);
|
||||
}, [diagnostics, monaco]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
||||
|
||||
[project]
|
||||
name = "ruff"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
authors = [
|
||||
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
|
||||
@@ -35,6 +35,7 @@ urls = { repository = "https://github.com/charliermarsh/ruff" }
|
||||
|
||||
[tool.maturin]
|
||||
bindings = "bin"
|
||||
python-source = "python"
|
||||
strip = true
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
6
resources/test/fixtures/flake8_bandit/S508.py
vendored
Normal file
6
resources/test/fixtures/flake8_bandit/S508.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
from pysnmp.hlapi import CommunityData
|
||||
|
||||
CommunityData("public", mpModel=0) # S508
|
||||
CommunityData("public", mpModel=1) # S508
|
||||
|
||||
CommunityData("public", mpModel=2) # OK
|
||||
7
resources/test/fixtures/flake8_bandit/S509.py
vendored
Normal file
7
resources/test/fixtures/flake8_bandit/S509.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
from pysnmp.hlapi import UsmUserData
|
||||
|
||||
|
||||
insecure = UsmUserData("securityName") # S509
|
||||
auth_no_priv = UsmUserData("securityName", "authName") # S509
|
||||
|
||||
less_insecure = UsmUserData("securityName", "authName", "privName") # OK
|
||||
29
resources/test/fixtures/flake8_bandit/S701.py
vendored
Normal file
29
resources/test/fixtures/flake8_bandit/S701.py
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
import jinja2
|
||||
from jinja2 import Environment, select_autoescape
|
||||
templateLoader = jinja2.FileSystemLoader( searchpath="/" )
|
||||
something = ''
|
||||
|
||||
Environment(loader=templateLoader, load=templateLoader, autoescape=True)
|
||||
templateEnv = jinja2.Environment(autoescape=True,
|
||||
loader=templateLoader )
|
||||
Environment(loader=templateLoader, load=templateLoader, autoescape=something) # S701
|
||||
templateEnv = jinja2.Environment(autoescape=False, loader=templateLoader ) # S701
|
||||
Environment(loader=templateLoader,
|
||||
load=templateLoader,
|
||||
autoescape=False) # S701
|
||||
|
||||
Environment(loader=templateLoader, # S701
|
||||
load=templateLoader)
|
||||
|
||||
Environment(loader=templateLoader, autoescape=select_autoescape())
|
||||
|
||||
Environment(loader=templateLoader,
|
||||
autoescape=select_autoescape(['html', 'htm', 'xml']))
|
||||
|
||||
Environment(loader=templateLoader,
|
||||
autoescape=jinja2.select_autoescape(['html', 'htm', 'xml']))
|
||||
|
||||
|
||||
def fake_func():
|
||||
return 'foobar'
|
||||
Environment(loader=templateLoader, autoescape=fake_func()) # S701
|
||||
114
resources/test/fixtures/flake8_bugbear/B023.py
vendored
114
resources/test/fixtures/flake8_bugbear/B023.py
vendored
@@ -25,10 +25,10 @@ for x in range(3):
|
||||
|
||||
|
||||
def check_inside_functions_too():
|
||||
ls = [lambda: x for x in range(2)]
|
||||
st = {lambda: x for x in range(2)}
|
||||
gn = (lambda: x for x in range(2))
|
||||
dt = {x: lambda: x for x in range(2)}
|
||||
ls = [lambda: x for x in range(2)] # error
|
||||
st = {lambda: x for x in range(2)} # error
|
||||
gn = (lambda: x for x in range(2)) # error
|
||||
dt = {x: lambda: x for x in range(2)} # error
|
||||
|
||||
|
||||
async def pointless_async_iterable():
|
||||
@@ -37,9 +37,9 @@ async def pointless_async_iterable():
|
||||
|
||||
async def container_for_problems():
|
||||
async for x in pointless_async_iterable():
|
||||
functions.append(lambda: x)
|
||||
functions.append(lambda: x) # error
|
||||
|
||||
[lambda: x async for x in pointless_async_iterable()]
|
||||
[lambda: x async for x in pointless_async_iterable()] # error
|
||||
|
||||
|
||||
a = 10
|
||||
@@ -47,10 +47,10 @@ b = 0
|
||||
while True:
|
||||
a = a_ = a - 1
|
||||
b += 1
|
||||
functions.append(lambda: a)
|
||||
functions.append(lambda: a_)
|
||||
functions.append(lambda: b)
|
||||
functions.append(lambda: c) # not a name error because of late binding!
|
||||
functions.append(lambda: a) # error
|
||||
functions.append(lambda: a_) # error
|
||||
functions.append(lambda: b) # error
|
||||
functions.append(lambda: c) # error, but not a name error due to late binding
|
||||
c: bool = a > 3
|
||||
if not c:
|
||||
break
|
||||
@@ -58,7 +58,7 @@ while True:
|
||||
# Nested loops should not duplicate reports
|
||||
for j in range(2):
|
||||
for k in range(3):
|
||||
lambda: j * k
|
||||
lambda: j * k # error
|
||||
|
||||
|
||||
for j, k, l in [(1, 2, 3)]:
|
||||
@@ -80,3 +80,95 @@ for var in range(2):
|
||||
|
||||
for i in range(3):
|
||||
lambda: f"{i}"
|
||||
|
||||
|
||||
# `query` is defined in the function, so also defining it in the loop should be OK.
|
||||
for name in ["a", "b"]:
|
||||
query = name
|
||||
|
||||
def myfunc(x):
|
||||
query = x
|
||||
query_post = x
|
||||
_ = query
|
||||
_ = query_post
|
||||
|
||||
query_post = name # in case iteration order matters
|
||||
|
||||
|
||||
# Bug here because two dict comprehensions reference `name`, one of which is inside
|
||||
# the lambda. This should be totally fine, of course.
|
||||
_ = {
|
||||
k: v
|
||||
for k, v in reduce(
|
||||
lambda data, event: merge_mappings(
|
||||
[data, {name: f(caches, data, event) for name, f in xx}]
|
||||
),
|
||||
events,
|
||||
{name: getattr(group, name) for name in yy},
|
||||
).items()
|
||||
if k in backfill_fields
|
||||
}
|
||||
|
||||
|
||||
# OK to define lambdas if they're immediately consumed, typically as the `key=`
|
||||
# argument or in a consumed `filter()` (even if a comprehension is better style)
|
||||
for x in range(2):
|
||||
# It's not a complete get-out-of-linting-free construct - these should fail:
|
||||
min([None, lambda: x], key=repr)
|
||||
sorted([None, lambda: x], key=repr)
|
||||
any(filter(bool, [None, lambda: x]))
|
||||
list(filter(bool, [None, lambda: x]))
|
||||
all(reduce(bool, [None, lambda: x]))
|
||||
|
||||
# But all these should be OK:
|
||||
min(range(3), key=lambda y: x * y)
|
||||
max(range(3), key=lambda y: x * y)
|
||||
sorted(range(3), key=lambda y: x * y)
|
||||
|
||||
any(map(lambda y: x < y, range(3)))
|
||||
all(map(lambda y: x < y, range(3)))
|
||||
set(map(lambda y: x < y, range(3)))
|
||||
list(map(lambda y: x < y, range(3)))
|
||||
tuple(map(lambda y: x < y, range(3)))
|
||||
sorted(map(lambda y: x < y, range(3)))
|
||||
frozenset(map(lambda y: x < y, range(3)))
|
||||
|
||||
any(filter(lambda y: x < y, range(3)))
|
||||
all(filter(lambda y: x < y, range(3)))
|
||||
set(filter(lambda y: x < y, range(3)))
|
||||
list(filter(lambda y: x < y, range(3)))
|
||||
tuple(filter(lambda y: x < y, range(3)))
|
||||
sorted(filter(lambda y: x < y, range(3)))
|
||||
frozenset(filter(lambda y: x < y, range(3)))
|
||||
|
||||
any(reduce(lambda y: x | y, range(3)))
|
||||
all(reduce(lambda y: x | y, range(3)))
|
||||
set(reduce(lambda y: x | y, range(3)))
|
||||
list(reduce(lambda y: x | y, range(3)))
|
||||
tuple(reduce(lambda y: x | y, range(3)))
|
||||
sorted(reduce(lambda y: x | y, range(3)))
|
||||
frozenset(reduce(lambda y: x | y, range(3)))
|
||||
|
||||
import functools
|
||||
|
||||
any(functools.reduce(lambda y: x | y, range(3)))
|
||||
all(functools.reduce(lambda y: x | y, range(3)))
|
||||
set(functools.reduce(lambda y: x | y, range(3)))
|
||||
list(functools.reduce(lambda y: x | y, range(3)))
|
||||
tuple(functools.reduce(lambda y: x | y, range(3)))
|
||||
sorted(functools.reduce(lambda y: x | y, range(3)))
|
||||
frozenset(functools.reduce(lambda y: x | y, range(3)))
|
||||
|
||||
# OK because the lambda which references a loop variable is defined in a `return`
|
||||
# statement, and after we return the loop variable can't be redefined.
|
||||
# In principle we could do something fancy with `break`, but it's not worth it.
|
||||
def iter_f(names):
|
||||
for name in names:
|
||||
if exists(name):
|
||||
return lambda: name if exists(name) else None
|
||||
|
||||
if foo(name):
|
||||
return [lambda: name] # known false alarm
|
||||
|
||||
if False:
|
||||
return [lambda: i for i in range(3)] # error
|
||||
|
||||
@@ -2,3 +2,10 @@ x = list(x for x in range(3))
|
||||
x = list(
|
||||
x for x in range(3)
|
||||
)
|
||||
|
||||
|
||||
def list(*args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
list(x for x in range(3))
|
||||
|
||||
@@ -2,3 +2,10 @@ x = set(x for x in range(3))
|
||||
x = set(
|
||||
x for x in range(3)
|
||||
)
|
||||
|
||||
|
||||
def set(*args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
set(x for x in range(3))
|
||||
|
||||
@@ -3,3 +3,10 @@ l = list()
|
||||
d1 = dict()
|
||||
d2 = dict(a=1)
|
||||
d3 = dict(**d2)
|
||||
|
||||
|
||||
def list():
|
||||
return [1, 2, 3]
|
||||
|
||||
|
||||
a = list()
|
||||
|
||||
@@ -4,3 +4,10 @@ list(sorted(x))
|
||||
reversed(sorted(x))
|
||||
reversed(sorted(x, key=lambda e: e))
|
||||
reversed(sorted(x, reverse=True))
|
||||
|
||||
|
||||
def reversed(*args, **kwargs):
|
||||
return None
|
||||
|
||||
|
||||
reversed(sorted(x, reverse=True))
|
||||
|
||||
7
resources/test/fixtures/flake8_pie/PIE794.py
vendored
7
resources/test/fixtures/flake8_pie/PIE794.py
vendored
@@ -31,3 +31,10 @@ class User(BaseModel):
|
||||
@buzz.setter
|
||||
def buzz(self, value: str | int) -> None:
|
||||
...
|
||||
|
||||
|
||||
class User:
|
||||
bar: str = StringField()
|
||||
foo: bool = BooleanField()
|
||||
# ...
|
||||
bar = StringField() # PIE794
|
||||
|
||||
@@ -17,6 +17,15 @@ def fun_with_params_no_docstring(a, b="""
|
||||
""" """docstring"""):
|
||||
pass
|
||||
|
||||
|
||||
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
|
||||
""" not a docstring """):
|
||||
pass
|
||||
|
||||
|
||||
def function_with_single_docstring(a):
|
||||
"Single line docstring"
|
||||
|
||||
|
||||
def double_inside_single(a):
|
||||
'Double inside "single "'
|
||||
|
||||
@@ -13,11 +13,19 @@ def foo2():
|
||||
|
||||
|
||||
def fun_with_params_no_docstring(a, b='''
|
||||
not a
|
||||
not a
|
||||
''' '''docstring'''):
|
||||
pass
|
||||
|
||||
|
||||
def fun_with_params_no_docstring2(a, b=c[foo():], c=\
|
||||
''' not a docstring '''):
|
||||
pass
|
||||
|
||||
|
||||
def function_with_single_docstring(a):
|
||||
'Single line docstring'
|
||||
|
||||
|
||||
def double_inside_single(a):
|
||||
"Double inside 'single '"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
this_should_raise_Q003 = 'This is a \'string\''
|
||||
this_should_raise_Q003 = 'This is \\ a \\\'string\''
|
||||
this_is_fine = '"This" is a \'string\''
|
||||
this_is_fine = "This is a 'string'"
|
||||
this_is_fine = "\"This\" is a 'string'"
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# Bad
|
||||
# SIM108
|
||||
if a:
|
||||
b = c
|
||||
else:
|
||||
b = d
|
||||
|
||||
# Good
|
||||
# OK
|
||||
b = c if a else d
|
||||
|
||||
# https://github.com/MartinThoma/flake8-simplify/issues/115
|
||||
# OK
|
||||
if a:
|
||||
b = c
|
||||
elif c:
|
||||
@@ -15,6 +15,7 @@ elif c:
|
||||
else:
|
||||
b = d
|
||||
|
||||
# OK
|
||||
if True:
|
||||
pass
|
||||
elif a:
|
||||
@@ -22,6 +23,7 @@ elif a:
|
||||
else:
|
||||
b = 2
|
||||
|
||||
# OK (false negative)
|
||||
if True:
|
||||
pass
|
||||
else:
|
||||
@@ -29,3 +31,63 @@ else:
|
||||
b = 1
|
||||
else:
|
||||
b = 2
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
# OK
|
||||
if sys.version_info >= (3, 9):
|
||||
randbytes = random.randbytes
|
||||
else:
|
||||
randbytes = _get_random_bytes
|
||||
|
||||
# OK
|
||||
if sys.platform == "darwin":
|
||||
randbytes = random.randbytes
|
||||
else:
|
||||
randbytes = _get_random_bytes
|
||||
|
||||
# OK
|
||||
if sys.platform.startswith("linux"):
|
||||
randbytes = random.randbytes
|
||||
else:
|
||||
randbytes = _get_random_bytes
|
||||
|
||||
|
||||
# OK (includes comments)
|
||||
if x > 0:
|
||||
# test test
|
||||
abc = x
|
||||
else:
|
||||
# test test test
|
||||
abc = -x
|
||||
|
||||
|
||||
# OK (too long)
|
||||
if parser.errno == BAD_FIRST_LINE:
|
||||
req = wrappers.Request(sock, server=self._server)
|
||||
else:
|
||||
req = wrappers.Request(
|
||||
sock,
|
||||
parser.get_method(),
|
||||
parser.get_scheme() or _scheme,
|
||||
parser.get_path(),
|
||||
parser.get_version(),
|
||||
parser.get_query_string(),
|
||||
server=self._server,
|
||||
)
|
||||
|
||||
|
||||
# SIM108
|
||||
if a:
|
||||
b = cccccccccccccccccccccccccccccccccccc
|
||||
else:
|
||||
b = ddddddddddddddddddddddddddddddddddddd
|
||||
|
||||
|
||||
# OK (too long)
|
||||
if True:
|
||||
if a:
|
||||
b = cccccccccccccccccccccccccccccccccccc
|
||||
else:
|
||||
b = ddddddddddddddddddddddddddddddddddddd
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
def f():
|
||||
for x in iterable: # SIM110
|
||||
# SIM110
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
return False
|
||||
@@ -20,14 +21,16 @@ def f():
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable: # SIM111
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable: # SIM111
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if not x.is_empty():
|
||||
return False
|
||||
return True
|
||||
@@ -45,3 +48,70 @@ def f():
|
||||
if check(x):
|
||||
return "foo"
|
||||
return "bar"
|
||||
|
||||
|
||||
def f():
|
||||
# SIM110
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM110
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
elif x.is_empty():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
elif x.is_empty():
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
def f():
|
||||
for x in iterable: # SIM110
|
||||
# SIM110
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
for el in [1, 2, 3]:
|
||||
if is_true(el):
|
||||
@@ -13,21 +21,97 @@ def f():
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable: # SIM111
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable: # SIM 111
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if not x.is_empty():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return "foo"
|
||||
return "bar"
|
||||
|
||||
|
||||
def f():
|
||||
# SIM110
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM110
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def f():
|
||||
# SIM111
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
elif x.is_empty():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def f():
|
||||
for x in iterable:
|
||||
if check(x):
|
||||
return True
|
||||
elif x.is_empty():
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
return False
|
||||
|
||||
19
resources/test/fixtures/flake8_simplify/SIM112.py
vendored
Normal file
19
resources/test/fixtures/flake8_simplify/SIM112.py
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
import os
|
||||
|
||||
# Bad
|
||||
os.environ['foo']
|
||||
|
||||
os.environ.get('foo')
|
||||
|
||||
os.environ.get('foo', 'bar')
|
||||
|
||||
os.getenv('foo')
|
||||
|
||||
# Good
|
||||
os.environ['FOO']
|
||||
|
||||
os.environ.get('FOO')
|
||||
|
||||
os.environ.get('FOO', 'bar')
|
||||
|
||||
os.getenv('FOO')
|
||||
6
resources/test/fixtures/flake8_simplify/SIM115.py
vendored
Normal file
6
resources/test/fixtures/flake8_simplify/SIM115.py
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
f = open('foo.txt') # SIM115
|
||||
data = f.read()
|
||||
f.close()
|
||||
|
||||
with open('foo.txt') as f: # OK
|
||||
data = f.read()
|
||||
87
resources/test/fixtures/flake8_simplify/SIM401.py
vendored
Normal file
87
resources/test/fixtures/flake8_simplify/SIM401.py
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
###
|
||||
# Positive cases
|
||||
###
|
||||
|
||||
# SIM401 (pattern-1)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
else:
|
||||
var = "default1"
|
||||
|
||||
# SIM401 (pattern-2)
|
||||
if key not in a_dict:
|
||||
var = "default2"
|
||||
else:
|
||||
var = a_dict[key]
|
||||
|
||||
# SIM401 (default with a complex expression)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
else:
|
||||
var = val1 + val2
|
||||
|
||||
# SIM401 (complex expression in key)
|
||||
if keys[idx] in a_dict:
|
||||
var = a_dict[keys[idx]]
|
||||
else:
|
||||
var = "default"
|
||||
|
||||
# SIM401 (complex expression in dict)
|
||||
if key in dicts[idx]:
|
||||
var = dicts[idx][key]
|
||||
else:
|
||||
var = "default"
|
||||
|
||||
# SIM401 (complex expression in var)
|
||||
if key in a_dict:
|
||||
vars[idx] = a_dict[key]
|
||||
else:
|
||||
vars[idx] = "default"
|
||||
|
||||
###
|
||||
# Negative cases
|
||||
###
|
||||
|
||||
# OK (false negative)
|
||||
if not key in a_dict:
|
||||
var = "default"
|
||||
else:
|
||||
var = a_dict[key]
|
||||
|
||||
# OK (different dict)
|
||||
if key in a_dict:
|
||||
var = other_dict[key]
|
||||
else:
|
||||
var = "default"
|
||||
|
||||
# OK (different key)
|
||||
if key in a_dict:
|
||||
var = a_dict[other_key]
|
||||
else:
|
||||
var = "default"
|
||||
|
||||
# OK (different var)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
else:
|
||||
other_var = "default"
|
||||
|
||||
# OK (extra vars in body)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
var2 = value2
|
||||
else:
|
||||
var = "default"
|
||||
|
||||
# OK (extra vars in orelse)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
else:
|
||||
var2 = value2
|
||||
var = "default"
|
||||
|
||||
# OK (complex default value)
|
||||
if key in a_dict:
|
||||
var = a_dict[key]
|
||||
else:
|
||||
var = foo()
|
||||
@@ -77,6 +77,34 @@ class C:
|
||||
def f(x):
|
||||
...
|
||||
|
||||
def f(self, x):
|
||||
"""Docstring."""
|
||||
|
||||
def f(self, x):
|
||||
"""Docstring."""
|
||||
...
|
||||
|
||||
def f(self, x):
|
||||
pass
|
||||
|
||||
def f(self, x):
|
||||
raise NotImplementedError
|
||||
|
||||
def f(self, x):
|
||||
raise NotImplementedError()
|
||||
|
||||
def f(self, x):
|
||||
raise NotImplementedError("...")
|
||||
|
||||
def f(self, x):
|
||||
raise NotImplemented
|
||||
|
||||
def f(self, x):
|
||||
raise NotImplemented()
|
||||
|
||||
def f(self, x):
|
||||
raise NotImplemented("...")
|
||||
|
||||
###
|
||||
# Unused functions attached to abstract methods (OK).
|
||||
###
|
||||
@@ -153,3 +181,17 @@ def f(a: int, b: int) -> str:
|
||||
|
||||
def f(a, b):
|
||||
return f"{a}{b}"
|
||||
|
||||
|
||||
###
|
||||
# Unused arguments on magic methods.
|
||||
###
|
||||
class C:
|
||||
def __init__(self, x) -> None:
|
||||
print("Hello, world!")
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "Hello, world!"
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback) -> None:
|
||||
print("Hello, world!")
|
||||
|
||||
11
resources/test/fixtures/isort/force_sort_within_sections.py
vendored
Normal file
11
resources/test/fixtures/isort/force_sort_within_sections.py
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
from a import a1 # import_from
|
||||
from c import * # import_from_star
|
||||
import a # import
|
||||
import c.d
|
||||
import b as b1 # import_as
|
||||
|
||||
from ..parent import *
|
||||
from .my import fn
|
||||
from . import my
|
||||
from .my.nested import fn2
|
||||
from ...grandparent import fn3
|
||||
3
resources/test/fixtures/isort/relative_imports_order.py
vendored
Normal file
3
resources/test/fixtures/isort/relative_imports_order.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
from ... import a
|
||||
from .. import b
|
||||
from . import c
|
||||
3
resources/test/fixtures/isort/required_imports/comment.py
vendored
Normal file
3
resources/test/fixtures/isort/required_imports/comment.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
x = 1
|
||||
3
resources/test/fixtures/isort/required_imports/docstring.py
vendored
Normal file
3
resources/test/fixtures/isort/required_imports/docstring.py
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Hello, world!"""
|
||||
|
||||
x = 1
|
||||
1
resources/test/fixtures/isort/required_imports/docstring_only.py
vendored
Normal file
1
resources/test/fixtures/isort/required_imports/docstring_only.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Hello, world!"""
|
||||
2
resources/test/fixtures/isort/required_imports/docstring_with_continuation.py
vendored
Normal file
2
resources/test/fixtures/isort/required_imports/docstring_with_continuation.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Hello, world!"""; x = \
|
||||
1; y = 2
|
||||
1
resources/test/fixtures/isort/required_imports/docstring_with_semicolon.py
vendored
Normal file
1
resources/test/fixtures/isort/required_imports/docstring_with_semicolon.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
"""Hello, world!"""; x = 1
|
||||
0
resources/test/fixtures/isort/required_imports/empty.py
vendored
Normal file
0
resources/test/fixtures/isort/required_imports/empty.py
vendored
Normal file
2
resources/test/fixtures/isort/required_imports/existing_import.py
vendored
Normal file
2
resources/test/fixtures/isort/required_imports/existing_import.py
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
from __future__ import generator_stop
|
||||
import os
|
||||
28
resources/test/fixtures/isort/skip.py
vendored
28
resources/test/fixtures/isort/skip.py
vendored
@@ -1,10 +1,20 @@
|
||||
# isort: off
|
||||
import sys
|
||||
import os
|
||||
import collections
|
||||
# isort: on
|
||||
def f():
|
||||
# isort: off
|
||||
import sys
|
||||
import os
|
||||
import collections
|
||||
# isort: on
|
||||
|
||||
import sys
|
||||
import os # isort: skip
|
||||
import collections
|
||||
import abc
|
||||
|
||||
def f():
|
||||
import sys
|
||||
import os # isort: skip
|
||||
import collections
|
||||
import abc
|
||||
|
||||
|
||||
def f():
|
||||
import sys
|
||||
import os # isort:skip
|
||||
import collections
|
||||
import abc
|
||||
|
||||
15
resources/test/fixtures/pycodestyle/W505.py
vendored
Normal file
15
resources/test/fixtures/pycodestyle/W505.py
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Here's a top-level docstring that's over the limit."""
|
||||
|
||||
|
||||
def f():
|
||||
"""Here's a docstring that's also over the limit."""
|
||||
|
||||
x = 1 # Here's a comment that's over the limit, but it's not standalone.
|
||||
|
||||
# Here's a standalone comment that's over the limit.
|
||||
|
||||
print("Here's a string that's over the limit, but it's not a docstring.")
|
||||
|
||||
|
||||
"This is also considered a docstring, and is over the limit."
|
||||
38
resources/test/fixtures/pyflakes/F601.py
vendored
38
resources/test/fixtures/pyflakes/F601.py
vendored
@@ -10,3 +10,41 @@ x = {
|
||||
b"123": 1,
|
||||
b"123": 4,
|
||||
}
|
||||
|
||||
x = {
|
||||
"a": 1,
|
||||
"a": 2,
|
||||
"a": 3,
|
||||
"a": 3,
|
||||
}
|
||||
|
||||
x = {
|
||||
"a": 1,
|
||||
"a": 2,
|
||||
"a": 3,
|
||||
"a": 3,
|
||||
"a": 4,
|
||||
}
|
||||
|
||||
x = {
|
||||
"a": 1,
|
||||
"a": 1,
|
||||
"a": 2,
|
||||
"a": 3,
|
||||
"a": 4,
|
||||
}
|
||||
|
||||
x = {
|
||||
a: 1,
|
||||
"a": 1,
|
||||
a: 1,
|
||||
"a": 2,
|
||||
a: 2,
|
||||
"a": 3,
|
||||
a: 3,
|
||||
"a": 3,
|
||||
a: 4,
|
||||
}
|
||||
|
||||
x = {"a": 1, "a": 1}
|
||||
x = {"a": 1, "b": 2, "a": 1}
|
||||
|
||||
38
resources/test/fixtures/pyflakes/F602.py
vendored
38
resources/test/fixtures/pyflakes/F602.py
vendored
@@ -5,3 +5,41 @@ x = {
|
||||
a: 2,
|
||||
b: 3,
|
||||
}
|
||||
|
||||
x = {
|
||||
a: 1,
|
||||
a: 2,
|
||||
a: 3,
|
||||
a: 3,
|
||||
}
|
||||
|
||||
x = {
|
||||
a: 1,
|
||||
a: 2,
|
||||
a: 3,
|
||||
a: 3,
|
||||
a: 4,
|
||||
}
|
||||
|
||||
x = {
|
||||
a: 1,
|
||||
a: 1,
|
||||
a: 2,
|
||||
a: 3,
|
||||
a: 4,
|
||||
}
|
||||
|
||||
x = {
|
||||
a: 1,
|
||||
"a": 1,
|
||||
a: 1,
|
||||
"a": 2,
|
||||
a: 2,
|
||||
"a": 3,
|
||||
a: 3,
|
||||
"a": 3,
|
||||
a: 4,
|
||||
}
|
||||
|
||||
x = {a: 1, a: 1}
|
||||
x = {a: 1, b: 2, a: 1}
|
||||
|
||||
1
resources/test/fixtures/pyflakes/builtins.py
vendored
Normal file
1
resources/test/fixtures/pyflakes/builtins.py
vendored
Normal file
@@ -0,0 +1 @@
|
||||
_("Translations")
|
||||
7
resources/test/fixtures/pyflakes/typing_modules.py
vendored
Normal file
7
resources/test/fixtures/pyflakes/typing_modules.py
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
from typing import Union
|
||||
|
||||
from airflow.typing_compat import Literal, Optional
|
||||
|
||||
|
||||
X = Union[Literal[False], Literal["db"]]
|
||||
y = Optional["Class"]
|
||||
8
resources/test/fixtures/pyupgrade/UP024_3.py
vendored
Normal file
8
resources/test/fixtures/pyupgrade/UP024_3.py
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
class SocketError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
raise SocketError()
|
||||
except SocketError:
|
||||
pass
|
||||
36
resources/test/fixtures/pyupgrade/UP030_0.py
vendored
Normal file
36
resources/test/fixtures/pyupgrade/UP030_0.py
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Invalid calls; errors expected.
|
||||
|
||||
"{0}" "{1}" "{2}".format(1, 2, 3)
|
||||
|
||||
"a {3} complicated {1} string with {0} {2}".format(
|
||||
"first", "second", "third", "fourth"
|
||||
)
|
||||
|
||||
'{0}'.format(1)
|
||||
|
||||
'{0:x}'.format(30)
|
||||
|
||||
x = '{0}'.format(1)
|
||||
|
||||
'''{0}\n{1}\n'''.format(1, 2)
|
||||
|
||||
x = "foo {0}" \
|
||||
"bar {1}".format(1, 2)
|
||||
|
||||
("{0}").format(1)
|
||||
|
||||
"\N{snowman} {0}".format(1)
|
||||
|
||||
'{' '0}'.format(1)
|
||||
|
||||
# These will not change because we are waiting for libcst to fix this issue:
|
||||
# https://github.com/Instagram/LibCST/issues/846
|
||||
print(
|
||||
'foo{0}'
|
||||
'bar{1}'.format(1, 2)
|
||||
)
|
||||
|
||||
print(
|
||||
'foo{0}' # ohai\n"
|
||||
'bar{1}'.format(1, 2)
|
||||
)
|
||||
23
resources/test/fixtures/pyupgrade/UP030_1.py
vendored
Normal file
23
resources/test/fixtures/pyupgrade/UP030_1.py
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# Valid calls; no errors expected.
|
||||
|
||||
'{}'.format(1)
|
||||
|
||||
|
||||
x = ('{0} {1}',)
|
||||
|
||||
'{0} {0}'.format(1)
|
||||
|
||||
'{0:<{1}}'.format(1, 4)
|
||||
|
||||
f"{0}".format(a)
|
||||
|
||||
f"{0}".format(1)
|
||||
|
||||
print(f"{0}".format(1))
|
||||
|
||||
# I did not include the following tests because ruff does not seem to work with
|
||||
# invalid python syntax (which is a good thing)
|
||||
|
||||
# "{0}"format(1)
|
||||
# '{'.format(1)", "'}'.format(1)
|
||||
# ("{0}" # {1}\n"{2}").format(1, 2, 3)
|
||||
@@ -17,7 +17,7 @@ resources/test/project/examples/docs/docs/file.py:8:5: F841 Local variable `x` i
|
||||
resources/test/project/project/file.py:1:8: F401 `os` imported but unused
|
||||
resources/test/project/project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
Found 7 error(s).
|
||||
6 potentially fixable with the --fix option.
|
||||
7 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Running from the project directory itself should exhibit the same behavior:
|
||||
@@ -32,7 +32,7 @@ examples/docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never
|
||||
project/file.py:1:8: F401 `os` imported but unused
|
||||
project/import_file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
Found 7 error(s).
|
||||
6 potentially fixable with the --fix option.
|
||||
7 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Running from the sub-package directory should exhibit the same behavior, but omit the top-level
|
||||
@@ -43,7 +43,7 @@ files:
|
||||
docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
Found 2 error(s).
|
||||
1 potentially fixable with the --fix option.
|
||||
2 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
`--config` should force Ruff to use the specified `pyproject.toml` for all files, and resolve
|
||||
@@ -74,7 +74,7 @@ docs/docs/file.py:1:1: I001 Import block is un-sorted or un-formatted
|
||||
docs/docs/file.py:8:5: F841 Local variable `x` is assigned to but never used
|
||||
excluded/script.py:5:5: F841 Local variable `x` is assigned to but never used
|
||||
Found 4 error(s).
|
||||
1 potentially fixable with the --fix option.
|
||||
4 potentially fixable with the --fix option.
|
||||
```
|
||||
|
||||
Passing an excluded directory directly should report errors in the contained files:
|
||||
|
||||
1193
ruff.schema.json
1193
ruff.schema.json
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,12 @@
|
||||
[package]
|
||||
name = "ruff_dev"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "ruff_dev"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = { version = "1.0.66" }
|
||||
clap = { version = "4.0.1", features = ["derive"] }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use anyhow::Result;
|
||||
use clap::Args;
|
||||
use itertools::Itertools;
|
||||
use ruff::registry::{CheckCategory, CheckCode};
|
||||
use ruff::registry::{RuleCode, RuleOrigin};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::utils::replace_readme_section;
|
||||
@@ -25,24 +25,24 @@ pub fn main(cli: &Cli) -> Result<()> {
|
||||
// Generate the table string.
|
||||
let mut table_out = String::new();
|
||||
let mut toc_out = String::new();
|
||||
for check_category in CheckCategory::iter() {
|
||||
let codes_csv: String = check_category.codes().iter().map(AsRef::as_ref).join(", ");
|
||||
table_out.push_str(&format!("### {} ({codes_csv})", check_category.title()));
|
||||
for origin in RuleOrigin::iter() {
|
||||
let codes_csv: String = origin.codes().iter().map(AsRef::as_ref).join(", ");
|
||||
table_out.push_str(&format!("### {} ({codes_csv})", origin.title()));
|
||||
table_out.push('\n');
|
||||
table_out.push('\n');
|
||||
|
||||
toc_out.push_str(&format!(
|
||||
" 1. [{} ({})](#{}-{})\n",
|
||||
check_category.title(),
|
||||
origin.title(),
|
||||
codes_csv,
|
||||
check_category.title().to_lowercase().replace(' ', "-"),
|
||||
origin.title().to_lowercase().replace(' ', "-"),
|
||||
codes_csv.to_lowercase().replace(',', "-").replace(' ', "")
|
||||
));
|
||||
|
||||
if let Some((url, platform)) = check_category.url() {
|
||||
if let Some((url, platform)) = origin.url() {
|
||||
table_out.push_str(&format!(
|
||||
"For more, see [{}]({}) on {}.",
|
||||
check_category.title(),
|
||||
origin.title(),
|
||||
url,
|
||||
platform
|
||||
));
|
||||
@@ -55,15 +55,15 @@ pub fn main(cli: &Cli) -> Result<()> {
|
||||
table_out.push_str("| ---- | ---- | ------- | --- |");
|
||||
table_out.push('\n');
|
||||
|
||||
for check_code in CheckCode::iter() {
|
||||
if check_code.category() == check_category {
|
||||
let check_kind = check_code.kind();
|
||||
let fix_token = if check_kind.fixable() { "🛠" } else { "" };
|
||||
for rule_code in RuleCode::iter() {
|
||||
if rule_code.origin() == origin {
|
||||
let kind = rule_code.kind();
|
||||
let fix_token = if kind.fixable() { "🛠" } else { "" };
|
||||
table_out.push_str(&format!(
|
||||
"| {} | {} | {} | {} |",
|
||||
check_kind.code().as_ref(),
|
||||
check_kind.as_ref(),
|
||||
check_kind.summary().replace('|', r"\|"),
|
||||
kind.code().as_ref(),
|
||||
kind.as_ref(),
|
||||
kind.summary().replace('|', r"\|"),
|
||||
fix_token
|
||||
));
|
||||
table_out.push('\n');
|
||||
|
||||
@@ -5,9 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use clap::Args;
|
||||
use ruff::source_code_generator::SourceCodeGenerator;
|
||||
use ruff::source_code_locator::SourceCodeLocator;
|
||||
use ruff::source_code_style::SourceCodeStyleDetector;
|
||||
use ruff::source_code::{Generator, Locator, Stylist};
|
||||
use rustpython_parser::parser;
|
||||
|
||||
#[derive(Args)]
|
||||
@@ -20,9 +18,9 @@ pub struct Cli {
|
||||
pub fn main(cli: &Cli) -> Result<()> {
|
||||
let contents = fs::read_to_string(&cli.file)?;
|
||||
let python_ast = parser::parse_program(&contents, &cli.file.to_string_lossy())?;
|
||||
let locator = SourceCodeLocator::new(&contents);
|
||||
let stylist = SourceCodeStyleDetector::from_contents(&contents, &locator);
|
||||
let mut generator: SourceCodeGenerator = (&stylist).into();
|
||||
let locator = Locator::new(&contents);
|
||||
let stylist = Stylist::from_contents(&contents, &locator);
|
||||
let mut generator: Generator = (&stylist).into();
|
||||
generator.unparse_suite(&python_ast);
|
||||
println!("{}", generator.generate());
|
||||
Ok(())
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[package]
|
||||
name = "ruff_macros"
|
||||
version = "0.0.214"
|
||||
version = "0.0.220"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
once_cell = { version = "1.17.0" }
|
||||
|
||||
@@ -12,10 +12,13 @@
|
||||
)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
use proc_macro2::Span;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, DeriveInput, Ident};
|
||||
|
||||
mod check_code_prefix;
|
||||
mod config;
|
||||
mod prefixes;
|
||||
mod rule_code_prefix;
|
||||
|
||||
#[proc_macro_derive(ConfigurationOptions, attributes(option, doc, option_group))]
|
||||
pub fn derive_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
@@ -26,11 +29,31 @@ pub fn derive_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(CheckCodePrefix)]
|
||||
pub fn derive_check_code_prefix(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
#[proc_macro_derive(RuleCodePrefix)]
|
||||
pub fn derive_rule_code_prefix(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
check_code_prefix::derive_impl(input)
|
||||
rule_code_prefix::derive_impl(input)
|
||||
.unwrap_or_else(syn::Error::into_compile_error)
|
||||
.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn origin_by_code(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let ident = parse_macro_input!(item as Ident).to_string();
|
||||
let mut iter = prefixes::PREFIX_TO_ORIGIN.iter();
|
||||
let origin = loop {
|
||||
let (prefix, origin) = iter
|
||||
.next()
|
||||
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
|
||||
if ident.starts_with(prefix) {
|
||||
break origin;
|
||||
}
|
||||
};
|
||||
let prefix = Ident::new(origin, Span::call_site());
|
||||
|
||||
quote! {
|
||||
RuleOrigin::#prefix
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
53
ruff_macros/src/prefixes.rs
Normal file
53
ruff_macros/src/prefixes.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
// Longer prefixes should come first so that you can find an origin for a code
|
||||
// by simply picking the first entry that starts with the given prefix.
|
||||
|
||||
pub const PREFIX_TO_ORIGIN: &[(&str, &str)] = &[
|
||||
("ANN", "Flake8Annotations"),
|
||||
("ARG", "Flake8UnusedArguments"),
|
||||
("A", "Flake8Builtins"),
|
||||
("BLE", "Flake8BlindExcept"),
|
||||
("B", "Flake8Bugbear"),
|
||||
("C4", "Flake8Comprehensions"),
|
||||
("C9", "McCabe"),
|
||||
("DTZ", "Flake8Datetimez"),
|
||||
("D", "Pydocstyle"),
|
||||
("ERA", "Eradicate"),
|
||||
("EM", "Flake8ErrMsg"),
|
||||
("E", "Pycodestyle"),
|
||||
("FBT", "Flake8BooleanTrap"),
|
||||
("F", "Pyflakes"),
|
||||
("ICN", "Flake8ImportConventions"),
|
||||
("ISC", "Flake8ImplicitStrConcat"),
|
||||
("I", "Isort"),
|
||||
("N", "PEP8Naming"),
|
||||
("PD", "PandasVet"),
|
||||
("PGH", "PygrepHooks"),
|
||||
("PL", "Pylint"),
|
||||
("PT", "Flake8PytestStyle"),
|
||||
("Q", "Flake8Quotes"),
|
||||
("RET", "Flake8Return"),
|
||||
("SIM", "Flake8Simplify"),
|
||||
("S", "Flake8Bandit"),
|
||||
("T10", "Flake8Debugger"),
|
||||
("T20", "Flake8Print"),
|
||||
("TID", "Flake8TidyImports"),
|
||||
("UP", "Pyupgrade"),
|
||||
("W", "Pycodestyle"),
|
||||
("YTT", "Flake82020"),
|
||||
("PIE", "Flake8Pie"),
|
||||
("RUF", "Ruff"),
|
||||
];
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::PREFIX_TO_ORIGIN;
|
||||
|
||||
#[test]
|
||||
fn order() {
|
||||
for (idx, (prefix, _)) in PREFIX_TO_ORIGIN.iter().enumerate() {
|
||||
for (prior_prefix, _) in PREFIX_TO_ORIGIN[..idx].iter() {
|
||||
assert!(!prefix.starts_with(prior_prefix));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,8 @@ use syn::{DataEnum, DeriveInput, Ident, Variant};
|
||||
|
||||
const ALL: &str = "ALL";
|
||||
|
||||
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
|
||||
/// A hash map from deprecated `RuleCodePrefix` to latest
|
||||
/// `RuleCodePrefix`.
|
||||
pub static PREFIX_REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
|
||||
HashMap::from_iter([
|
||||
// TODO(charlie): Remove by 2023-01-01.
|
||||
@@ -90,7 +91,7 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
|
||||
let syn::Data::Enum(DataEnum { variants, .. }) = data else {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"Can only derive `CheckCodePrefix` from enums.",
|
||||
"Can only derive `RuleCodePrefix` from enums.",
|
||||
));
|
||||
};
|
||||
|
||||
@@ -117,7 +118,7 @@ fn expand(
|
||||
prefix_ident: &Ident,
|
||||
variants: &Punctuated<Variant, Comma>,
|
||||
) -> proc_macro2::TokenStream {
|
||||
// Build up a map from prefix to matching CheckCodes.
|
||||
// Build up a map from prefix to matching RuleCodes.
|
||||
let mut prefix_to_codes: BTreeMap<Ident, BTreeSet<String>> = BTreeMap::default();
|
||||
for variant in variants {
|
||||
let span = variant.ident.span();
|
||||
@@ -141,12 +142,12 @@ fn expand(
|
||||
}
|
||||
|
||||
// Add any prefix aliases (e.g., "U" to "UP").
|
||||
for (alias, check_code) in PREFIX_REDIRECTS.iter() {
|
||||
for (alias, rule_code) in PREFIX_REDIRECTS.iter() {
|
||||
prefix_to_codes.insert(
|
||||
Ident::new(alias, Span::call_site()),
|
||||
prefix_to_codes
|
||||
.get(&Ident::new(check_code, Span::call_site()))
|
||||
.unwrap_or_else(|| panic!("Unknown CheckCode: {alias:?}"))
|
||||
.get(&Ident::new(rule_code, Span::call_site()))
|
||||
.unwrap_or_else(|| panic!("Unknown RuleCode: {alias:?}"))
|
||||
.clone(),
|
||||
);
|
||||
}
|
||||
@@ -159,8 +160,8 @@ fn expand(
|
||||
|
||||
let prefix_impl = generate_impls(ident, prefix_ident, &prefix_to_codes);
|
||||
|
||||
let prefix_redirects = PREFIX_REDIRECTS.iter().map(|(alias, check_code)| {
|
||||
let code = Ident::new(check_code, Span::call_site());
|
||||
let prefix_redirects = PREFIX_REDIRECTS.iter().map(|(alias, rule_code)| {
|
||||
let code = Ident::new(rule_code, Span::call_site());
|
||||
quote! {
|
||||
(#alias, #prefix_ident::#code)
|
||||
}
|
||||
@@ -186,7 +187,7 @@ fn expand(
|
||||
|
||||
#prefix_impl
|
||||
|
||||
/// A hash map from deprecated `CheckCodePrefix` to latest `CheckCodePrefix`.
|
||||
/// A hash map from deprecated `RuleCodePrefix` to latest `RuleCodePrefix`.
|
||||
pub static PREFIX_REDIRECTS: ::once_cell::sync::Lazy<::rustc_hash::FxHashMap<&'static str, #prefix_ident>> = ::once_cell::sync::Lazy::new(|| {
|
||||
::rustc_hash::FxHashMap::from_iter([
|
||||
#(#prefix_redirects),*
|
||||
@@ -211,11 +212,8 @@ fn generate_impls(
|
||||
if let Some(target) = PREFIX_REDIRECTS.get(prefix_str.as_str()) {
|
||||
quote! {
|
||||
#prefix_ident::#prefix => {
|
||||
crate::one_time_warning!(
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
format!("`{}` has been remapped to `{}`", #prefix_str, #target).bold()
|
||||
crate::warn_user_once!(
|
||||
"`{}` has been remapped to `{}`", #prefix_str, #target
|
||||
);
|
||||
vec![#(#codes),*]
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate boilerplate for a new check.
|
||||
|
||||
Example usage:
|
||||
|
||||
python scripts/add_check.py \
|
||||
--name PreferListBuiltin \
|
||||
--code PIE807 \
|
||||
--plugin flake8-pie
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def dir_name(plugin: str) -> str:
|
||||
return plugin.replace("-", "_")
|
||||
|
||||
|
||||
def pascal_case(plugin: str) -> str:
|
||||
"""Convert from snake-case to PascalCase."""
|
||||
return "".join(word.title() for word in plugin.split("-"))
|
||||
|
||||
|
||||
def snake_case(name: str) -> str:
|
||||
"""Convert from PascalCase to snake_case."""
|
||||
return "".join(f"_{word.lower()}" if word.isupper() else word for word in name).lstrip("_")
|
||||
|
||||
|
||||
def main(*, name: str, code: str, plugin: str) -> None:
|
||||
# Create a test fixture.
|
||||
with open(
|
||||
os.path.join(ROOT_DIR, f"resources/test/fixtures/{dir_name(plugin)}/{code}.py"),
|
||||
"a",
|
||||
):
|
||||
pass
|
||||
|
||||
# Add the relevant `#testcase` macro.
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "r") as fp:
|
||||
content = fp.read()
|
||||
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "w") as fp:
|
||||
for line in content.splitlines():
|
||||
if line.strip() == "fn checks(check_code: CheckCode, path: &Path) -> Result<()> {":
|
||||
indent = line.split("fn checks(check_code: CheckCode, path: &Path) -> Result<()> {")[0]
|
||||
fp.write(f'{indent}#[test_case(CheckCode::{code}, Path::new("{code}.py"); "{code}")]')
|
||||
fp.write("\n")
|
||||
|
||||
fp.write(line)
|
||||
fp.write("\n")
|
||||
|
||||
# Add the relevant plugin function.
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/plugins.rs"), "a") as fp:
|
||||
fp.write(
|
||||
f"""
|
||||
/// {code}
|
||||
pub fn {snake_case(name)}(checker: &mut Checker) {{}}
|
||||
"""
|
||||
)
|
||||
fp.write("\n")
|
||||
|
||||
# Add the relevant sections to `src/registry.rs`.
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "r") as fp:
|
||||
content = fp.read()
|
||||
|
||||
index = 0
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
|
||||
for line in content.splitlines():
|
||||
fp.write(line)
|
||||
fp.write("\n")
|
||||
|
||||
if line.strip() == f"// {plugin}":
|
||||
if index == 0:
|
||||
# `CheckCode` definition
|
||||
indent = line.split(f"// {plugin}")[0]
|
||||
fp.write(f"{indent}{code},")
|
||||
fp.write("\n")
|
||||
|
||||
elif index == 1:
|
||||
# `CheckKind` definition
|
||||
indent = line.split(f"// {plugin}")[0]
|
||||
fp.write(f"{indent}{name},")
|
||||
fp.write("\n")
|
||||
|
||||
elif index == 2:
|
||||
# `CheckCode#kind()`
|
||||
indent = line.split(f"// {plugin}")[0]
|
||||
fp.write(f"{indent}CheckCode::{code} => CheckKind::{name},")
|
||||
fp.write("\n")
|
||||
|
||||
elif index == 3:
|
||||
# `CheckCode#category()`
|
||||
indent = line.split(f"// {plugin}")[0]
|
||||
fp.write(f"{indent}CheckCode::{code} => CheckCategory::{pascal_case(plugin)},")
|
||||
fp.write("\n")
|
||||
|
||||
elif index == 4:
|
||||
# `CheckKind#code()`
|
||||
indent = line.split(f"// {plugin}")[0]
|
||||
fp.write(f"{indent}CheckKind::{name} => &CheckCode::{code},")
|
||||
fp.write("\n")
|
||||
|
||||
elif index == 5:
|
||||
# `CheckCode#body`
|
||||
indent = line.split(f"// {plugin}")[0]
|
||||
fp.write(f'{indent}CheckKind::{name} => todo!("Write message body for {code}"),')
|
||||
fp.write("\n")
|
||||
|
||||
index += 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate boilerplate for a new check.",
|
||||
epilog="python scripts/add_check.py --name PreferListBuiltin --code PIE807 --plugin flake8-pie",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The name of the check to generate, in PascalCase (e.g., 'LineTooLong').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--code",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The code of the check to generate (e.g., 'A001').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--plugin",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The plugin with which the check is associated (e.g., 'flake8-builtins').",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(name=args.name, code=args.code, plugin=args.plugin)
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate boilerplate for a new plugin.
|
||||
"""Generate boilerplate for a new Flake8 plugin.
|
||||
|
||||
Example usage:
|
||||
|
||||
@@ -31,10 +31,10 @@ def main(*, plugin: str, url: str) -> None:
|
||||
|
||||
# Create the Rust module.
|
||||
os.makedirs(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}"), exist_ok=True)
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/plugins.rs"), "a"):
|
||||
pass
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/rules.rs"), "w+") as fp:
|
||||
fp.write("use crate::checkers::ast::Checker;\n")
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(plugin)}/mod.rs"), "w+") as fp:
|
||||
fp.write("pub mod plugins;\n")
|
||||
fp.write("pub(crate) mod rules;\n")
|
||||
fp.write("\n")
|
||||
fp.write(
|
||||
"""#[cfg(test)]
|
||||
@@ -45,19 +45,19 @@ mod tests {
|
||||
use anyhow::Result;
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::registry::CheckCode;
|
||||
use crate::registry::RuleCode;
|
||||
use crate::linter::test_path;
|
||||
use crate::settings;
|
||||
|
||||
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
||||
let checks = test_path(
|
||||
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics =test_path(
|
||||
Path::new("./resources/test/fixtures/%s")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&settings::Settings::for_rule(check_code),
|
||||
&settings::Settings::for_rule(rule_code),
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
insta::assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -67,10 +67,10 @@ mod tests {
|
||||
|
||||
# Add the plugin to `lib.rs`.
|
||||
with open(os.path.join(ROOT_DIR, "src/lib.rs"), "a") as fp:
|
||||
fp.write(f"pub mod {dir_name(plugin)};")
|
||||
fp.write(f"mod {dir_name(plugin)};")
|
||||
|
||||
# Add the relevant sections to `src/registry.rs`.
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "r") as fp:
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp:
|
||||
content = fp.read()
|
||||
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
|
||||
@@ -85,23 +85,37 @@ mod tests {
|
||||
fp.write(f"{indent}{pascal_case(plugin)},")
|
||||
fp.write("\n")
|
||||
|
||||
elif line.strip() == 'CheckCategory::Ruff => "Ruff-specific rules",':
|
||||
indent = line.split('CheckCategory::Ruff => "Ruff-specific rules",')[0]
|
||||
fp.write(f'{indent}CheckCategory::{pascal_case(plugin)} => "{plugin}",')
|
||||
elif line.strip() == 'RuleOrigin::Ruff => "Ruff-specific rules",':
|
||||
indent = line.split('RuleOrigin::Ruff => "Ruff-specific rules",')[0]
|
||||
fp.write(f'{indent}RuleOrigin::{pascal_case(plugin)} => "{plugin}",')
|
||||
fp.write("\n")
|
||||
|
||||
elif line.strip() == "CheckCategory::Ruff => vec![CheckCodePrefix::RUF],":
|
||||
indent = line.split("CheckCategory::Ruff => vec![CheckCodePrefix::RUF],")[0]
|
||||
elif line.strip() == "RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],":
|
||||
indent = line.split("RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],")[0]
|
||||
fp.write(
|
||||
f"{indent}CheckCategory::{pascal_case(plugin)} => vec![\n"
|
||||
f"{indent}RuleOrigin::{pascal_case(plugin)} => vec![\n"
|
||||
f'{indent} todo!("Fill-in prefix after generating codes")\n'
|
||||
f"{indent}],"
|
||||
)
|
||||
fp.write("\n")
|
||||
|
||||
elif line.strip() == "CheckCategory::Ruff => None,":
|
||||
indent = line.split("CheckCategory::Ruff => None,")[0]
|
||||
fp.write(f"{indent}CheckCategory::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),')
|
||||
elif line.strip() == "RuleOrigin::Ruff => None,":
|
||||
indent = line.split("RuleOrigin::Ruff => None,")[0]
|
||||
fp.write(f"{indent}RuleOrigin::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),')
|
||||
fp.write("\n")
|
||||
|
||||
fp.write(line)
|
||||
fp.write("\n")
|
||||
|
||||
# Add the relevant section to `src/violations.rs`.
|
||||
with open(os.path.join(ROOT_DIR, "src/violations.rs")) as fp:
|
||||
content = fp.read()
|
||||
|
||||
with open(os.path.join(ROOT_DIR, "src/violations.rs"), "w") as fp:
|
||||
for line in content.splitlines():
|
||||
if line.strip() == "// Ruff":
|
||||
indent = line.split("// Ruff")[0]
|
||||
fp.write(f"{indent}// {plugin}")
|
||||
fp.write("\n")
|
||||
|
||||
fp.write(line)
|
||||
@@ -110,7 +124,7 @@ mod tests {
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate boilerplate for a new plugin.",
|
||||
description="Generate boilerplate for a new Flake8 plugin.",
|
||||
epilog=(
|
||||
"Example usage: python scripts/add_plugin.py flake8-pie "
|
||||
"--url https://pypi.org/project/flake8-pie/0.16.0/"
|
||||
@@ -118,7 +132,6 @@ if __name__ == "__main__":
|
||||
)
|
||||
parser.add_argument(
|
||||
"plugin",
|
||||
required=True,
|
||||
type=str,
|
||||
help="The name of the plugin to generate.",
|
||||
)
|
||||
|
||||
145
scripts/add_rule.py
Normal file
145
scripts/add_rule.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate boilerplate for a new rule.
|
||||
|
||||
Example usage:
|
||||
|
||||
python scripts/add_rule.py \
|
||||
--name PreferListBuiltin \
|
||||
--code PIE807 \
|
||||
--origin flake8-pie
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
|
||||
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
def dir_name(origin: str) -> str:
|
||||
return origin.replace("-", "_")
|
||||
|
||||
|
||||
def pascal_case(origin: str) -> str:
|
||||
"""Convert from snake-case to PascalCase."""
|
||||
return "".join(word.title() for word in origin.split("-"))
|
||||
|
||||
|
||||
def snake_case(name: str) -> str:
|
||||
"""Convert from PascalCase to snake_case."""
|
||||
return "".join(f"_{word.lower()}" if word.isupper() else word for word in name).lstrip("_")
|
||||
|
||||
|
||||
def main(*, name: str, code: str, origin: str) -> None:
|
||||
# Create a test fixture.
|
||||
with open(
|
||||
os.path.join(ROOT_DIR, f"resources/test/fixtures/{dir_name(origin)}/{code}.py"),
|
||||
"a",
|
||||
):
|
||||
pass
|
||||
|
||||
# Add the relevant `#testcase` macro.
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/mod.rs")) as fp:
|
||||
content = fp.read()
|
||||
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/mod.rs"), "w") as fp:
|
||||
for line in content.splitlines():
|
||||
if line.strip() == "fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {":
|
||||
indent = line.split("fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {")[0]
|
||||
fp.write(f'{indent}#[test_case(RuleCode::{code}, Path::new("{code}.py"); "{code}")]')
|
||||
fp.write("\n")
|
||||
|
||||
fp.write(line)
|
||||
fp.write("\n")
|
||||
|
||||
# Add the relevant rule function.
|
||||
with open(os.path.join(ROOT_DIR, f"src/{dir_name(origin)}/rules.rs"), "a") as fp:
|
||||
fp.write(
|
||||
f"""
|
||||
/// {code}
|
||||
pub fn {snake_case(name)}(checker: &mut Checker) {{}}
|
||||
"""
|
||||
)
|
||||
fp.write("\n")
|
||||
|
||||
# Add the relevant struct to `src/violations.rs`.
|
||||
with open(os.path.join(ROOT_DIR, "src/violations.rs")) as fp:
|
||||
content = fp.read()
|
||||
|
||||
with open(os.path.join(ROOT_DIR, "src/violations.rs"), "w") as fp:
|
||||
for line in content.splitlines():
|
||||
fp.write(line)
|
||||
fp.write("\n")
|
||||
|
||||
if line.startswith(f"// {origin}"):
|
||||
fp.write(
|
||||
"""define_violation!(
|
||||
pub struct %s;
|
||||
);
|
||||
impl Violation for %s {
|
||||
fn message(&self) -> String {
|
||||
todo!("Implement message")
|
||||
}
|
||||
|
||||
fn placeholder() -> Self {
|
||||
%s
|
||||
}
|
||||
}
|
||||
"""
|
||||
% (name, name, name)
|
||||
)
|
||||
fp.write("\n")
|
||||
|
||||
# Add the relevant code-to-violation pair to `src/registry.rs`.
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs")) as fp:
|
||||
content = fp.read()
|
||||
|
||||
seen_macro = False
|
||||
has_written = False
|
||||
with open(os.path.join(ROOT_DIR, "src/registry.rs"), "w") as fp:
|
||||
for line in content.splitlines():
|
||||
fp.write(line)
|
||||
fp.write("\n")
|
||||
|
||||
if has_written:
|
||||
continue
|
||||
|
||||
if line.startswith("define_rule_mapping!"):
|
||||
seen_macro = True
|
||||
continue
|
||||
|
||||
if not seen_macro:
|
||||
continue
|
||||
|
||||
if line.strip() == f"// {origin}":
|
||||
indent = line.split("//")[0]
|
||||
fp.write(f"{indent}{code} => violations::{name},")
|
||||
fp.write("\n")
|
||||
has_written = True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate boilerplate for a new rule.",
|
||||
epilog="python scripts/add_rule.py --name PreferListBuiltin --code PIE807 --origin flake8-pie",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--name",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The name of the check to generate, in PascalCase (e.g., 'LineTooLong').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--code",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The code of the check to generate (e.g., 'A001').",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--origin",
|
||||
type=str,
|
||||
required=True,
|
||||
help="The source with which the check originated (e.g., 'flake8-builtins').",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
main(name=args.name, code=args.code, origin=args.origin)
|
||||
@@ -1,5 +1,12 @@
|
||||
use rustpython_ast::{Expr, Stmt, StmtKind};
|
||||
|
||||
pub fn name(stmt: &Stmt) -> &str {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { name, .. } | StmtKind::AsyncFunctionDef { name, .. } => name,
|
||||
_ => panic!("Expected StmtKind::FunctionDef | StmtKind::AsyncFunctionDef"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decorator_list(stmt: &Stmt) -> &Vec<Expr> {
|
||||
match &stmt.node {
|
||||
StmtKind::FunctionDef { decorator_list, .. }
|
||||
|
||||
530
src/ast/comparable.rs
Normal file
530
src/ast/comparable.rs
Normal file
@@ -0,0 +1,530 @@
|
||||
//! An equivalent object hierarchy to the `Expr` hierarchy, but with the ability
|
||||
//! to compare expressions for equality (via `Eq` and `Hash`).
|
||||
|
||||
use num_bigint::BigInt;
|
||||
use rustpython_ast::{
|
||||
Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Keyword,
|
||||
Operator, Unaryop,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableExprContext {
|
||||
Load,
|
||||
Store,
|
||||
Del,
|
||||
}
|
||||
|
||||
impl From<&ExprContext> for ComparableExprContext {
|
||||
fn from(ctx: &ExprContext) -> Self {
|
||||
match ctx {
|
||||
ExprContext::Load => Self::Load,
|
||||
ExprContext::Store => Self::Store,
|
||||
ExprContext::Del => Self::Del,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableBoolop {
|
||||
And,
|
||||
Or,
|
||||
}
|
||||
|
||||
impl From<&Boolop> for ComparableBoolop {
|
||||
fn from(op: &Boolop) -> Self {
|
||||
match op {
|
||||
Boolop::And => Self::And,
|
||||
Boolop::Or => Self::Or,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableOperator {
|
||||
Add,
|
||||
Sub,
|
||||
Mult,
|
||||
MatMult,
|
||||
Div,
|
||||
Mod,
|
||||
Pow,
|
||||
LShift,
|
||||
RShift,
|
||||
BitOr,
|
||||
BitXor,
|
||||
BitAnd,
|
||||
FloorDiv,
|
||||
}
|
||||
|
||||
impl From<&Operator> for ComparableOperator {
|
||||
fn from(op: &Operator) -> Self {
|
||||
match op {
|
||||
Operator::Add => Self::Add,
|
||||
Operator::Sub => Self::Sub,
|
||||
Operator::Mult => Self::Mult,
|
||||
Operator::MatMult => Self::MatMult,
|
||||
Operator::Div => Self::Div,
|
||||
Operator::Mod => Self::Mod,
|
||||
Operator::Pow => Self::Pow,
|
||||
Operator::LShift => Self::LShift,
|
||||
Operator::RShift => Self::RShift,
|
||||
Operator::BitOr => Self::BitOr,
|
||||
Operator::BitXor => Self::BitXor,
|
||||
Operator::BitAnd => Self::BitAnd,
|
||||
Operator::FloorDiv => Self::FloorDiv,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableUnaryop {
|
||||
Invert,
|
||||
Not,
|
||||
UAdd,
|
||||
USub,
|
||||
}
|
||||
|
||||
impl From<&Unaryop> for ComparableUnaryop {
|
||||
fn from(op: &Unaryop) -> Self {
|
||||
match op {
|
||||
Unaryop::Invert => Self::Invert,
|
||||
Unaryop::Not => Self::Not,
|
||||
Unaryop::UAdd => Self::UAdd,
|
||||
Unaryop::USub => Self::USub,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableCmpop {
|
||||
Eq,
|
||||
NotEq,
|
||||
Lt,
|
||||
LtE,
|
||||
Gt,
|
||||
GtE,
|
||||
Is,
|
||||
IsNot,
|
||||
In,
|
||||
NotIn,
|
||||
}
|
||||
|
||||
impl From<&Cmpop> for ComparableCmpop {
|
||||
fn from(op: &Cmpop) -> Self {
|
||||
match op {
|
||||
Cmpop::Eq => Self::Eq,
|
||||
Cmpop::NotEq => Self::NotEq,
|
||||
Cmpop::Lt => Self::Lt,
|
||||
Cmpop::LtE => Self::LtE,
|
||||
Cmpop::Gt => Self::Gt,
|
||||
Cmpop::GtE => Self::GtE,
|
||||
Cmpop::Is => Self::Is,
|
||||
Cmpop::IsNot => Self::IsNot,
|
||||
Cmpop::In => Self::In,
|
||||
Cmpop::NotIn => Self::NotIn,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableConstant<'a> {
|
||||
None,
|
||||
Bool(&'a bool),
|
||||
Str(&'a str),
|
||||
Bytes(&'a [u8]),
|
||||
Int(&'a BigInt),
|
||||
Tuple(Vec<ComparableConstant<'a>>),
|
||||
Float(u64),
|
||||
Complex { real: u64, imag: u64 },
|
||||
Ellipsis,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Constant> for ComparableConstant<'a> {
|
||||
fn from(constant: &'a Constant) -> Self {
|
||||
match constant {
|
||||
Constant::None => Self::None,
|
||||
Constant::Bool(value) => Self::Bool(value),
|
||||
Constant::Str(value) => Self::Str(value),
|
||||
Constant::Bytes(value) => Self::Bytes(value),
|
||||
Constant::Int(value) => Self::Int(value),
|
||||
Constant::Tuple(value) => {
|
||||
Self::Tuple(value.iter().map(std::convert::Into::into).collect())
|
||||
}
|
||||
Constant::Float(value) => Self::Float(value.to_bits()),
|
||||
Constant::Complex { real, imag } => Self::Complex {
|
||||
real: real.to_bits(),
|
||||
imag: imag.to_bits(),
|
||||
},
|
||||
Constant::Ellipsis => Self::Ellipsis,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableArguments<'a> {
|
||||
pub posonlyargs: Vec<ComparableArg<'a>>,
|
||||
pub args: Vec<ComparableArg<'a>>,
|
||||
pub vararg: Option<ComparableArg<'a>>,
|
||||
pub kwonlyargs: Vec<ComparableArg<'a>>,
|
||||
pub kw_defaults: Vec<ComparableExpr<'a>>,
|
||||
pub kwarg: Option<ComparableArg<'a>>,
|
||||
pub defaults: Vec<ComparableExpr<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Arguments> for ComparableArguments<'a> {
|
||||
fn from(arguments: &'a Arguments) -> Self {
|
||||
Self {
|
||||
posonlyargs: arguments
|
||||
.posonlyargs
|
||||
.iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
args: arguments
|
||||
.args
|
||||
.iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
vararg: arguments.vararg.as_ref().map(std::convert::Into::into),
|
||||
kwonlyargs: arguments
|
||||
.kwonlyargs
|
||||
.iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
kw_defaults: arguments
|
||||
.kw_defaults
|
||||
.iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
kwarg: arguments.vararg.as_ref().map(std::convert::Into::into),
|
||||
defaults: arguments
|
||||
.defaults
|
||||
.iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Arg>> for ComparableArg<'a> {
|
||||
fn from(arg: &'a Box<Arg>) -> Self {
|
||||
(&**arg).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableArg<'a> {
|
||||
pub arg: &'a str,
|
||||
pub annotation: Option<Box<ComparableExpr<'a>>>,
|
||||
pub type_comment: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Arg> for ComparableArg<'a> {
|
||||
fn from(arg: &'a Arg) -> Self {
|
||||
Self {
|
||||
arg: &arg.node.arg,
|
||||
annotation: arg.node.annotation.as_ref().map(std::convert::Into::into),
|
||||
type_comment: arg.node.type_comment.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableKeyword<'a> {
|
||||
pub arg: Option<&'a str>,
|
||||
pub value: ComparableExpr<'a>,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Keyword> for ComparableKeyword<'a> {
|
||||
fn from(keyword: &'a Keyword) -> Self {
|
||||
Self {
|
||||
arg: keyword.node.arg.as_deref(),
|
||||
value: (&keyword.node.value).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub struct ComparableComprehension<'a> {
|
||||
pub target: ComparableExpr<'a>,
|
||||
pub iter: ComparableExpr<'a>,
|
||||
pub ifs: Vec<ComparableExpr<'a>>,
|
||||
pub is_async: &'a usize,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> {
|
||||
fn from(comprehension: &'a Comprehension) -> Self {
|
||||
Self {
|
||||
target: (&comprehension.target).into(),
|
||||
iter: (&comprehension.iter).into(),
|
||||
ifs: comprehension
|
||||
.ifs
|
||||
.iter()
|
||||
.map(std::convert::Into::into)
|
||||
.collect(),
|
||||
is_async: &comprehension.is_async,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||
pub enum ComparableExpr<'a> {
|
||||
BoolOp {
|
||||
op: ComparableBoolop,
|
||||
values: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
NamedExpr {
|
||||
target: Box<ComparableExpr<'a>>,
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
BinOp {
|
||||
left: Box<ComparableExpr<'a>>,
|
||||
op: ComparableOperator,
|
||||
right: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
UnaryOp {
|
||||
op: ComparableUnaryop,
|
||||
operand: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Lambda {
|
||||
args: ComparableArguments<'a>,
|
||||
body: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
IfExp {
|
||||
test: Box<ComparableExpr<'a>>,
|
||||
body: Box<ComparableExpr<'a>>,
|
||||
orelse: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Dict {
|
||||
keys: Vec<ComparableExpr<'a>>,
|
||||
values: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Set {
|
||||
elts: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
ListComp {
|
||||
elt: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
SetComp {
|
||||
elt: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
DictComp {
|
||||
key: Box<ComparableExpr<'a>>,
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
GeneratorExp {
|
||||
elt: Box<ComparableExpr<'a>>,
|
||||
generators: Vec<ComparableComprehension<'a>>,
|
||||
},
|
||||
Await {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Yield {
|
||||
value: Option<Box<ComparableExpr<'a>>>,
|
||||
},
|
||||
YieldFrom {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
},
|
||||
Compare {
|
||||
left: Box<ComparableExpr<'a>>,
|
||||
ops: Vec<ComparableCmpop>,
|
||||
comparators: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Call {
|
||||
func: Box<ComparableExpr<'a>>,
|
||||
args: Vec<ComparableExpr<'a>>,
|
||||
keywords: Vec<ComparableKeyword<'a>>,
|
||||
},
|
||||
FormattedValue {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
conversion: &'a usize,
|
||||
format_spec: Option<Box<ComparableExpr<'a>>>,
|
||||
},
|
||||
JoinedStr {
|
||||
values: Vec<ComparableExpr<'a>>,
|
||||
},
|
||||
Constant {
|
||||
value: ComparableConstant<'a>,
|
||||
kind: Option<&'a str>,
|
||||
},
|
||||
Attribute {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
attr: &'a str,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Subscript {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
slice: Box<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Starred {
|
||||
value: Box<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Name {
|
||||
id: &'a str,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
List {
|
||||
elts: Vec<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Tuple {
|
||||
elts: Vec<ComparableExpr<'a>>,
|
||||
ctx: ComparableExprContext,
|
||||
},
|
||||
Slice {
|
||||
lower: Option<Box<ComparableExpr<'a>>>,
|
||||
upper: Option<Box<ComparableExpr<'a>>>,
|
||||
step: Option<Box<ComparableExpr<'a>>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Expr>> for Box<ComparableExpr<'a>> {
|
||||
fn from(expr: &'a Box<Expr>) -> Self {
|
||||
Box::new((&**expr).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Box<Expr>> for ComparableExpr<'a> {
|
||||
fn from(expr: &'a Box<Expr>) -> Self {
|
||||
(&**expr).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Expr> for ComparableExpr<'a> {
|
||||
fn from(expr: &'a Expr) -> Self {
|
||||
match &expr.node {
|
||||
ExprKind::BoolOp { op, values } => Self::BoolOp {
|
||||
op: op.into(),
|
||||
values: values.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::NamedExpr { target, value } => Self::NamedExpr {
|
||||
target: target.into(),
|
||||
value: value.into(),
|
||||
},
|
||||
ExprKind::BinOp { left, op, right } => Self::BinOp {
|
||||
left: left.into(),
|
||||
op: op.into(),
|
||||
right: right.into(),
|
||||
},
|
||||
ExprKind::UnaryOp { op, operand } => Self::UnaryOp {
|
||||
op: op.into(),
|
||||
operand: operand.into(),
|
||||
},
|
||||
ExprKind::Lambda { args, body } => Self::Lambda {
|
||||
args: (&**args).into(),
|
||||
body: body.into(),
|
||||
},
|
||||
ExprKind::IfExp { test, body, orelse } => Self::IfExp {
|
||||
test: test.into(),
|
||||
body: body.into(),
|
||||
orelse: orelse.into(),
|
||||
},
|
||||
ExprKind::Dict { keys, values } => Self::Dict {
|
||||
keys: keys.iter().map(std::convert::Into::into).collect(),
|
||||
values: values.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::Set { elts } => Self::Set {
|
||||
elts: elts.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::ListComp { elt, generators } => Self::ListComp {
|
||||
elt: elt.into(),
|
||||
generators: generators.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::SetComp { elt, generators } => Self::SetComp {
|
||||
elt: elt.into(),
|
||||
generators: generators.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::DictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
} => Self::DictComp {
|
||||
key: key.into(),
|
||||
value: value.into(),
|
||||
generators: generators.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::GeneratorExp { elt, generators } => Self::GeneratorExp {
|
||||
elt: elt.into(),
|
||||
generators: generators.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::Await { value } => Self::Await {
|
||||
value: value.into(),
|
||||
},
|
||||
ExprKind::Yield { value } => Self::Yield {
|
||||
value: value.as_ref().map(std::convert::Into::into),
|
||||
},
|
||||
ExprKind::YieldFrom { value } => Self::YieldFrom {
|
||||
value: value.into(),
|
||||
},
|
||||
ExprKind::Compare {
|
||||
left,
|
||||
ops,
|
||||
comparators,
|
||||
} => Self::Compare {
|
||||
left: left.into(),
|
||||
ops: ops.iter().map(std::convert::Into::into).collect(),
|
||||
comparators: comparators.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => Self::Call {
|
||||
func: func.into(),
|
||||
args: args.iter().map(std::convert::Into::into).collect(),
|
||||
keywords: keywords.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::FormattedValue {
|
||||
value,
|
||||
conversion,
|
||||
format_spec,
|
||||
} => Self::FormattedValue {
|
||||
value: value.into(),
|
||||
conversion,
|
||||
format_spec: format_spec.as_ref().map(std::convert::Into::into),
|
||||
},
|
||||
ExprKind::JoinedStr { values } => Self::JoinedStr {
|
||||
values: values.iter().map(std::convert::Into::into).collect(),
|
||||
},
|
||||
ExprKind::Constant { value, kind } => Self::Constant {
|
||||
value: value.into(),
|
||||
kind: kind.as_ref().map(String::as_str),
|
||||
},
|
||||
ExprKind::Attribute { value, attr, ctx } => Self::Attribute {
|
||||
value: value.into(),
|
||||
attr,
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Subscript { value, slice, ctx } => Self::Subscript {
|
||||
value: value.into(),
|
||||
slice: slice.into(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Starred { value, ctx } => Self::Starred {
|
||||
value: value.into(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Name { id, ctx } => Self::Name {
|
||||
id,
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::List { elts, ctx } => Self::List {
|
||||
elts: elts.iter().map(std::convert::Into::into).collect(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Tuple { elts, ctx } => Self::Tuple {
|
||||
elts: elts.iter().map(std::convert::Into::into).collect(),
|
||||
ctx: ctx.into(),
|
||||
},
|
||||
ExprKind::Slice { lower, upper, step } => Self::Slice {
|
||||
lower: lower.as_ref().map(std::convert::Into::into),
|
||||
upper: upper.as_ref().map(std::convert::Into::into),
|
||||
step: step.as_ref().map(std::convert::Into::into),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,9 +12,7 @@ use rustpython_parser::lexer::Tok;
|
||||
use rustpython_parser::token::StringKind;
|
||||
|
||||
use crate::ast::types::{Binding, BindingKind, Range};
|
||||
use crate::source_code_generator::SourceCodeGenerator;
|
||||
use crate::source_code_style::SourceCodeStyleDetector;
|
||||
use crate::SourceCodeLocator;
|
||||
use crate::source_code::{Generator, Locator, Stylist};
|
||||
|
||||
/// Create an `Expr` with default location from an `ExprKind`.
|
||||
pub fn create_expr(node: ExprKind) -> Expr {
|
||||
@@ -27,15 +25,15 @@ pub fn create_stmt(node: StmtKind) -> Stmt {
|
||||
}
|
||||
|
||||
/// Generate source code from an `Expr`.
|
||||
pub fn unparse_expr(expr: &Expr, stylist: &SourceCodeStyleDetector) -> String {
|
||||
let mut generator: SourceCodeGenerator = stylist.into();
|
||||
pub fn unparse_expr(expr: &Expr, stylist: &Stylist) -> String {
|
||||
let mut generator: Generator = stylist.into();
|
||||
generator.unparse_expr(expr, 0);
|
||||
generator.generate()
|
||||
}
|
||||
|
||||
/// Generate source code from an `Stmt`.
|
||||
pub fn unparse_stmt(stmt: &Stmt, stylist: &SourceCodeStyleDetector) -> String {
|
||||
let mut generator: SourceCodeGenerator = stylist.into();
|
||||
pub fn unparse_stmt(stmt: &Stmt, stylist: &Stylist) -> String {
|
||||
let mut generator: Generator = stylist.into();
|
||||
generator.unparse_stmt(stmt);
|
||||
generator.generate()
|
||||
}
|
||||
@@ -179,6 +177,131 @@ pub fn match_call_path(
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `true` if the `Expr` contains a reference to `${module}.${target}`.
|
||||
pub fn contains_call_path(
|
||||
expr: &Expr,
|
||||
module: &str,
|
||||
member: &str,
|
||||
import_aliases: &FxHashMap<&str, &str>,
|
||||
from_imports: &FxHashMap<&str, FxHashSet<&str>>,
|
||||
) -> bool {
|
||||
any_over_expr(expr, &|expr| {
|
||||
let call_path = collect_call_paths(expr);
|
||||
if !call_path.is_empty() {
|
||||
if match_call_path(
|
||||
&dealias_call_path(call_path, import_aliases),
|
||||
module,
|
||||
member,
|
||||
from_imports,
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Call `func` over every `Expr` in `expr`, returning `true` if any expression
|
||||
/// returns `true`..
|
||||
pub fn any_over_expr<F>(expr: &Expr, func: &F) -> bool
|
||||
where
|
||||
F: Fn(&Expr) -> bool,
|
||||
{
|
||||
if func(expr) {
|
||||
return true;
|
||||
}
|
||||
match &expr.node {
|
||||
ExprKind::BoolOp { values, .. } | ExprKind::JoinedStr { values } => {
|
||||
values.iter().any(|expr| any_over_expr(expr, func))
|
||||
}
|
||||
ExprKind::NamedExpr { target, value } => {
|
||||
any_over_expr(target, func) || any_over_expr(value, func)
|
||||
}
|
||||
ExprKind::BinOp { left, right, .. } => {
|
||||
any_over_expr(left, func) || any_over_expr(right, func)
|
||||
}
|
||||
ExprKind::UnaryOp { operand, .. } => any_over_expr(operand, func),
|
||||
ExprKind::Lambda { body, .. } => any_over_expr(body, func),
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
any_over_expr(test, func) || any_over_expr(body, func) || any_over_expr(orelse, func)
|
||||
}
|
||||
ExprKind::Dict { keys, values } => values
|
||||
.iter()
|
||||
.chain(keys.iter())
|
||||
.any(|expr| any_over_expr(expr, func)),
|
||||
ExprKind::Set { elts } | ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
|
||||
elts.iter().any(|expr| any_over_expr(expr, func))
|
||||
}
|
||||
ExprKind::ListComp { elt, generators }
|
||||
| ExprKind::SetComp { elt, generators }
|
||||
| ExprKind::GeneratorExp { elt, generators } => {
|
||||
any_over_expr(elt, func)
|
||||
|| generators.iter().any(|generator| {
|
||||
any_over_expr(&generator.target, func)
|
||||
|| any_over_expr(&generator.iter, func)
|
||||
|| generator.ifs.iter().any(|expr| any_over_expr(expr, func))
|
||||
})
|
||||
}
|
||||
ExprKind::DictComp {
|
||||
key,
|
||||
value,
|
||||
generators,
|
||||
} => {
|
||||
any_over_expr(key, func)
|
||||
|| any_over_expr(value, func)
|
||||
|| generators.iter().any(|generator| {
|
||||
any_over_expr(&generator.target, func)
|
||||
|| any_over_expr(&generator.iter, func)
|
||||
|| generator.ifs.iter().any(|expr| any_over_expr(expr, func))
|
||||
})
|
||||
}
|
||||
ExprKind::Await { value }
|
||||
| ExprKind::YieldFrom { value }
|
||||
| ExprKind::Attribute { value, .. }
|
||||
| ExprKind::Starred { value, .. } => any_over_expr(value, func),
|
||||
ExprKind::Yield { value } => value
|
||||
.as_ref()
|
||||
.map_or(false, |value| any_over_expr(value, func)),
|
||||
ExprKind::Compare {
|
||||
left, comparators, ..
|
||||
} => any_over_expr(left, func) || comparators.iter().any(|expr| any_over_expr(expr, func)),
|
||||
ExprKind::Call {
|
||||
func: call_func,
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
any_over_expr(call_func, func)
|
||||
|| args.iter().any(|expr| any_over_expr(expr, func))
|
||||
|| keywords
|
||||
.iter()
|
||||
.any(|keyword| any_over_expr(&keyword.node.value, func))
|
||||
}
|
||||
ExprKind::FormattedValue {
|
||||
value, format_spec, ..
|
||||
} => {
|
||||
any_over_expr(value, func)
|
||||
|| format_spec
|
||||
.as_ref()
|
||||
.map_or(false, |value| any_over_expr(value, func))
|
||||
}
|
||||
ExprKind::Subscript { value, slice, .. } => {
|
||||
any_over_expr(value, func) || any_over_expr(slice, func)
|
||||
}
|
||||
ExprKind::Slice { lower, upper, step } => {
|
||||
lower
|
||||
.as_ref()
|
||||
.map_or(false, |value| any_over_expr(value, func))
|
||||
|| upper
|
||||
.as_ref()
|
||||
.map_or(false, |value| any_over_expr(value, func))
|
||||
|| step
|
||||
.as_ref()
|
||||
.map_or(false, |value| any_over_expr(value, func))
|
||||
}
|
||||
ExprKind::Name { .. } | ExprKind::Constant { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
static DUNDER_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"__[^\s]+__").unwrap());
|
||||
|
||||
/// Return `true` if the `Stmt` is an assignment to a dunder (like `__all__`).
|
||||
@@ -191,12 +314,12 @@ pub fn is_assignment_to_a_dunder(stmt: &Stmt) -> bool {
|
||||
return false;
|
||||
}
|
||||
match &targets[0].node {
|
||||
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
|
||||
ExprKind::Name { id, .. } => DUNDER_REGEX.is_match(id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
StmtKind::AnnAssign { target, .. } => match &target.node {
|
||||
ExprKind::Name { id, ctx: _ } => DUNDER_REGEX.is_match(id),
|
||||
ExprKind::Name { id, .. } => DUNDER_REGEX.is_match(id),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
@@ -305,6 +428,13 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
|
||||
arg_names
|
||||
}
|
||||
|
||||
/// Returns `true` if a statement or expression includes at least one comment.
|
||||
pub fn has_comments<T>(located: &Located<T>, locator: &Locator) -> bool {
|
||||
lexer::make_tokenizer(&locator.slice_source_code_range(&Range::from_located(located)))
|
||||
.flatten()
|
||||
.any(|(_, tok, _)| matches!(tok, Tok::Comment(..)))
|
||||
}
|
||||
|
||||
/// Returns `true` if a call is an argumented `super` invocation.
|
||||
pub fn is_super_call_with_arguments(func: &Expr, args: &[Expr]) -> bool {
|
||||
if let ExprKind::Name { id, .. } = &func.node {
|
||||
@@ -351,14 +481,14 @@ pub fn to_absolute(relative: Location, base: Location) -> Location {
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` has leading content.
|
||||
pub fn match_leading_content(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
|
||||
pub fn match_leading_content(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
let range = Range::new(Location::new(stmt.location.row(), 0), stmt.location);
|
||||
let prefix = locator.slice_source_code_range(&range);
|
||||
prefix.chars().any(|char| !char.is_whitespace())
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` has trailing content.
|
||||
pub fn match_trailing_content(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
|
||||
pub fn match_trailing_content(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
let range = Range::new(
|
||||
stmt.end_location.unwrap(),
|
||||
Location::new(stmt.end_location.unwrap().row() + 1, 0),
|
||||
@@ -376,7 +506,7 @@ pub fn match_trailing_content(stmt: &Stmt, locator: &SourceCodeLocator) -> bool
|
||||
}
|
||||
|
||||
/// Return the number of trailing empty lines following a statement.
|
||||
pub fn count_trailing_lines(stmt: &Stmt, locator: &SourceCodeLocator) -> usize {
|
||||
pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize {
|
||||
let suffix =
|
||||
locator.slice_source_code_at(&Location::new(stmt.end_location.unwrap().row() + 1, 0));
|
||||
suffix
|
||||
@@ -388,7 +518,7 @@ pub fn count_trailing_lines(stmt: &Stmt, locator: &SourceCodeLocator) -> usize {
|
||||
/// Return the appropriate visual `Range` for any message that spans a `Stmt`.
|
||||
/// Specifically, this method returns the range of a function or class name,
|
||||
/// rather than that of the entire function or class body.
|
||||
pub fn identifier_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Range {
|
||||
pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> Range {
|
||||
if matches!(
|
||||
stmt.node,
|
||||
StmtKind::ClassDef { .. }
|
||||
@@ -407,7 +537,7 @@ pub fn identifier_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Range {
|
||||
}
|
||||
|
||||
/// Like `identifier_range`, but accepts a `Binding`.
|
||||
pub fn binding_range(binding: &Binding, locator: &SourceCodeLocator) -> Range {
|
||||
pub fn binding_range(binding: &Binding, locator: &Locator) -> Range {
|
||||
if matches!(
|
||||
binding.kind,
|
||||
BindingKind::ClassDefinition | BindingKind::FunctionDefinition
|
||||
@@ -423,7 +553,7 @@ pub fn binding_range(binding: &Binding, locator: &SourceCodeLocator) -> Range {
|
||||
}
|
||||
|
||||
// Return the ranges of `Name` tokens within a specified node.
|
||||
pub fn find_names<T>(located: &Located<T>, locator: &SourceCodeLocator) -> Vec<Range> {
|
||||
pub fn find_names<T>(located: &Located<T>, locator: &Locator) -> Vec<Range> {
|
||||
let contents = locator.slice_source_code_range(&Range::from_located(located));
|
||||
lexer::make_tokenizer_located(&contents, located.location)
|
||||
.flatten()
|
||||
@@ -436,10 +566,7 @@ pub fn find_names<T>(located: &Located<T>, locator: &SourceCodeLocator) -> Vec<R
|
||||
}
|
||||
|
||||
/// Return the `Range` of `name` in `Excepthandler`.
|
||||
pub fn excepthandler_name_range(
|
||||
handler: &Excepthandler,
|
||||
locator: &SourceCodeLocator,
|
||||
) -> Option<Range> {
|
||||
pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> Option<Range> {
|
||||
let ExcepthandlerKind::ExceptHandler {
|
||||
name, type_, body, ..
|
||||
} = &handler.node;
|
||||
@@ -462,7 +589,7 @@ pub fn excepthandler_name_range(
|
||||
}
|
||||
|
||||
/// Return the `Range` of `except` in `Excepthandler`.
|
||||
pub fn except_range(handler: &Excepthandler, locator: &SourceCodeLocator) -> Range {
|
||||
pub fn except_range(handler: &Excepthandler, locator: &Locator) -> Range {
|
||||
let ExcepthandlerKind::ExceptHandler { body, type_, .. } = &handler.node;
|
||||
let end = if let Some(type_) = type_ {
|
||||
type_.location
|
||||
@@ -487,7 +614,7 @@ pub fn except_range(handler: &Excepthandler, locator: &SourceCodeLocator) -> Ran
|
||||
}
|
||||
|
||||
/// Find f-strings that don't contain any formatted values in a `JoinedStr`.
|
||||
pub fn find_useless_f_strings(expr: &Expr, locator: &SourceCodeLocator) -> Vec<(Range, Range)> {
|
||||
pub fn find_useless_f_strings(expr: &Expr, locator: &Locator) -> Vec<(Range, Range)> {
|
||||
let contents = locator.slice_source_code_range(&Range::from_located(expr));
|
||||
lexer::make_tokenizer_located(&contents, expr.location)
|
||||
.flatten()
|
||||
@@ -524,7 +651,7 @@ pub fn find_useless_f_strings(expr: &Expr, locator: &SourceCodeLocator) -> Vec<(
|
||||
}
|
||||
|
||||
/// Return the `Range` of `else` in `For`, `AsyncFor`, and `While` statements.
|
||||
pub fn else_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Option<Range> {
|
||||
pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
|
||||
match &stmt.node {
|
||||
StmtKind::For { body, orelse, .. }
|
||||
| StmtKind::AsyncFor { body, orelse, .. }
|
||||
@@ -558,7 +685,7 @@ pub fn else_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Option<Range> {
|
||||
|
||||
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
|
||||
/// other statements preceding it.
|
||||
pub fn preceded_by_continuation(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
|
||||
pub fn preceded_by_continuation(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
// Does the previous line end in a continuation? This will have a specific
|
||||
// false-positive, which is that if the previous line ends in a comment, it
|
||||
// will be treated as a continuation. So we should only use this information to
|
||||
@@ -579,16 +706,31 @@ pub fn preceded_by_continuation(stmt: &Stmt, locator: &SourceCodeLocator) -> boo
|
||||
|
||||
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
|
||||
/// other statements preceding it.
|
||||
pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
|
||||
pub fn preceded_by_multi_statement_line(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
match_leading_content(stmt, locator) || preceded_by_continuation(stmt, locator)
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` appears to be part of a multi-statement line, with
|
||||
/// other statements following it.
|
||||
pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
|
||||
pub fn followed_by_multi_statement_line(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
match_trailing_content(stmt, locator)
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` is a docstring.
|
||||
pub fn is_docstring_stmt(stmt: &Stmt) -> bool {
|
||||
if let StmtKind::Expr { value } = &stmt.node {
|
||||
matches!(
|
||||
value.node,
|
||||
ExprKind::Constant {
|
||||
value: Constant::Str { .. },
|
||||
..
|
||||
}
|
||||
)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// A simple representation of a call's positional and keyword arguments.
|
||||
pub struct SimpleCallArgs<'a> {
|
||||
@@ -634,6 +776,11 @@ impl<'a> SimpleCallArgs<'a> {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the number of positional and keyword arguments used.
|
||||
pub fn len(&self) -> usize {
|
||||
self.args.len() + self.kwargs.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -647,7 +794,7 @@ mod tests {
|
||||
else_range, identifier_range, match_module_member, match_trailing_content,
|
||||
};
|
||||
use crate::ast::types::Range;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
#[test]
|
||||
fn builtin() -> Result<()> {
|
||||
@@ -797,25 +944,25 @@ mod tests {
|
||||
let contents = "x = 1";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert!(!match_trailing_content(stmt, &locator));
|
||||
|
||||
let contents = "x = 1; y = 2";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert!(match_trailing_content(stmt, &locator));
|
||||
|
||||
let contents = "x = 1 ";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert!(!match_trailing_content(stmt, &locator));
|
||||
|
||||
let contents = "x = 1 # Comment";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert!(!match_trailing_content(stmt, &locator));
|
||||
|
||||
let contents = r#"
|
||||
@@ -825,7 +972,7 @@ y = 2
|
||||
.trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert!(!match_trailing_content(stmt, &locator));
|
||||
|
||||
Ok(())
|
||||
@@ -836,7 +983,7 @@ y = 2
|
||||
let contents = "def f(): pass".trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
identifier_range(stmt, &locator),
|
||||
Range::new(Location::new(1, 4), Location::new(1, 5),)
|
||||
@@ -850,7 +997,7 @@ def \
|
||||
.trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
identifier_range(stmt, &locator),
|
||||
Range::new(Location::new(2, 2), Location::new(2, 3),)
|
||||
@@ -859,7 +1006,7 @@ def \
|
||||
let contents = "class Class(): pass".trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
identifier_range(stmt, &locator),
|
||||
Range::new(Location::new(1, 6), Location::new(1, 11),)
|
||||
@@ -868,7 +1015,7 @@ def \
|
||||
let contents = "class Class: pass".trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
identifier_range(stmt, &locator),
|
||||
Range::new(Location::new(1, 6), Location::new(1, 11),)
|
||||
@@ -882,7 +1029,7 @@ class Class():
|
||||
.trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
identifier_range(stmt, &locator),
|
||||
Range::new(Location::new(2, 6), Location::new(2, 11),)
|
||||
@@ -891,7 +1038,7 @@ class Class():
|
||||
let contents = r#"x = y + 1"#.trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
identifier_range(stmt, &locator),
|
||||
Range::new(Location::new(1, 0), Location::new(1, 9),)
|
||||
@@ -911,7 +1058,7 @@ else:
|
||||
.trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
let range = else_range(stmt, &locator).unwrap();
|
||||
assert_eq!(range.location.row(), 3);
|
||||
assert_eq!(range.location.column(), 0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
pub mod branch_detection;
|
||||
pub mod cast;
|
||||
pub mod comparable;
|
||||
pub mod function_type;
|
||||
pub mod helpers;
|
||||
pub mod operations;
|
||||
|
||||
@@ -4,6 +4,7 @@ use rustpython_parser::ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
|
||||
use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::Tok;
|
||||
|
||||
use crate::ast::helpers::any_over_expr;
|
||||
use crate::ast::types::{Binding, BindingKind, Scope};
|
||||
use crate::ast::visitor;
|
||||
use crate::ast::visitor::Visitor;
|
||||
@@ -129,76 +130,13 @@ pub fn in_nested_block<'a>(mut parents: impl Iterator<Item = &'a Stmt>) -> bool
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns `true` if `parent` contains `child`.
|
||||
fn contains(parent: &Expr, child: &Expr) -> bool {
|
||||
match &parent.node {
|
||||
ExprKind::BoolOp { values, .. } => values.iter().any(|parent| contains(parent, child)),
|
||||
ExprKind::NamedExpr { target, value } => contains(target, child) || contains(value, child),
|
||||
ExprKind::BinOp { left, right, .. } => contains(left, child) || contains(right, child),
|
||||
ExprKind::UnaryOp { operand, .. } => contains(operand, child),
|
||||
ExprKind::Lambda { body, .. } => contains(body, child),
|
||||
ExprKind::IfExp { test, body, orelse } => {
|
||||
contains(test, child) || contains(body, child) || contains(orelse, child)
|
||||
}
|
||||
ExprKind::Dict { keys, values } => keys
|
||||
.iter()
|
||||
.chain(values.iter())
|
||||
.any(|parent| contains(parent, child)),
|
||||
ExprKind::Set { elts } => elts.iter().any(|parent| contains(parent, child)),
|
||||
ExprKind::ListComp { elt, .. } => contains(elt, child),
|
||||
ExprKind::SetComp { elt, .. } => contains(elt, child),
|
||||
ExprKind::DictComp { key, value, .. } => contains(key, child) || contains(value, child),
|
||||
ExprKind::GeneratorExp { elt, .. } => contains(elt, child),
|
||||
ExprKind::Await { value } => contains(value, child),
|
||||
ExprKind::Yield { value } => value.as_ref().map_or(false, |value| contains(value, child)),
|
||||
ExprKind::YieldFrom { value } => contains(value, child),
|
||||
ExprKind::Compare {
|
||||
left, comparators, ..
|
||||
} => contains(left, child) || comparators.iter().any(|parent| contains(parent, child)),
|
||||
ExprKind::Call {
|
||||
func,
|
||||
args,
|
||||
keywords,
|
||||
} => {
|
||||
contains(func, child)
|
||||
|| args.iter().any(|parent| contains(parent, child))
|
||||
|| keywords
|
||||
.iter()
|
||||
.any(|keyword| contains(&keyword.node.value, child))
|
||||
}
|
||||
ExprKind::FormattedValue {
|
||||
value, format_spec, ..
|
||||
} => {
|
||||
contains(value, child)
|
||||
|| format_spec
|
||||
.as_ref()
|
||||
.map_or(false, |value| contains(value, child))
|
||||
}
|
||||
ExprKind::JoinedStr { values } => values.iter().any(|parent| contains(parent, child)),
|
||||
ExprKind::Constant { .. } => false,
|
||||
ExprKind::Attribute { value, .. } => contains(value, child),
|
||||
ExprKind::Subscript { value, slice, .. } => {
|
||||
contains(value, child) || contains(slice, child)
|
||||
}
|
||||
ExprKind::Starred { value, .. } => contains(value, child),
|
||||
ExprKind::Name { .. } => parent == child,
|
||||
ExprKind::List { elts, .. } => elts.iter().any(|parent| contains(parent, child)),
|
||||
ExprKind::Tuple { elts, .. } => elts.iter().any(|parent| contains(parent, child)),
|
||||
ExprKind::Slice { lower, upper, step } => {
|
||||
lower.as_ref().map_or(false, |value| contains(value, child))
|
||||
|| upper.as_ref().map_or(false, |value| contains(value, child))
|
||||
|| step.as_ref().map_or(false, |value| contains(value, child))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a node represents an unpacking assignment.
|
||||
pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool {
|
||||
match &parent.node {
|
||||
StmtKind::With { items, .. } => items.iter().any(|item| {
|
||||
if let Some(optional_vars) = &item.optional_vars {
|
||||
if matches!(optional_vars.node, ExprKind::Tuple { .. }) {
|
||||
if contains(optional_vars, child) {
|
||||
if any_over_expr(optional_vars, &|expr| expr == child) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -227,7 +165,7 @@ pub fn is_unpacking_assignment(parent: &Stmt, child: &Expr) -> bool {
|
||||
matches!(
|
||||
item.node,
|
||||
ExprKind::Set { .. } | ExprKind::List { .. } | ExprKind::Tuple { .. }
|
||||
) && contains(item, child)
|
||||
) && any_over_expr(item, &|expr| expr == child)
|
||||
});
|
||||
|
||||
// If our child is a tuple, and value is not, it's always an unpacking
|
||||
|
||||
@@ -74,7 +74,6 @@ pub enum ScopeKind<'a> {
|
||||
Function(FunctionDef<'a>),
|
||||
Generator,
|
||||
Module,
|
||||
Arg,
|
||||
Lambda(Lambda<'a>),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,225 +1 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ropey::RopeBuilder;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::registry::Check;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub enum Mode {
|
||||
Generate,
|
||||
Apply,
|
||||
Diff,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<bool> for Mode {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Mode::Apply
|
||||
} else {
|
||||
Mode::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file<'a>(
|
||||
checks: &'a [Check],
|
||||
locator: &'a SourceCodeLocator<'a>,
|
||||
) -> Option<(Cow<'a, str>, usize)> {
|
||||
if checks.iter().all(|check| check.fix.is_none()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(apply_fixes(
|
||||
checks.iter().filter_map(|check| check.fix.as_ref()),
|
||||
locator,
|
||||
))
|
||||
}
|
||||
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(
|
||||
fixes: impl Iterator<Item = &'a Fix>,
|
||||
locator: &'a SourceCodeLocator<'a>,
|
||||
) -> (Cow<'a, str>, usize) {
|
||||
let mut output = RopeBuilder::new();
|
||||
let mut last_pos: Location = Location::new(1, 0);
|
||||
let mut applied: BTreeSet<&Fix> = BTreeSet::default();
|
||||
let mut num_fixed: usize = 0;
|
||||
|
||||
for fix in fixes.sorted_by_key(|fix| fix.location) {
|
||||
// If we already applied an identical fix as part of another correction, skip
|
||||
// any re-application.
|
||||
if applied.contains(&fix) {
|
||||
num_fixed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied,
|
||||
// skip it.
|
||||
if last_pos > fix.location {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add all contents from `last_pos` to `fix.location`.
|
||||
let slice = locator.slice_source_code_range(&Range::new(last_pos, fix.location));
|
||||
output.append(&slice);
|
||||
|
||||
// Add the patch itself.
|
||||
output.append(&fix.content);
|
||||
|
||||
// Track that the fix was applied.
|
||||
last_pos = fix.end_location;
|
||||
applied.insert(fix);
|
||||
num_fixed += 1;
|
||||
}
|
||||
|
||||
// Add the remaining content.
|
||||
let slice = locator.slice_source_code_at(&last_pos);
|
||||
output.append(&slice);
|
||||
|
||||
(Cow::from(output.finish()), num_fixed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::fixer::apply_fixes;
|
||||
use crate::autofix::Fix;
|
||||
use crate::SourceCodeLocator;
|
||||
|
||||
#[test]
|
||||
fn empty_file() {
|
||||
let fixes = vec![];
|
||||
let locator = SourceCodeLocator::new(r#""#);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(contents, "");
|
||||
assert_eq!(fixed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() {
|
||||
let fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 14),
|
||||
}];
|
||||
let locator = SourceCodeLocator::new(
|
||||
r#"
|
||||
class A(object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A(Bar):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
assert_eq!(fixed, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() {
|
||||
let fixes = vec![Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
}];
|
||||
let locator = SourceCodeLocator::new(
|
||||
r#"
|
||||
class A(object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A:
|
||||
...
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixed, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_double_removal() {
|
||||
let fixes = vec![
|
||||
Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 16),
|
||||
},
|
||||
Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 16),
|
||||
end_location: Location::new(1, 23),
|
||||
},
|
||||
];
|
||||
let locator = SourceCodeLocator::new(
|
||||
r#"
|
||||
class A(object, object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A:
|
||||
...
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixed, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() {
|
||||
let fixes = vec![
|
||||
Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 11),
|
||||
},
|
||||
];
|
||||
let locator = SourceCodeLocator::new(
|
||||
r#"
|
||||
class A(object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A:
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
assert_eq!(fixed, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ use crate::ast::helpers;
|
||||
use crate::ast::helpers::to_absolute;
|
||||
use crate::ast::types::Range;
|
||||
use crate::ast::whitespace::LinesWithTrailingNewline;
|
||||
use crate::autofix::Fix;
|
||||
use crate::cst::helpers::compose_module_path;
|
||||
use crate::cst::matchers::match_module;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::fix::Fix;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
/// Determine if a body contains only a single statement, taking into account
|
||||
/// deleted.
|
||||
@@ -78,7 +78,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
|
||||
|
||||
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
|
||||
/// of a multi-statement line.
|
||||
fn trailing_semicolon(stmt: &Stmt, locator: &SourceCodeLocator) -> Option<Location> {
|
||||
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
|
||||
let contents = locator.slice_source_code_at(&stmt.end_location.unwrap());
|
||||
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
|
||||
let trimmed = line.trim();
|
||||
@@ -100,7 +100,7 @@ fn trailing_semicolon(stmt: &Stmt, locator: &SourceCodeLocator) -> Option<Locati
|
||||
}
|
||||
|
||||
/// Find the next valid break for a `Stmt` after a semicolon.
|
||||
fn next_stmt_break(semicolon: Location, locator: &SourceCodeLocator) -> Location {
|
||||
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
|
||||
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
|
||||
let contents = locator.slice_source_code_at(&start_location);
|
||||
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
|
||||
@@ -133,7 +133,7 @@ fn next_stmt_break(semicolon: Location, locator: &SourceCodeLocator) -> Location
|
||||
}
|
||||
|
||||
/// Return `true` if a `Stmt` occurs at the end of a file.
|
||||
fn is_end_of_file(stmt: &Stmt, locator: &SourceCodeLocator) -> bool {
|
||||
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
|
||||
let contents = locator.slice_source_code_at(&stmt.end_location.unwrap());
|
||||
contents.is_empty()
|
||||
}
|
||||
@@ -155,7 +155,7 @@ pub fn delete_stmt(
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
locator: &SourceCodeLocator,
|
||||
locator: &Locator,
|
||||
) -> Result<Fix> {
|
||||
if parent
|
||||
.map(|parent| is_lone_child(stmt, parent, deleted))
|
||||
@@ -197,7 +197,7 @@ pub fn remove_unused_imports<'a>(
|
||||
stmt: &Stmt,
|
||||
parent: Option<&Stmt>,
|
||||
deleted: &[&Stmt],
|
||||
locator: &SourceCodeLocator,
|
||||
locator: &Locator,
|
||||
) -> Result<Fix> {
|
||||
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
|
||||
let mut tree = match_module(&module_text)?;
|
||||
@@ -299,20 +299,20 @@ mod tests {
|
||||
use rustpython_parser::parser;
|
||||
|
||||
use crate::autofix::helpers::{next_stmt_break, trailing_semicolon};
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
#[test]
|
||||
fn find_semicolon() -> Result<()> {
|
||||
let contents = "x = 1";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(trailing_semicolon(stmt, &locator), None);
|
||||
|
||||
let contents = "x = 1; y = 1";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
trailing_semicolon(stmt, &locator),
|
||||
Some(Location::new(1, 5))
|
||||
@@ -321,7 +321,7 @@ mod tests {
|
||||
let contents = "x = 1 ; y = 1";
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
trailing_semicolon(stmt, &locator),
|
||||
Some(Location::new(1, 6))
|
||||
@@ -334,7 +334,7 @@ x = 1 \
|
||||
.trim();
|
||||
let program = parser::parse_program(contents, "<filename>")?;
|
||||
let stmt = program.first().unwrap();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
trailing_semicolon(stmt, &locator),
|
||||
Some(Location::new(2, 2))
|
||||
@@ -346,14 +346,14 @@ x = 1 \
|
||||
#[test]
|
||||
fn find_next_stmt_break() {
|
||||
let contents = "x = 1; y = 1";
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
next_stmt_break(Location::new(1, 4), &locator),
|
||||
Location::new(1, 5)
|
||||
);
|
||||
|
||||
let contents = "x = 1 ; y = 1";
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
next_stmt_break(Location::new(1, 5), &locator),
|
||||
Location::new(1, 6)
|
||||
@@ -364,7 +364,7 @@ x = 1 \
|
||||
; y = 1
|
||||
"#
|
||||
.trim();
|
||||
let locator = SourceCodeLocator::new(contents);
|
||||
let locator = Locator::new(contents);
|
||||
assert_eq!(
|
||||
next_stmt_break(Location::new(2, 2), &locator),
|
||||
Location::new(2, 4)
|
||||
|
||||
@@ -1,38 +1,210 @@
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use itertools::Itertools;
|
||||
use ropey::RopeBuilder;
|
||||
use rustpython_ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::fix::Fix;
|
||||
use crate::registry::Diagnostic;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
pub mod fixer;
|
||||
pub mod helpers;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct Fix {
|
||||
pub content: String,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
/// Auto-fix errors in a file, and write the fixed source code to disk.
|
||||
pub fn fix_file<'a>(
|
||||
diagnostics: &'a [Diagnostic],
|
||||
locator: &'a Locator<'a>,
|
||||
) -> Option<(Cow<'a, str>, usize)> {
|
||||
if diagnostics.iter().all(|check| check.fix.is_none()) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(apply_fixes(
|
||||
diagnostics.iter().filter_map(|check| check.fix.as_ref()),
|
||||
locator,
|
||||
))
|
||||
}
|
||||
|
||||
impl Fix {
|
||||
pub fn deletion(start: Location, end: Location) -> Self {
|
||||
Self {
|
||||
/// Apply a series of fixes.
|
||||
fn apply_fixes<'a>(
|
||||
fixes: impl Iterator<Item = &'a Fix>,
|
||||
locator: &'a Locator<'a>,
|
||||
) -> (Cow<'a, str>, usize) {
|
||||
let mut output = RopeBuilder::new();
|
||||
let mut last_pos: Location = Location::new(1, 0);
|
||||
let mut applied: BTreeSet<&Fix> = BTreeSet::default();
|
||||
let mut num_fixed: usize = 0;
|
||||
|
||||
for fix in fixes.sorted_by_key(|fix| fix.location) {
|
||||
// If we already applied an identical fix as part of another correction, skip
|
||||
// any re-application.
|
||||
if applied.contains(&fix) {
|
||||
num_fixed += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Best-effort approach: if this fix overlaps with a fix we've already applied,
|
||||
// skip it.
|
||||
if last_pos > fix.location {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add all contents from `last_pos` to `fix.location`.
|
||||
let slice = locator.slice_source_code_range(&Range::new(last_pos, fix.location));
|
||||
output.append(&slice);
|
||||
|
||||
// Add the patch itself.
|
||||
output.append(&fix.content);
|
||||
|
||||
// Track that the fix was applied.
|
||||
last_pos = fix.end_location;
|
||||
applied.insert(fix);
|
||||
num_fixed += 1;
|
||||
}
|
||||
|
||||
// Add the remaining content.
|
||||
let slice = locator.slice_source_code_at(&last_pos);
|
||||
output.append(&slice);
|
||||
|
||||
(Cow::from(output.finish()), num_fixed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::autofix::apply_fixes;
|
||||
use crate::fix::Fix;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
#[test]
|
||||
fn empty_file() {
|
||||
let fixes = vec![];
|
||||
let locator = Locator::new(r#""#);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(contents, "");
|
||||
assert_eq!(fixed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_replacement() {
|
||||
let fixes = vec![Fix {
|
||||
content: "Bar".to_string(),
|
||||
location: Location::new(1, 8),
|
||||
end_location: Location::new(1, 14),
|
||||
}];
|
||||
let locator = Locator::new(
|
||||
r#"
|
||||
class A(object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A(Bar):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
assert_eq!(fixed, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_single_removal() {
|
||||
let fixes = vec![Fix {
|
||||
content: String::new(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
}
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
}];
|
||||
let locator = Locator::new(
|
||||
r#"
|
||||
class A(object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A:
|
||||
...
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixed, 1);
|
||||
}
|
||||
|
||||
pub fn replacement(content: String, start: Location, end: Location) -> Self {
|
||||
Self {
|
||||
content,
|
||||
location: start,
|
||||
end_location: end,
|
||||
}
|
||||
#[test]
|
||||
fn apply_double_removal() {
|
||||
let fixes = vec![
|
||||
Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 16),
|
||||
},
|
||||
Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 16),
|
||||
end_location: Location::new(1, 23),
|
||||
},
|
||||
];
|
||||
let locator = Locator::new(
|
||||
r#"
|
||||
class A(object, object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A:
|
||||
...
|
||||
"#
|
||||
.trim()
|
||||
);
|
||||
assert_eq!(fixed, 2);
|
||||
}
|
||||
|
||||
pub fn insertion(content: String, at: Location) -> Self {
|
||||
Self {
|
||||
content,
|
||||
location: at,
|
||||
end_location: at,
|
||||
}
|
||||
#[test]
|
||||
fn ignore_overlapping_fixes() {
|
||||
let fixes = vec![
|
||||
Fix {
|
||||
content: String::new(),
|
||||
location: Location::new(1, 7),
|
||||
end_location: Location::new(1, 15),
|
||||
},
|
||||
Fix {
|
||||
content: "ignored".to_string(),
|
||||
location: Location::new(1, 9),
|
||||
end_location: Location::new(1, 11),
|
||||
},
|
||||
];
|
||||
let locator = Locator::new(
|
||||
r#"
|
||||
class A(object):
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
|
||||
assert_eq!(
|
||||
contents,
|
||||
r#"
|
||||
class A:
|
||||
...
|
||||
"#
|
||||
.trim(),
|
||||
);
|
||||
assert_eq!(fixed, 1);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/cache.rs
20
src/cache.rs
@@ -1,6 +1,6 @@
|
||||
#![cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File, Metadata};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -8,16 +8,15 @@ use std::path::{Path, PathBuf};
|
||||
use anyhow::Result;
|
||||
use filetime::FileTime;
|
||||
use log::error;
|
||||
use once_cell::sync::Lazy;
|
||||
use path_absolutize::Absolutize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::message::Message;
|
||||
use crate::settings::{flags, Settings};
|
||||
|
||||
pub const CACHE_DIR_NAME: &str = ".ruff_cache";
|
||||
|
||||
const CARGO_PKG_VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
static CACHE_DIR: Lazy<Option<String>> = Lazy::new(|| std::env::var("RUFF_CACHE_DIR").ok());
|
||||
pub const DEFAULT_CACHE_DIR_NAME: &str = ".ruff_cache";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct CacheMetadata {
|
||||
@@ -39,9 +38,7 @@ struct CheckResult {
|
||||
/// Return the cache directory for a given project root. Defers to the
|
||||
/// `RUFF_CACHE_DIR` environment variable, if set.
|
||||
pub fn cache_dir(project_root: &Path) -> PathBuf {
|
||||
CACHE_DIR
|
||||
.as_ref()
|
||||
.map_or_else(|| project_root.join(DEFAULT_CACHE_DIR_NAME), PathBuf::from)
|
||||
project_root.join(CACHE_DIR_NAME)
|
||||
}
|
||||
|
||||
fn content_dir() -> &'static Path {
|
||||
@@ -57,10 +54,11 @@ fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: flags::Autof
|
||||
hasher.finish()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Initialize the cache at the specified `Path`.
|
||||
pub fn init(path: &Path) -> Result<()> {
|
||||
// Create the cache directories.
|
||||
create_dir_all(path.join(content_dir()))?;
|
||||
fs::create_dir_all(path.join(content_dir()))?;
|
||||
|
||||
// Add the CACHEDIR.TAG.
|
||||
if !cachedir::is_tagged(path)? {
|
||||
@@ -70,7 +68,7 @@ pub fn init(path: &Path) -> Result<()> {
|
||||
// Add the .gitignore.
|
||||
let gitignore_path = path.join(".gitignore");
|
||||
if !gitignore_path.exists() {
|
||||
let mut file = File::create(gitignore_path)?;
|
||||
let mut file = fs::File::create(gitignore_path)?;
|
||||
file.write_all(b"*")?;
|
||||
}
|
||||
|
||||
@@ -91,7 +89,7 @@ fn read_sync(cache_dir: &Path, key: u64) -> Result<Vec<u8>, std::io::Error> {
|
||||
/// Get a value from the cache.
|
||||
pub fn get<P: AsRef<Path>>(
|
||||
path: P,
|
||||
metadata: &Metadata,
|
||||
metadata: &fs::Metadata,
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Option<Vec<Message>> {
|
||||
@@ -115,7 +113,7 @@ pub fn get<P: AsRef<Path>>(
|
||||
/// Set a value in the cache.
|
||||
pub fn set<P: AsRef<Path>>(
|
||||
path: P,
|
||||
metadata: &Metadata,
|
||||
metadata: &fs::Metadata,
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
messages: &[Message],
|
||||
|
||||
2122
src/checkers/ast.rs
2122
src/checkers/ast.rs
File diff suppressed because it is too large
Load Diff
@@ -7,47 +7,49 @@ use rustpython_parser::ast::Suite;
|
||||
use crate::ast::visitor::Visitor;
|
||||
use crate::directives::IsortDirectives;
|
||||
use crate::isort;
|
||||
use crate::isort::track::ImportTracker;
|
||||
use crate::registry::Check;
|
||||
use crate::isort::track::{Block, ImportTracker};
|
||||
use crate::registry::{Diagnostic, RuleCode};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::source_code_style::SourceCodeStyleDetector;
|
||||
|
||||
fn check_import_blocks(
|
||||
tracker: ImportTracker,
|
||||
locator: &SourceCodeLocator,
|
||||
settings: &Settings,
|
||||
stylist: &SourceCodeStyleDetector,
|
||||
autofix: flags::Autofix,
|
||||
package: Option<&Path>,
|
||||
) -> Vec<Check> {
|
||||
let mut checks = vec![];
|
||||
for block in tracker.into_iter() {
|
||||
if !block.imports.is_empty() {
|
||||
if let Some(check) =
|
||||
isort::plugins::check_imports(&block, locator, settings, stylist, autofix, package)
|
||||
{
|
||||
checks.push(check);
|
||||
}
|
||||
}
|
||||
}
|
||||
checks
|
||||
}
|
||||
use crate::source_code::{Locator, Stylist};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn check_imports(
|
||||
python_ast: &Suite,
|
||||
locator: &SourceCodeLocator,
|
||||
locator: &Locator,
|
||||
directives: &IsortDirectives,
|
||||
settings: &Settings,
|
||||
stylist: &SourceCodeStyleDetector,
|
||||
stylist: &Stylist,
|
||||
autofix: flags::Autofix,
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
) -> Vec<Check> {
|
||||
let mut tracker = ImportTracker::new(locator, directives, path);
|
||||
for stmt in python_ast {
|
||||
tracker.visit_stmt(stmt);
|
||||
) -> Vec<Diagnostic> {
|
||||
// Extract all imports from the AST.
|
||||
let tracker = {
|
||||
let mut tracker = ImportTracker::new(locator, directives, path);
|
||||
for stmt in python_ast {
|
||||
tracker.visit_stmt(stmt);
|
||||
}
|
||||
tracker
|
||||
};
|
||||
let blocks: Vec<&Block> = tracker.iter().collect();
|
||||
|
||||
// Enforce import rules.
|
||||
let mut diagnostics = vec![];
|
||||
if settings.enabled.contains(&RuleCode::I001) {
|
||||
for block in &blocks {
|
||||
if !block.imports.is_empty() {
|
||||
if let Some(diagnostic) = isort::rules::organize_imports(
|
||||
block, locator, settings, stylist, autofix, package,
|
||||
) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
check_import_blocks(tracker, locator, settings, stylist, autofix, package)
|
||||
if settings.enabled.contains(&RuleCode::I002) {
|
||||
diagnostics.extend(isort::rules::add_required_imports(
|
||||
&blocks, python_ast, locator, settings, autofix,
|
||||
));
|
||||
}
|
||||
diagnostics
|
||||
}
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
//! Lint rules based on checking raw physical lines.
|
||||
|
||||
use crate::pycodestyle::checks::{line_too_long, no_newline_at_end_of_file};
|
||||
use crate::pygrep_hooks::plugins::{blanket_noqa, blanket_type_ignore};
|
||||
use crate::pyupgrade::checks::unnecessary_coding_comment;
|
||||
use crate::registry::{Check, CheckCode};
|
||||
use crate::pycodestyle::rules::{doc_line_too_long, line_too_long, no_newline_at_end_of_file};
|
||||
use crate::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
|
||||
use crate::pyupgrade::rules::unnecessary_coding_comment;
|
||||
use crate::registry::{Diagnostic, RuleCode};
|
||||
use crate::settings::{flags, Settings};
|
||||
|
||||
pub fn check_lines(
|
||||
contents: &str,
|
||||
commented_lines: &[usize],
|
||||
doc_lines: &[usize],
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
let enforce_unnecessary_coding_comment = settings.enabled.contains(&CheckCode::UP009);
|
||||
let enforce_line_too_long = settings.enabled.contains(&CheckCode::E501);
|
||||
let enforce_no_newline_at_end_of_file = settings.enabled.contains(&CheckCode::W292);
|
||||
let enforce_blanket_type_ignore = settings.enabled.contains(&CheckCode::PGH003);
|
||||
let enforce_blanket_noqa = settings.enabled.contains(&CheckCode::PGH004);
|
||||
let enforce_blanket_noqa = settings.enabled.contains(&RuleCode::PGH004);
|
||||
let enforce_blanket_type_ignore = settings.enabled.contains(&RuleCode::PGH003);
|
||||
let enforce_doc_line_too_long = settings.enabled.contains(&RuleCode::W505);
|
||||
let enforce_line_too_long = settings.enabled.contains(&RuleCode::E501);
|
||||
let enforce_no_newline_at_end_of_file = settings.enabled.contains(&RuleCode::W292);
|
||||
let enforce_unnecessary_coding_comment = settings.enabled.contains(&RuleCode::UP009);
|
||||
|
||||
let mut commented_lines_iter = commented_lines.iter().peekable();
|
||||
let mut doc_lines_iter = doc_lines.iter().peekable();
|
||||
for (index, line) in contents.lines().enumerate() {
|
||||
while commented_lines_iter
|
||||
.next_if(|lineno| &(index + 1) == *lineno)
|
||||
@@ -28,59 +31,66 @@ pub fn check_lines(
|
||||
{
|
||||
if enforce_unnecessary_coding_comment {
|
||||
if index < 2 {
|
||||
if let Some(check) = unnecessary_coding_comment(
|
||||
if let Some(diagnostic) = unnecessary_coding_comment(
|
||||
index,
|
||||
line,
|
||||
matches!(autofix, flags::Autofix::Enabled)
|
||||
&& settings.fixable.contains(&CheckCode::UP009),
|
||||
&& settings.fixable.contains(&RuleCode::UP009),
|
||||
) {
|
||||
checks.push(check);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_blanket_type_ignore {
|
||||
if commented_lines.contains(&(index + 1)) {
|
||||
if let Some(check) = blanket_type_ignore(index, line) {
|
||||
checks.push(check);
|
||||
}
|
||||
if let Some(diagnostic) = blanket_type_ignore(index, line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_blanket_noqa {
|
||||
if commented_lines.contains(&(index + 1)) {
|
||||
if let Some(check) = blanket_noqa(index, line) {
|
||||
checks.push(check);
|
||||
}
|
||||
if let Some(diagnostic) = blanket_noqa(index, line) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while doc_lines_iter
|
||||
.next_if(|lineno| &(index + 1) == *lineno)
|
||||
.is_some()
|
||||
{
|
||||
if enforce_doc_line_too_long {
|
||||
if let Some(diagnostic) = doc_line_too_long(index, line, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_line_too_long {
|
||||
if let Some(check) = line_too_long(index, line, settings) {
|
||||
checks.push(check);
|
||||
if let Some(diagnostic) = line_too_long(index, line, settings) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if enforce_no_newline_at_end_of_file {
|
||||
if let Some(check) = no_newline_at_end_of_file(
|
||||
if let Some(diagnostic) = no_newline_at_end_of_file(
|
||||
contents,
|
||||
matches!(autofix, flags::Autofix::Enabled)
|
||||
&& settings.fixable.contains(&CheckCode::W292),
|
||||
&& settings.fixable.contains(&RuleCode::W292),
|
||||
) {
|
||||
checks.push(check);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
checks
|
||||
diagnostics
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::check_lines;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::registry::RuleCode;
|
||||
use crate::settings::{flags, Settings};
|
||||
|
||||
#[test]
|
||||
@@ -90,9 +100,10 @@ mod tests {
|
||||
check_lines(
|
||||
line,
|
||||
&[],
|
||||
&[],
|
||||
&Settings {
|
||||
line_length,
|
||||
..Settings::for_rule(CheckCode::E501)
|
||||
..Settings::for_rule(RuleCode::E501)
|
||||
},
|
||||
flags::Autofix::Enabled,
|
||||
)
|
||||
|
||||
@@ -6,14 +6,15 @@ use nohash_hasher::IntMap;
|
||||
use rustpython_parser::ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::fix::Fix;
|
||||
use crate::noqa::{is_file_exempt, Directive};
|
||||
use crate::registry::{Check, CheckCode, CheckKind, UnusedCodes, CODE_REDIRECTS};
|
||||
use crate::registry::{Diagnostic, DiagnosticKind, RuleCode, CODE_REDIRECTS};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::violations::UnusedCodes;
|
||||
use crate::{noqa, violations};
|
||||
|
||||
pub fn check_noqa(
|
||||
checks: &mut Vec<Check>,
|
||||
diagnostics: &mut Vec<Diagnostic>,
|
||||
contents: &str,
|
||||
commented_lines: &[usize],
|
||||
noqa_line_for: &IntMap<usize, usize>,
|
||||
@@ -23,13 +24,13 @@ pub fn check_noqa(
|
||||
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
|
||||
let mut ignored = vec![];
|
||||
|
||||
let enforce_noqa = settings.enabled.contains(&CheckCode::RUF100);
|
||||
let enforce_noqa = settings.enabled.contains(&RuleCode::RUF100);
|
||||
|
||||
let lines: Vec<&str> = contents.lines().collect();
|
||||
for lineno in commented_lines {
|
||||
// If we hit an exemption for the entire file, bail.
|
||||
if is_file_exempt(lines[lineno - 1]) {
|
||||
checks.drain(..);
|
||||
diagnostics.drain(..);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,14 +41,14 @@ pub fn check_noqa(
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any ignored checks.
|
||||
for (index, check) in checks.iter().enumerate() {
|
||||
if matches!(check.kind, CheckKind::BlanketNOQA(..)) {
|
||||
// Remove any ignored diagnostics.
|
||||
for (index, diagnostic) in diagnostics.iter().enumerate() {
|
||||
if matches!(diagnostic.kind, DiagnosticKind::BlanketNOQA(..)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Is the check ignored by a `noqa` directive on the parent line?
|
||||
if let Some(parent_lineno) = check.parent.map(|location| location.row()) {
|
||||
// Is the violation ignored by a `noqa` directive on the parent line?
|
||||
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
|
||||
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
let noqa = noqa_directives.entry(noqa_lineno - 1).or_insert_with(|| {
|
||||
@@ -55,13 +56,13 @@ pub fn check_noqa(
|
||||
});
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
matches.push(diagnostic.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
continue;
|
||||
}
|
||||
(Directive::Codes(.., codes), matches) => {
|
||||
if noqa::includes(check.kind.code(), codes) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
if noqa::includes(diagnostic.kind.code(), codes) {
|
||||
matches.push(diagnostic.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
continue;
|
||||
}
|
||||
@@ -71,21 +72,23 @@ pub fn check_noqa(
|
||||
}
|
||||
}
|
||||
|
||||
// Is the check ignored by a `noqa` directive on the same line?
|
||||
let check_lineno = check.location.row();
|
||||
let noqa_lineno = noqa_line_for.get(&check_lineno).unwrap_or(&check_lineno);
|
||||
// Is the diagnostic ignored by a `noqa` directive on the same line?
|
||||
let diagnostic_lineno = diagnostic.location.row();
|
||||
let noqa_lineno = noqa_line_for
|
||||
.get(&diagnostic_lineno)
|
||||
.unwrap_or(&diagnostic_lineno);
|
||||
if commented_lines.contains(noqa_lineno) {
|
||||
let noqa = noqa_directives
|
||||
.entry(noqa_lineno - 1)
|
||||
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![]));
|
||||
match noqa {
|
||||
(Directive::All(..), matches) => {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
matches.push(diagnostic.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
}
|
||||
(Directive::Codes(.., codes), matches) => {
|
||||
if noqa::includes(check.kind.code(), codes) {
|
||||
matches.push(check.kind.code().as_ref());
|
||||
if noqa::includes(diagnostic.kind.code(), codes) {
|
||||
matches.push(diagnostic.kind.code().as_ref());
|
||||
ignored.push(index);
|
||||
}
|
||||
}
|
||||
@@ -100,19 +103,19 @@ pub fn check_noqa(
|
||||
match directive {
|
||||
Directive::All(spaces, start, end) => {
|
||||
if matches.is_empty() {
|
||||
let mut check = Check::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
violations::UnusedNOQA(None),
|
||||
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
|
||||
);
|
||||
if matches!(autofix, flags::Autofix::Enabled)
|
||||
&& settings.fixable.contains(check.kind.code())
|
||||
&& settings.fixable.contains(diagnostic.kind.code())
|
||||
{
|
||||
check.amend(Fix::deletion(
|
||||
diagnostic.amend(Fix::deletion(
|
||||
Location::new(row + 1, start - spaces),
|
||||
Location::new(row + 1, lines[row].chars().count()),
|
||||
));
|
||||
}
|
||||
checks.push(check);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
Directive::Codes(spaces, start, end, codes) => {
|
||||
@@ -123,7 +126,7 @@ pub fn check_noqa(
|
||||
let mut self_ignore = false;
|
||||
for code in codes {
|
||||
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
|
||||
if code == CheckCode::RUF100.as_ref() {
|
||||
if code == RuleCode::RUF100.as_ref() {
|
||||
self_ignore = true;
|
||||
break;
|
||||
}
|
||||
@@ -131,8 +134,8 @@ pub fn check_noqa(
|
||||
if matches.contains(&code) || settings.external.contains(code) {
|
||||
valid_codes.push(code);
|
||||
} else {
|
||||
if let Ok(check_code) = CheckCode::from_str(code) {
|
||||
if settings.enabled.contains(&check_code) {
|
||||
if let Ok(rule_code) = RuleCode::from_str(code) {
|
||||
if settings.enabled.contains(&rule_code) {
|
||||
unmatched_codes.push(code);
|
||||
} else {
|
||||
disabled_codes.push(code);
|
||||
@@ -151,7 +154,7 @@ pub fn check_noqa(
|
||||
&& unknown_codes.is_empty()
|
||||
&& unmatched_codes.is_empty())
|
||||
{
|
||||
let mut check = Check::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
violations::UnusedNOQA(Some(UnusedCodes {
|
||||
disabled: disabled_codes
|
||||
.iter()
|
||||
@@ -169,22 +172,22 @@ pub fn check_noqa(
|
||||
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
|
||||
);
|
||||
if matches!(autofix, flags::Autofix::Enabled)
|
||||
&& settings.fixable.contains(check.kind.code())
|
||||
&& settings.fixable.contains(diagnostic.kind.code())
|
||||
{
|
||||
if valid_codes.is_empty() {
|
||||
check.amend(Fix::deletion(
|
||||
diagnostic.amend(Fix::deletion(
|
||||
Location::new(row + 1, start - spaces),
|
||||
Location::new(row + 1, lines[row].chars().count()),
|
||||
));
|
||||
} else {
|
||||
check.amend(Fix::replacement(
|
||||
diagnostic.amend(Fix::replacement(
|
||||
format!("# noqa: {}", valid_codes.join(", ")),
|
||||
Location::new(row + 1, start),
|
||||
Location::new(row + 1, lines[row].chars().count()),
|
||||
));
|
||||
}
|
||||
}
|
||||
checks.push(check);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
Directive::None => {}
|
||||
@@ -194,6 +197,6 @@ pub fn check_noqa(
|
||||
|
||||
ignored.sort_unstable();
|
||||
for index in ignored.iter().rev() {
|
||||
checks.swap_remove(*index);
|
||||
diagnostics.swap_remove(*index);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,31 +3,31 @@
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::lex::docstring_detection::StateMachine;
|
||||
use crate::registry::{Check, CheckCode};
|
||||
use crate::ruff::checks::Context;
|
||||
use crate::settings::flags;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::{eradicate, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff, Settings};
|
||||
use crate::registry::{Diagnostic, RuleCode};
|
||||
use crate::ruff::rules::Context;
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::source_code::Locator;
|
||||
use crate::{eradicate, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff};
|
||||
|
||||
pub fn check_tokens(
|
||||
locator: &SourceCodeLocator,
|
||||
locator: &Locator,
|
||||
tokens: &[LexResult],
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Vec<Check> {
|
||||
let mut checks: Vec<Check> = vec![];
|
||||
) -> Vec<Diagnostic> {
|
||||
let mut diagnostics: Vec<Diagnostic> = vec![];
|
||||
|
||||
let enforce_ambiguous_unicode_character = settings.enabled.contains(&CheckCode::RUF001)
|
||||
|| settings.enabled.contains(&CheckCode::RUF002)
|
||||
|| settings.enabled.contains(&CheckCode::RUF003);
|
||||
let enforce_quotes = settings.enabled.contains(&CheckCode::Q000)
|
||||
|| settings.enabled.contains(&CheckCode::Q001)
|
||||
|| settings.enabled.contains(&CheckCode::Q002)
|
||||
|| settings.enabled.contains(&CheckCode::Q003);
|
||||
let enforce_commented_out_code = settings.enabled.contains(&CheckCode::ERA001);
|
||||
let enforce_invalid_escape_sequence = settings.enabled.contains(&CheckCode::W605);
|
||||
let enforce_implicit_string_concatenation = settings.enabled.contains(&CheckCode::ISC001)
|
||||
|| settings.enabled.contains(&CheckCode::ISC002);
|
||||
let enforce_ambiguous_unicode_character = settings.enabled.contains(&RuleCode::RUF001)
|
||||
|| settings.enabled.contains(&RuleCode::RUF002)
|
||||
|| settings.enabled.contains(&RuleCode::RUF003);
|
||||
let enforce_quotes = settings.enabled.contains(&RuleCode::Q000)
|
||||
|| settings.enabled.contains(&RuleCode::Q001)
|
||||
|| settings.enabled.contains(&RuleCode::Q002)
|
||||
|| settings.enabled.contains(&RuleCode::Q003);
|
||||
let enforce_commented_out_code = settings.enabled.contains(&RuleCode::ERA001);
|
||||
let enforce_invalid_escape_sequence = settings.enabled.contains(&RuleCode::W605);
|
||||
let enforce_implicit_string_concatenation = settings.enabled.contains(&RuleCode::ISC001)
|
||||
|| settings.enabled.contains(&RuleCode::ISC002);
|
||||
|
||||
let mut state_machine = StateMachine::default();
|
||||
for &(start, ref tok, end) in tokens.iter().flatten() {
|
||||
@@ -40,7 +40,7 @@ pub fn check_tokens(
|
||||
// RUF001, RUF002, RUF003
|
||||
if enforce_ambiguous_unicode_character {
|
||||
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
|
||||
checks.extend(ruff::checks::ambiguous_unicode_character(
|
||||
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
@@ -62,15 +62,16 @@ pub fn check_tokens(
|
||||
// flake8-quotes
|
||||
if enforce_quotes {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
if let Some(check) = flake8_quotes::checks::quotes(
|
||||
if let Some(diagnostic) = flake8_quotes::rules::quotes(
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
is_docstring,
|
||||
&settings.flake8_quotes,
|
||||
settings,
|
||||
autofix,
|
||||
) {
|
||||
if settings.enabled.contains(check.kind.code()) {
|
||||
checks.push(check);
|
||||
if settings.enabled.contains(diagnostic.kind.code()) {
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,10 +80,10 @@ pub fn check_tokens(
|
||||
// eradicate
|
||||
if enforce_commented_out_code {
|
||||
if matches!(tok, Tok::Comment(_)) {
|
||||
if let Some(check) =
|
||||
eradicate::checks::commented_out_code(locator, start, end, settings, autofix)
|
||||
if let Some(diagnostic) =
|
||||
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
|
||||
{
|
||||
checks.push(check);
|
||||
diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,12 +91,12 @@ pub fn check_tokens(
|
||||
// W605
|
||||
if enforce_invalid_escape_sequence {
|
||||
if matches!(tok, Tok::String { .. }) {
|
||||
checks.extend(pycodestyle::checks::invalid_escape_sequence(
|
||||
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
matches!(autofix, flags::Autofix::Enabled)
|
||||
&& settings.fixable.contains(&CheckCode::W605),
|
||||
&& settings.fixable.contains(&RuleCode::W605),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -103,12 +104,12 @@ pub fn check_tokens(
|
||||
|
||||
// ISC001, ISC002
|
||||
if enforce_implicit_string_concatenation {
|
||||
checks.extend(
|
||||
flake8_implicit_str_concat::checks::implicit(tokens, locator)
|
||||
diagnostics.extend(
|
||||
flake8_implicit_str_concat::rules::implicit(tokens, locator)
|
||||
.into_iter()
|
||||
.filter(|check| settings.enabled.contains(check.kind.code())),
|
||||
.filter(|diagnostic| settings.enabled.contains(diagnostic.kind.code())),
|
||||
);
|
||||
}
|
||||
|
||||
checks
|
||||
diagnostics
|
||||
}
|
||||
|
||||
181
src/cli.rs
181
src/cli.rs
@@ -4,12 +4,13 @@ use clap::{command, Parser};
|
||||
use regex::Regex;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use crate::fs;
|
||||
use crate::logging::LogLevel;
|
||||
use crate::registry::{CheckCode, CheckCodePrefix};
|
||||
use crate::registry::{RuleCode, RuleCodePrefix};
|
||||
use crate::resolver::ConfigProcessor;
|
||||
use crate::settings::types::{
|
||||
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
|
||||
};
|
||||
use crate::{fs, mccabe};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(author, about = "Ruff: An extremely fast Python linter.")]
|
||||
@@ -20,31 +21,31 @@ pub struct Cli {
|
||||
pub files: Vec<PathBuf>,
|
||||
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
|
||||
/// configuration.
|
||||
#[arg(long)]
|
||||
#[arg(long, conflicts_with = "isolated")]
|
||||
pub config: Option<PathBuf>,
|
||||
/// Enable verbose logging.
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub verbose: bool,
|
||||
/// Only log errors.
|
||||
/// Print lint violations, but nothing else.
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub quiet: bool,
|
||||
/// Disable all logging (but still exit with status code "1" upon detecting
|
||||
/// errors).
|
||||
/// lint violations).
|
||||
#[arg(short, long, group = "verbosity")]
|
||||
pub silent: bool,
|
||||
/// Exit with status code "0", even upon detecting errors.
|
||||
/// Exit with status code "0", even upon detecting lint violations.
|
||||
#[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.
|
||||
/// Attempt to automatically fix lint violations.
|
||||
#[arg(long, overrides_with("no_fix"))]
|
||||
fix: bool,
|
||||
#[clap(long, overrides_with("fix"), hide = true)]
|
||||
no_fix: bool,
|
||||
/// Fix any fixable lint errors, but don't report on leftover violations.
|
||||
/// Implies `--fix`.
|
||||
/// Fix any fixable lint violations, but don't report on leftover
|
||||
/// violations. Implies `--fix`.
|
||||
#[arg(long, overrides_with("no_fix_only"))]
|
||||
fix_only: bool,
|
||||
#[clap(long, overrides_with("fix_only"), hide = true)]
|
||||
@@ -56,47 +57,50 @@ pub struct Cli {
|
||||
/// Disable cache reads.
|
||||
#[arg(short, long)]
|
||||
pub no_cache: bool,
|
||||
/// Comma-separated list of error codes to enable (or ALL, to enable all
|
||||
/// checks).
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub select: Option<Vec<CheckCodePrefix>>,
|
||||
/// Like --select, but adds additional error codes on top of the selected
|
||||
/// Ignore all configuration files.
|
||||
#[arg(long, conflicts_with = "config")]
|
||||
pub isolated: bool,
|
||||
/// Comma-separated list of rule codes to enable (or ALL, to enable all
|
||||
/// rules).
|
||||
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
|
||||
pub select: Option<Vec<RuleCodePrefix>>,
|
||||
/// Like --select, but adds additional rule codes on top of the selected
|
||||
/// ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_select: Option<Vec<CheckCodePrefix>>,
|
||||
/// Comma-separated list of error codes to disable.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub ignore: Option<Vec<CheckCodePrefix>>,
|
||||
/// Like --ignore, but adds additional error codes on top of the ignored
|
||||
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
|
||||
pub extend_select: Option<Vec<RuleCodePrefix>>,
|
||||
/// Comma-separated list of rule codes to disable.
|
||||
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
|
||||
pub ignore: Option<Vec<RuleCodePrefix>>,
|
||||
/// Like --ignore, but adds additional rule codes on top of the ignored
|
||||
/// ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
|
||||
/// List of paths, used to exclude files and/or directories from checks.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
|
||||
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
|
||||
/// List of paths, used to omit files and/or directories from analysis.
|
||||
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
/// Like --exclude, but adds additional files and directories on top of the
|
||||
/// excluded ones.
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
/// Like --exclude, but adds additional files and directories on top of
|
||||
/// those already excluded.
|
||||
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
|
||||
pub extend_exclude: Option<Vec<FilePattern>>,
|
||||
/// List of error codes to treat as eligible for autofix. Only applicable
|
||||
/// List of rule codes to treat as eligible for autofix. Only applicable
|
||||
/// when autofix itself is enabled (e.g., via `--fix`).
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub fixable: Option<Vec<CheckCodePrefix>>,
|
||||
/// List of error codes to treat as ineligible for autofix. Only applicable
|
||||
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
|
||||
pub fixable: Option<Vec<RuleCodePrefix>>,
|
||||
/// List of rule codes to treat as ineligible for autofix. Only applicable
|
||||
/// when autofix itself is enabled (e.g., via `--fix`).
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub unfixable: Option<Vec<CheckCodePrefix>>,
|
||||
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
|
||||
pub unfixable: Option<Vec<RuleCodePrefix>>,
|
||||
/// List of mappings from file pattern to code to exclude
|
||||
#[arg(long, value_delimiter = ',')]
|
||||
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
/// Output serialization format for error messages.
|
||||
#[arg(long, value_enum)]
|
||||
/// Output serialization format for violations.
|
||||
#[arg(long, value_enum, env = "RUFF_FORMAT")]
|
||||
pub format: Option<SerializationFormat>,
|
||||
/// The name of the file when passing it through stdin.
|
||||
#[arg(long)]
|
||||
pub stdin_filename: Option<PathBuf>,
|
||||
/// Path to the cache directory.
|
||||
#[arg(long)]
|
||||
#[arg(long, env = "RUFF_CACHE_DIR")]
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
/// Show violations with source code.
|
||||
#[arg(long, overrides_with("no_show_source"))]
|
||||
@@ -126,7 +130,7 @@ pub struct Cli {
|
||||
/// The minimum Python version that should be supported.
|
||||
#[arg(long)]
|
||||
pub target_version: Option<PythonVersion>,
|
||||
/// Set the line-length for length-associated checks and automatic
|
||||
/// Set the line-length for length-associated rules and automatic
|
||||
/// formatting.
|
||||
#[arg(long)]
|
||||
pub line_length: Option<usize>,
|
||||
@@ -176,7 +180,7 @@ pub struct Cli {
|
||||
conflicts_with = "stdin_filename",
|
||||
conflicts_with = "watch",
|
||||
)]
|
||||
pub explain: Option<CheckCode>,
|
||||
pub explain: Option<RuleCode>,
|
||||
/// Generate shell completion
|
||||
#[arg(
|
||||
long,
|
||||
@@ -209,7 +213,7 @@ pub struct Cli {
|
||||
conflicts_with = "watch",
|
||||
)]
|
||||
pub show_files: bool,
|
||||
/// See the settings Ruff will use to check a given Python file.
|
||||
/// See the settings Ruff will use to lint a given Python file.
|
||||
#[arg(
|
||||
long,
|
||||
// Fake subcommands.
|
||||
@@ -240,6 +244,7 @@ impl Cli {
|
||||
explain: self.explain,
|
||||
files: self.files,
|
||||
generate_shell_completion: self.generate_shell_completion,
|
||||
isolated: self.isolated,
|
||||
no_cache: self.no_cache,
|
||||
quiet: self.quiet,
|
||||
show_files: self.show_files,
|
||||
@@ -298,9 +303,10 @@ pub struct Arguments {
|
||||
pub config: Option<PathBuf>,
|
||||
pub diff: bool,
|
||||
pub exit_zero: bool,
|
||||
pub explain: Option<CheckCode>,
|
||||
pub explain: Option<RuleCode>,
|
||||
pub files: Vec<PathBuf>,
|
||||
pub generate_shell_completion: Option<clap_complete_command::Shell>,
|
||||
pub isolated: bool,
|
||||
pub no_cache: bool,
|
||||
pub quiet: bool,
|
||||
pub show_files: bool,
|
||||
@@ -318,18 +324,18 @@ pub struct Overrides {
|
||||
pub dummy_variable_rgx: Option<Regex>,
|
||||
pub exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_exclude: Option<Vec<FilePattern>>,
|
||||
pub extend_ignore: Option<Vec<CheckCodePrefix>>,
|
||||
pub extend_select: Option<Vec<CheckCodePrefix>>,
|
||||
pub fixable: Option<Vec<CheckCodePrefix>>,
|
||||
pub ignore: Option<Vec<CheckCodePrefix>>,
|
||||
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
|
||||
pub extend_select: Option<Vec<RuleCodePrefix>>,
|
||||
pub fixable: Option<Vec<RuleCodePrefix>>,
|
||||
pub ignore: Option<Vec<RuleCodePrefix>>,
|
||||
pub line_length: Option<usize>,
|
||||
pub max_complexity: Option<usize>,
|
||||
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
|
||||
pub respect_gitignore: Option<bool>,
|
||||
pub select: Option<Vec<CheckCodePrefix>>,
|
||||
pub select: Option<Vec<RuleCodePrefix>>,
|
||||
pub show_source: Option<bool>,
|
||||
pub target_version: Option<PythonVersion>,
|
||||
pub unfixable: Option<Vec<CheckCodePrefix>>,
|
||||
pub unfixable: Option<Vec<RuleCodePrefix>>,
|
||||
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
|
||||
pub cache_dir: Option<PathBuf>,
|
||||
pub fix: Option<bool>,
|
||||
@@ -339,6 +345,87 @@ pub struct Overrides {
|
||||
pub update_check: Option<bool>,
|
||||
}
|
||||
|
||||
impl ConfigProcessor for &Overrides {
|
||||
fn process_config(&self, config: &mut crate::settings::configuration::Configuration) {
|
||||
if let Some(cache_dir) = &self.cache_dir {
|
||||
config.cache_dir = Some(cache_dir.clone());
|
||||
}
|
||||
if let Some(dummy_variable_rgx) = &self.dummy_variable_rgx {
|
||||
config.dummy_variable_rgx = Some(dummy_variable_rgx.clone());
|
||||
}
|
||||
if let Some(exclude) = &self.exclude {
|
||||
config.exclude = Some(exclude.clone());
|
||||
}
|
||||
if let Some(extend_exclude) = &self.extend_exclude {
|
||||
config.extend_exclude.extend(extend_exclude.clone());
|
||||
}
|
||||
if let Some(fix) = &self.fix {
|
||||
config.fix = Some(*fix);
|
||||
}
|
||||
if let Some(fix_only) = &self.fix_only {
|
||||
config.fix_only = Some(*fix_only);
|
||||
}
|
||||
if let Some(fixable) = &self.fixable {
|
||||
config.fixable = Some(fixable.clone());
|
||||
}
|
||||
if let Some(format) = &self.format {
|
||||
config.format = Some(*format);
|
||||
}
|
||||
if let Some(force_exclude) = &self.force_exclude {
|
||||
config.force_exclude = Some(*force_exclude);
|
||||
}
|
||||
if let Some(ignore) = &self.ignore {
|
||||
config.ignore = Some(ignore.clone());
|
||||
}
|
||||
if let Some(line_length) = &self.line_length {
|
||||
config.line_length = Some(*line_length);
|
||||
}
|
||||
if let Some(max_complexity) = &self.max_complexity {
|
||||
config.mccabe = Some(mccabe::settings::Options {
|
||||
max_complexity: Some(*max_complexity),
|
||||
});
|
||||
}
|
||||
if let Some(per_file_ignores) = &self.per_file_ignores {
|
||||
config.per_file_ignores = Some(collect_per_file_ignores(per_file_ignores.clone()));
|
||||
}
|
||||
if let Some(respect_gitignore) = &self.respect_gitignore {
|
||||
config.respect_gitignore = Some(*respect_gitignore);
|
||||
}
|
||||
if let Some(select) = &self.select {
|
||||
config.select = Some(select.clone());
|
||||
}
|
||||
if let Some(show_source) = &self.show_source {
|
||||
config.show_source = Some(*show_source);
|
||||
}
|
||||
if let Some(target_version) = &self.target_version {
|
||||
config.target_version = Some(*target_version);
|
||||
}
|
||||
if let Some(unfixable) = &self.unfixable {
|
||||
config.unfixable = Some(unfixable.clone());
|
||||
}
|
||||
if let Some(update_check) = &self.update_check {
|
||||
config.update_check = Some(*update_check);
|
||||
}
|
||||
// Special-case: `extend_ignore` and `extend_select` are parallel arrays, so
|
||||
// push an empty array if only one of the two is provided.
|
||||
match (&self.extend_ignore, &self.extend_select) {
|
||||
(Some(extend_ignore), Some(extend_select)) => {
|
||||
config.extend_ignore.push(extend_ignore.clone());
|
||||
config.extend_select.push(extend_select.clone());
|
||||
}
|
||||
(Some(extend_ignore), None) => {
|
||||
config.extend_ignore.push(extend_ignore.clone());
|
||||
config.extend_select.push(Vec::new());
|
||||
}
|
||||
(None, Some(extend_select)) => {
|
||||
config.extend_ignore.push(Vec::new());
|
||||
config.extend_select.push(extend_select.clone());
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Map the CLI settings to a `LogLevel`.
|
||||
pub fn extract_log_level(cli: &Arguments) -> LogLevel {
|
||||
if cli.silent {
|
||||
@@ -354,7 +441,7 @@ pub fn extract_log_level(cli: &Arguments) -> LogLevel {
|
||||
|
||||
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
|
||||
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<CheckCodePrefix>> = FxHashMap::default();
|
||||
let mut per_file_ignores: FxHashMap<String, Vec<RuleCodePrefix>> = FxHashMap::default();
|
||||
for pair in pairs {
|
||||
per_file_ignores
|
||||
.entry(pair.pattern)
|
||||
|
||||
@@ -15,18 +15,18 @@ use rustpython_ast::Location;
|
||||
use serde::Serialize;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::autofix::fixer;
|
||||
use crate::cache::DEFAULT_CACHE_DIR_NAME;
|
||||
use crate::cache::CACHE_DIR_NAME;
|
||||
use crate::cli::Overrides;
|
||||
use crate::diagnostics::{lint_path, lint_stdin, Diagnostics};
|
||||
use crate::iterators::par_iter;
|
||||
use crate::linter::{add_noqa_to_path, lint_path, lint_stdin, Diagnostics};
|
||||
use crate::linter::add_noqa_to_path;
|
||||
use crate::logging::LogLevel;
|
||||
use crate::message::Message;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::registry::RuleCode;
|
||||
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
|
||||
use crate::settings::flags;
|
||||
use crate::settings::types::SerializationFormat;
|
||||
use crate::{cache, fs, one_time_warning, packages, resolver, violations};
|
||||
use crate::{cache, fix, fs, packaging, resolver, violations, warn_user_once};
|
||||
|
||||
/// Run the linter over a collection of files.
|
||||
pub fn run(
|
||||
@@ -35,7 +35,7 @@ pub fn run(
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
cache: flags::Cache,
|
||||
autofix: fixer::Mode,
|
||||
autofix: fix::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
// Collect all the Python files to check.
|
||||
let start = Instant::now();
|
||||
@@ -45,12 +45,7 @@ pub fn run(
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
if paths.is_empty() {
|
||||
one_time_warning!(
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
"No Python files found under the given path(s)".bold()
|
||||
);
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
return Ok(Diagnostics::default());
|
||||
}
|
||||
|
||||
@@ -82,7 +77,7 @@ pub fn run(
|
||||
};
|
||||
|
||||
// Discover the package root for each Python file.
|
||||
let package_roots = packages::detect_package_roots(
|
||||
let package_roots = packaging::detect_package_roots(
|
||||
&paths
|
||||
.iter()
|
||||
.flatten()
|
||||
@@ -117,7 +112,7 @@ pub fn run(
|
||||
.unwrap_or_else(|(path, message)| {
|
||||
if let Some(path) = &path {
|
||||
let settings = resolver.resolve(path, pyproject_strategy);
|
||||
if settings.enabled.contains(&CheckCode::E902) {
|
||||
if settings.enabled.contains(&RuleCode::E902) {
|
||||
Diagnostics::new(vec![Message {
|
||||
kind: violations::IOError(message).into(),
|
||||
location: Location::default(),
|
||||
@@ -161,7 +156,7 @@ pub fn run_stdin(
|
||||
pyproject_strategy: &PyprojectDiscovery,
|
||||
file_strategy: &FileDiscovery,
|
||||
overrides: &Overrides,
|
||||
autofix: fixer::Mode,
|
||||
autofix: fix::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
if let Some(filename) = filename {
|
||||
if !resolver::python_file_at_path(filename, pyproject_strategy, file_strategy, overrides)? {
|
||||
@@ -174,7 +169,7 @@ pub fn run_stdin(
|
||||
};
|
||||
let package_root = filename
|
||||
.and_then(Path::parent)
|
||||
.and_then(packages::detect_package_root);
|
||||
.and_then(packaging::detect_package_root);
|
||||
let stdin = read_from_stdin()?;
|
||||
let mut diagnostics = lint_stdin(filename, package_root, &stdin, settings, autofix)?;
|
||||
diagnostics.messages.sort_unstable();
|
||||
@@ -196,12 +191,7 @@ pub fn add_noqa(
|
||||
debug!("Identified files to lint in: {:?}", duration);
|
||||
|
||||
if paths.is_empty() {
|
||||
one_time_warning!(
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
"No Python files found under the given path(s)".bold()
|
||||
);
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
@@ -271,12 +261,7 @@ pub fn show_files(
|
||||
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
|
||||
|
||||
if paths.is_empty() {
|
||||
one_time_warning!(
|
||||
"{}{} {}",
|
||||
"warning".yellow().bold(),
|
||||
":".bold(),
|
||||
"No Python files found under the given path(s)".bold()
|
||||
);
|
||||
warn_user_once!("No Python files found under the given path(s)");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -298,18 +283,18 @@ pub fn show_files(
|
||||
#[derive(Serialize)]
|
||||
struct Explanation<'a> {
|
||||
code: &'a str,
|
||||
category: &'a str,
|
||||
origin: &'a str,
|
||||
summary: &'a str,
|
||||
}
|
||||
|
||||
/// Explain a `CheckCode` to the user.
|
||||
pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
|
||||
/// Explain a `RuleCode` to the user.
|
||||
pub fn explain(code: &RuleCode, format: &SerializationFormat) -> Result<()> {
|
||||
match format {
|
||||
SerializationFormat::Text | SerializationFormat::Grouped => {
|
||||
println!(
|
||||
"{} ({}): {}",
|
||||
code.as_ref(),
|
||||
code.category().title(),
|
||||
code.origin().title(),
|
||||
code.kind().summary()
|
||||
);
|
||||
}
|
||||
@@ -318,7 +303,7 @@ pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
|
||||
"{}",
|
||||
serde_json::to_string_pretty(&Explanation {
|
||||
code: code.as_ref(),
|
||||
category: code.category().title(),
|
||||
origin: code.origin().title(),
|
||||
summary: &code.kind().summary(),
|
||||
})?
|
||||
);
|
||||
@@ -340,10 +325,10 @@ pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
|
||||
pub fn clean(level: &LogLevel) -> Result<()> {
|
||||
for entry in WalkDir::new(&*path_dedot::CWD)
|
||||
.into_iter()
|
||||
.filter_map(std::result::Result::ok)
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.file_type().is_dir())
|
||||
{
|
||||
let cache = entry.path().join(DEFAULT_CACHE_DIR_NAME);
|
||||
let cache = entry.path().join(CACHE_DIR_NAME);
|
||||
if cache.is_dir() {
|
||||
if level >= &LogLevel::Default {
|
||||
eprintln!("Removing cache at: {}", fs::relativize_path(&cache).bold());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::{bail, Result};
|
||||
use libcst_native::{Expr, Import, ImportFrom, Module, SmallStatement, Statement};
|
||||
use libcst_native::{
|
||||
Call, Expr, Expression, Import, ImportFrom, Module, SmallStatement, Statement,
|
||||
};
|
||||
|
||||
pub fn match_module(module_text: &str) -> Result<Module> {
|
||||
match libcst_native::parse_module(module_text, None) {
|
||||
@@ -8,6 +10,13 @@ pub fn match_module(module_text: &str) -> Result<Module> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_expression(expression_text: &str) -> Result<Expression> {
|
||||
match libcst_native::parse_expression(expression_text) {
|
||||
Ok(expression) => Ok(expression),
|
||||
Err(_) => bail!("Failed to extract CST from source"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>> {
|
||||
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
|
||||
if let Some(SmallStatement::Expr(expr)) = expr.body.first_mut() {
|
||||
@@ -43,3 +52,11 @@ pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut I
|
||||
bail!("Expected Statement::Simple")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn match_call<'a, 'b>(expression: &'a mut Expression<'b>) -> Result<&'a mut Call<'b>> {
|
||||
if let Expression::Call(call) = expression {
|
||||
Ok(call)
|
||||
} else {
|
||||
bail!("Expected SmallStatement::Expr")
|
||||
}
|
||||
}
|
||||
|
||||
154
src/diagnostics.rs
Normal file
154
src/diagnostics.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
#![cfg_attr(target_family = "wasm", allow(dead_code))]
|
||||
use std::fs::write;
|
||||
use std::io;
|
||||
use std::io::Write;
|
||||
use std::ops::AddAssign;
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::Result;
|
||||
use log::debug;
|
||||
use similar::TextDiff;
|
||||
|
||||
use crate::linter::{lint_fix, lint_only};
|
||||
use crate::message::Message;
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::{cache, fix, fs};
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Diagnostics {
|
||||
pub messages: Vec<Message>,
|
||||
pub fixed: usize,
|
||||
}
|
||||
|
||||
impl Diagnostics {
|
||||
pub fn new(messages: Vec<Message>) -> Self {
|
||||
Self { messages, fixed: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl AddAssign for Diagnostics {
|
||||
fn add_assign(&mut self, other: Self) {
|
||||
self.messages.extend(other.messages);
|
||||
self.fixed += other.fixed;
|
||||
}
|
||||
}
|
||||
|
||||
/// Lint the source code at the given `Path`.
|
||||
pub fn lint_path(
|
||||
path: &Path,
|
||||
package: Option<&Path>,
|
||||
settings: &Settings,
|
||||
cache: flags::Cache,
|
||||
autofix: fix::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
// Validate the `Settings` and return any errors.
|
||||
settings.validate()?;
|
||||
|
||||
// Check the cache.
|
||||
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
|
||||
// side-effects that aren't captured in the cache. (In practice, it's fine
|
||||
// to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll
|
||||
// write the fixes to disk, thus invalidating the cache. But it's a bit hard
|
||||
// to reason about. We need to come up with a better solution here.)
|
||||
let metadata = if matches!(cache, flags::Cache::Enabled)
|
||||
&& matches!(autofix, fix::FixMode::None | fix::FixMode::Generate)
|
||||
{
|
||||
let metadata = path.metadata()?;
|
||||
if let Some(messages) = cache::get(path, &metadata, settings, autofix.into()) {
|
||||
debug!("Cache hit for: {}", path.to_string_lossy());
|
||||
return Ok(Diagnostics::new(messages));
|
||||
}
|
||||
Some(metadata)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// Read the file from disk.
|
||||
let contents = fs::read_file(path)?;
|
||||
|
||||
// Lint the file.
|
||||
let (messages, fixed) = if matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff) {
|
||||
let (transformed, fixed, messages) = lint_fix(&contents, path, package, settings)?;
|
||||
if fixed > 0 {
|
||||
if matches!(autofix, fix::FixMode::Apply) {
|
||||
write(path, transformed)?;
|
||||
} else if matches!(autofix, fix::FixMode::Diff) {
|
||||
let mut stdout = io::stdout().lock();
|
||||
TextDiff::from_lines(&contents, &transformed)
|
||||
.unified_diff()
|
||||
.header(&fs::relativize_path(path), &fs::relativize_path(path))
|
||||
.to_writer(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
}
|
||||
(messages, fixed)
|
||||
} else {
|
||||
let messages = lint_only(&contents, path, package, settings, autofix.into())?;
|
||||
let fixed = 0;
|
||||
(messages, fixed)
|
||||
};
|
||||
|
||||
// Re-populate the cache.
|
||||
if let Some(metadata) = metadata {
|
||||
cache::set(path, &metadata, settings, autofix.into(), &messages);
|
||||
}
|
||||
|
||||
Ok(Diagnostics { messages, fixed })
|
||||
}
|
||||
|
||||
/// Generate `Diagnostic`s from source code content derived from
|
||||
/// stdin.
|
||||
pub fn lint_stdin(
|
||||
path: Option<&Path>,
|
||||
package: Option<&Path>,
|
||||
contents: &str,
|
||||
settings: &Settings,
|
||||
autofix: fix::FixMode,
|
||||
) -> Result<Diagnostics> {
|
||||
// Validate the `Settings` and return any errors.
|
||||
settings.validate()?;
|
||||
|
||||
// Lint the inputs.
|
||||
let (messages, fixed) = if matches!(autofix, fix::FixMode::Apply | fix::FixMode::Diff) {
|
||||
let (transformed, fixed, messages) = lint_fix(
|
||||
contents,
|
||||
path.unwrap_or_else(|| Path::new("-")),
|
||||
package,
|
||||
settings,
|
||||
)?;
|
||||
|
||||
if matches!(autofix, fix::FixMode::Apply) {
|
||||
// Write the contents to stdout, regardless of whether any errors were fixed.
|
||||
io::stdout().write_all(transformed.as_bytes())?;
|
||||
} else if matches!(autofix, fix::FixMode::Diff) {
|
||||
// But only write a diff if it's non-empty.
|
||||
if fixed > 0 {
|
||||
let text_diff = TextDiff::from_lines(contents, &transformed);
|
||||
let mut unified_diff = text_diff.unified_diff();
|
||||
if let Some(path) = path {
|
||||
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
|
||||
}
|
||||
|
||||
let mut stdout = io::stdout().lock();
|
||||
unified_diff.to_writer(&mut stdout)?;
|
||||
stdout.write_all(b"\n")?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
}
|
||||
|
||||
(messages, fixed)
|
||||
} else {
|
||||
let messages = lint_only(
|
||||
contents,
|
||||
path.unwrap_or_else(|| Path::new("-")),
|
||||
package,
|
||||
settings,
|
||||
autofix.into(),
|
||||
)?;
|
||||
let fixed = 0;
|
||||
(messages, fixed)
|
||||
};
|
||||
|
||||
Ok(Diagnostics { messages, fixed })
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use rustpython_ast::Location;
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::registry::LintSource;
|
||||
use crate::Settings;
|
||||
use crate::settings::Settings;
|
||||
|
||||
bitflags! {
|
||||
pub struct Flags: u32 {
|
||||
@@ -20,7 +20,7 @@ impl Flags {
|
||||
if settings
|
||||
.enabled
|
||||
.iter()
|
||||
.any(|check_code| matches!(check_code.lint_source(), LintSource::Imports))
|
||||
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::Imports))
|
||||
{
|
||||
Flags::NOQA | Flags::ISORT
|
||||
} else {
|
||||
@@ -33,6 +33,7 @@ impl Flags {
|
||||
pub struct IsortDirectives {
|
||||
pub exclusions: IntSet<usize>,
|
||||
pub splits: Vec<usize>,
|
||||
pub skip_file: bool,
|
||||
}
|
||||
|
||||
pub struct Directives {
|
||||
@@ -89,26 +90,26 @@ pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
|
||||
pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
let mut exclusions: IntSet<usize> = IntSet::default();
|
||||
let mut splits: Vec<usize> = Vec::default();
|
||||
let mut skip_file: bool = false;
|
||||
let mut off: Option<Location> = None;
|
||||
let mut last: Option<Location> = None;
|
||||
for &(start, ref tok, end) in lxr.iter().flatten() {
|
||||
last = Some(end);
|
||||
|
||||
// No need to keep processing, but we do need to determine the last token.
|
||||
if skip_file {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Tok::Comment(comment_text) = tok else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// `isort` allows for `# isort: skip` and `# isort: skip_file` to include or
|
||||
// omit a space after the colon. The remaining action comments are
|
||||
// required to include the space, and must appear on their own lines.
|
||||
let comment_text = comment_text.trim_end();
|
||||
if comment_text == "# isort: split" {
|
||||
splits.push(start.row());
|
||||
} else if comment_text == "# isort: skip_file" {
|
||||
skip_file = true;
|
||||
} else if comment_text == "# isort: skip_file" || comment_text == "# isort:skip_file" {
|
||||
return IsortDirectives {
|
||||
skip_file: true,
|
||||
..IsortDirectives::default()
|
||||
};
|
||||
} else if off.is_some() {
|
||||
if comment_text == "# isort: on" {
|
||||
if let Some(start) = off {
|
||||
@@ -119,7 +120,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
off = None;
|
||||
}
|
||||
} else {
|
||||
if comment_text.contains("isort: skip") {
|
||||
if comment_text.contains("isort: skip") || comment_text.contains("isort:skip") {
|
||||
exclusions.insert(start.row());
|
||||
} else if comment_text == "# isort: off" {
|
||||
off = Some(start);
|
||||
@@ -127,14 +128,7 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
}
|
||||
}
|
||||
|
||||
if skip_file {
|
||||
// Enforce `isort: skip_file`.
|
||||
if let Some(end) = last {
|
||||
for row in 1..=end.row() {
|
||||
exclusions.insert(row);
|
||||
}
|
||||
}
|
||||
} else if let Some(start) = off {
|
||||
if let Some(start) = off {
|
||||
// Enforce unterminated `isort: off`.
|
||||
if let Some(end) = last {
|
||||
for row in start.row() + 1..=end.row() {
|
||||
@@ -142,7 +136,11 @@ pub fn extract_isort_directives(lxr: &[LexResult]) -> IsortDirectives {
|
||||
}
|
||||
}
|
||||
}
|
||||
IsortDirectives { exclusions, splits }
|
||||
IsortDirectives {
|
||||
exclusions,
|
||||
splits,
|
||||
..IsortDirectives::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -280,10 +278,7 @@ x = 1
|
||||
y = 2
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr).exclusions,
|
||||
IntSet::from_iter([1, 2, 3, 4])
|
||||
);
|
||||
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
|
||||
|
||||
let contents = "# isort: off
|
||||
x = 1
|
||||
@@ -292,10 +287,7 @@ y = 2
|
||||
# isort: skip_file
|
||||
z = x + 1";
|
||||
let lxr: Vec<LexResult> = lexer::make_tokenizer(contents).collect();
|
||||
assert_eq!(
|
||||
extract_isort_directives(&lxr).exclusions,
|
||||
IntSet::from_iter([1, 2, 3, 4, 5, 6])
|
||||
);
|
||||
assert_eq!(extract_isort_directives(&lxr).exclusions, IntSet::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
58
src/doc_lines.rs
Normal file
58
src/doc_lines.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Doc line extraction. In this context, a doc line is a line consisting of a
|
||||
//! standalone comment or a constant string statement.
|
||||
|
||||
use rustpython_ast::{Constant, ExprKind, Stmt, StmtKind, Suite};
|
||||
use rustpython_parser::lexer::{LexResult, Tok};
|
||||
|
||||
use crate::ast::visitor;
|
||||
use crate::ast::visitor::Visitor;
|
||||
|
||||
/// Extract doc lines (standalone comments) from a token sequence.
|
||||
pub fn doc_lines_from_tokens(lxr: &[LexResult]) -> Vec<usize> {
|
||||
let mut doc_lines: Vec<usize> = Vec::default();
|
||||
let mut prev: Option<usize> = None;
|
||||
for (start, tok, end) in lxr.iter().flatten() {
|
||||
if matches!(tok, Tok::Indent | Tok::Dedent | Tok::Newline) {
|
||||
continue;
|
||||
}
|
||||
if matches!(tok, Tok::Comment(..)) {
|
||||
if let Some(prev) = prev {
|
||||
if start.row() > prev {
|
||||
doc_lines.push(start.row());
|
||||
}
|
||||
} else {
|
||||
doc_lines.push(start.row());
|
||||
}
|
||||
}
|
||||
prev = Some(end.row());
|
||||
}
|
||||
doc_lines
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct StringLinesVisitor {
|
||||
string_lines: Vec<usize>,
|
||||
}
|
||||
|
||||
impl Visitor<'_> for StringLinesVisitor {
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
if let StmtKind::Expr { value } = &stmt.node {
|
||||
if let ExprKind::Constant {
|
||||
value: Constant::Str(..),
|
||||
..
|
||||
} = &value.node
|
||||
{
|
||||
self.string_lines
|
||||
.extend(value.location.row()..=value.end_location.unwrap().row());
|
||||
}
|
||||
}
|
||||
visitor::walk_stmt(self, stmt);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract doc lines (standalone strings) from an AST.
|
||||
pub fn doc_lines_from_ast(python_ast: &Suite) -> Vec<usize> {
|
||||
let mut visitor = StringLinesVisitor::default();
|
||||
visitor.visit_body(python_ast);
|
||||
visitor.string_lines
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
pub mod checks;
|
||||
pub mod detection;
|
||||
pub(crate) mod detection;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -10,19 +10,19 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::linter::test_path;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::registry::RuleCode;
|
||||
use crate::settings;
|
||||
|
||||
#[test_case(CheckCode::ERA001, Path::new("ERA001.py"); "ERA001")]
|
||||
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
||||
let checks = test_path(
|
||||
#[test_case(RuleCode::ERA001, Path::new("ERA001.py"); "ERA001")]
|
||||
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/eradicate")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&settings::Settings::for_rule(check_code),
|
||||
&settings::Settings::for_rule(rule_code),
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
insta::assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use rustpython_ast::Location;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::eradicate::detection::comment_contains_code;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::settings::flags;
|
||||
use crate::{violations, Check, Settings, SourceCodeLocator};
|
||||
use crate::fix::Fix;
|
||||
use crate::registry::{Diagnostic, RuleCode};
|
||||
use crate::settings::{flags, Settings};
|
||||
use crate::source_code::Locator;
|
||||
use crate::violations;
|
||||
|
||||
fn is_standalone_comment(line: &str) -> bool {
|
||||
for char in line.chars() {
|
||||
@@ -20,25 +21,25 @@ fn is_standalone_comment(line: &str) -> bool {
|
||||
|
||||
/// ERA001
|
||||
pub fn commented_out_code(
|
||||
locator: &SourceCodeLocator,
|
||||
locator: &Locator,
|
||||
start: Location,
|
||||
end: Location,
|
||||
settings: &Settings,
|
||||
autofix: flags::Autofix,
|
||||
) -> Option<Check> {
|
||||
) -> Option<Diagnostic> {
|
||||
let location = Location::new(start.row(), 0);
|
||||
let end_location = Location::new(end.row() + 1, 0);
|
||||
let line = locator.slice_source_code_range(&Range::new(location, end_location));
|
||||
|
||||
// Verify that the comment is on its own line, and that it contains code.
|
||||
if is_standalone_comment(&line) && comment_contains_code(&line, &settings.task_tags[..]) {
|
||||
let mut check = Check::new(violations::CommentedOutCode, Range::new(start, end));
|
||||
let mut diagnostic = Diagnostic::new(violations::CommentedOutCode, Range::new(start, end));
|
||||
if matches!(autofix, flags::Autofix::Enabled)
|
||||
&& settings.fixable.contains(&CheckCode::ERA001)
|
||||
&& settings.fixable.contains(&RuleCode::ERA001)
|
||||
{
|
||||
check.amend(Fix::deletion(location, end_location));
|
||||
diagnostic.amend(Fix::deletion(location, end_location));
|
||||
}
|
||||
Some(check)
|
||||
Some(diagnostic)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
53
src/fix.rs
Normal file
53
src/fix.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use rustpython_ast::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Copy, Clone, Hash)]
|
||||
pub enum FixMode {
|
||||
Generate,
|
||||
Apply,
|
||||
Diff,
|
||||
None,
|
||||
}
|
||||
|
||||
impl From<bool> for FixMode {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
FixMode::Apply
|
||||
} else {
|
||||
FixMode::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct Fix {
|
||||
pub content: String,
|
||||
pub location: Location,
|
||||
pub end_location: Location,
|
||||
}
|
||||
|
||||
impl Fix {
|
||||
pub fn deletion(start: Location, end: Location) -> Self {
|
||||
Self {
|
||||
content: String::new(),
|
||||
location: start,
|
||||
end_location: end,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn replacement(content: String, start: Location, end: Location) -> Self {
|
||||
Self {
|
||||
content,
|
||||
location: start,
|
||||
end_location: end,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insertion(content: String, at: Location) -> Self {
|
||||
Self {
|
||||
content,
|
||||
location: at,
|
||||
end_location: at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod plugins;
|
||||
pub(crate) mod rules;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
@@ -9,28 +9,28 @@ mod tests {
|
||||
use test_case::test_case;
|
||||
|
||||
use crate::linter::test_path;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::registry::RuleCode;
|
||||
use crate::settings;
|
||||
|
||||
#[test_case(CheckCode::YTT101, Path::new("YTT101.py"); "YTT101")]
|
||||
#[test_case(CheckCode::YTT102, Path::new("YTT102.py"); "YTT102")]
|
||||
#[test_case(CheckCode::YTT103, Path::new("YTT103.py"); "YTT103")]
|
||||
#[test_case(CheckCode::YTT201, Path::new("YTT201.py"); "YTT201")]
|
||||
#[test_case(CheckCode::YTT202, Path::new("YTT202.py"); "YTT202")]
|
||||
#[test_case(CheckCode::YTT203, Path::new("YTT203.py"); "YTT203")]
|
||||
#[test_case(CheckCode::YTT204, Path::new("YTT204.py"); "YTT204")]
|
||||
#[test_case(CheckCode::YTT301, Path::new("YTT301.py"); "YTT301")]
|
||||
#[test_case(CheckCode::YTT302, Path::new("YTT302.py"); "YTT302")]
|
||||
#[test_case(CheckCode::YTT303, Path::new("YTT303.py"); "YTT303")]
|
||||
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
|
||||
let checks = test_path(
|
||||
#[test_case(RuleCode::YTT101, Path::new("YTT101.py"); "YTT101")]
|
||||
#[test_case(RuleCode::YTT102, Path::new("YTT102.py"); "YTT102")]
|
||||
#[test_case(RuleCode::YTT103, Path::new("YTT103.py"); "YTT103")]
|
||||
#[test_case(RuleCode::YTT201, Path::new("YTT201.py"); "YTT201")]
|
||||
#[test_case(RuleCode::YTT202, Path::new("YTT202.py"); "YTT202")]
|
||||
#[test_case(RuleCode::YTT203, Path::new("YTT203.py"); "YTT203")]
|
||||
#[test_case(RuleCode::YTT204, Path::new("YTT204.py"); "YTT204")]
|
||||
#[test_case(RuleCode::YTT301, Path::new("YTT301.py"); "YTT301")]
|
||||
#[test_case(RuleCode::YTT302, Path::new("YTT302.py"); "YTT302")]
|
||||
#[test_case(RuleCode::YTT303, Path::new("YTT303.py"); "YTT303")]
|
||||
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
|
||||
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_2020")
|
||||
.join(path)
|
||||
.as_path(),
|
||||
&settings::Settings::for_rule(check_code),
|
||||
&settings::Settings::for_rule(rule_code),
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(snapshot, checks);
|
||||
insta::assert_yaml_snapshot!(snapshot, diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
|
||||
use crate::ast::helpers::match_module_member;
|
||||
use crate::ast::types::Range;
|
||||
use crate::checkers::ast::Checker;
|
||||
use crate::registry::{Check, CheckCode};
|
||||
use crate::registry::{Diagnostic, RuleCode};
|
||||
use crate::violations;
|
||||
|
||||
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
|
||||
@@ -32,17 +32,16 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
..
|
||||
} = &upper.node
|
||||
{
|
||||
if *i == BigInt::from(1)
|
||||
&& checker.settings.enabled.contains(&CheckCode::YTT303)
|
||||
if *i == BigInt::from(1) && checker.settings.enabled.contains(&RuleCode::YTT303)
|
||||
{
|
||||
checker.checks.push(Check::new(
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionSlice1Referenced,
|
||||
Range::from_located(value),
|
||||
));
|
||||
} else if *i == BigInt::from(3)
|
||||
&& checker.settings.enabled.contains(&CheckCode::YTT101)
|
||||
&& checker.settings.enabled.contains(&RuleCode::YTT101)
|
||||
{
|
||||
checker.checks.push(Check::new(
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionSlice3Referenced,
|
||||
Range::from_located(value),
|
||||
));
|
||||
@@ -54,15 +53,15 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
|
||||
value: Constant::Int(i),
|
||||
..
|
||||
} => {
|
||||
if *i == BigInt::from(2) && checker.settings.enabled.contains(&CheckCode::YTT102) {
|
||||
checker.checks.push(Check::new(
|
||||
if *i == BigInt::from(2) && checker.settings.enabled.contains(&RuleCode::YTT102) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersion2Referenced,
|
||||
Range::from_located(value),
|
||||
));
|
||||
} else if *i == BigInt::from(0)
|
||||
&& checker.settings.enabled.contains(&CheckCode::YTT301)
|
||||
&& checker.settings.enabled.contains(&RuleCode::YTT301)
|
||||
{
|
||||
checker.checks.push(Check::new(
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersion0Referenced,
|
||||
Range::from_located(value),
|
||||
));
|
||||
@@ -97,9 +96,9 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
|
||||
) = (ops, comparators)
|
||||
{
|
||||
if *n == BigInt::from(3)
|
||||
&& checker.settings.enabled.contains(&CheckCode::YTT201)
|
||||
&& checker.settings.enabled.contains(&RuleCode::YTT201)
|
||||
{
|
||||
checker.checks.push(Check::new(
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionInfo0Eq3Referenced,
|
||||
Range::from_located(left),
|
||||
));
|
||||
@@ -118,8 +117,8 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
|
||||
}],
|
||||
) = (ops, comparators)
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::YTT203) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::YTT203) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionInfo1CmpInt,
|
||||
Range::from_located(left),
|
||||
));
|
||||
@@ -144,8 +143,8 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
|
||||
}],
|
||||
) = (ops, comparators)
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::YTT204) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::YTT204) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionInfoMinorCmpInt,
|
||||
Range::from_located(left),
|
||||
));
|
||||
@@ -170,14 +169,14 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
|
||||
) = (ops, comparators)
|
||||
{
|
||||
if s.len() == 1 {
|
||||
if checker.settings.enabled.contains(&CheckCode::YTT302) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::YTT302) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionCmpStr10,
|
||||
Range::from_located(left),
|
||||
));
|
||||
}
|
||||
} else if checker.settings.enabled.contains(&CheckCode::YTT103) {
|
||||
checker.checks.push(Check::new(
|
||||
} else if checker.settings.enabled.contains(&RuleCode::YTT103) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SysVersionCmpStr3,
|
||||
Range::from_located(left),
|
||||
));
|
||||
@@ -195,7 +194,7 @@ pub fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
|
||||
&checker.from_imports,
|
||||
&checker.import_aliases,
|
||||
) {
|
||||
checker.checks.push(Check::new(
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::SixPY3Referenced,
|
||||
Range::from_located(expr),
|
||||
));
|
||||
@@ -4,11 +4,11 @@ use rustpython_parser::lexer;
|
||||
use rustpython_parser::lexer::Tok;
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::autofix::Fix;
|
||||
use crate::source_code_locator::SourceCodeLocator;
|
||||
use crate::fix::Fix;
|
||||
use crate::source_code::Locator;
|
||||
|
||||
/// ANN204
|
||||
pub fn add_return_none_annotation(locator: &SourceCodeLocator, stmt: &Stmt) -> Result<Fix> {
|
||||
pub fn add_return_none_annotation(locator: &Locator, stmt: &Stmt) -> Result<Fix> {
|
||||
let range = Range::from_located(stmt);
|
||||
let contents = locator.slice_source_code_range(&range);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
mod fixes;
|
||||
pub mod helpers;
|
||||
pub mod plugins;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod rules;
|
||||
pub mod settings;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -9,37 +9,38 @@ mod tests {
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::flake8_annotations;
|
||||
use crate::linter::test_path;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::{flake8_annotations, Settings};
|
||||
use crate::registry::RuleCode;
|
||||
use crate::settings::Settings;
|
||||
|
||||
#[test]
|
||||
fn defaults() -> Result<()> {
|
||||
let checks = test_path(
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"),
|
||||
&Settings {
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::ANN001,
|
||||
CheckCode::ANN002,
|
||||
CheckCode::ANN003,
|
||||
CheckCode::ANN101,
|
||||
CheckCode::ANN102,
|
||||
CheckCode::ANN201,
|
||||
CheckCode::ANN202,
|
||||
CheckCode::ANN204,
|
||||
CheckCode::ANN205,
|
||||
CheckCode::ANN206,
|
||||
CheckCode::ANN401,
|
||||
RuleCode::ANN001,
|
||||
RuleCode::ANN002,
|
||||
RuleCode::ANN003,
|
||||
RuleCode::ANN101,
|
||||
RuleCode::ANN102,
|
||||
RuleCode::ANN201,
|
||||
RuleCode::ANN202,
|
||||
RuleCode::ANN204,
|
||||
RuleCode::ANN205,
|
||||
RuleCode::ANN206,
|
||||
RuleCode::ANN401,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
insta::assert_yaml_snapshot!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppress_dummy_args() -> Result<()> {
|
||||
let checks = test_path(
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/suppress_dummy_args.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -49,21 +50,21 @@ mod tests {
|
||||
allow_star_arg_any: false,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::ANN001,
|
||||
CheckCode::ANN002,
|
||||
CheckCode::ANN003,
|
||||
CheckCode::ANN101,
|
||||
CheckCode::ANN102,
|
||||
RuleCode::ANN001,
|
||||
RuleCode::ANN002,
|
||||
RuleCode::ANN003,
|
||||
RuleCode::ANN101,
|
||||
RuleCode::ANN102,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
insta::assert_yaml_snapshot!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mypy_init_return() -> Result<()> {
|
||||
let checks = test_path(
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/mypy_init_return.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -73,21 +74,21 @@ mod tests {
|
||||
allow_star_arg_any: false,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::ANN201,
|
||||
CheckCode::ANN202,
|
||||
CheckCode::ANN204,
|
||||
CheckCode::ANN205,
|
||||
CheckCode::ANN206,
|
||||
RuleCode::ANN201,
|
||||
RuleCode::ANN202,
|
||||
RuleCode::ANN204,
|
||||
RuleCode::ANN205,
|
||||
RuleCode::ANN206,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
insta::assert_yaml_snapshot!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn suppress_none_returning() -> Result<()> {
|
||||
let checks = test_path(
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/suppress_none_returning.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -97,21 +98,21 @@ mod tests {
|
||||
allow_star_arg_any: false,
|
||||
},
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::ANN201,
|
||||
CheckCode::ANN202,
|
||||
CheckCode::ANN204,
|
||||
CheckCode::ANN205,
|
||||
CheckCode::ANN206,
|
||||
RuleCode::ANN201,
|
||||
RuleCode::ANN202,
|
||||
RuleCode::ANN204,
|
||||
RuleCode::ANN205,
|
||||
RuleCode::ANN206,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
insta::assert_yaml_snapshot!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_star_arg_any() -> Result<()> {
|
||||
let checks = test_path(
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/allow_star_arg_any.py"),
|
||||
&Settings {
|
||||
flake8_annotations: flake8_annotations::settings::Settings {
|
||||
@@ -120,28 +121,28 @@ mod tests {
|
||||
suppress_none_returning: false,
|
||||
allow_star_arg_any: true,
|
||||
},
|
||||
..Settings::for_rules(vec![CheckCode::ANN401])
|
||||
..Settings::for_rules(vec![RuleCode::ANN401])
|
||||
},
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
insta::assert_yaml_snapshot!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allow_overload() -> Result<()> {
|
||||
let checks = test_path(
|
||||
let diagnostics = test_path(
|
||||
Path::new("./resources/test/fixtures/flake8_annotations/allow_overload.py"),
|
||||
&Settings {
|
||||
..Settings::for_rules(vec![
|
||||
CheckCode::ANN201,
|
||||
CheckCode::ANN202,
|
||||
CheckCode::ANN204,
|
||||
CheckCode::ANN205,
|
||||
CheckCode::ANN206,
|
||||
RuleCode::ANN201,
|
||||
RuleCode::ANN202,
|
||||
RuleCode::ANN204,
|
||||
RuleCode::ANN205,
|
||||
RuleCode::ANN206,
|
||||
])
|
||||
},
|
||||
)?;
|
||||
insta::assert_yaml_snapshot!(checks);
|
||||
insta::assert_yaml_snapshot!(diagnostics);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ use crate::checkers::ast::Checker;
|
||||
use crate::docstrings::definition::{Definition, DefinitionKind};
|
||||
use crate::flake8_annotations::fixes;
|
||||
use crate::flake8_annotations::helpers::match_function_def;
|
||||
use crate::registry::CheckCode;
|
||||
use crate::registry::{Diagnostic, RuleCode};
|
||||
use crate::visibility::Visibility;
|
||||
use crate::{violations, visibility, Check};
|
||||
use crate::{violations, visibility};
|
||||
|
||||
#[derive(Default)]
|
||||
struct ReturnStatementVisitor<'a> {
|
||||
@@ -57,7 +57,7 @@ where
|
||||
F: FnOnce() -> String,
|
||||
{
|
||||
if checker.match_typing_expr(annotation, "Any") {
|
||||
checker.checks.push(Check::new(
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::DynamicallyTypedExpression(func()),
|
||||
Range::from_located(annotation),
|
||||
));
|
||||
@@ -85,15 +85,15 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
.chain(args.kwonlyargs.iter())
|
||||
{
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
check_dynamically_typed(checker, expr, || arg.node.arg.to_string());
|
||||
};
|
||||
} else {
|
||||
if !(checker.settings.flake8_annotations.suppress_dummy_args
|
||||
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN001) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN001) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeFunctionArgument(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -106,7 +106,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if let Some(arg) = &args.vararg {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
if !checker.settings.flake8_annotations.allow_star_arg_any {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
let name = arg.node.arg.to_string();
|
||||
check_dynamically_typed(checker, expr, || format!("*{name}"));
|
||||
}
|
||||
@@ -115,8 +115,8 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if !(checker.settings.flake8_annotations.suppress_dummy_args
|
||||
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN002) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN002) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeArgs(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -129,7 +129,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if let Some(arg) = &args.kwarg {
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
if !checker.settings.flake8_annotations.allow_star_arg_any {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
let name = arg.node.arg.to_string();
|
||||
check_dynamically_typed(checker, expr, || format!("**{name}"));
|
||||
}
|
||||
@@ -138,8 +138,8 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if !(checker.settings.flake8_annotations.suppress_dummy_args
|
||||
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN003) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN003) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeKwargs(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -150,7 +150,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
|
||||
// ANN201, ANN202, ANN401
|
||||
if let Some(expr) = &returns {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
check_dynamically_typed(checker, expr, || name.to_string());
|
||||
};
|
||||
} else {
|
||||
@@ -164,16 +164,16 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
|
||||
match visibility {
|
||||
Visibility::Public => {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN201) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN201) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypePublicFunction(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
Visibility::Private => {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN202) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN202) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypePrivateFunction(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
@@ -203,15 +203,15 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
// ANN401 for dynamically typed arguments
|
||||
if let Some(annotation) = &arg.node.annotation {
|
||||
has_any_typed_arg = true;
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
check_dynamically_typed(checker, annotation, || arg.node.arg.to_string());
|
||||
}
|
||||
} else {
|
||||
if !(checker.settings.flake8_annotations.suppress_dummy_args
|
||||
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN001) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN001) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeFunctionArgument(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -225,7 +225,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
has_any_typed_arg = true;
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
if !checker.settings.flake8_annotations.allow_star_arg_any {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
let name = arg.node.arg.to_string();
|
||||
check_dynamically_typed(checker, expr, || format!("*{name}"));
|
||||
}
|
||||
@@ -234,8 +234,8 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if !(checker.settings.flake8_annotations.suppress_dummy_args
|
||||
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN002) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN002) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeArgs(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -249,7 +249,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
has_any_typed_arg = true;
|
||||
if let Some(expr) = &arg.node.annotation {
|
||||
if !checker.settings.flake8_annotations.allow_star_arg_any {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
let name = arg.node.arg.to_string();
|
||||
check_dynamically_typed(checker, expr, || format!("**{name}"));
|
||||
}
|
||||
@@ -258,8 +258,8 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if !(checker.settings.flake8_annotations.suppress_dummy_args
|
||||
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
|
||||
{
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN003) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN003) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeKwargs(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -273,15 +273,15 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
if let Some(arg) = args.args.first() {
|
||||
if arg.node.annotation.is_none() {
|
||||
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN102) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN102) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeCls(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN101) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN101) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingTypeSelf(arg.node.arg.to_string()),
|
||||
Range::from_located(arg),
|
||||
));
|
||||
@@ -293,7 +293,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
|
||||
// ANN201, ANN202
|
||||
if let Some(expr) = &returns {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN401) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN401) {
|
||||
check_dynamically_typed(checker, expr, || name.to_string());
|
||||
}
|
||||
} else {
|
||||
@@ -306,44 +306,44 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
}
|
||||
|
||||
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN206) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN206) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypeClassMethod(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
} else if visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN205) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN205) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypeStaticMethod(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
} else if visibility::is_init(stmt) {
|
||||
} else if visibility::is_init(cast::name(stmt)) {
|
||||
// Allow omission of return annotation in `__init__` functions, as long as at
|
||||
// least one argument is typed.
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN204) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN204) {
|
||||
if !(checker.settings.flake8_annotations.mypy_init_return
|
||||
&& has_any_typed_arg)
|
||||
{
|
||||
let mut check = Check::new(
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
violations::MissingReturnTypeSpecialMethod(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
);
|
||||
if checker.patch(check.kind.code()) {
|
||||
if checker.patch(diagnostic.kind.code()) {
|
||||
match fixes::add_return_none_annotation(checker.locator, stmt) {
|
||||
Ok(fix) => {
|
||||
check.amend(fix);
|
||||
diagnostic.amend(fix);
|
||||
}
|
||||
Err(e) => error!("Failed to generate fix: {e}"),
|
||||
}
|
||||
}
|
||||
checker.checks.push(check);
|
||||
checker.diagnostics.push(diagnostic);
|
||||
}
|
||||
}
|
||||
} else if visibility::is_magic(stmt) {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN204) {
|
||||
checker.checks.push(Check::new(
|
||||
} else if visibility::is_magic(cast::name(stmt)) {
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN204) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypeSpecialMethod(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
@@ -351,16 +351,16 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
|
||||
} else {
|
||||
match visibility {
|
||||
Visibility::Public => {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN201) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN201) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypePublicFunction(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
}
|
||||
}
|
||||
Visibility::Private => {
|
||||
if checker.settings.enabled.contains(&CheckCode::ANN202) {
|
||||
checker.checks.push(Check::new(
|
||||
if checker.settings.enabled.contains(&RuleCode::ANN202) {
|
||||
checker.diagnostics.push(Diagnostic::new(
|
||||
violations::MissingReturnTypePrivateFunction(name.to_string()),
|
||||
helpers::identifier_range(stmt, checker.locator),
|
||||
));
|
||||
@@ -26,7 +26,7 @@ pub struct Options {
|
||||
value_type = "bool",
|
||||
example = "suppress-dummy-args = true"
|
||||
)]
|
||||
/// Whether to suppress `ANN000`-level errors for arguments matching the
|
||||
/// Whether to suppress `ANN000`-level violations for arguments matching the
|
||||
/// "dummy" variable regex (like `_`).
|
||||
pub suppress_dummy_args: Option<bool>,
|
||||
#[option(
|
||||
@@ -34,8 +34,8 @@ pub struct Options {
|
||||
value_type = "bool",
|
||||
example = "suppress-none-returning = true"
|
||||
)]
|
||||
/// Whether to suppress `ANN200`-level errors for functions that meet either
|
||||
/// of the following criteria:
|
||||
/// Whether to suppress `ANN200`-level violations for functions that meet
|
||||
/// either of the following criteria:
|
||||
///
|
||||
/// - Contain no `return` statement.
|
||||
/// - Explicit `return` statement(s) all return `None` (explicitly or
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
use rustpython_ast::{Located, StmtKind};
|
||||
|
||||
use crate::ast::types::Range;
|
||||
use crate::registry::Check;
|
||||
use crate::violations;
|
||||
|
||||
/// S101
|
||||
pub fn assert_used(stmt: &Located<StmtKind>) -> Check {
|
||||
Check::new(violations::AssertUsed, Range::from_located(stmt))
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user