Compare commits

...

78 Commits

Author SHA1 Message Date
Charlie Marsh
2315db7d13 Bump version to 0.0.206 2023-01-01 16:39:29 -05:00
Charlie Marsh
f1a183c171 Rewrite mock.mock attribute accesses (#1533) 2023-01-01 13:14:09 -05:00
Harutaka Kawamura
509c6d5ec7 Add visit_format_spec to avoid false positives for F541 in f-string format specifier (#1528) 2023-01-01 13:03:32 -05:00
Maksudul Haque
6695988b59 Do not Change Quotation Style for SIM118 Autofix (#1529) 2023-01-01 12:53:46 -05:00
Harutaka Kawamura
e3867b172d Simplify unused snapshot check (#1525) 2023-01-01 02:43:07 -05:00
Harutaka Kawamura
4b8e30f350 Fix Name node range in NamedExpr node (#1526) 2023-01-01 02:41:49 -05:00
Charlie Marsh
8fd0d8e9d8 Bump pyupgrade implementation count 2022-12-31 21:25:34 -05:00
Colin Delahunty
70895a8f1e Pyupgrade: import mock to from unittest import mock (#1488) 2022-12-31 21:25:06 -05:00
Charlie Marsh
f2c9f94f73 Avoid some false positives for ends-in-period checks (#1521) 2022-12-31 18:38:22 -05:00
Charlie Marsh
605c6069e2 Ignore property assignments in RET504 (#1520) 2022-12-31 18:04:13 -05:00
Charlie Marsh
92c2981b6d Add dark mode variant for benchmark image (#1519) 2022-12-31 17:47:32 -05:00
Colin Delahunty
4ad8db3d61 Pyupgrade: Turn errors into OSError (#1434) 2022-12-31 16:36:05 -05:00
Charlie Marsh
0e8c237167 Bump version to 0.0.205 2022-12-31 13:44:39 -05:00
Harutaka Kawamura
960c5e2006 Use more precise error ranges for names (#1513) 2022-12-31 13:42:39 -05:00
Charlie Marsh
9ba17fbf92 Avoid flagging nested f-strings (#1516) 2022-12-31 13:41:21 -05:00
Charlie Marsh
bfdf972a5d Add code kind to Quick Fix action 2022-12-31 10:26:47 -05:00
Charlie Marsh
0c215365ae Bump version to 0.0.204 2022-12-31 08:20:09 -05:00
Maksudul Haque
815284f890 Check for Unsupported Files and Display Errors and Warnings (#1509) 2022-12-31 08:12:55 -05:00
Charlie Marsh
6880338a9a Restore pyproject.toml 2022-12-31 08:06:08 -05:00
Charlie Marsh
68b749c67d Remove foo directory 2022-12-31 08:05:04 -05:00
Reiner Gerecke
c0fc55b812 Generate source code with detected line ending (#1487) 2022-12-31 08:02:29 -05:00
Reiner Gerecke
ba9cf70917 Adjust test_path helper to detect round-trip autofix issues (#1501) 2022-12-31 08:02:13 -05:00
Harutaka Kawamura
f73dfbbfd3 Fix E722 and F707 ranges (#1508) 2022-12-31 07:58:46 -05:00
Reiner Gerecke
62c273cd22 Include fix commit message when showing violations together with source (#1505) 2022-12-31 07:54:41 -05:00
Harutaka Kawamura
938ad9a39e Fix N818 range (#1503) 2022-12-31 07:43:03 -05:00
Harutaka Kawamura
14248cb8cb Improve PLW0120 range (#1500) 2022-12-31 07:42:49 -05:00
Harutaka Kawamura
3a280039e1 Improve F811 range for function and class definitions (#1499) 2022-12-31 07:42:18 -05:00
Harutaka Kawamura
4e9e58bdc0 Improve T20X ranges (#1502) 2022-12-31 07:41:53 -05:00
Harutaka Kawamura
926b5494ad Remove unused snapshots (#1497) 2022-12-31 07:40:38 -05:00
Reiner Gerecke
6717b48ca5 Fix detection of changed imports in isort plugin (#1504) 2022-12-31 07:37:27 -05:00
Harutaka Kawamura
f7bb5bc858 Remove F831 (#1495) 2022-12-30 23:57:51 -05:00
Harutaka Kawamura
3e23fd1487 Stop overriding locations for expressions within f-strings (#1494) 2022-12-30 23:43:59 -05:00
Charlie Marsh
01c74e0629 Add a "fix message" to every autofix-able check (#1489) 2022-12-30 23:16:03 -05:00
Charlie Marsh
1e3cf87f67 Escape strings when formatting check messages (#1493) 2022-12-30 22:11:01 -05:00
Charlie Marsh
248447e139 Trim CLI help during generation (#1492) 2022-12-30 22:03:58 -05:00
Charlie Marsh
95f139583a Modify pyproject.toml to meet schema compliance 2022-12-30 15:51:35 -05:00
Charlie Marsh
74903f23d6 Bump version to 0.0.203 2022-12-30 15:33:30 -05:00
Charlie Marsh
3ee20a70d3 Remove lingering ruff_options.ts references 2022-12-30 15:33:09 -05:00
Charlie Marsh
4c2fbb7ac0 Remove hidden autoformat command (#1486) 2022-12-30 15:32:05 -05:00
Charlie Marsh
7a66f98590 Move some argument validation into Clap (#1485) 2022-12-30 15:29:41 -05:00
Charlie Marsh
a2bf3916f3 Make clean a standalone command 2022-12-30 15:20:18 -05:00
Reiner Gerecke
c9aa7b9308 Generate the README's --help output automatically via cargo +nightly dev generate-all (#1483) 2022-12-30 15:06:32 -05:00
Charlie Marsh
d880ca6cc6 Add a command to clear the Ruff cache (#1484) 2022-12-30 13:52:16 -05:00
Reiner Gerecke
080f99b908 Detect line endings and use them during code generation (#1482) 2022-12-30 12:59:40 -05:00
Charlie Marsh
a7dc491ff1 Fix clippy 2022-12-30 12:34:43 -05:00
Charlie Marsh
cdc8f8c91a Remove support for ur prefixes (#1481) 2022-12-30 11:21:05 -05:00
Colin Delahunty
138c46e793 Simplified code for unicode fix (#1475) 2022-12-30 11:18:52 -05:00
Charlie Marsh
a86c57a832 Support multi-line noqa directives for 'import from' (#1479) 2022-12-30 11:16:50 -05:00
Charlie Marsh
818582fe8a Bump version to 0.0.202 2022-12-30 08:16:32 -05:00
Charlie Marsh
90574c1088 Set editor background on top-level component (#1478) 2022-12-30 07:55:54 -05:00
Charlie Marsh
3061a35e7c Use more precise ranges for class and function checks (#1476) 2022-12-30 07:39:20 -05:00
Martin Fischer
87681697ae Improve CLI help for --select (#1471) 2022-12-30 07:16:44 -05:00
Martin Fischer
e9ec2a7b36 Add use test_case::test_case; to CONTRIBUTING.md
I previously tried adding the #[test_case()] attribute macro and got
confused because the Rust compilation suddenly failed with:

    error[E0658]: use of unstable library feature 'custom_test_frameworks': custom test frameworks are an unstable feature

which is a quite confusing error message.  The solution is to just add
`use test_case::test_case;`, so this commit adds that line to the
example in CONTRIBUTING.md to spare others this source of confusion.
2022-12-30 07:13:48 -05:00
Martin Fischer
b0bb75dc1c Add --select to command suggested in CONTRIBUTING.md
By default only E* and F* lints are enabled. I previously followed the
CONTRIBUTING.md instructions to implement a TID* lint and was confused
why my lint wasn't being run.
2022-12-30 07:13:48 -05:00
Martin Fischer
ebca5c2df8 Make banned-api config setting optional (#1465) 2022-12-30 07:09:56 -05:00
Charlie Marsh
16b10c42f0 Fix lint issues 2022-12-29 23:12:28 -05:00
Charlie Marsh
4a6e5d1549 Bump version to 0.0.201 2022-12-29 23:01:35 -05:00
Charlie Marsh
b078050732 Implicit flake8-implicit-str-concat (#1463) 2022-12-29 23:00:55 -05:00
Charlie Marsh
5f796b39b4 Run cargo fmt 2022-12-29 22:25:14 -05:00
Martin Fischer
9d34da23bd Implement TID251 (banning modules & module members) (#1436) 2022-12-29 22:11:12 -05:00
Charlie Marsh
bde12c3bb3 Restore quick fixes for playground 2022-12-29 20:16:19 -05:00
Colin Delahunty
f735660801 Removed unicode literals (#1448) 2022-12-29 20:11:33 -05:00
Charlie Marsh
34cd22dfc1 Copy URL but don't update the hash (#1458) 2022-12-29 19:46:50 -05:00
Charlie Marsh
9fafe16a55 Re-add GitHub badge to the bottom of the page 2022-12-29 19:38:33 -05:00
Charlie Marsh
e9a4cb1c1d Remove generated TypeScript options (#1456) 2022-12-29 19:37:49 -05:00
Charlie Marsh
9db825c731 Use trailingComma: 'all' (#1457) 2022-12-29 19:36:51 -05:00
Charlie Marsh
2c7464604a Implement dark mode (#1455) 2022-12-29 19:33:46 -05:00
Charlie Marsh
cd2099f772 Move default options into WASM interface (#1453) 2022-12-29 18:06:57 -05:00
Adam Turner
091d36cd30 Add Sphinx to user list (#1451) 2022-12-29 18:06:09 -05:00
Mathieu Kniewallner
02f156c6cb docs(README): add missing flake8-simplify (#1449) 2022-12-29 17:02:26 -05:00
Charlie Marsh
9f7350961e Rename config to settings in the playground (#1450) 2022-12-29 16:59:38 -05:00
Charlie Marsh
118a93260a Bump version to 0.0.200 2022-12-29 13:31:23 -05:00
Charlie Marsh
1c16255884 Include docstrings for settings enum members (#1446) 2022-12-29 13:15:44 -05:00
Charlie Marsh
16c4552946 Update snapshots 2022-12-29 13:13:43 -05:00
Charlie Marsh
0ba3989b3d Make update check enablement cofnigurable (#1445) 2022-12-29 13:06:22 -05:00
Charlie Marsh
3435e15cba Avoid caching diffs (#1441) 2022-12-29 12:51:58 -05:00
Maksudul Haque
781bbbc286 [pygrep-hooks] Adds Check for Blanket # noqa (#1440) 2022-12-29 12:43:16 -05:00
Charlie Marsh
acf0b82f19 Re-style the Ruff playground (#1438) 2022-12-29 11:47:27 -05:00
617 changed files with 10924 additions and 3397 deletions

View File

@@ -40,8 +40,7 @@ jobs:
- 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 src/checks_gen.rs || echo "::error file=src/checks_gen.rs::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 --quiet playground/src/ruff_options.ts || echo "::error file=playground/src/ruff_options.ts::This file is outdated. Run 'cargo +nightly dev generate-all'."
- run: git diff --exit-code -- README.md src/checks_gen.rs ruff.schema.json playground/src/ruff_options.ts
- run: git diff --exit-code -- README.md src/checks_gen.rs ruff.schema.json
cargo-fmt:
name: "cargo fmt"
@@ -117,8 +116,12 @@ jobs:
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- run: cargo install cargo-insta
- run: pip install black[d]==22.12.0
- run: cargo test --all
- name: Run tests
run: |
cargo insta test --all --delete-unreferenced-snapshots
git diff --exit-code
- run: cargo test --package ruff --test black_compatibility_test -- --ignored
# TODO(charlie): Re-enable the `wasm-pack` tests.

1
.gitignore vendored
View File

@@ -181,3 +181,4 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
.vimspector.json

View File

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

View File

@@ -66,11 +66,13 @@ understand how other, similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures`, named to match the `CheckCode`
you defined earlier (e.g., `E402.py`). This file should contain a variety of violations and
non-violations designed to evaluate and demonstrate the behavior of your lint rule. Run Ruff locally
with (e.g.) `cargo run resources/test/fixtures/E402.py --no-cache`. Once you're satisfied with the
with (e.g.) `cargo run resources/test/fixtures/E402.py --no-cache --select E402`. Once you're satisfied with the
output, codify the behavior as a snapshot test by adding a new `testcase` macro to the `mod tests`
section of `src/linter.rs`, like so:
```rust
use test_case::test_case;
#[test_case(CheckCode::A001, Path::new("A001.py"); "A001")]
...
```

16
Cargo.lock generated
View File

@@ -750,7 +750,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.199-dev.0"
version = "0.0.206-dev.0"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1878,7 +1878,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.199"
version = "0.0.206"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1946,7 +1946,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.199"
version = "0.0.206"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1967,7 +1967,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.199"
version = "0.0.206"
dependencies = [
"proc-macro2",
"quote",
@@ -2010,7 +2010,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.1.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=68d26955b3e24198a150315e7959719b03709dee#68d26955b3e24198a150315e7959719b03709dee"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2020,7 +2020,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.0.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=68d26955b3e24198a150315e7959719b03709dee#68d26955b3e24198a150315e7959719b03709dee"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
dependencies = [
"ascii",
"cfg-if 1.0.0",
@@ -2043,7 +2043,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=68d26955b3e24198a150315e7959719b03709dee#68d26955b3e24198a150315e7959719b03709dee"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
dependencies = [
"bincode",
"bitflags",
@@ -2060,7 +2060,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.1.2"
source = "git+https://github.com/RustPython/RustPython.git?rev=68d26955b3e24198a150315e7959719b03709dee#68d26955b3e24198a150315e7959719b03709dee"
source = "git+https://github.com/RustPython/RustPython.git?rev=71becd4059fdce4bce7010f1208ed3b1c883abba#71becd4059fdce4bce7010f1208ed3b1c883abba"
dependencies = [
"ahash",
"anyhow",

View File

@@ -6,7 +6,7 @@ members = [
[package]
name = "ruff"
version = "0.0.199"
version = "0.0.206"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -51,11 +51,11 @@ 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.199", path = "ruff_macros" }
ruff_macros = { version = "0.0.206", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }

25
LICENSE
View File

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

120
README.md
View File

@@ -8,7 +8,11 @@
An extremely fast Python linter, written in Rust.
<p align="center">
<img alt="Bar chart with benchmark results" src="https://user-images.githubusercontent.com/1309177/187504482-6d9df992-a81d-4e86-9f6a-d958741c8182.svg">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/210156880-a97c2a0d-2c03-4393-8695-36547935a94e.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/210156881-a88fd142-5008-4695-9407-d028cec3eff7.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/210156881-a88fd142-5008-4695-9407-d028cec3eff7.svg">
</picture>
</p>
<p align="center">
@@ -44,6 +48,7 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Bokeh](https://github.com/bokeh/bokeh)
- [Zulip](https://github.com/zulip/zulip)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Sphinx](https://github.com/sphinx-doc/sphinx)
- [Hatch](https://github.com/pypa/hatch)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Synapse](https://github.com/matrix-org/synapse)
@@ -94,6 +99,7 @@ of [Conda](https://docs.conda.io/en/latest/):
1. [flake8-comprehensions (C4)](#flake8-comprehensions-c4)
1. [flake8-debugger (T10)](#flake8-debugger-t10)
1. [flake8-errmsg (EM)](#flake8-errmsg-em)
1. [flake8-implicit-str-concat (ISC)](#flake8-implicit-str-concat-isc)
1. [flake8-import-conventions (ICN)](#flake8-import-conventions-icn)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-quotes (Q)](#flake8-quotes-q)
@@ -167,7 +173,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.199'
rev: 'v0.0.206'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -298,6 +304,7 @@ ruff path/to/code/ --select F401 --select F403
See `ruff --help` for more:
<!-- Begin auto-generated cli help. -->
```shell
Ruff: An extremely fast Python linter.
@@ -328,11 +335,11 @@ Options:
-n, --no-cache
Disable cache reads
--select <SELECT>
List of error codes to enable
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>
List of error codes to 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>
@@ -347,18 +354,18 @@ Options:
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]
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
--cache-dir <CACHE_DIR>
Path to the cache directory
--show-source
Show violations with source code
--respect-gitignore
Respect file exclusions via `.gitignore` and other standard ignore files
--force-exclude
Enforce exclusions, even for paths passed to Ruff directly on the command-line
--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
--add-noqa
Enable automatic additions of noqa directives to failing lines
--update-check
Enable or disable automatic update checks
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
--target-version <TARGET_VERSION>
@@ -366,18 +373,23 @@ Options:
--line-length <LINE_LENGTH>
Set the line-length for length-associated checks and automatic formatting
--max-complexity <MAX_COMPLEXITY>
Max McCabe complexity allowed for a function
--stdin-filename <STDIN_FILENAME>
The name of the file when passing it through stdin
Maximum McCabe complexity allowed for a given function
--add-noqa
Enable automatic additions of `noqa` directives to failing lines
--clean
Clear any caches in the current directory or any subdirectories
--explain <EXPLAIN>
Explain a rule
--cache-dir <CACHE_DIR>
Path to the cache directory
--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
-h, --help
Print help information
-V, --version
Print version information
```
<!-- End auto-generated cli help. -->
### `pyproject.toml` discovery
@@ -537,7 +549,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F621 | ExpressionsInStarAssignment | Too many expressions in star-unpacking assignment | |
| F622 | TwoStarredExpressions | Two starred expressions in assignment | |
| F631 | AssertTuple | Assert test is a non-empty tuple, which is always `True` | |
| F632 | IsLiteral | Use `==` and `!=` to compare constant literals | 🛠 |
| F632 | IsLiteral | Use `==` to compare constant literals | 🛠 |
| F633 | InvalidPrintSyntax | Use of `>>` is invalid with `print` function | |
| F634 | IfTuple | If test is a tuple, which is always `True` | |
| F701 | BreakOutsideLoop | `break` outside loop | |
@@ -550,7 +562,6 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
| F821 | UndefinedName | Undefined name `...` | |
| F822 | UndefinedExport | Undefined name `...` in `__all__` | |
| F823 | UndefinedLocal | Local variable `...` referenced before assignment | |
| F831 | DuplicateArgumentName | Duplicate argument name in function definition | |
| F841 | UnusedVariable | Local variable `...` is assigned to but never used | |
| F842 | UnusedAnnotation | Local variable `...` is annotated but never used | |
| F901 | RaiseNotImplemented | `raise NotImplemented` should be `raise NotImplementedError` | 🛠 |
@@ -570,7 +581,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
| E714 | NotIsTest | Test for object identity should be `is not` | 🛠 |
| E721 | TypeComparison | Do not compare types, use `isinstance()` | |
| E722 | DoNotUseBareExcept | Do not use bare `except` | |
| E731 | DoNotAssignLambda | Do not assign a lambda expression, use a def | 🛠 |
| E731 | DoNotAssignLambda | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E741 | AmbiguousVariableName | Ambiguous variable name: `...` | |
| E742 | AmbiguousClassName | Ambiguous class name: `...` | |
| E743 | AmbiguousFunctionName | Ambiguous function name: `...` | |
@@ -655,7 +666,7 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| ---- | ---- | ------- | --- |
| UP001 | UselessMetaclassType | `__metaclass__ = type` is implied | 🛠 |
| UP003 | TypeOfPrimitive | Use `str` instead of `type(...)` | 🛠 |
| UP004 | UselessObjectInheritance | Class `...` inherits from object | 🛠 |
| UP004 | UselessObjectInheritance | Class `...` inherits from `object` | 🛠 |
| UP005 | DeprecatedUnittestAlias | `assertEquals` is deprecated, use `assertEqual` | 🛠 |
| UP006 | UsePEP585Annotation | Use `list` instead of `List` for type annotations | 🛠 |
| UP007 | UsePEP604Annotation | Use `X \| Y` for type annotations | 🛠 |
@@ -669,12 +680,15 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP015 | RedundantOpenModes | Unnecessary open mode parameters | 🛠 |
| UP016 | RemoveSixCompat | Unnecessary `six` compatibility usage | 🛠 |
| UP017 | DatetimeTimezoneUTC | Use `datetime.UTC` alias | 🛠 |
| UP018 | NativeLiterals | Unnecessary call to `str` and `bytes` | 🛠 |
| UP018 | NativeLiterals | Unnecessary call to `str` | 🛠 |
| UP019 | TypingTextStrAlias | `typing.Text` is deprecated, use `str` | 🛠 |
| UP020 | OpenAlias | Use builtin `open` | 🛠 |
| UP021 | ReplaceUniversalNewlines | `universal_newlines` is deprecated, use `text` | 🛠 |
| UP022 | ReplaceStdoutStderr | Sending stdout and stderr to pipe is deprecated, use `capture_output` | 🛠 |
| UP023 | RewriteCElementTree | `cElementTree` is deprecated, use `ElementTree` | 🛠 |
| UP024 | OSErrorAlias | Replace aliased errors with `OSError` | 🛠 |
| UP025 | RewriteUnicodeLiteral | Remove unicode literals from strings | 🛠 |
| UP026 | RewriteMockImport | `mock` is deprecated, use `unittest.mock` | 🛠 |
### pep8-naming (N)
@@ -850,6 +864,16 @@ For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/0.4.0/) on
| EM102 | FStringInException | Exception must not use an f-string literal, assign to variable first | |
| EM103 | DotFormatInException | Exception must not use a `.format()` string directly, assign to variable first | |
### flake8-implicit-str-concat (ISC)
For more, see [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/0.3.0/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ISC001 | SingleLineImplicitStringConcatenation | Implicitly concatenated string literals on one line | |
| ISC002 | MultiLineImplicitStringConcatenation | Implicitly concatenated string literals over continuation line | |
| ISC003 | ExplicitStringConcatenation | Explicitly concatenated string should be implicitly concatenated | |
### flake8-import-conventions (ICN)
| Code | Name | Message | Fix |
@@ -905,6 +929,7 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TID251 | BannedApi | `...` is banned: ... | |
| TID252 | BannedRelativeImport | Relative imports are banned | |
### flake8-unused-arguments (ARG)
@@ -971,6 +996,7 @@ 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` | |
### Pylint (PLC, PLE, PLR, PLW)
@@ -997,7 +1023,7 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
| ---- | ---- | ------- | --- |
| RUF001 | AmbiguousUnicodeCharacterString | String contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 |
| RUF002 | AmbiguousUnicodeCharacterDocstring | Docstring contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 |
| RUF003 | AmbiguousUnicodeCharacterComment | Comment contains ambiguous unicode character '𝐁' (did you mean 'B'?) | |
| RUF003 | AmbiguousUnicodeCharacterComment | Comment contains ambiguous unicode character '𝐁' (did you mean 'B'?) | 🛠 |
| RUF004 | KeywordArgumentBeforeStarArgument | Keyword argument `...` must come after starred arguments | |
| RUF100 | UnusedNOQA | Unused blanket `noqa` directive | 🛠 |
@@ -1268,10 +1294,12 @@ natively, including:
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-eradicate`](https://pypi.org/project/flake8-eradicate/)
- [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/)
- [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-return`](https://pypi.org/project/flake8-return/)
- [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) (1/37)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`isort`](https://pypi.org/project/isort/)
@@ -1279,7 +1307,7 @@ natively, including:
- [`pep8-naming`](https://pypi.org/project/pep8-naming/)
- [`pydocstyle`](https://pypi.org/project/pydocstyle/)
- [`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (20/33)
- [`pyupgrade`](https://pypi.org/project/pyupgrade/) (21/33)
- [`yesqa`](https://github.com/asottile/yesqa)
Note that, in some cases, Ruff uses different error code prefixes than would be found in the
@@ -1323,10 +1351,12 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-eradicate`](https://pypi.org/project/flake8-eradicate/)
- [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/)
- [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-return`](https://pypi.org/project/flake8-return/)
- [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) (1/37)
- [`flake8-super`](https://pypi.org/project/flake8-super/)
- [`flake8-tidy-imports`](https://pypi.org/project/flake8-tidy-imports/) (1/3)
- [`mccabe`](https://pypi.org/project/mccabe/)
@@ -1336,7 +1366,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
Ruff can also replace [`isort`](https://pypi.org/project/isort/),
[`yesqa`](https://github.com/asottile/yesqa), [`eradicate`](https://pypi.org/project/eradicate/),
[`pygrep-hooks`](https://github.com/pre-commit/pygrep-hooks) (3/10), and a subset of the rules
implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (20/33).
implemented in [`pyupgrade`](https://pypi.org/project/pyupgrade/) (21/33).
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, free to file an Issue.
@@ -1648,7 +1678,6 @@ Summary
<!-- Sections automatically generated by `cargo dev generate-options`. -->
<!-- Begin auto-generated options sections. -->
#### [`allowed-confusables`](#allowed-confusables)
A list of allowed "confusable" Unicode characters to ignore when
@@ -2197,6 +2226,24 @@ unfixable = ["F401"]
---
#### [`update-check`](#update-check)
Enable or disable automatic update checks (overridden by the
`--update-check` and `--no-update-check` command-line flags).
**Default value**: `true`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
update-check = false
```
---
### `flake8-annotations`
#### [`allow-star-arg-any`](#allow-star-arg-any)
@@ -2440,7 +2487,7 @@ multiline-quotes = "single"
#### [`ban-relative-imports`](#ban-relative-imports)
Whether to ban all relative imports (`"all"`), or only those imports
that extend into the parent module and beyond (`"parents"`).
that extend into the parent module or beyond (`"parents"`).
**Default value**: `"parents"`
@@ -2456,6 +2503,27 @@ 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,
and can be circumvented via `eval` or `importlib`.
**Default value**: `{}`
**Type**: `HashMap<String, BannedApi>`
**Example usage**:
```toml
[tool.ruff.flake8-tidy-imports]
[tool.ruff.flake8-tidy-imports.banned-api]
"cgi".msg = "The cgi module is deprecated, see https://peps.python.org/pep-0594/#cgi."
"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
```
---
### `flake8-unused-arguments`
#### [`ignore-variadic-names`](#ignore-variadic-names)
@@ -2725,7 +2793,7 @@ Whether to use Google-style or Numpy-style conventions when detecting
docstring sections. By default, conventions will be inferred from
the available sections.
**Default value**: `"convention"`
**Default value**: `None`
**Type**: `Convention`

View File

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

View File

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

View File

@@ -164,17 +164,17 @@ pub fn convert(
// 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::Single),
"\"" | "double" => flake8_quotes.inline_quotes = Some(Quote::Double),
_ => eprintln!("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::Single),
"\"" | "double" => flake8_quotes.multiline_quotes = Some(Quote::Double),
_ => eprintln!("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::Single),
"\"" | "double" => flake8_quotes.docstring_quotes = Some(Quote::Double),
_ => eprintln!("Unexpected '{key}' value: {value}"),
},
"avoid-escape" | "avoid_escape" => match parser::parse_bool(value.as_ref()) {
@@ -296,6 +296,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -323,7 +324,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
@@ -354,6 +355,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -381,7 +383,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
@@ -412,6 +414,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -439,7 +442,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
@@ -470,6 +473,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -497,7 +501,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
@@ -528,6 +532,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -555,7 +560,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
@@ -594,6 +599,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -657,7 +663,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,
@@ -690,6 +696,7 @@ mod tests {
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
@@ -718,7 +725,7 @@ mod tests {
src: None,
target_version: None,
unfixable: None,
cache_dir: None,
update_check: None,
flake8_annotations: None,
flake8_bugbear: None,
flake8_errmsg: None,

View File

@@ -16,16 +16,17 @@ pub enum Plugin {
Flake8Datetimez,
Flake8Debugger,
Flake8Docstrings,
Flake8ErrMsg,
Flake8Eradicate,
Flake8ErrMsg,
Flake8ImplicitStrConcat,
Flake8Print,
Flake8Quotes,
Flake8Return,
Flake8Simplify,
Flake8TidyImports,
McCabe,
PandasVet,
PEP8Naming,
PandasVet,
Pyupgrade,
}
@@ -45,6 +46,7 @@ impl FromStr for Plugin {
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8Eradicate),
"flake8-errmsg" => Ok(Plugin::Flake8ErrMsg),
"flake8-implicit-str-concat" => Ok(Plugin::Flake8ImplicitStrConcat),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-return" => Ok(Plugin::Flake8Return),
@@ -76,14 +78,15 @@ impl fmt::Debug for Plugin {
Plugin::Flake8Docstrings => "flake8-docstrings",
Plugin::Flake8Eradicate => "flake8-eradicate",
Plugin::Flake8ErrMsg => "flake8-errmsg",
Plugin::Flake8ImplicitStrConcat => "flake8-implicit-str-concat",
Plugin::Flake8Print => "flake8-print",
Plugin::Flake8Quotes => "flake8-quotes",
Plugin::Flake8Return => "flake8-return",
Plugin::Flake8Simplify => "flake8-simplify",
Plugin::Flake8TidyImports => "flake8-tidy-imports",
Plugin::McCabe => "mccabe",
Plugin::PandasVet => "pandas-vet",
Plugin::PEP8Naming => "pep8-naming",
Plugin::PandasVet => "pandas-vet",
Plugin::Pyupgrade => "pyupgrade",
}
)
@@ -106,6 +109,7 @@ impl Plugin {
// 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::Flake8Quotes => CheckCodePrefix::Q,
Plugin::Flake8Return => CheckCodePrefix::RET,
@@ -146,6 +150,7 @@ impl Plugin {
}
Plugin::Flake8Eradicate => vec![CheckCodePrefix::ERA],
Plugin::Flake8ErrMsg => vec![CheckCodePrefix::EM],
Plugin::Flake8ImplicitStrConcat => vec![CheckCodePrefix::ISC],
Plugin::Flake8Print => vec![CheckCodePrefix::T2],
Plugin::Flake8Quotes => vec![CheckCodePrefix::Q],
Plugin::Flake8Return => vec![CheckCodePrefix::RET],
@@ -449,6 +454,7 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<CheckCodePrefix>) -> Vec<Plugin
Plugin::Flake8Docstrings,
Plugin::Flake8Eradicate,
Plugin::Flake8ErrMsg,
Plugin::Flake8ImplicitStrConcat,
Plugin::Flake8Print,
Plugin::Flake8Quotes,
Plugin::Flake8Return,

3
foo.py Normal file
View File

@@ -0,0 +1,3 @@
import mock.mock
x = mock.mock.Mock()

View File

@@ -1 +0,0 @@
src/ruff_options.ts

View File

@@ -0,0 +1,3 @@
{
"trailingComma": "all"
}

View File

@@ -8,3 +8,7 @@ In-browser playground for Ruff. Available [https://ruff.pages.dev/](https://ruff
root directory.
- Install TypeScript dependencies with: `npm install`.
- Start the development server with: `npm run dev`.
## Implementation
Design based on [Tailwind Play](https://play.tailwindcss.com/). Themed with [`ayu`](https://github.com/dempfi/ayu).

View File

@@ -13,10 +13,11 @@
rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🛠️</text></svg>"
/>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
</head>
<body>
<div id="root"></div>
<div style="display: flex; position: fixed; right: 16px; top: 16px">
<div style="display: flex; position: fixed; right: 16px; bottom: 16px">
<a href="https://GitHub.com/charliermarsh/ruff"
><img
src="https://img.shields.io/github/stars/charliermarsh/ruff.svg?style=social&label=GitHub&maxAge=2592000&?logoWidth=100"

File diff suppressed because it is too large Load Diff

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"@monaco-editor/react": "^4.4.6",
"classnames": "^2.3.2",
"lz-string": "^1.4.4",
"monaco-editor": "^0.34.1",
"react": "^18.2.0",
@@ -25,13 +26,16 @@
"@typescript-eslint/eslint-plugin": "^5.47.1",
"@typescript-eslint/parser": "^5.47.1",
"@vitejs/plugin-react-swc": "^3.0.0",
"autoprefixer": "^10.4.13",
"eslint": "^8.30.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.31.11",
"eslint-plugin-react-hooks": "^4.6.0",
"postcss": "^8.4.20",
"prettier": "^2.8.1",
"tailwindcss": "^3.2.4",
"typescript": "^4.9.3",
"vite": "^4.0.0"
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@@ -1,200 +0,0 @@
import lzstring from "lz-string";
import Editor, { useMonaco } from "@monaco-editor/react";
import { MarkerSeverity } from "monaco-editor/esm/vs/editor/editor.api";
import { useEffect, useState, useCallback } from "react";
import init, { Check, check } from "./pkg/ruff.js";
import { AVAILABLE_OPTIONS } from "./ruff_options";
import { Config, getDefaultConfig, toRuffConfig } from "./config";
import { Options } from "./Options";
const DEFAULT_SOURCE =
"# Define a function that takes an integer n and returns the nth number in the Fibonacci\n" +
"# sequence.\n" +
"def fibonacci(n):\n" +
" if n == 0:\n" +
" return 0\n" +
" elif n == 1:\n" +
" return 1\n" +
" else:\n" +
" return fibonacci(n-1) + fibonacci(n-2)\n" +
"\n" +
"# Use a for loop to generate and print the first 10 numbers in the Fibonacci sequence.\n" +
"for i in range(10):\n" +
" print(fibonacci(i))\n" +
"\n" +
"# Output:\n" +
"# 0\n" +
"# 1\n" +
"# 1\n" +
"# 2\n" +
"# 3\n" +
"# 5\n" +
"# 8\n" +
"# 13\n" +
"# 21\n" +
"# 34\n";
function restoreConfigAndSource(): [Config, string] {
const value = lzstring.decompressFromEncodedURIComponent(
window.location.hash.slice(1)
);
let config = {};
let source = DEFAULT_SOURCE;
if (value) {
const parts = value.split("$$$");
config = JSON.parse(parts[0]);
source = parts[1];
}
return [config, source];
}
function persistConfigAndSource(config: Config, source: string) {
window.location.hash = lzstring.compressToEncodedURIComponent(
JSON.stringify(config) + "$$$" + source
);
}
const defaultConfig = getDefaultConfig(AVAILABLE_OPTIONS);
export default function App() {
const monaco = useMonaco();
const [initialized, setInitialized] = useState<boolean>(false);
const [config, setConfig] = useState<Config | null>(null);
const [source, setSource] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
init().then(() => setInitialized(true));
}, []);
useEffect(() => {
if (source == null && config == null && monaco) {
const [config, source] = restoreConfigAndSource();
setConfig(config);
setSource(source);
}
}, [monaco, source, config]);
useEffect(() => {
if (config != null && source != null) {
persistConfigAndSource(config, source);
}
}, [config, source]);
useEffect(() => {
const editor = monaco?.editor;
const model = editor?.getModels()[0];
if (!editor || !model || !initialized || source == null || config == null) {
return;
}
let checks: Check[];
try {
checks = check(source, toRuffConfig(config));
setError(null);
} catch (e) {
setError(String(e));
return;
}
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}`,
severity: MarkerSeverity.Error,
}))
);
const codeActionProvider = monaco?.languages.registerCodeActionProvider(
"python",
{
// @ts-expect-error: The type definition is wrong.
provideCodeActions: function (model, position) {
const actions = checks
.filter((check) => position.startLineNumber === check.location.row)
.filter((check) => check.fix)
.map((check) => ({
title: `Fix ${check.code}`,
id: `fix-${check.code}`,
kind: "quickfix",
edit: check.fix
? {
edits: [
{
resource: model.uri,
versionId: model.getVersionId(),
edit: {
range: {
startLineNumber: check.fix.location.row,
startColumn: check.fix.location.column + 1,
endLineNumber: check.fix.end_location.row,
endColumn: check.fix.end_location.column + 1,
},
text: check.fix.content,
},
},
],
}
: undefined,
}));
return { actions, dispose: () => {} };
},
}
);
return () => {
codeActionProvider?.dispose();
};
}, [config, source, monaco, initialized]);
const handleEditorChange = useCallback(
(value: string | undefined) => {
setSource(value || "");
},
[setSource]
);
const handleOptionChange = useCallback(
(groupName: string, fieldName: string, value: string) => {
const group = Object.assign({}, (config || {})[groupName]);
if (value === defaultConfig[groupName][fieldName] || value === "") {
delete group[fieldName];
} else {
group[fieldName] = value;
}
setConfig({
...config,
[groupName]: group,
});
},
[config]
);
return (
<div id="app">
<Options
config={config}
defaultConfig={defaultConfig}
onChange={handleOptionChange}
/>
<Editor
options={{ readOnly: false, minimap: { enabled: false } }}
wrapperProps={{ className: "editor" }}
defaultLanguage="python"
value={source || ""}
theme={"light"}
onChange={handleEditorChange}
/>
{error && <div id="error">{error}</div>}
</div>
);
}

View File

@@ -0,0 +1,151 @@
import { useCallback, useEffect, useState } from "react";
import { DEFAULT_PYTHON_SOURCE } from "../constants";
import init, { check, Check, currentVersion, defaultSettings } from "../pkg";
import { ErrorMessage } from "./ErrorMessage";
import Header from "./Header";
import { useTheme } from "./theme";
import { persist, restore, stringify } from "./settings";
import SettingsEditor from "./SettingsEditor";
import SourceEditor from "./SourceEditor";
import MonacoThemes from "./MonacoThemes";
type Tab = "Source" | "Settings";
export default function Editor() {
const [initialized, setInitialized] = useState<boolean>(false);
const [version, setVersion] = useState<string | null>(null);
const [tab, setTab] = useState<Tab>("Source");
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 [error, setError] = useState<string | null>(null);
const [theme, setTheme] = useTheme();
useEffect(() => {
init().then(() => setInitialized(true));
}, []);
useEffect(() => {
if (!initialized || settingsSource == null || pythonSource == null) {
return;
}
let config: any;
let checks: Check[];
try {
config = JSON.parse(settingsSource);
} catch (e) {
setChecks([]);
setError((e as Error).message);
return;
}
try {
checks = check(pythonSource, config);
} catch (e) {
setError(e as string);
return;
}
setError(null);
setChecks(checks);
}, [initialized, settingsSource, pythonSource]);
useEffect(() => {
if (!initialized) {
return;
}
if (settingsSource == null || pythonSource == null) {
const payload = restore();
if (payload) {
const [settingsSource, pythonSource] = payload;
setSettingsSource(settingsSource);
setPythonSource(pythonSource);
} else {
setSettingsSource(stringify(defaultSettings()));
setPythonSource(DEFAULT_PYTHON_SOURCE);
}
}
}, [initialized, settingsSource, pythonSource]);
useEffect(() => {
if (!initialized) {
return;
}
setVersion(currentVersion());
}, [initialized]);
const handleShare = useCallback(() => {
if (!initialized || settingsSource == null || pythonSource == null) {
return;
}
persist(settingsSource, pythonSource);
}, [initialized, settingsSource, pythonSource]);
const handlePythonSourceChange = useCallback((pythonSource: string) => {
setEdit((edit) => edit + 1);
setPythonSource(pythonSource);
}, []);
const handleSettingsSourceChange = useCallback((settingsSource: string) => {
setEdit((edit) => edit + 1);
setSettingsSource(settingsSource);
}, []);
return (
<main
className={
"h-full w-full flex flex-auto bg-ayu-background dark:bg-ayu-background-dark"
}
>
<Header
edit={edit}
tab={tab}
theme={theme}
version={version}
onChangeTab={setTab}
onChangeTheme={setTheme}
onShare={initialized ? handleShare : undefined}
/>
<MonacoThemes />
<div className={"mt-12 relative flex-auto"}>
{initialized && settingsSource != null && pythonSource != null ? (
<>
<SourceEditor
visible={tab === "Source"}
source={pythonSource}
theme={theme}
checks={checks}
onChange={handlePythonSourceChange}
/>
<SettingsEditor
visible={tab === "Settings"}
source={settingsSource}
theme={theme}
onChange={handleSettingsSourceChange}
/>
</>
) : null}
</div>
{error && tab === "Source" ? (
<div
style={{
position: "fixed",
left: "10%",
right: "10%",
bottom: "10%",
}}
>
<ErrorMessage>{error}</ErrorMessage>
</div>
) : null}
</main>
);
}

View File

@@ -0,0 +1,26 @@
function truncate(str: string, length: number) {
if (str.length > length) {
return str.slice(0, length) + "...";
} else {
return str;
}
}
export function ErrorMessage({ children }: { children: string }) {
return (
<div
className="bg-orange-100 border-l-4 border-orange-500 text-orange-700 p-4"
role="alert"
>
<p className="font-bold">Error</p>
<p className="block sm:inline">
{truncate(
children.startsWith("Error: ")
? children.slice("Error: ".length)
: children,
120,
)}
</p>
</div>
);
}

View File

@@ -0,0 +1,99 @@
import classNames from "classnames";
import ThemeButton from "./ThemeButton";
import ShareButton from "./ShareButton";
import { Theme } from "./theme";
import VersionTag from "./VersionTag";
export type Tab = "Source" | "Settings";
export default function Header({
edit,
tab,
theme,
version,
onChangeTab,
onChangeTheme,
onShare,
}: {
edit: number;
tab: Tab;
theme: Theme;
version: string | null;
onChangeTab: (tab: Tab) => void;
onChangeTheme: (theme: Theme) => void;
onShare?: () => void;
}) {
return (
<div
className={classNames(
"w-full",
"flex",
"items-center",
"justify-between",
"flex-none",
"pl-5",
"sm:pl-6",
"pr-4",
"lg:pr-6",
"absolute",
"z-10",
"top-0",
"left-0",
"-mb-px",
"antialiased",
"border-b",
"border-gray-200",
"dark:border-gray-800",
)}
>
<div className="flex space-x-5">
<button
type="button"
className={classNames(
"relative flex py-3 text-sm leading-6 font-semibold focus:outline-none",
tab === "Source"
? "text-ayu-accent"
: "text-gray-700 hover:text-gray-900 focus:text-gray-900 dark:text-gray-300 dark:hover:text-white",
)}
onClick={() => onChangeTab("Source")}
>
<span
className={classNames(
"absolute bottom-0 inset-x-0 bg-ayu-accent h-0.5 rounded-full transition-opacity duration-150",
tab === "Source" ? "opacity-100" : "opacity-0",
)}
/>
Source
</button>
<button
type="button"
className={classNames(
"relative flex py-3 text-sm leading-6 font-semibold focus:outline-none",
tab === "Settings"
? "text-ayu-accent"
: "text-gray-700 hover:text-gray-900 focus:text-gray-900 dark:text-gray-300 dark:hover:text-white",
)}
onClick={() => onChangeTab("Settings")}
>
<span
className={classNames(
"absolute bottom-0 inset-x-0 bg-ayu-accent h-0.5 rounded-full transition-opacity duration-150",
tab === "Settings" ? "opacity-100" : "opacity-0",
)}
/>
Settings
</button>
{version ? (
<div className={"flex items-center"}>
<VersionTag>v{version}</VersionTag>
</div>
) : null}
</div>
<div className={"hidden sm:flex items-center min-w-0"}>
<ShareButton key={edit} onShare={onShare} />
<div className="hidden sm:block mx-6 lg:mx-4 w-px h-6 bg-gray-200 dark:bg-gray-700" />
<ThemeButton theme={theme} onChange={onChangeTheme} />
</div>
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,57 @@
/**
* Editor for the settings JSON.
*/
import Editor, { useMonaco } from "@monaco-editor/react";
import { useCallback, useEffect } from "react";
import schema from "../../../ruff.schema.json";
import { Theme } from "./theme";
export default function SettingsEditor({
visible,
source,
theme,
onChange,
}: {
visible: boolean;
source: string;
theme: Theme;
onChange: (source: string) => void;
}) {
const monaco = useMonaco();
useEffect(() => {
monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
schemas: [
{
uri: "https://raw.githubusercontent.com/charliermarsh/ruff/main/ruff.schema.json",
fileMatch: ["*"],
schema,
},
],
});
}, [monaco]);
const handleChange = useCallback(
(value: string | undefined) => {
onChange(value ?? "");
},
[onChange],
);
return (
<Editor
options={{
readOnly: false,
minimap: { enabled: false },
fontSize: 14,
roundedSelection: false,
scrollBeyondLastLine: false,
}}
wrapperProps={visible ? {} : { style: { display: "none" } }}
language={"json"}
value={source}
theme={theme === "light" ? "Ayu-Light" : "Ayu-Dark"}
onChange={handleChange}
/>
);
}

View File

@@ -0,0 +1,53 @@
import { useEffect, useState } from "react";
export default function ShareButton({ onShare }: { onShare?: () => void }) {
const [copied, setCopied] = useState(false);
useEffect(() => {
if (copied) {
const timeout = setTimeout(() => setCopied(false), 2000);
return () => clearTimeout(timeout);
}
}, [copied]);
return copied ? (
<button
type="button"
className="relative flex-none rounded-md text-sm font-semibold leading-6 py-1.5 px-3 cursor-auto text-ayu-accent shadow-copied dark:bg-ayu-accent/10"
>
<span
className="absolute inset-0 flex items-center justify-center invisible"
aria-hidden="true"
>
Share
</span>
<span className="" aria-hidden="false">
Copied!
</span>
</button>
) : (
<button
type="button"
className="relative flex-none rounded-md text-sm font-semibold leading-6 py-1.5 px-3 enabled:hover:bg-ayu-accent/80 bg-ayu-accent text-white shadow-sm dark:shadow-highlight/20 disabled:opacity-50"
disabled={!onShare || copied}
onClick={
onShare
? () => {
setCopied(true);
onShare();
}
: undefined
}
>
<span
className="absolute inset-0 flex items-center justify-center"
aria-hidden="false"
>
Share
</span>
<span className="invisible" aria-hidden="true">
Copied!
</span>
</button>
);
}

View File

@@ -0,0 +1,117 @@
/**
* Editor for the Python source code.
*/
import Editor, { useMonaco } from "@monaco-editor/react";
import { MarkerSeverity, MarkerTag } from "monaco-editor";
import { useCallback, useEffect } from "react";
import { Check } from "../pkg";
import { Theme } from "./theme";
export default function SourceEditor({
visible,
source,
theme,
checks,
onChange,
}: {
visible: boolean;
source: string;
checks: Check[];
theme: Theme;
onChange: (pythonSource: string) => void;
}) {
const monaco = useMonaco();
useEffect(() => {
const editor = monaco?.editor;
const model = editor?.getModels()[0];
if (!editor || !model) {
return;
}
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}`,
severity: MarkerSeverity.Error,
tags:
check.code === "F401" || check.code === "F841"
? [MarkerTag.Unnecessary]
: [],
})),
);
const codeActionProvider = monaco?.languages.registerCodeActionProvider(
"python",
{
// @ts-expect-error: The type definition is wrong.
provideCodeActions: function (model, position) {
const actions = checks
.filter((check) => position.startLineNumber === check.location.row)
.filter((check) => check.fix)
.map((check) => ({
title: check.fix
? `${check.code}: ${check.fix.message}` ?? `Fix ${check.code}`
: "Autofix",
id: `fix-${check.code}`,
kind: "quickfix",
edit: check.fix
? {
edits: [
{
resource: model.uri,
versionId: model.getVersionId(),
edit: {
range: {
startLineNumber: check.fix.location.row,
startColumn: check.fix.location.column + 1,
endLineNumber: check.fix.end_location.row,
endColumn: check.fix.end_location.column + 1,
},
text: check.fix.content,
},
},
],
}
: undefined,
}));
return { actions, dispose: () => {} };
},
},
);
return () => {
codeActionProvider?.dispose();
};
}, [checks, monaco]);
const handleChange = useCallback(
(value: string | undefined) => {
onChange(value ?? "");
},
[onChange],
);
return (
<Editor
options={{
readOnly: false,
minimap: { enabled: false },
fontSize: 14,
roundedSelection: false,
scrollBeyondLastLine: false,
}}
language={"python"}
wrapperProps={visible ? {} : { style: { display: "none" } }}
theme={theme === "light" ? "Ayu-Light" : "Ayu-Dark"}
value={source}
onChange={handleChange}
/>
);
}

View File

@@ -0,0 +1,49 @@
/**
* Button to toggle between light and dark mode themes.
*/
import { Theme } from "./theme";
export default function ThemeButton({
theme,
onChange,
}: {
theme: Theme;
onChange: (theme: Theme) => void;
}) {
return (
<button
type="button"
className="ml-4 sm:ml-0 ring-1 ring-gray-900/5 shadow-sm hover:bg-gray-50 dark:ring-0 dark:bg-gray-800 dark:hover:bg-gray-700 dark:shadow-highlight/4 group focus:outline-none focus-visible:ring-2 rounded-md focus-visible:ring-ayu-accent dark:focus-visible:ring-2 dark:focus-visible:ring-gray-400"
onClick={() => onChange(theme === "light" ? "dark" : "light")}
>
<span className="sr-only">
<span className="dark:hidden">Switch to dark theme</span>
<span className="hidden dark:inline">Switch to light theme</span>
</span>
<svg
width="36"
height="36"
viewBox="-6 -6 36 36"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="stroke-ayu-accent fill-ayu-accent/10 group-hover:stroke-ayu-accent/80 dark:stroke-gray-400 dark:fill-gray-400/20 dark:group-hover:stroke-gray-300"
>
<g className="dark:opacity-0">
<path d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"></path>
<path
d="M12 4v.01M17.66 6.345l-.007.007M20.005 12.005h-.01M17.66 17.665l-.007-.007M12 20.01V20M6.34 17.665l.007-.007M3.995 12.005h.01M6.34 6.344l.007.007"
fill="none"
/>
</g>
<g className="opacity-0 dark:opacity-100">
<path d="M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z" />
<path
d="M12 3v1M18.66 5.345l-.828.828M21.005 12.005h-1M18.66 18.665l-.828-.828M12 21.01V20M5.34 18.666l.835-.836M2.995 12.005h1.01M5.34 5.344l.835.836"
fill="none"
/>
</g>
</svg>
</button>
);
}

View File

@@ -0,0 +1,26 @@
import classNames from "classnames";
import { ReactNode } from "react";
export default function VersionTag({ children }: { children: ReactNode }) {
return (
<div
className={classNames(
"text-gray-500",
"text-xs",
"leading-5",
"font-semibold",
"bg-gray-400/10",
"rounded-full",
"py-1",
"px-3",
"flex",
"items-center",
"dark:bg-gray-800",
"dark:text-gray-400",
"dark:shadow-highlight/4",
)}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,3 @@
import Editor from "./Editor";
export default Editor;

View File

@@ -0,0 +1,50 @@
import lzstring from "lz-string";
export type Settings = { [K: string]: any };
/**
* Stringify a settings object to JSON.
*/
export function stringify(settings: Settings): string {
return JSON.stringify(
settings,
(k, v) => {
if (v instanceof Map) {
return Object.fromEntries(v.entries());
} else {
return v;
}
},
2,
);
}
/**
* Persist the configuration to a URL.
*/
export async function persist(settingsSource: string, pythonSource: string) {
const hash = lzstring.compressToEncodedURIComponent(
settingsSource + "$$$" + pythonSource,
);
await navigator.clipboard.writeText(
window.location.href.split("#")[0] + "#" + hash,
);
}
/**
* Restore the configuration from a URL.
*/
export function restore(): [string, string] | null {
const value = lzstring.decompressFromEncodedURIComponent(
window.location.hash.slice(1),
);
if (value) {
const parts = value.split("$$$");
const settingsSource = parts[0];
const pythonSource = parts[1];
return [settingsSource, pythonSource];
} else {
return null;
}
}

View File

@@ -0,0 +1,35 @@
/**
* Light and dark mode theming.
*/
import { useEffect, useState } from "react";
export type Theme = "dark" | "light";
export function useTheme(): [Theme, (theme: Theme) => void] {
const [localTheme, setLocalTheme] = useState<Theme>("light");
const setTheme = (mode: Theme) => {
if (mode === "dark") {
document.body.classList.add("dark");
} else {
document.body.classList.remove("dark");
}
localStorage.setItem("theme", mode);
setLocalTheme(mode);
};
useEffect(() => {
const initialTheme = localStorage.getItem("theme");
if (initialTheme === "dark") {
setTheme("dark");
} else if (initialTheme === "light") {
setTheme("light");
} else if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
setTheme("dark");
} else {
setTheme("light");
}
}, []);
return [localTheme, setTheme];
}

View File

@@ -0,0 +1,7 @@
export async function copyTextToClipboard(text: string) {
if ("clipboard" in navigator) {
return await navigator.clipboard.writeText(text);
} else {
return document.execCommand("copy", true, text);
}
}

View File

@@ -1,72 +0,0 @@
import { Config } from "./config";
import { AVAILABLE_OPTIONS } from "./ruff_options";
function OptionEntry({
config,
defaultConfig,
groupName,
fieldName,
onChange,
}: {
config: Config | null;
defaultConfig: Config;
groupName: string;
fieldName: string;
onChange: (groupName: string, fieldName: string, value: string) => void;
}) {
const value =
config && config[groupName] && config[groupName][fieldName]
? config[groupName][fieldName]
: "";
return (
<span>
<label>
{fieldName}
<input
value={value}
placeholder={defaultConfig[groupName][fieldName]}
type="text"
onChange={(event) => {
onChange(groupName, fieldName, event.target.value);
}}
/>
</label>
</span>
);
}
export function Options({
config,
defaultConfig,
onChange,
}: {
config: Config | null;
defaultConfig: Config;
onChange: (groupName: string, fieldName: string, value: string) => void;
}) {
return (
<div className="options">
{AVAILABLE_OPTIONS.map((group) => (
<details key={group.name}>
<summary>{group.name}</summary>
<div>
<ul>
{group.fields.map((field) => (
<li key={field.name}>
<OptionEntry
config={config}
defaultConfig={defaultConfig}
groupName={group.name}
fieldName={field.name}
onChange={onChange}
/>
</li>
))}
</ul>
</div>
</details>
))}
</div>
);
}

View File

@@ -1,52 +0,0 @@
import { OptionGroup } from "./ruff_options";
export type Config = { [key: string]: { [key: string]: string } };
export function getDefaultConfig(availableOptions: OptionGroup[]): Config {
const config: Config = {};
availableOptions.forEach((group) => {
config[group.name] = {};
group.fields.forEach((f) => {
config[group.name][f.name] = f.default;
});
});
return config;
}
/**
* Convert the config in the application to something Ruff accepts.
*
* Application config is always nested one level. Ruff allows for some
* top-level options.
*
* Any option value is parsed as JSON to convert it to a native JS object.
* If that fails, e.g. while a user is typing, we let the application handle that
* and show an error.
*/
export function toRuffConfig(config: Config): any {
const convertValue = (value: string): any => {
return value === "None" ? null : JSON.parse(value);
};
const result: any = {};
Object.keys(config).forEach((group_name) => {
const fields = config[group_name];
if (!fields || Object.keys(fields).length === 0) {
return;
}
if (group_name === "globals") {
Object.keys(fields).forEach((field_name) => {
result[field_name] = convertValue(fields[field_name]);
});
} else {
result[group_name] = {};
Object.keys(fields).forEach((field_name) => {
result[group_name][field_name] = convertValue(fields[field_name]);
});
}
});
return result;
}

View File

@@ -0,0 +1,31 @@
export const DEFAULT_PYTHON_SOURCE =
"import os\n" +
"\n" +
"# Define a function that takes an integer n and returns the nth number in the Fibonacci\n" +
"# sequence.\n" +
"def fibonacci(n):\n" +
' """Compute the nth number in the Fibonacci sequence."""\n' +
" x = 1\n" +
" if n == 0:\n" +
" return 0\n" +
" elif n == 1:\n" +
" return 1\n" +
" else:\n" +
" return fibonacci(n - 1) + fibonacci(n - 2)\n" +
"\n" +
"\n" +
"# Use a for loop to generate and print the first 10 numbers in the Fibonacci sequence.\n" +
"for i in range(10):\n" +
" print(fibonacci(i))\n" +
"\n" +
"# Output:\n" +
"# 0\n" +
"# 1\n" +
"# 1\n" +
"# 2\n" +
"# 3\n" +
"# 5\n" +
"# 8\n" +
"# 13\n" +
"# 21\n" +
"# 34\n";

24
playground/src/index.css Normal file
View File

@@ -0,0 +1,24 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
box-sizing: border-box;
}
body,
html,
#root {
margin: 0;
height: 100%;
width: 100%;
}
.shadow-copied {
--tw-shadow: 0 0 0 1px #f07171, inset 0 0 0 1px #f07171;
--tw-shadow-colored: 0 0 0 1px var(--tw-shadow-color),
inset 0 0 0 1px var(--tw-shadow-color);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000),
var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
}

View File

@@ -1,10 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./style.css";
import Editor from "./Editor";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
<Editor />
</React.StrictMode>,
);

View File

@@ -1,239 +0,0 @@
// This file is auto-generated by `cargo dev generate-playground-options`.
export interface OptionGroup {
name: string;
fields: {
name: string;
default: string;
type: string;
}[];
};
export const AVAILABLE_OPTIONS: OptionGroup[] = [
{"name": "globals", "fields": [
{
"name": "allowed-confusables",
"default": '[]',
"type": 'Vec<char>',
},
{
"name": "dummy-variable-rgx",
"default": '"^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"',
"type": 'Regex',
},
{
"name": "extend-ignore",
"default": '[]',
"type": 'Vec<CheckCodePrefix>',
},
{
"name": "extend-select",
"default": '[]',
"type": 'Vec<CheckCodePrefix>',
},
{
"name": "external",
"default": '[]',
"type": 'Vec<String>',
},
{
"name": "fix-only",
"default": 'false',
"type": 'bool',
},
{
"name": "ignore",
"default": '[]',
"type": 'Vec<CheckCodePrefix>',
},
{
"name": "line-length",
"default": '88',
"type": 'usize',
},
{
"name": "required-version",
"default": 'None',
"type": 'String',
},
{
"name": "select",
"default": '["E", "F"]',
"type": 'Vec<CheckCodePrefix>',
},
{
"name": "target-version",
"default": '"py310"',
"type": 'PythonVersion',
},
{
"name": "unfixable",
"default": '[]',
"type": 'Vec<CheckCodePrefix>',
},
]},
{"name": "flake8-annotations", "fields": [
{
"name": "allow-star-arg-any",
"default": 'false',
"type": 'bool',
},
{
"name": "mypy-init-return",
"default": 'false',
"type": 'bool',
},
{
"name": "suppress-dummy-args",
"default": 'false',
"type": 'bool',
},
{
"name": "suppress-none-returning",
"default": 'false',
"type": 'bool',
},
]},
{"name": "flake8-bugbear", "fields": [
{
"name": "extend-immutable-calls",
"default": '[]',
"type": 'Vec<String>',
},
]},
{"name": "flake8-errmsg", "fields": [
{
"name": "max-string-length",
"default": '0',
"type": 'usize',
},
]},
{"name": "flake8-import-conventions", "fields": [
{
"name": "aliases",
"default": '{"altair": "alt", "matplotlib.pyplot": "plt", "numpy": "np", "pandas": "pd", "seaborn": "sns"}',
"type": 'FxHashMap<String, String>',
},
{
"name": "extend-aliases",
"default": '{}',
"type": 'FxHashMap<String, String>',
},
]},
{"name": "flake8-quotes", "fields": [
{
"name": "avoid-escape",
"default": 'true',
"type": 'bool',
},
{
"name": "docstring-quotes",
"default": '"double"',
"type": 'Quote',
},
{
"name": "inline-quotes",
"default": '"double"',
"type": 'Quote',
},
{
"name": "multiline-quotes",
"default": '"double"',
"type": 'Quote',
},
]},
{"name": "flake8-tidy-imports", "fields": [
{
"name": "ban-relative-imports",
"default": '"parents"',
"type": 'Strictness',
},
]},
{"name": "flake8-unused-arguments", "fields": [
{
"name": "ignore-variadic-names",
"default": 'false',
"type": 'bool',
},
]},
{"name": "isort", "fields": [
{
"name": "combine-as-imports",
"default": 'false',
"type": 'bool',
},
{
"name": "extra-standard-library",
"default": '[]',
"type": 'Vec<String>',
},
{
"name": "force-single-line",
"default": 'false',
"type": 'bool',
},
{
"name": "force-wrap-aliases",
"default": 'false',
"type": 'bool',
},
{
"name": "known-first-party",
"default": '[]',
"type": 'Vec<String>',
},
{
"name": "known-third-party",
"default": '[]',
"type": 'Vec<String>',
},
{
"name": "single-line-exclusions",
"default": '[]',
"type": 'Vec<String>',
},
{
"name": "split-on-trailing-comma",
"default": 'true',
"type": 'bool',
},
]},
{"name": "mccabe", "fields": [
{
"name": "max-complexity",
"default": '10',
"type": 'usize',
},
]},
{"name": "pep8-naming", "fields": [
{
"name": "classmethod-decorators",
"default": '["classmethod"]',
"type": 'Vec<String>',
},
{
"name": "ignore-names",
"default": '["setUp", "tearDown", "setUpClass", "tearDownClass", "setUpModule", "tearDownModule", "asyncSetUp", "asyncTearDown", "setUpTestData", "failureException", "longMessage", "maxDiff"]',
"type": 'Vec<String>',
},
{
"name": "staticmethod-decorators",
"default": '["staticmethod"]',
"type": 'Vec<String>',
},
]},
{"name": "pydocstyle", "fields": [
{
"name": "convention",
"default": '"convention"',
"type": 'Convention',
},
]},
{"name": "pyupgrade", "fields": [
{
"name": "keep-runtime-typing",
"default": 'false',
"type": 'bool',
},
]},
];

View File

@@ -1,60 +0,0 @@
* {
box-sizing: border-box;
}
body,
html,
#root,
#app {
margin: 0;
height: 100%;
width: 100%;
}
#app {
display: flex;
}
.options {
height: 100vh;
overflow-y: scroll;
padding: 1em;
min-width: 300px;
border-right: 1px solid lightgray;
}
.options ul {
padding-left: 1em;
list-style-type: none;
}
.options li {
margin-bottom: 0.3em;
}
.options details {
margin-bottom: 1em;
}
.options summary {
font-size: 1.3rem;
}
.options input {
display: block;
width: 100%;
}
.editor {
padding: 1em;
}
#error {
position: fixed;
bottom: 0;
width: 100%;
min-height: 1em;
padding: 1em;
background: darkred;
color: white;
}

View File

@@ -1,6 +1,6 @@
declare module "lz-string" {
function decompressFromEncodedURIComponent(
input: string | null
input: string | null,
): string | null;
function compressToEncodedURIComponent(input: string | null): string;
}

View File

@@ -0,0 +1,22 @@
/** @type {import('tailwindcss').Config} */
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
darkMode: "class",
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
"ayu-accent": "#f07171",
"ayu-background": {
DEFAULT: "#f8f9fa",
dark: "#0b0e14",
},
},
fontFamily: {
sans: ["Inter var", ...defaultTheme.fontFamily.sans],
},
},
},
plugins: [],
};

View File

@@ -1,5 +1,19 @@
[build-system]
requires = ["maturin>=0.14,<0.15"]
build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.206"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
]
maintainers = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },
]
requires-python = ">=3.7"
license = { file = "LICENSE" }
keywords = ["automation", "flake8", "pycodestyle", "pyflakes", "pylint", "clippy"]
classifiers = [
"Development Status :: 3 - Alpha",
@@ -17,23 +31,14 @@ classifiers = [
"Topic :: Software Development :: Libraries :: Python Modules",
"Topic :: Software Development :: Quality Assurance",
]
author = "Charlie Marsh"
author_email = "charlie.r.marsh@gmail.com"
description = "An extremely fast Python linter, written in Rust."
requires-python = ">=3.7"
[project.urls]
repository = "https://github.com/charliermarsh/ruff"
[build-system]
requires = ["maturin>=0.14,<0.15"]
build-backend = "maturin"
urls = { repository = "https://github.com/charliermarsh/ruff-lsp" }
[tool.maturin]
bindings = "bin"
strip = true
[tool.ruff]
line-length = 88
[tool.ruff.isort]
force-wrap-aliases = true

View File

@@ -51,3 +51,12 @@ secret == "s3cr3t"
token == "s3cr3t"
secrete == "s3cr3t"
password == safe == "s3cr3t"
if token == "1\n2":
pass
if token == "3\t4":
pass
if token == "5\r6":
pass

View File

@@ -0,0 +1,36 @@
_ = "a" "b" "c"
_ = "abc" + "def"
_ = "abc" \
"def"
_ = (
"abc" +
"def"
)
_ = (
f"abc" +
"def"
)
_ = (
b"abc" +
b"def"
)
_ = (
"abc"
"def"
)
_ = (
f"abc"
"def"
)
_ = (
b"abc"
b"def"
)

View File

@@ -137,6 +137,14 @@ def x():
return a
# Considered OK, since attribute assignments can have side effects.
class X:
def x(self):
a = self.property
self.property = None
return a
# Test cases for using value for assignment then returning it
# See:https://github.com/afonasev/flake8-return/issues/47
def resolve_from_url(self, url: str) -> dict:
@@ -238,13 +246,16 @@ def close(self):
report(traceback.format_exc())
return any_failed
def global_assignment():
global X
X = 1
return X
def nonlocal_assignment():
X = 1
def inner():
nonlocal X
X = 1

View File

@@ -0,0 +1,33 @@
## Banned modules ##
import cgi
from cgi import *
from cgi import a, b, c
# banning a module also bans any submodules
import cgi.foo.bar
from cgi.foo import bar
from cgi.foo.bar import *
## Banned module members ##
from typing import TypedDict
import typing
# attribute access is checked
typing.TypedDict
typing.TypedDict.anything
# function calls are checked
typing.TypedDict()
typing.TypedDict.anything()
# import aliases are resolved
import typing as totally_not_typing
totally_not_typing.TypedDict

View File

@@ -0,0 +1,11 @@
# module members cannot be imported with that syntax
import typing.TypedDict
# we don't track reassignments
import typing, other
typing = other
typing.TypedDict()
# yet another false positive
def foo(typing):
typing.TypedDict()

View File

@@ -0,0 +1 @@
from long_module_name import member_one, member_two, member_three, member_four, member_five

View File

@@ -0,0 +1,2 @@
from long_module_name import member_one, member_two, member_three, member_four, member_five

View File

@@ -0,0 +1,2 @@
from long_module_name import member_one, member_two, member_three, member_four, member_five

View File

@@ -35,7 +35,6 @@ def f():
...
def f():
r"Here's a line without a period"
...
@@ -71,3 +70,24 @@ def f():
Here's a line without a period,
but here's the next line with trailing space """
...
def f(rounds: list[int], number: int) -> bool:
"""
:param rounds: list - rounds played.
:param number: int - round number.
:return: bool - was the round played?
"""
return number in rounds
def f(rounds: list[int], number: int) -> bool:
"""
Args:
rounds (list): rounds played.
number (int): round number.
Returns:
bool: was the round played?
"""
return number in rounds

View File

@@ -1,16 +1,66 @@
"""Test: noqa directives."""
from module import (
A, # noqa: F401
B,
# This should ignore both errors.
from typing import ( # noqa: F401
List,
Sequence,
)
from module import (
A, # noqa: F401
B, # noqa: F401
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
from module import (
A,
B,
# This should ignore both errors.
from typing import (
List, # noqa: F401
Sequence, # noqa: F401
)
# This should ignore both errors.
from typing import (
List, # noqa
Sequence, # noqa
)
# This should ignore the first error.
from typing import (
Mapping, # noqa: F401
Union,
)
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa: F401
Optional, # noqa: F401
)
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa
Optional, # noqa: F401
)
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F401
Dict, # noqa: F501
)
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F501
Tuple, # noqa: F401
)
# This should ignore both errors.
from typing import Any, AnyStr # noqa: F401
# This should ignore both errors.
from typing import AsyncIterable, AsyncGenerator # noqa
# This should mark F501 as unused.
from typing import Awaitable, AwaitableGenerator # noqa: F501

View File

@@ -1,6 +1,8 @@
# OK
a = "abc"
b = f"ghi{'jkl'}"
# Errors
c = f"def"
d = f"def" + "ghi"
e = (
@@ -8,5 +10,18 @@ e = (
"ghi"
)
# OK
g = f"ghi{123:{45}}"
# Error
h = "x" "y" f"z"
v = 23.234234
# OK
f"{v:0.2f}"
f"{f'{v:0.2f}'}"
# Errors
f"{v:{f'0.2f'}}"
f"{f''}"

View File

@@ -89,7 +89,8 @@ A = (
f'B'
f'{B}'
)
C = f'{A:{B}}'
C = f'{A:{f"{B}"}}'
from typing import Annotated, Literal

View File

@@ -1,10 +0,0 @@
def foo(x: int, y: int, x: int) -> None:
pass
def bar(x: int, y: int, *, x: int) -> None:
pass
def baz(x: int, y: int, **x: int) -> None:
pass

View File

@@ -0,0 +1,11 @@
x = 1 # noqa
x = 1 # NOQA:F401,W203
# noqa
# NOQA
# noqa:F401
# noqa:F401,W203
x = 1
x = 1 # noqa: F401, W203
# noqa: F401
# noqa: F401, W203

View File

@@ -42,6 +42,10 @@ staticmethod-decorators = ["staticmethod"]
[tool.ruff.flake8-tidy-imports]
ban-relative-imports = "parents"
[tool.ruff.flake8-tidy-imports.banned-api]
"cgi".msg = "The cgi module is deprecated."
"typing.TypedDict".msg = "Use typing_extensions.TypedDict instead."
[tool.ruff.flake8-errmsg]
max-string-length = 20

View File

@@ -0,0 +1,98 @@
import mmap, select, socket
from mmap import error
# These should be fixed
try:
pass
except EnvironmentError:
pass
try:
pass
except IOError:
pass
try:
pass
except WindowsError:
pass
try:
pass
except mmap.error:
pass
try:
pass
except select.error:
pass
try:
pass
except socket.error:
pass
try:
pass
except error:
pass
# Should NOT be in parentheses when replaced
try:
pass
except (IOError,):
pass
try:
pass
except (mmap.error,):
pass
try:
pass
except (EnvironmentError, IOError, OSError, select.error):
pass
# Should be kept in parentheses (because multiple)
try:
pass
except (IOError, KeyError, OSError):
pass
# First should change, second should not
from .mmap import error
try:
pass
except (IOError, error):
pass
# These should not change
from foo import error
try:
pass
except (OSError, error):
pass
try:
pass
except:
pass
try:
pass
except AssertionError:
pass
try:
pass
except (mmap).error:
pass
try:
pass
except OSError:
pass
try:
pass
except (OSError, KeyError):
pass

View File

@@ -0,0 +1,17 @@
import mmap, socket, select
try:
pass
except (OSError, mmap.error, IOError):
pass
except (OSError, socket.error, KeyError):
pass
try:
pass
except (
OSError,
select.error,
IOError,
):
pass

View File

@@ -0,0 +1,50 @@
# These should not change
raise ValueError
raise ValueError(1)
from .mmap import error
raise error
# Testing the modules
import socket, mmap, select
raise socket.error
raise mmap.error
raise select.error
raise socket.error()
raise mmap.error(1)
raise select.error(1, 2)
raise socket.error(
1,
2,
3,
)
from mmap import error
raise error
from socket import error
raise error(1)
from select import error
raise error(1, 2)
# Testing the names
raise EnvironmentError
raise IOError
raise WindowsError
raise EnvironmentError()
raise IOError(1)
raise WindowsError(1, 2)
raise EnvironmentError(
1,
2,
3,
)
raise WindowsError
raise EnvironmentError(1)
raise IOError(1, 2)

View File

@@ -0,0 +1,27 @@
# These should change
x = u"Hello"
u'world'
print(u"Hello")
print(u'world')
import foo
foo(u"Hello", U"world", a=u"Hello", b=u"world")
# These should stay quoted they way they are
x = u'hello'
x = u"""hello"""
x = u'''hello'''
x = u'Hello "World"'
# These should not change
u = "Hello"
u = u
def hello():
return"Hello"

View File

@@ -0,0 +1,74 @@
# These should be changed
if True:
import mock
if True:
import mock, sys
# This goes to from unitest import mock
import mock.mock
# Mock should go on a new line as `from unittest import mock`
import contextlib, mock, sys
# Mock should go on a new line as `from unittest import mock`
import mock, sys
x = "This code should be preserved one line below the mock"
# Mock should go on a new line as `from unittest import mock`
from mock import mock
# Should keep trailing comma
from mock import (
mock,
a,
b,
c,
)
# Should not get a trailing comma
from mock import (
mock,
a,
b,
c
)
if True:
if False:
from mock import (
mock,
a,
b,
c
)
# These should not change:
import os, io
# Mock should go on a new line as `from unittest import mock`
import mock, mock
# Mock should go on a new line as `from unittest import mock as foo`
import mock as foo
# Mock should go on a new line as `from unittest import mock as foo`
from mock import mock as foo
if True:
# This should yield multiple, aliased imports.
import mock as foo, mock as bar, mock
# This should yield multiple, aliased imports, and preserve `os`.
import mock as foo, mock as bar, mock, os
if True:
# This should yield multiple, aliased imports.
from mock import mock as foo, mock as bar, mock
# This should be unchanged.
x = mock.Mock()
# This should change to `mock.Mock`().
x = mock.mock.Mock()

View File

@@ -0,0 +1,64 @@
# This should ignore both errors.
from typing import ( # noqa: F401
List,
Sequence,
)
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore both errors.
from typing import (
List, # noqa: F401
Sequence, # noqa: F401
)
# This should ignore both errors.
from typing import (
List, # noqa
Sequence, # noqa
)
# This should ignore the first error.
from typing import (
Mapping, # noqa: F401
Union,
)
# This should ignore both errors.
from typing import ( # noqa
List,
Sequence,
)
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa: F401
Optional, # noqa: F401
)
# This should ignore the error, but the inner noqa should be marked as unused.
from typing import ( # noqa
Optional, # noqa: F401
)
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F401
Dict, # noqa: F501
)
# This should ignore the error, but mark F501 as unused.
from typing import ( # noqa: F501
Tuple, # noqa: F401
)
# This should ignore both errors.
from typing import Any, AnyStr # noqa: F401
# This should ignore both errors.
from typing import AsyncIterable, AsyncGenerator # noqa
# This should mark F501 as unused.
from typing import Awaitable, AwaitableGenerator # noqa: F501

View File

@@ -364,10 +364,30 @@
"items": {
"$ref": "#/definitions/CheckCodePrefix"
}
},
"update-check": {
"description": "Enable or disable automatic update checks (overridden by the `--update-check` and `--no-update-check` command-line flags).",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false,
"definitions": {
"BannedApi": {
"type": "object",
"required": [
"msg"
],
"properties": {
"msg": {
"description": "The message to display when the API is used.",
"type": "string"
}
},
"additionalProperties": false
},
"CheckCodePrefix": {
"type": "string",
"enum": [
@@ -632,8 +652,6 @@
"F821",
"F822",
"F823",
"F83",
"F831",
"F84",
"F841",
"F842",
@@ -663,6 +681,12 @@
"ICN0",
"ICN00",
"ICN001",
"ISC",
"ISC0",
"ISC00",
"ISC001",
"ISC002",
"ISC003",
"M",
"M0",
"M001",
@@ -726,6 +750,7 @@
"PGH001",
"PGH002",
"PGH003",
"PGH004",
"PLC",
"PLC0",
"PLC04",
@@ -834,6 +859,7 @@
"TID",
"TID2",
"TID25",
"TID251",
"TID252",
"U",
"U0",
@@ -883,6 +909,9 @@
"UP021",
"UP022",
"UP023",
"UP024",
"UP025",
"UP026",
"W",
"W2",
"W29",
@@ -910,10 +939,21 @@
]
},
"Convention": {
"type": "string",
"enum": [
"google",
"numpy"
"oneOf": [
{
"description": "Use Google-style docstrings.",
"type": "string",
"enum": [
"google"
]
},
{
"description": "Use NumPy-style docstrings.",
"type": "string",
"enum": [
"numpy"
]
}
]
},
"Flake8AnnotationsOptions": {
@@ -1057,7 +1097,7 @@
"type": "object",
"properties": {
"ban-relative-imports": {
"description": "Whether to ban all relative imports (`\"all\"`), or only those imports that extend into the parent module and beyond (`\"parents\"`).",
"description": "Whether to ban all relative imports (`\"all\"`), or only those imports that extend into the parent module or beyond (`\"parents\"`).",
"anyOf": [
{
"$ref": "#/definitions/Strictness"
@@ -1066,6 +1106,16 @@
"type": "null"
}
]
},
"banned-api": {
"description": "Specific modules or module members that may not be imported or accessed. Note that this check is only meant to flag accidental uses, and can be circumvented via `eval` or `importlib`.",
"type": [
"object",
"null"
],
"additionalProperties": {
"$ref": "#/definitions/BannedApi"
}
}
},
"additionalProperties": false
@@ -1253,10 +1303,21 @@
]
},
"Quote": {
"type": "string",
"enum": [
"single",
"double"
"oneOf": [
{
"description": "Use single quotes (`'`).",
"type": "string",
"enum": [
"single"
]
},
{
"description": "Use double quotes (`\"`).",
"type": "string",
"enum": [
"double"
]
}
]
},
"SerializationFormat": {
@@ -1271,10 +1332,21 @@
]
},
"Strictness": {
"type": "string",
"enum": [
"parents",
"all"
"oneOf": [
{
"description": "Ban imports that extend into the parent module or beyond.",
"type": "string",
"enum": [
"parents"
]
},
{
"description": "Ban all relative imports.",
"type": "string",
"enum": [
"all"
]
}
]
},
"Version": {

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.199"
version = "0.0.206"
edition = "2021"
[dependencies]
@@ -11,9 +11,9 @@ itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
once_cell = { version = "1.16.0" }
ruff = { path = ".." }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "68d26955b3e24198a150315e7959719b03709dee" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "71becd4059fdce4bce7010f1208ed3b1c883abba" }
schemars = { version = "0.8.11" }
serde_json = {version="1.0.91"}
strum = { version = "0.24.1", features = ["strum_macros"] }

View File

@@ -4,8 +4,8 @@ use anyhow::Result;
use clap::Args;
use crate::{
generate_check_code_prefix, generate_json_schema, generate_options,
generate_playground_options, generate_rules_table,
generate_check_code_prefix, generate_cli_help, generate_json_schema, generate_options,
generate_rules_table,
};
#[derive(Args)]
@@ -28,7 +28,7 @@ pub fn main(cli: &Cli) -> Result<()> {
generate_options::main(&generate_options::Cli {
dry_run: cli.dry_run,
})?;
generate_playground_options::main(&generate_playground_options::Cli {
generate_cli_help::main(&generate_cli_help::Cli {
dry_run: cli.dry_run,
})?;
Ok(())

View File

@@ -0,0 +1,38 @@
//! Generate CLI help.
use anyhow::Result;
use clap::{Args, CommandFactory};
use ruff::cli::Cli as MainCli;
use crate::utils::replace_readme_section;
const HELP_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated cli help. -->";
const HELP_END_PRAGMA: &str = "<!-- End auto-generated cli help. -->";
#[derive(Args)]
pub struct Cli {
/// Write the generated help to stdout (rather than to `README.md`).
#[arg(long)]
pub(crate) dry_run: bool,
}
fn trim_lines(s: &str) -> String {
s.lines().map(str::trim_end).collect::<Vec<_>>().join("\n")
}
pub fn main(cli: &Cli) -> Result<()> {
let mut cmd = MainCli::command();
let output = trim_lines(cmd.render_help().to_string().trim());
if cli.dry_run {
print!("{output}");
} else {
replace_readme_section(
&format!("```shell\n{output}\n```\n"),
HELP_BEGIN_PRAGMA,
HELP_END_PRAGMA,
)?;
}
Ok(())
}

View File

@@ -1,16 +1,13 @@
//! Generate a Markdown-compatible listing of configuration options.
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
use crate::utils::replace_readme_section;
const BEGIN_PRAGMA: &str = "<!-- Begin auto-generated options sections. -->";
const END_PRAGMA: &str = "<!-- End auto-generated options sections. -->";
@@ -95,30 +92,7 @@ pub fn main(cli: &Cli) -> Result<()> {
if cli.dry_run {
print!("{output}");
} else {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(BEGIN_PRAGMA)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + BEGIN_PRAGMA.len()];
// Extract the suffix.
let index = existing
.find(END_PRAGMA)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
write!(f, "{prefix}\n\n")?;
write!(f, "{output}")?;
write!(f, "{suffix}")?;
replace_readme_section(&output, BEGIN_PRAGMA, END_PRAGMA)?;
}
Ok(())

View File

@@ -1,142 +0,0 @@
//! Generate typescript file defining options to be used by the web playground.
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::settings::options::Options;
use ruff::settings::options_base::{ConfigurationOptions, OptionEntry, OptionField};
#[derive(Args)]
pub struct Cli {
/// Write the generated table to stdout (rather than to `TODO`).
#[arg(long)]
pub(crate) dry_run: bool,
}
fn emit_field(output: &mut String, field: &OptionField) {
output.push_str(&textwrap::indent(
&textwrap::dedent(&format!(
"
{{
\"name\": \"{}\",
\"default\": '{}',
\"type\": '{}',
}},",
field.name, field.default, field.value_type
)),
" ",
));
}
pub fn main(cli: &Cli) -> Result<()> {
let mut output = String::new();
// Generate all the top-level fields.
output.push_str(&format!("{{\"name\": \"{}\", \"fields\": [", "globals"));
for field in Options::get_available_options()
.into_iter()
.filter_map(|entry| {
if let OptionEntry::Field(field) = entry {
Some(field)
} else {
None
}
})
// Filter out options that don't make sense in the playground.
.filter(|field| {
!matches!(
field.name,
"src"
| "fix"
| "format"
| "exclude"
| "extend"
| "extend-exclude"
| "fixable"
| "force-exclude"
| "ignore-init-module-imports"
| "respect-gitignore"
| "show-source"
| "cache-dir"
| "per-file-ignores"
)
})
.sorted_by_key(|field| field.name)
{
emit_field(&mut output, &field);
}
output.push_str("\n]},\n");
// Generate all the sub-groups.
for group in Options::get_available_options()
.into_iter()
.filter_map(|entry| {
if let OptionEntry::Group(group) = entry {
Some(group)
} else {
None
}
})
.sorted_by_key(|group| group.name)
{
output.push_str(&format!("{{\"name\": \"{}\", \"fields\": [", group.name));
for field in group
.fields
.iter()
.filter_map(|entry| {
if let OptionEntry::Field(field) = entry {
Some(field)
} else {
None
}
})
.sorted_by_key(|field| field.name)
{
emit_field(&mut output, field);
}
output.push_str("\n]},\n");
}
let prefix = textwrap::dedent(
r"
// This file is auto-generated by `cargo dev generate-playground-options`.
export interface OptionGroup {
name: string;
fields: {
name: string;
default: string;
type: string;
}[];
};
export const AVAILABLE_OPTIONS: OptionGroup[] = [
",
);
let postfix = "];";
if cli.dry_run {
print!("{output}");
} else {
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("playground")
.join("src")
.join("ruff_options.ts");
let mut f = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(file)?;
write!(f, "{prefix}")?;
write!(f, "{}", textwrap::indent(&output, " "))?;
write!(f, "{postfix}")?;
}
Ok(())
}

View File

@@ -1,16 +1,13 @@
//! Generate a Markdown-compatible table of supported lint rules.
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
use clap::Args;
use itertools::Itertools;
use ruff::checks::{CheckCategory, CheckCode};
use strum::IntoEnumIterator;
use crate::utils::replace_readme_section;
const TABLE_BEGIN_PRAGMA: &str = "<!-- Begin auto-generated sections. -->";
const TABLE_END_PRAGMA: &str = "<!-- End auto-generated sections. -->";
@@ -85,32 +82,3 @@ pub fn main(cli: &Cli) -> Result<()> {
Ok(())
}
fn replace_readme_section(content: &str, begin_pragma: &str, end_pragma: &str) -> Result<()> {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(begin_pragma)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + begin_pragma.len()];
// Extract the suffix.
let index = existing
.find(end_pragma)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
writeln!(f, "{prefix}")?;
write!(f, "{content}")?;
write!(f, "{suffix}")?;
Ok(())
}

View File

@@ -13,11 +13,12 @@
pub mod generate_all;
pub mod generate_check_code_prefix;
pub mod generate_cli_help;
pub mod generate_json_schema;
pub mod generate_options;
pub mod generate_playground_options;
pub mod generate_rules_table;
pub mod print_ast;
pub mod print_cst;
pub mod print_tokens;
pub mod round_trip;
mod utils;

View File

@@ -14,9 +14,8 @@
use anyhow::Result;
use clap::{Parser, Subcommand};
use ruff_dev::{
generate_all, generate_check_code_prefix, generate_json_schema, generate_options,
generate_playground_options, generate_rules_table, print_ast, print_cst, print_tokens,
round_trip,
generate_all, generate_check_code_prefix, generate_cli_help, generate_json_schema,
generate_options, generate_rules_table, print_ast, print_cst, print_tokens, round_trip,
};
#[derive(Parser)]
@@ -39,9 +38,8 @@ enum Commands {
GenerateRulesTable(generate_rules_table::Cli),
/// Generate a Markdown-compatible listing of configuration options.
GenerateOptions(generate_options::Cli),
/// Generate typescript file defining options to be used by the web
/// playground.
GeneratePlaygroundOptions(generate_playground_options::Cli),
/// Generate CLI help.
GenerateCliHelp(generate_cli_help::Cli),
/// Print the AST for a given Python file.
PrintAST(print_ast::Cli),
/// Print the LibCST CST for a given Python file.
@@ -60,7 +58,7 @@ fn main() -> Result<()> {
Commands::GenerateJSONSchema(args) => generate_json_schema::main(args)?,
Commands::GenerateRulesTable(args) => generate_rules_table::main(args)?,
Commands::GenerateOptions(args) => generate_options::main(args)?,
Commands::GeneratePlaygroundOptions(args) => generate_playground_options::main(args)?,
Commands::GenerateCliHelp(args) => generate_cli_help::main(args)?,
Commands::PrintAST(args) => print_ast::main(args)?,
Commands::PrintCST(args) => print_cst::main(args)?,
Commands::PrintTokens(args) => print_tokens::main(args)?,

View File

@@ -22,7 +22,11 @@ pub fn main(cli: &Cli) -> Result<()> {
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::new(stylist.indentation(), stylist.quote());
let mut generator = SourceCodeGenerator::new(
stylist.indentation(),
stylist.quote(),
stylist.line_ending(),
);
generator.unparse_suite(&python_ast);
println!("{}", generator.generate()?);
Ok(())

35
ruff_dev/src/utils.rs Normal file
View File

@@ -0,0 +1,35 @@
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::PathBuf;
use anyhow::Result;
pub fn replace_readme_section(content: &str, begin_pragma: &str, end_pragma: &str) -> Result<()> {
// Read the existing file.
let file = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.expect("Failed to find root directory")
.join("README.md");
let existing = fs::read_to_string(&file)?;
// Extract the prefix.
let index = existing
.find(begin_pragma)
.expect("Unable to find begin pragma");
let prefix = &existing[..index + begin_pragma.len()];
// Extract the suffix.
let index = existing
.find(end_pragma)
.expect("Unable to find end pragma");
let suffix = &existing[index..];
// Write the prefix, new contents, and suffix.
let mut f = OpenOptions::new().write(true).truncate(true).open(&file)?;
writeln!(f, "{prefix}")?;
write!(f, "{content}")?;
write!(f, "{suffix}")?;
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_macros"
version = "0.0.199"
version = "0.0.206"
edition = "2021"
[lib]

View File

@@ -5,7 +5,7 @@ use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{
Arguments, Constant, Excepthandler, ExcepthandlerKind, Expr, ExprKind, Keyword, KeywordData,
Location, Stmt, StmtKind,
Located, Location, Stmt, StmtKind,
};
use rustpython_parser::lexer;
use rustpython_parser::lexer::Tok;
@@ -335,20 +335,17 @@ 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 {
let range = Range {
location: Location::new(stmt.location.row(), 0),
end_location: stmt.location,
};
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 {
let range = Range {
location: stmt.end_location.unwrap(),
end_location: Location::new(stmt.end_location.unwrap().row() + 1, 0),
};
let range = Range::new(
stmt.end_location.unwrap(),
Location::new(stmt.end_location.unwrap().row() + 1, 0),
);
let suffix = locator.slice_source_code_range(&range);
for char in suffix.chars() {
if char == '#' {
@@ -384,10 +381,7 @@ pub fn identifier_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Range {
let contents = locator.slice_source_code_range(&Range::from_located(stmt));
for (start, tok, end) in lexer::make_tokenizer_located(&contents, stmt.location).flatten() {
if matches!(tok, Tok::Name { .. }) {
return Range {
location: start,
end_location: end,
};
return Range::new(start, end);
}
}
error!("Failed to find identifier for {:?}", stmt);
@@ -395,6 +389,19 @@ pub fn identifier_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Range {
Range::from_located(stmt)
}
// Return the ranges of `Name` tokens within a specified node.
pub fn find_names<T>(located: &Located<T>, locator: &SourceCodeLocator) -> Vec<Range> {
let contents = locator.slice_source_code_range(&Range::from_located(located));
lexer::make_tokenizer_located(&contents, located.location)
.flatten()
.filter(|(_, tok, _)| matches!(tok, Tok::Name { .. }))
.map(|(start, _, end)| Range {
location: start,
end_location: end,
})
.collect()
}
/// Return the `Range` of `name` in `Excepthandler`.
pub fn excepthandler_name_range(
handler: &Excepthandler,
@@ -406,17 +413,70 @@ pub fn excepthandler_name_range(
match (name, type_) {
(Some(_), Some(type_)) => {
let type_end_location = type_.end_location.unwrap();
let contents = locator.slice_source_code_range(&Range {
location: type_end_location,
end_location: body[0].location,
});
let contents =
locator.slice_source_code_range(&Range::new(type_end_location, body[0].location));
let range = lexer::make_tokenizer_located(&contents, type_end_location)
.flatten()
.tuple_windows()
.find(|(tok, next_tok)| {
matches!(tok.1, Tok::As) && matches!(next_tok.1, Tok::Name { .. })
})
.map(|((..), (location, _, end_location))| Range {
.map(|((..), (location, _, end_location))| Range::new(location, end_location));
range
}
_ => None,
}
}
/// Return the `Range` of `except` in `Excepthandler`.
pub fn except_range(handler: &Excepthandler, locator: &SourceCodeLocator) -> Range {
let ExcepthandlerKind::ExceptHandler { body, type_, .. } = &handler.node;
let end = if let Some(type_) = type_ {
type_.location
} else {
body.first()
.expect("Expected body to be non-empty")
.location
};
let contents = locator.slice_source_code_range(&Range {
location: handler.location,
end_location: end,
});
let range = lexer::make_tokenizer_located(&contents, handler.location)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Except { .. }))
.map(|(location, _, end_location)| Range {
location,
end_location,
})
.expect("Failed to find `except` range");
range
}
/// Return the `Range` of `else` in `For`, `AsyncFor`, and `While` statements.
pub fn else_range(stmt: &Stmt, locator: &SourceCodeLocator) -> Option<Range> {
match &stmt.node {
StmtKind::For { body, orelse, .. }
| StmtKind::AsyncFor { body, orelse, .. }
| StmtKind::While { body, orelse, .. }
if !orelse.is_empty() =>
{
let body_end = body
.last()
.expect("Expected body to be non-empty")
.end_location
.unwrap();
let contents = locator.slice_source_code_range(&Range {
location: body_end,
end_location: orelse
.first()
.expect("Expected orelse to be non-empty")
.location,
});
let range = lexer::make_tokenizer_located(&contents, body_end)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Else))
.map(|(location, _, end_location)| Range {
location,
end_location,
});
@@ -435,10 +495,10 @@ pub fn preceded_by_continuation(stmt: &Stmt, locator: &SourceCodeLocator) -> boo
// make conservative choices.
// TODO(charlie): Come up with a more robust strategy.
if stmt.location.row() > 1 {
let range = Range {
location: Location::new(stmt.location.row() - 1, 0),
end_location: Location::new(stmt.location.row(), 0),
};
let range = Range::new(
Location::new(stmt.location.row() - 1, 0),
Location::new(stmt.location.row(), 0),
);
let line = locator.slice_source_code_range(&range);
if line.trim().ends_with('\\') {
return true;
@@ -466,7 +526,9 @@ mod tests {
use rustpython_ast::Location;
use rustpython_parser::parser;
use crate::ast::helpers::{identifier_range, match_module_member, match_trailing_content};
use crate::ast::helpers::{
else_range, identifier_range, match_module_member, match_trailing_content,
};
use crate::ast::types::Range;
use crate::source_code_locator::SourceCodeLocator;
@@ -660,10 +722,7 @@ y = 2
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 4),
end_location: Location::new(1, 5),
}
Range::new(Location::new(1, 4), Location::new(1, 5),)
);
let contents = r#"
@@ -677,10 +736,7 @@ def \
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(2, 2),
end_location: Location::new(2, 3),
}
Range::new(Location::new(2, 2), Location::new(2, 3),)
);
let contents = "class Class(): pass".trim();
@@ -689,10 +745,7 @@ def \
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 6),
end_location: Location::new(1, 11),
}
Range::new(Location::new(1, 6), Location::new(1, 11),)
);
let contents = "class Class: pass".trim();
@@ -701,10 +754,7 @@ def \
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 6),
end_location: Location::new(1, 11),
}
Range::new(Location::new(1, 6), Location::new(1, 11),)
);
let contents = r#"
@@ -718,10 +768,7 @@ class Class():
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(2, 6),
end_location: Location::new(2, 11),
}
Range::new(Location::new(2, 6), Location::new(2, 11),)
);
let contents = r#"x = y + 1"#.trim();
@@ -730,12 +777,29 @@ class Class():
let locator = SourceCodeLocator::new(contents);
assert_eq!(
identifier_range(stmt, &locator),
Range {
location: Location::new(1, 0),
end_location: Location::new(1, 9),
}
Range::new(Location::new(1, 0), Location::new(1, 9),)
);
Ok(())
}
#[test]
fn test_else_range() -> Result<()> {
let contents = r#"
for x in y:
pass
else:
pass
"#
.trim();
let program = parser::parse_program(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = SourceCodeLocator::new(contents);
let range = else_range(stmt, &locator).unwrap();
assert_eq!(range.location.row(), 3);
assert_eq!(range.location.column(), 0);
assert_eq!(range.end_location.row(), 3);
assert_eq!(range.end_location.column(), 4);
Ok(())
}
}

View File

@@ -22,12 +22,16 @@ pub struct Range {
}
impl Range {
pub fn from_located<T>(located: &Located<T>) -> Self {
Range {
location: located.location,
end_location: located.end_location.unwrap(),
pub fn new(location: Location, end_location: Location) -> Self {
Self {
location,
end_location,
}
}
pub fn from_located<T>(located: &Located<T>) -> Self {
Range::new(located.location, located.end_location.unwrap())
}
}
#[derive(Debug)]

View File

@@ -38,6 +38,9 @@ pub trait Visitor<'a> {
fn visit_excepthandler(&mut self, excepthandler: &'a Excepthandler) {
walk_excepthandler(self, excepthandler);
}
fn visit_format_spec(&mut self, format_spec: &'a Expr) {
walk_expr(self, format_spec);
}
fn visit_arguments(&mut self, arguments: &'a Arguments) {
walk_arguments(self, arguments);
}
@@ -387,7 +390,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
} => {
visitor.visit_expr(value);
if let Some(expr) = format_spec {
visitor.visit_expr(expr);
visitor.visit_format_spec(expr);
}
}
ExprKind::JoinedStr { values } => {

View File

@@ -9,10 +9,10 @@ use crate::checkers::ast::Checker;
/// Extract the leading indentation from a line.
pub fn indentation<'a, T>(checker: &'a Checker, located: &'a Located<T>) -> Cow<'a, str> {
let range = Range::from_located(located);
checker.locator.slice_source_code_range(&Range {
location: Location::new(range.location.row(), 0),
end_location: Location::new(range.location.row(), range.location.column()),
})
checker.locator.slice_source_code_range(&Range::new(
Location::new(range.location.row(), 0),
Location::new(range.location.row(), range.location.column()),
))
}
/// Extract the leading words from a line of text.

View File

@@ -68,10 +68,7 @@ fn apply_fixes<'a>(
}
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice_source_code_range(&Range {
location: last_pos,
end_location: fix.location,
});
let slice = locator.slice_source_code_range(&Range::new(last_pos, fix.location));
output.append(&slice);
// Add the patch itself.

View File

@@ -15,8 +15,9 @@ use serde::{Deserialize, Serialize};
use crate::message::Message;
use crate::settings::{flags, Settings};
static CACHE_DIR: Lazy<Option<String>> = Lazy::new(|| std::env::var("RUFF_CACHE_DIR").ok());
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 {
@@ -40,7 +41,7 @@ struct CheckResult {
pub fn cache_dir(project_root: &Path) -> PathBuf {
CACHE_DIR
.as_ref()
.map_or_else(|| project_root.join(".ruff_cache"), PathBuf::from)
.map_or_else(|| project_root.join(DEFAULT_CACHE_DIR_NAME), PathBuf::from)
}
fn content_dir() -> &'static Path {

View File

@@ -39,10 +39,10 @@ use crate::visibility::{module_visibility, transition_scope, Modifier, Visibilit
use crate::{
docstrings, flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except,
flake8_boolean_trap, flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez,
flake8_debugger, flake8_errmsg, flake8_import_conventions, flake8_print, flake8_return,
flake8_simplify, flake8_tidy_imports, flake8_unused_arguments, mccabe, noqa, pandas_vet,
pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff,
visibility,
flake8_debugger, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions,
flake8_print, flake8_return, flake8_simplify, flake8_tidy_imports, flake8_unused_arguments,
mccabe, noqa, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint,
pyupgrade, ruff, visibility,
};
const GLOBAL_SCOPE_INDEX: usize = 0;
@@ -87,7 +87,6 @@ pub struct Checker<'a> {
deferred_assignments: Vec<DeferralContext<'a>>,
// Internal, derivative state.
visible_scope: VisibleScope,
in_f_string: Option<Range>,
in_annotation: bool,
in_type_definition: bool,
in_deferred_string_type_definition: bool,
@@ -144,7 +143,6 @@ impl<'a> Checker<'a> {
modifier: Modifier::Module,
visibility: module_visibility(path),
},
in_f_string: None,
in_annotation: false,
in_type_definition: false,
in_deferred_string_type_definition: false,
@@ -161,14 +159,7 @@ impl<'a> Checker<'a> {
}
/// Add a `Check` to the `Checker`.
pub(crate) fn add_check(&mut self, mut check: Check) {
// If we're in an f-string, override the location. RustPython doesn't produce
// reliable locations for expressions within f-strings, so we use the
// span of the f-string itself as a best-effort default.
if let Some(range) = self.in_f_string {
check.location = range.location;
check.end_location = range.end_location;
}
pub(crate) fn add_check(&mut self, check: Check) {
self.checks.push(check);
}
@@ -184,16 +175,7 @@ impl<'a> Checker<'a> {
pub fn patch(&self, code: &CheckCode) -> bool {
// TODO(charlie): We can't fix errors in f-strings until RustPython adds
// location data.
matches!(self.autofix, flags::Autofix::Enabled)
&& self.in_f_string.is_none()
&& self.settings.fixable.contains(code)
}
/// Return the amended `Range` from a `Located`.
pub fn range_for<T>(&self, located: &Located<T>) -> Range {
// If we're in an f-string, override the location.
self.in_f_string
.unwrap_or_else(|| Range::from_located(located))
matches!(self.autofix, flags::Autofix::Enabled) && self.settings.fixable.contains(code)
}
/// Return `true` if the `Expr` is a reference to `typing.${target}`.
@@ -231,10 +213,10 @@ impl<'a> Checker<'a> {
return false;
}
let noqa_lineno = self.noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = self.locator.slice_source_code_range(&Range {
location: Location::new(*noqa_lineno, 0),
end_location: Location::new(noqa_lineno + 1, 0),
});
let line = self.locator.slice_source_code_range(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match noqa::extract_noqa_directive(&line) {
Directive::None => false,
Directive::All(..) => true,
@@ -282,16 +264,17 @@ where
match &stmt.node {
StmtKind::Global { names } => {
let scope_index = *self.scope_stack.last().expect("No current scope found");
let ranges = helpers::find_names(stmt, self.locator);
if scope_index != GLOBAL_SCOPE_INDEX {
// Add the binding to the current scope.
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for name in names {
for (name, range) in names.iter().zip(ranges.iter()) {
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Global,
used: usage,
range: Range::from_located(stmt),
range: *range,
source: Some(RefEquality(stmt)),
});
scope.values.insert(name, index);
@@ -302,8 +285,9 @@ where
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, stmt)
.zip(ranges.iter())
.filter_map(|(name, range)| {
pycodestyle::checks::ambiguous_variable_name(name, *range)
})
.into_iter(),
);
@@ -311,16 +295,17 @@ where
}
StmtKind::Nonlocal { names } => {
let scope_index = *self.scope_stack.last().expect("No current scope found");
let ranges = helpers::find_names(stmt, self.locator);
if scope_index != GLOBAL_SCOPE_INDEX {
let scope = &mut self.scopes[scope_index];
let usage = Some((scope.id, Range::from_located(stmt)));
for name in names {
for (name, range) in names.iter().zip(ranges.iter()) {
// Add a binding to the current scope.
let index = self.bindings.len();
self.bindings.push(Binding {
kind: BindingKind::Nonlocal,
used: usage,
range: Range::from_located(stmt),
range: *range,
source: Some(RefEquality(stmt)),
});
scope.values.insert(name, index);
@@ -328,7 +313,7 @@ where
// Mark the binding in the defining scopes as used too. (Skip the global scope
// and the current scope.)
for name in names {
for (name, range) in names.iter().zip(ranges.iter()) {
let mut exists = false;
for index in self.scope_stack.iter().skip(1).rev().skip(1) {
if let Some(index) = self.scopes[*index].values.get(&name.as_str()) {
@@ -342,7 +327,7 @@ where
if self.settings.enabled.contains(&CheckCode::PLE0117) {
self.add_check(Check::new(
CheckKind::NonlocalWithoutBinding(name.to_string()),
Range::from_located(stmt),
*range,
));
}
}
@@ -353,8 +338,9 @@ where
self.add_checks(
names
.iter()
.filter_map(|name| {
pycodestyle::checks::ambiguous_variable_name(name, stmt)
.zip(ranges.iter())
.filter_map(|(name, range)| {
pycodestyle::checks::ambiguous_variable_name(name, *range)
})
.into_iter(),
);
@@ -397,10 +383,9 @@ where
..
} => {
if self.settings.enabled.contains(&CheckCode::E743) {
if let Some(check) = pycodestyle::checks::ambiguous_function_name(
name,
Range::from_located(stmt),
) {
if let Some(check) = pycodestyle::checks::ambiguous_function_name(name, || {
helpers::identifier_range(stmt, self.locator)
}) {
self.add_check(check);
}
}
@@ -551,7 +536,7 @@ where
Binding {
kind: BindingKind::FunctionDefinition,
used: None,
range: Range::from_located(stmt),
range: helpers::identifier_range(stmt, self.locator),
source: Some(self.current_stmt().clone()),
},
);
@@ -585,9 +570,9 @@ where
}
if self.settings.enabled.contains(&CheckCode::E742) {
if let Some(check) =
pycodestyle::checks::ambiguous_class_name(name, Range::from_located(stmt))
{
if let Some(check) = pycodestyle::checks::ambiguous_class_name(name, || {
helpers::identifier_range(stmt, self.locator)
}) {
self.add_check(check);
}
}
@@ -601,9 +586,12 @@ where
}
if self.settings.enabled.contains(&CheckCode::N818) {
if let Some(check) =
pep8_naming::checks::error_suffix_on_exception_name(stmt, bases, name)
{
if let Some(check) = pep8_naming::checks::error_suffix_on_exception_name(
stmt,
bases,
name,
self.locator,
) {
self.add_check(check);
}
}
@@ -653,6 +641,9 @@ where
if self.settings.enabled.contains(&CheckCode::UP023) {
pyupgrade::plugins::replace_c_element_tree(self, stmt);
}
if self.settings.enabled.contains(&CheckCode::UP026) {
pyupgrade::plugins::rewrite_mock_import(self, stmt);
}
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
@@ -723,6 +714,17 @@ where
}
}
// flake8_tidy_imports
if self.settings.enabled.contains(&CheckCode::TID251) {
if let Some(check) = flake8_tidy_imports::checks::name_or_parent_is_banned(
alias,
&alias.node.name,
&self.settings.flake8_tidy_imports.banned_api,
) {
self.add_check(check);
}
}
// pylint
if self.settings.enabled.contains(&CheckCode::PLC0414) {
pylint::plugins::useless_import_alias(self, alias);
@@ -853,6 +855,30 @@ where
pyupgrade::plugins::unnecessary_future_import(self, stmt, names);
}
}
if self.settings.enabled.contains(&CheckCode::UP026) {
pyupgrade::plugins::rewrite_mock_import(self, stmt);
}
if self.settings.enabled.contains(&CheckCode::TID251) {
if let Some(module) = module {
for name in names {
if let Some(check) = flake8_tidy_imports::checks::name_is_banned(
module,
name,
&self.settings.flake8_tidy_imports.banned_api,
) {
self.add_check(check);
}
}
if let Some(check) = flake8_tidy_imports::checks::name_or_parent_is_banned(
stmt,
module,
&self.settings.flake8_tidy_imports.banned_api,
) {
self.add_check(check);
}
}
}
for alias in names {
if let Some("__future__") = module.as_deref() {
@@ -1085,6 +1111,11 @@ where
flake8_errmsg::plugins::string_in_exception(self, exc);
}
}
if self.settings.enabled.contains(&CheckCode::UP024) {
if let Some(item) = exc {
pyupgrade::plugins::os_error_alias(self, item);
}
}
}
StmtKind::AugAssign { target, .. } => {
self.handle_node_load(target);
@@ -1155,7 +1186,9 @@ where
}
StmtKind::Try { handlers, .. } => {
if self.settings.enabled.contains(&CheckCode::F707) {
if let Some(check) = pyflakes::checks::default_except_not_last(handlers) {
if let Some(check) =
pyflakes::checks::default_except_not_last(handlers, self.locator)
{
self.add_check(check);
}
}
@@ -1167,6 +1200,9 @@ where
if self.settings.enabled.contains(&CheckCode::B013) {
flake8_bugbear::plugins::redundant_tuple_in_exception_handler(self, handlers);
}
if self.settings.enabled.contains(&CheckCode::UP024) {
pyupgrade::plugins::os_error_alias(self, handlers);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.enabled.contains(&CheckCode::E731) {
@@ -1399,7 +1435,7 @@ where
Binding {
kind: BindingKind::ClassDefinition,
used: None,
range: Range::from_located(stmt),
range: helpers::identifier_range(stmt, self.locator),
source: Some(self.current_stmt().clone()),
},
);
@@ -1448,7 +1484,6 @@ where
self.push_expr(expr);
let prev_in_f_string = self.in_f_string;
let prev_in_literal = self.in_literal;
let prev_in_type_definition = self.in_type_definition;
@@ -1523,9 +1558,10 @@ where
}
ExprContext::Store => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) =
pycodestyle::checks::ambiguous_variable_name(id, expr)
{
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
id,
Range::from_located(expr),
) {
self.add_check(check);
}
}
@@ -1570,6 +1606,10 @@ where
if self.settings.enabled.contains(&CheckCode::UP019) {
pyupgrade::plugins::typing_text_str_alias(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP026) {
pyupgrade::plugins::rewrite_mock_attribute(self, expr);
}
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
@@ -1586,6 +1626,15 @@ where
};
}
}
if self.settings.enabled.contains(&CheckCode::TID251) {
flake8_tidy_imports::checks::banned_attribute_access(
self,
&dealias_call_path(collect_call_paths(expr), &self.import_aliases),
expr,
&self.settings.flake8_tidy_imports.banned_api,
);
}
}
ExprKind::Call {
func,
@@ -1682,12 +1731,15 @@ where
if self.settings.enabled.contains(&CheckCode::UP022) {
pyupgrade::plugins::replace_stdout_stderr(self, expr, keywords);
}
if self.settings.enabled.contains(&CheckCode::UP024) {
pyupgrade::plugins::os_error_alias(self, expr);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
|| self.settings.enabled.contains(&CheckCode::T203)
{
flake8_print::plugins::print_call(self, expr, func, keywords);
flake8_print::plugins::print_call(self, func, keywords);
}
// flake8-bugbear
@@ -2126,10 +2178,9 @@ where
}
ExprKind::JoinedStr { values } => {
if self.settings.enabled.contains(&CheckCode::F541) {
if self.in_f_string.is_none()
&& !values
.iter()
.any(|value| matches!(value.node, ExprKind::FormattedValue { .. }))
if !values
.iter()
.any(|value| matches!(value.node, ExprKind::FormattedValue { .. }))
{
self.add_check(Check::new(
CheckKind::FStringMissingPlaceholders,
@@ -2137,7 +2188,6 @@ where
));
}
}
self.in_f_string = Some(Range::from_located(expr));
}
ExprKind::BinOp {
left,
@@ -2230,6 +2280,15 @@ where
}
}
}
ExprKind::BinOp {
op: Operator::Add, ..
} => {
if self.settings.enabled.contains(&CheckCode::ISC003) {
if let Some(check) = flake8_implicit_str_concat::checks::explicit(expr) {
self.add_check(check);
}
}
}
ExprKind::UnaryOp { op, operand } => {
let check_not_in = self.settings.enabled.contains(&CheckCode::E713);
let check_not_is = self.settings.enabled.contains(&CheckCode::E714);
@@ -2329,7 +2388,7 @@ where
}
ExprKind::Constant {
value: Constant::Str(value),
..
kind,
} => {
if self.in_type_definition && !self.in_literal {
self.deferred_string_type_definitions.push((
@@ -2347,6 +2406,9 @@ where
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::UP025) {
pyupgrade::plugins::rewrite_unicode_literal(self, expr, kind);
}
}
ExprKind::Lambda { args, body, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.
@@ -2618,6 +2680,9 @@ where
}
self.in_subscript = prev_in_subscript;
}
ExprKind::JoinedStr { .. } => {
visitor::walk_expr(self, expr);
}
_ => visitor::walk_expr(self, expr),
}
@@ -2635,7 +2700,6 @@ where
self.in_type_definition = prev_in_type_definition;
self.in_literal = prev_in_literal;
self.in_f_string = prev_in_f_string;
self.pop_expr();
}
@@ -2649,7 +2713,8 @@ where
if let Some(check) = pycodestyle::checks::do_not_use_bare_except(
type_.as_deref(),
body,
Range::from_located(excepthandler),
excepthandler,
self.locator,
) {
self.add_check(check);
}
@@ -2668,9 +2733,11 @@ where
match name {
Some(name) => {
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) =
pycodestyle::checks::ambiguous_variable_name(name, excepthandler)
{
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
name,
helpers::excepthandler_name_range(excepthandler, self.locator)
.expect("Failed to find `name` range"),
) {
self.add_check(check);
}
}
@@ -2736,11 +2803,18 @@ where
}
}
fn visit_arguments(&mut self, arguments: &'b Arguments) {
if self.settings.enabled.contains(&CheckCode::F831) {
self.checks
.extend(pyflakes::checks::duplicate_arguments(arguments));
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match &format_spec.node {
ExprKind::JoinedStr { values } => {
for value in values {
self.visit_expr(value);
}
}
_ => unreachable!("Unexpected expression for format_spec"),
}
}
fn visit_arguments(&mut self, arguments: &'b Arguments) {
if self.settings.enabled.contains(&CheckCode::B006) {
flake8_bugbear::plugins::mutable_argument_default(self, arguments);
}
@@ -2791,7 +2865,10 @@ where
);
if self.settings.enabled.contains(&CheckCode::E741) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(&arg.node.arg, arg) {
if let Some(check) = pycodestyle::checks::ambiguous_variable_name(
&arg.node.arg,
Range::from_located(arg),
) {
self.add_check(check);
}
}
@@ -3621,7 +3698,22 @@ impl<'a> Checker<'a> {
let defined_by = binding.source.as_ref().unwrap();
let defined_in = self.child_to_parent.get(defined_by);
if self.is_ignored(&CheckCode::F401, binding.range.location.row()) {
let child = defined_by.0;
let check_lineno = binding.range.location.row();
let parent_lineno = if matches!(child.node, StmtKind::ImportFrom { .. })
&& child.location.row() != check_lineno
{
Some(child.location.row())
} else {
None
};
if self.is_ignored(&CheckCode::F401, check_lineno)
|| parent_lineno.map_or(false, |parent_lineno| {
self.is_ignored(&CheckCode::F401, parent_lineno)
})
{
ignored
.entry((defined_by, defined_in))
.or_default()
@@ -3668,26 +3760,40 @@ impl<'a> Checker<'a> {
None
};
let multiple = unused_imports.len() > 1;
for (full_name, range) in unused_imports {
let mut check = Check::new(
CheckKind::UnusedImport(full_name.clone(), ignore_init),
CheckKind::UnusedImport(full_name.clone(), ignore_init, multiple),
*range,
);
if matches!(child.node, StmtKind::ImportFrom { .. })
&& child.location.row() != range.location.row()
{
check.parent(child.location);
}
if let Some(fix) = fix.as_ref() {
check.amend(fix.clone());
}
checks.push(check);
}
}
for (_, unused_imports) in ignored
for ((defined_by, ..), unused_imports) in ignored
.into_iter()
.sorted_by_key(|((defined_by, _), _)| defined_by.0.location)
{
let child = defined_by.0;
let multiple = unused_imports.len() > 1;
for (full_name, range) in unused_imports {
checks.push(Check::new(
CheckKind::UnusedImport(full_name.clone(), ignore_init),
let mut check = Check::new(
CheckKind::UnusedImport(full_name.clone(), ignore_init, multiple),
*range,
));
);
if matches!(child.node, StmtKind::ImportFrom { .. })
&& child.location.row() != range.location.row()
{
check.parent(child.location);
}
checks.push(check);
}
}
}
@@ -3787,10 +3893,10 @@ impl<'a> Checker<'a> {
let content = self
.locator
.slice_source_code_range(&Range::from_located(expr));
let indentation = self.locator.slice_source_code_range(&Range {
location: Location::new(expr.location.row(), 0),
end_location: Location::new(expr.location.row(), expr.location.column()),
});
let indentation = self.locator.slice_source_code_range(&Range::new(
Location::new(expr.location.row(), 0),
Location::new(expr.location.row(), expr.location.column()),
));
let body = pydocstyle::helpers::raw_contents(&content);
let docstring = Docstring {
kind: definition.kind,

View File

@@ -11,11 +11,13 @@ use crate::isort;
use crate::isort::track::ImportTracker;
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> {
@@ -23,7 +25,7 @@ fn check_import_blocks(
for block in tracker.into_iter() {
if !block.imports.is_empty() {
if let Some(check) =
isort::plugins::check_imports(&block, locator, settings, autofix, package)
isort::plugins::check_imports(&block, locator, settings, stylist, autofix, package)
{
checks.push(check);
}
@@ -32,11 +34,13 @@ fn check_import_blocks(
checks
}
#[allow(clippy::too_many_arguments)]
pub fn check_imports(
python_ast: &Suite,
locator: &SourceCodeLocator,
directives: &IsortDirectives,
settings: &Settings,
stylist: &SourceCodeStyleDetector,
autofix: flags::Autofix,
path: &Path,
package: Option<&Path>,
@@ -45,5 +49,5 @@ pub fn check_imports(
for stmt in python_ast {
tracker.visit_stmt(stmt);
}
check_import_blocks(tracker, locator, settings, autofix, package)
check_import_blocks(tracker, locator, settings, stylist, autofix, package)
}

View File

@@ -2,7 +2,7 @@
use crate::checks::{Check, CheckCode};
use crate::pycodestyle::checks::{line_too_long, no_newline_at_end_of_file};
use crate::pygrep_hooks::plugins::blanket_type_ignore;
use crate::pygrep_hooks::plugins::{blanket_noqa, blanket_type_ignore};
use crate::pyupgrade::checks::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
@@ -18,6 +18,7 @@ pub fn check_lines(
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 mut commented_lines_iter = commented_lines.iter().peekable();
for (index, line) in contents.lines().enumerate() {
@@ -45,6 +46,14 @@ pub fn check_lines(
}
}
}
if enforce_blanket_noqa {
if commented_lines.contains(&(index + 1)) {
if let Some(check) = blanket_noqa(index, line) {
checks.push(check);
}
}
}
}
if enforce_line_too_long {

View File

@@ -25,12 +25,6 @@ pub fn check_noqa(
let enforce_noqa = settings.enabled.contains(&CheckCode::RUF100);
checks.sort_by_key(|check| check.location);
let mut checks_iter = checks.iter().enumerate().peekable();
if let Some((_index, check)) = checks_iter.peek() {
assert!(check.location.row() >= 1);
}
let lines: Vec<&str> = contents.lines().collect();
for lineno in commented_lines {
// If we hit an exemption for the entire file, bail.
@@ -44,17 +38,18 @@ pub fn check_noqa(
.entry(lineno - 1)
.or_insert_with(|| (noqa::extract_noqa_directive(lines[lineno - 1]), vec![]));
}
}
// Remove any ignored checks.
while let Some((index, check)) =
checks_iter.next_if(|(_index, check)| check.location.row() <= *lineno)
{
// Grab the noqa (logical) line number for the current (physical) line.
// If there are newlines at the end of the file, they won't be represented in
// `noqa_line_for`, so fallback to the current line.
let check_lineno = check.location.row();
let noqa_lineno = noqa_line_for.get(&check_lineno).unwrap_or(&check_lineno);
if noqa_lineno == lineno {
// Remove any ignored checks.
for (index, check) in checks.iter().enumerate() {
if check.kind == CheckKind::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()) {
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(|| {
(noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![])
});
@@ -62,17 +57,41 @@ pub fn check_noqa(
(Directive::All(..), matches) => {
matches.push(check.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());
ignored.push(index);
continue;
}
}
(Directive::None, ..) => {}
}
}
}
// 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);
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());
ignored.push(index);
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(check.kind.code(), codes) {
matches.push(check.kind.code().as_ref());
ignored.push(index);
}
}
(Directive::None, ..) => {}
}
}
}
// Enforce that the noqa directive was actually used (RUF100).
@@ -83,10 +102,7 @@ pub fn check_noqa(
if matches.is_empty() {
let mut check = Check::new(
CheckKind::UnusedNOQA(None),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
);
if matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(check.kind.code())
@@ -150,10 +166,7 @@ pub fn check_noqa(
.map(|code| (*code).to_string())
.collect(),
})),
Range {
location: Location::new(row + 1, start),
end_location: Location::new(row + 1, end),
},
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
);
if matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(check.kind.code())

View File

@@ -7,7 +7,7 @@ use crate::lex::docstring_detection::StateMachine;
use crate::ruff::checks::Context;
use crate::settings::flags;
use crate::source_code_locator::SourceCodeLocator;
use crate::{eradicate, flake8_quotes, pycodestyle, ruff, Settings};
use crate::{eradicate, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff, Settings};
pub fn check_tokens(
locator: &SourceCodeLocator,
@@ -26,6 +26,8 @@ pub fn check_tokens(
|| 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 mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
@@ -99,5 +101,14 @@ pub fn check_tokens(
}
}
// ISC001, ISC002
if enforce_implicit_string_concatenation {
checks.extend(
flake8_implicit_str_concat::checks::implicit(tokens, locator)
.into_iter()
.filter(|check| settings.enabled.contains(check.kind.code())),
);
}
checks
}

View File

@@ -3,6 +3,7 @@ use std::fmt;
use itertools::Itertools;
use once_cell::sync::Lazy;
use rustc_hash::FxHashMap;
use rustpython_ast::Cmpop;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
@@ -91,7 +92,6 @@ pub enum CheckCode {
F821,
F822,
F823,
F831,
F841,
F842,
F901,
@@ -165,6 +165,7 @@ pub enum CheckCode {
// mccabe
C901,
// flake8-tidy-imports
TID251,
TID252,
// flake8-return
RET501,
@@ -175,6 +176,10 @@ pub enum CheckCode {
RET506,
RET507,
RET508,
// flake8-implicit-str-concat
ISC001,
ISC002,
ISC003,
// flake8-print
T201,
T203,
@@ -231,6 +236,9 @@ pub enum CheckCode {
UP021,
UP022,
UP023,
UP024,
UP025,
UP026,
// pydocstyle
D100,
D101,
@@ -336,6 +344,7 @@ pub enum CheckCode {
PGH001,
PGH002,
PGH003,
PGH004,
// pandas-vet
PD002,
PD003,
@@ -374,6 +383,7 @@ pub enum CheckCategory {
Flake8Comprehensions,
Flake8Debugger,
Flake8ErrMsg,
Flake8ImplicitStrConcat,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
@@ -417,6 +427,7 @@ impl CheckCategory {
CheckCategory::Flake8Comprehensions => "flake8-comprehensions",
CheckCategory::Flake8Debugger => "flake8-debugger",
CheckCategory::Flake8ErrMsg => "flake8-errmsg",
CheckCategory::Flake8ImplicitStrConcat => "flake8-implicit-str-concat",
CheckCategory::Flake8ImportConventions => "flake8-import-conventions",
CheckCategory::Flake8Print => "flake8-print",
CheckCategory::Flake8Quotes => "flake8-quotes",
@@ -450,19 +461,21 @@ impl CheckCategory {
CheckCategory::Flake8Bugbear => vec![CheckCodePrefix::B],
CheckCategory::Flake8Builtins => vec![CheckCodePrefix::A],
CheckCategory::Flake8Comprehensions => vec![CheckCodePrefix::C4],
CheckCategory::Flake8Datetimez => vec![CheckCodePrefix::DTZ],
CheckCategory::Flake8Debugger => vec![CheckCodePrefix::T10],
CheckCategory::Flake8ErrMsg => vec![CheckCodePrefix::EM],
CheckCategory::Flake8ImplicitStrConcat => vec![CheckCodePrefix::ISC],
CheckCategory::Flake8ImportConventions => vec![CheckCodePrefix::ICN],
CheckCategory::Flake8Print => vec![CheckCodePrefix::T20],
CheckCategory::Flake8Quotes => vec![CheckCodePrefix::Q],
CheckCategory::Flake8Return => vec![CheckCodePrefix::RET],
CheckCategory::Flake8Simplify => vec![CheckCodePrefix::SIM],
CheckCategory::Flake8TidyImports => vec![CheckCodePrefix::TID],
CheckCategory::Flake8UnusedArguments => vec![CheckCodePrefix::ARG],
CheckCategory::Flake8Datetimez => vec![CheckCodePrefix::DTZ],
CheckCategory::Isort => vec![CheckCodePrefix::I],
CheckCategory::McCabe => vec![CheckCodePrefix::C90],
CheckCategory::PandasVet => vec![CheckCodePrefix::PD],
CheckCategory::PEP8Naming => vec![CheckCodePrefix::N],
CheckCategory::PandasVet => vec![CheckCodePrefix::PD],
CheckCategory::Pycodestyle => vec![CheckCodePrefix::E, CheckCodePrefix::W],
CheckCategory::Pydocstyle => vec![CheckCodePrefix::D],
CheckCategory::Pyflakes => vec![CheckCodePrefix::F],
@@ -474,7 +487,6 @@ impl CheckCategory {
CheckCodePrefix::PLW,
],
CheckCategory::Pyupgrade => vec![CheckCodePrefix::UP],
CheckCategory::Flake8ImportConventions => vec![CheckCodePrefix::ICN],
CheckCategory::Ruff => vec![CheckCodePrefix::RUF],
}
}
@@ -524,6 +536,10 @@ impl CheckCategory {
"https://pypi.org/project/flake8-errmsg/0.4.0/",
&Platform::PyPI,
)),
CheckCategory::Flake8ImplicitStrConcat => Some((
"https://pypi.org/project/flake8-implicit-str-concat/0.3.0/",
&Platform::PyPI,
)),
CheckCategory::Flake8ImportConventions => None,
CheckCategory::Flake8Print => Some((
"https://pypi.org/project/flake8-print/5.0.0/",
@@ -604,11 +620,37 @@ pub enum LintSource {
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum RejectedCmpop {
pub enum EqCmpop {
Eq,
NotEq,
}
impl From<&Cmpop> for EqCmpop {
fn from(cmpop: &Cmpop) -> Self {
match cmpop {
Cmpop::Eq => EqCmpop::Eq,
Cmpop::NotEq => EqCmpop::NotEq,
_ => unreachable!("Expected Cmpop::Eq | Cmpop::NotEq"),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum IsCmpop {
Is,
IsNot,
}
impl From<&Cmpop> for IsCmpop {
fn from(cmpop: &Cmpop) -> Self {
match cmpop {
Cmpop::Is => IsCmpop::Is,
Cmpop::IsNot => IsCmpop::IsNot,
_ => unreachable!("Expected Cmpop::Is | Cmpop::IsNot"),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeferralKeyword {
Yield,
@@ -641,6 +683,21 @@ impl fmt::Display for Branch {
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum LiteralType {
Str,
Bytes,
}
impl fmt::Display for LiteralType {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
match self {
LiteralType::Str => fmt.write_str("str"),
LiteralType::Bytes => fmt.write_str("bytes"),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnusedCodes {
pub unknown: Vec<String>,
@@ -648,23 +705,29 @@ pub struct UnusedCodes {
pub unmatched: Vec<String>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum MockReference {
Import,
Attribute,
}
#[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum CheckKind {
// pycodestyle errors
AmbiguousClassName(String),
AmbiguousFunctionName(String),
AmbiguousVariableName(String),
DoNotAssignLambda,
DoNotAssignLambda(String),
DoNotUseBareExcept,
IOError(String),
LineTooLong(usize, usize),
ModuleImportNotAtTopOfFile,
MultipleImportsOnOneLine,
NoneComparison(RejectedCmpop),
NoneComparison(EqCmpop),
NotInTest,
NotIsTest,
SyntaxError(String),
TrueFalseComparison(bool, RejectedCmpop),
TrueFalseComparison(bool, EqCmpop),
TypeComparison,
// pycodestyle warnings
NoNewLineAtEndOfFile,
@@ -674,7 +737,6 @@ pub enum CheckKind {
BreakOutsideLoop,
ContinueOutsideLoop,
DefaultExceptNotLast,
DuplicateArgumentName,
ExpressionsInStarAssignment,
FStringMissingPlaceholders,
ForwardAnnotationSyntaxError(String),
@@ -685,7 +747,7 @@ pub enum CheckKind {
ImportStarUsage(String, Vec<String>),
ImportStarUsed(String),
InvalidPrintSyntax,
IsLiteral,
IsLiteral(IsCmpop),
LateFutureImport,
MultiValueRepeatedKeyLiteral,
MultiValueRepeatedKeyVariable(String),
@@ -711,7 +773,7 @@ pub enum CheckKind {
UndefinedLocal(String),
UnusedAnnotation(String),
UndefinedName(String),
UnusedImport(String, bool),
UnusedImport(String, bool, bool),
UnusedVariable(String),
YieldOutsideFunction(DeferralKeyword),
// pylint
@@ -782,6 +844,7 @@ pub enum CheckKind {
// flake8-debugger
Debugger(DebuggerUsingType),
// flake8-tidy-imports
BannedApi { name: String, message: String },
BannedRelativeImport(Strictness),
// flake8-return
UnnecessaryReturnNone,
@@ -792,6 +855,10 @@ pub enum CheckKind {
SuperfluousElseRaise(Branch),
SuperfluousElseContinue(Branch),
SuperfluousElseBreak(Branch),
// flake8-implicit-str-concat
SingleLineImplicitStringConcatenation,
MultiLineImplicitStringConcatenation,
ExplicitStringConcatenation,
// flake8-print
PrintFound,
PPrintFound,
@@ -840,14 +907,17 @@ pub enum CheckKind {
UnnecessaryEncodeUTF8,
ConvertTypedDictFunctionalToClass(String),
ConvertNamedTupleFunctionalToClass(String),
RedundantOpenModes,
RedundantOpenModes(Option<String>),
RemoveSixCompat,
DatetimeTimezoneUTC,
NativeLiterals,
NativeLiterals(LiteralType),
OpenAlias,
ReplaceUniversalNewlines,
ReplaceStdoutStderr,
RewriteCElementTree,
OSErrorAlias(Option<String>),
RewriteUnicodeLiteral,
RewriteMockImport(MockReference),
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -931,6 +1001,7 @@ pub enum CheckKind {
NoEval,
DeprecatedLogWarn,
BlanketTypeIgnore,
BlanketNOQA,
// flake8-unused-arguments
UnusedFunctionArgument(String),
UnusedMethodArgument(String),
@@ -980,10 +1051,14 @@ impl CheckCode {
pub fn lint_source(&self) -> &'static LintSource {
match self {
CheckCode::RUF100 => &LintSource::NoQA,
CheckCode::E501 | CheckCode::W292 | CheckCode::UP009 | CheckCode::PGH003 => {
&LintSource::Lines
}
CheckCode::E501
| CheckCode::W292
| CheckCode::UP009
| CheckCode::PGH003
| CheckCode::PGH004 => &LintSource::Lines,
CheckCode::ERA001
| CheckCode::ISC001
| CheckCode::ISC002
| CheckCode::Q000
| CheckCode::Q001
| CheckCode::Q002
@@ -1005,13 +1080,13 @@ impl CheckCode {
CheckCode::E401 => CheckKind::MultipleImportsOnOneLine,
CheckCode::E402 => CheckKind::ModuleImportNotAtTopOfFile,
CheckCode::E501 => CheckKind::LineTooLong(89, 88),
CheckCode::E711 => CheckKind::NoneComparison(RejectedCmpop::Eq),
CheckCode::E712 => CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq),
CheckCode::E711 => CheckKind::NoneComparison(EqCmpop::Eq),
CheckCode::E712 => CheckKind::TrueFalseComparison(true, EqCmpop::Eq),
CheckCode::E713 => CheckKind::NotInTest,
CheckCode::E714 => CheckKind::NotIsTest,
CheckCode::E721 => CheckKind::TypeComparison,
CheckCode::E722 => CheckKind::DoNotUseBareExcept,
CheckCode::E731 => CheckKind::DoNotAssignLambda,
CheckCode::E731 => CheckKind::DoNotAssignLambda("...".to_string()),
CheckCode::E741 => CheckKind::AmbiguousVariableName("...".to_string()),
CheckCode::E742 => CheckKind::AmbiguousClassName("...".to_string()),
CheckCode::E743 => CheckKind::AmbiguousFunctionName("...".to_string()),
@@ -1021,7 +1096,7 @@ impl CheckCode {
CheckCode::W292 => CheckKind::NoNewLineAtEndOfFile,
CheckCode::W605 => CheckKind::InvalidEscapeSequence('c'),
// pyflakes
CheckCode::F401 => CheckKind::UnusedImport("...".to_string(), false),
CheckCode::F401 => CheckKind::UnusedImport("...".to_string(), false, false),
CheckCode::F402 => CheckKind::ImportShadowedByLoopVar("...".to_string(), 1),
CheckCode::F403 => CheckKind::ImportStarUsed("...".to_string()),
CheckCode::F404 => CheckKind::LateFutureImport,
@@ -1054,7 +1129,7 @@ impl CheckCode {
CheckCode::F621 => CheckKind::ExpressionsInStarAssignment,
CheckCode::F622 => CheckKind::TwoStarredExpressions,
CheckCode::F631 => CheckKind::AssertTuple,
CheckCode::F632 => CheckKind::IsLiteral,
CheckCode::F632 => CheckKind::IsLiteral(IsCmpop::Is),
CheckCode::F633 => CheckKind::InvalidPrintSyntax,
CheckCode::F634 => CheckKind::IfTuple,
CheckCode::F701 => CheckKind::BreakOutsideLoop,
@@ -1067,7 +1142,6 @@ impl CheckCode {
CheckCode::F821 => CheckKind::UndefinedName("...".to_string()),
CheckCode::F822 => CheckKind::UndefinedExport("...".to_string()),
CheckCode::F823 => CheckKind::UndefinedLocal("...".to_string()),
CheckCode::F831 => CheckKind::DuplicateArgumentName,
CheckCode::F841 => CheckKind::UnusedVariable("...".to_string()),
CheckCode::F842 => CheckKind::UnusedAnnotation("...".to_string()),
CheckCode::F901 => CheckKind::RaiseNotImplemented,
@@ -1158,6 +1232,10 @@ impl CheckCode {
// flake8-debugger
CheckCode::T100 => CheckKind::Debugger(DebuggerUsingType::Import("...".to_string())),
// flake8-tidy-imports
CheckCode::TID251 => CheckKind::BannedApi {
name: "...".to_string(),
message: "...".to_string(),
},
CheckCode::TID252 => CheckKind::BannedRelativeImport(Strictness::All),
// flake8-return
CheckCode::RET501 => CheckKind::UnnecessaryReturnNone,
@@ -1168,6 +1246,10 @@ impl CheckCode {
CheckCode::RET506 => CheckKind::SuperfluousElseRaise(Branch::Else),
CheckCode::RET507 => CheckKind::SuperfluousElseContinue(Branch::Else),
CheckCode::RET508 => CheckKind::SuperfluousElseBreak(Branch::Else),
// flake8-implicit-str-concat
CheckCode::ISC001 => CheckKind::SingleLineImplicitStringConcatenation,
CheckCode::ISC002 => CheckKind::MultiLineImplicitStringConcatenation,
CheckCode::ISC003 => CheckKind::ExplicitStringConcatenation,
// flake8-print
CheckCode::T201 => CheckKind::PrintFound,
CheckCode::T203 => CheckKind::PPrintFound,
@@ -1220,15 +1302,18 @@ impl CheckCode {
CheckCode::UP012 => CheckKind::UnnecessaryEncodeUTF8,
CheckCode::UP013 => CheckKind::ConvertTypedDictFunctionalToClass("...".to_string()),
CheckCode::UP014 => CheckKind::ConvertNamedTupleFunctionalToClass("...".to_string()),
CheckCode::UP015 => CheckKind::RedundantOpenModes,
CheckCode::UP015 => CheckKind::RedundantOpenModes(None),
CheckCode::UP016 => CheckKind::RemoveSixCompat,
CheckCode::UP017 => CheckKind::DatetimeTimezoneUTC,
CheckCode::UP018 => CheckKind::NativeLiterals,
CheckCode::UP018 => CheckKind::NativeLiterals(LiteralType::Str),
CheckCode::UP019 => CheckKind::TypingTextStrAlias,
CheckCode::UP020 => CheckKind::OpenAlias,
CheckCode::UP021 => CheckKind::ReplaceUniversalNewlines,
CheckCode::UP022 => CheckKind::ReplaceStdoutStderr,
CheckCode::UP023 => CheckKind::RewriteCElementTree,
CheckCode::UP024 => CheckKind::OSErrorAlias(None),
CheckCode::UP025 => CheckKind::RewriteUnicodeLiteral,
CheckCode::UP026 => CheckKind::RewriteMockImport(MockReference::Import),
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1327,6 +1412,7 @@ impl CheckCode {
CheckCode::PGH001 => CheckKind::NoEval,
CheckCode::PGH002 => CheckKind::DeprecatedLogWarn,
CheckCode::PGH003 => CheckKind::BlanketTypeIgnore,
CheckCode::PGH004 => CheckKind::BlanketNOQA,
// flake8-unused-arguments
CheckCode::ARG001 => CheckKind::UnusedFunctionArgument("...".to_string()),
CheckCode::ARG002 => CheckKind::UnusedMethodArgument("...".to_string()),
@@ -1554,7 +1640,6 @@ impl CheckCode {
CheckCode::F821 => CheckCategory::Pyflakes,
CheckCode::F822 => CheckCategory::Pyflakes,
CheckCode::F823 => CheckCategory::Pyflakes,
CheckCode::F831 => CheckCategory::Pyflakes,
CheckCode::F841 => CheckCategory::Pyflakes,
CheckCode::F842 => CheckCategory::Pyflakes,
CheckCode::F901 => CheckCategory::Pyflakes,
@@ -1562,8 +1647,10 @@ impl CheckCode {
CheckCode::FBT002 => CheckCategory::Flake8BooleanTrap,
CheckCode::FBT003 => CheckCategory::Flake8BooleanTrap,
CheckCode::I001 => CheckCategory::Isort,
CheckCode::TID252 => CheckCategory::Flake8TidyImports,
CheckCode::ICN001 => CheckCategory::Flake8ImportConventions,
CheckCode::ISC001 => CheckCategory::Flake8ImplicitStrConcat,
CheckCode::ISC002 => CheckCategory::Flake8ImplicitStrConcat,
CheckCode::ISC003 => CheckCategory::Flake8ImplicitStrConcat,
CheckCode::N801 => CheckCategory::PEP8Naming,
CheckCode::N802 => CheckCategory::PEP8Naming,
CheckCode::N803 => CheckCategory::PEP8Naming,
@@ -1594,6 +1681,7 @@ impl CheckCode {
CheckCode::PGH001 => CheckCategory::PygrepHooks,
CheckCode::PGH002 => CheckCategory::PygrepHooks,
CheckCode::PGH003 => CheckCategory::PygrepHooks,
CheckCode::PGH004 => CheckCategory::PygrepHooks,
CheckCode::PLC0414 => CheckCategory::Pylint,
CheckCode::PLC2201 => CheckCategory::Pylint,
CheckCode::PLC3002 => CheckCategory::Pylint,
@@ -1633,6 +1721,8 @@ impl CheckCode {
CheckCode::T100 => CheckCategory::Flake8Debugger,
CheckCode::T201 => CheckCategory::Flake8Print,
CheckCode::T203 => CheckCategory::Flake8Print,
CheckCode::TID251 => CheckCategory::Flake8TidyImports,
CheckCode::TID252 => CheckCategory::Flake8TidyImports,
CheckCode::UP001 => CheckCategory::Pyupgrade,
CheckCode::UP003 => CheckCategory::Pyupgrade,
CheckCode::UP004 => CheckCategory::Pyupgrade,
@@ -1655,6 +1745,9 @@ impl CheckCode {
CheckCode::UP021 => CheckCategory::Pyupgrade,
CheckCode::UP022 => CheckCategory::Pyupgrade,
CheckCode::UP023 => CheckCategory::Pyupgrade,
CheckCode::UP024 => CheckCategory::Pyupgrade,
CheckCode::UP025 => CheckCategory::Pyupgrade,
CheckCode::UP026 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1683,9 +1776,8 @@ impl CheckKind {
CheckKind::BreakOutsideLoop => &CheckCode::F701,
CheckKind::ContinueOutsideLoop => &CheckCode::F702,
CheckKind::DefaultExceptNotLast => &CheckCode::F707,
CheckKind::DoNotAssignLambda => &CheckCode::E731,
CheckKind::DoNotAssignLambda(..) => &CheckCode::E731,
CheckKind::DoNotUseBareExcept => &CheckCode::E722,
CheckKind::DuplicateArgumentName => &CheckCode::F831,
CheckKind::FStringMissingPlaceholders => &CheckCode::F541,
CheckKind::ForwardAnnotationSyntaxError(..) => &CheckCode::F722,
CheckKind::FutureFeatureNotDefined(..) => &CheckCode::F407,
@@ -1696,7 +1788,7 @@ impl CheckKind {
CheckKind::ImportStarUsage(..) => &CheckCode::F405,
CheckKind::ImportStarUsed(..) => &CheckCode::F403,
CheckKind::InvalidPrintSyntax => &CheckCode::F633,
CheckKind::IsLiteral => &CheckCode::F632,
CheckKind::IsLiteral(..) => &CheckCode::F632,
CheckKind::LateFutureImport => &CheckCode::F404,
CheckKind::LineTooLong(..) => &CheckCode::E501,
CheckKind::MultipleImportsOnOneLine => &CheckCode::E401,
@@ -1806,6 +1898,7 @@ impl CheckKind {
// flake8-debugger
CheckKind::Debugger(..) => &CheckCode::T100,
// flake8-tidy-imports
CheckKind::BannedApi { .. } => &CheckCode::TID251,
CheckKind::BannedRelativeImport(..) => &CheckCode::TID252,
// flake8-return
CheckKind::UnnecessaryReturnNone => &CheckCode::RET501,
@@ -1816,6 +1909,10 @@ impl CheckKind {
CheckKind::SuperfluousElseRaise(..) => &CheckCode::RET506,
CheckKind::SuperfluousElseContinue(..) => &CheckCode::RET507,
CheckKind::SuperfluousElseBreak(..) => &CheckCode::RET508,
// flake8-implicit-str-concat
CheckKind::SingleLineImplicitStringConcatenation => &CheckCode::ISC001,
CheckKind::MultiLineImplicitStringConcatenation => &CheckCode::ISC002,
CheckKind::ExplicitStringConcatenation => &CheckCode::ISC003,
// flake8-print
CheckKind::PrintFound => &CheckCode::T201,
CheckKind::PPrintFound => &CheckCode::T203,
@@ -1863,15 +1960,18 @@ impl CheckKind {
CheckKind::UnnecessaryEncodeUTF8 => &CheckCode::UP012,
CheckKind::ConvertTypedDictFunctionalToClass(..) => &CheckCode::UP013,
CheckKind::ConvertNamedTupleFunctionalToClass(..) => &CheckCode::UP014,
CheckKind::RedundantOpenModes => &CheckCode::UP015,
CheckKind::RedundantOpenModes(..) => &CheckCode::UP015,
CheckKind::RemoveSixCompat => &CheckCode::UP016,
CheckKind::DatetimeTimezoneUTC => &CheckCode::UP017,
CheckKind::NativeLiterals => &CheckCode::UP018,
CheckKind::NativeLiterals(..) => &CheckCode::UP018,
CheckKind::TypingTextStrAlias => &CheckCode::UP019,
CheckKind::OpenAlias => &CheckCode::UP020,
CheckKind::ReplaceUniversalNewlines => &CheckCode::UP021,
CheckKind::ReplaceStdoutStderr => &CheckCode::UP022,
CheckKind::RewriteCElementTree => &CheckCode::UP023,
CheckKind::OSErrorAlias(..) => &CheckCode::UP024,
CheckKind::RewriteUnicodeLiteral => &CheckCode::UP025,
CheckKind::RewriteMockImport(..) => &CheckCode::UP026,
// pydocstyle
CheckKind::BlankLineAfterLastSection(..) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(..) => &CheckCode::D410,
@@ -1955,6 +2055,7 @@ impl CheckKind {
CheckKind::NoEval => &CheckCode::PGH001,
CheckKind::DeprecatedLogWarn => &CheckCode::PGH002,
CheckKind::BlanketTypeIgnore => &CheckCode::PGH003,
CheckKind::BlanketNOQA => &CheckCode::PGH004,
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(..) => &CheckCode::ARG001,
CheckKind::UnusedMethodArgument(..) => &CheckCode::ARG002,
@@ -2020,13 +2121,10 @@ impl CheckKind {
CheckKind::DefaultExceptNotLast => {
"An `except` block as not the last exception handler".to_string()
}
CheckKind::DoNotAssignLambda => {
"Do not assign a lambda expression, use a def".to_string()
CheckKind::DoNotAssignLambda(..) => {
"Do not assign a `lambda` expression, use a `def`".to_string()
}
CheckKind::DoNotUseBareExcept => "Do not use bare `except`".to_string(),
CheckKind::DuplicateArgumentName => {
"Duplicate argument name in function definition".to_string()
}
CheckKind::ForwardAnnotationSyntaxError(body) => {
format!("Syntax error in forward annotation: `{body}`")
}
@@ -2057,7 +2155,10 @@ impl CheckKind {
.join(", ");
format!("`{name}` may be undefined, or defined from star imports: {sources}")
}
CheckKind::IsLiteral => "Use `==` and `!=` to compare constant literals".to_string(),
CheckKind::IsLiteral(cmpop) => match cmpop {
IsCmpop::Is => "Use `==` to compare constant literals".to_string(),
IsCmpop::IsNot => "Use `!=` to compare constant literals".to_string(),
},
CheckKind::LateFutureImport => {
"`from __future__` imports must occur at the beginning of the file".to_string()
}
@@ -2075,10 +2176,8 @@ impl CheckKind {
format!("Dictionary key `{name}` repeated")
}
CheckKind::NoneComparison(op) => match op {
RejectedCmpop::Eq => "Comparison to `None` should be `cond is None`".to_string(),
RejectedCmpop::NotEq => {
"Comparison to `None` should be `cond is not None`".to_string()
}
EqCmpop::Eq => "Comparison to `None` should be `cond is None`".to_string(),
EqCmpop::NotEq => "Comparison to `None` should be `cond is not None`".to_string(),
},
CheckKind::NotInTest => "Test for membership should be `not in`".to_string(),
CheckKind::NotIsTest => "Test for object identity should be `is not`".to_string(),
@@ -2142,16 +2241,16 @@ impl CheckKind {
CheckKind::ExpressionsInStarAssignment => {
"Too many expressions in star-unpacking assignment".to_string()
}
CheckKind::TrueFalseComparison(true, RejectedCmpop::Eq) => {
CheckKind::TrueFalseComparison(true, EqCmpop::Eq) => {
"Comparison to `True` should be `cond is True`".to_string()
}
CheckKind::TrueFalseComparison(true, RejectedCmpop::NotEq) => {
CheckKind::TrueFalseComparison(true, EqCmpop::NotEq) => {
"Comparison to `True` should be `cond is not True`".to_string()
}
CheckKind::TrueFalseComparison(false, RejectedCmpop::Eq) => {
CheckKind::TrueFalseComparison(false, EqCmpop::Eq) => {
"Comparison to `False` should be `cond is False`".to_string()
}
CheckKind::TrueFalseComparison(false, RejectedCmpop::NotEq) => {
CheckKind::TrueFalseComparison(false, EqCmpop::NotEq) => {
"Comparison to `False` should be `cond is not False`".to_string()
}
CheckKind::TwoStarredExpressions => "Two starred expressions in assignment".to_string(),
@@ -2168,7 +2267,7 @@ impl CheckKind {
CheckKind::UnusedAnnotation(name) => {
format!("Local variable `{name}` is annotated but never used")
}
CheckKind::UnusedImport(name, ignore_init) => {
CheckKind::UnusedImport(name, ignore_init, ..) => {
if *ignore_init {
format!(
"`{name}` imported but unused; consider adding to `__all__` or using a \
@@ -2432,6 +2531,7 @@ impl CheckKind {
DebuggerUsingType::Import(name) => format!("Import for `{name}` found"),
},
// flake8-tidy-imports
CheckKind::BannedApi { name, message } => format!("`{name}` is banned: {message}"),
CheckKind::BannedRelativeImport(strictness) => match strictness {
Strictness::Parents => {
"Relative imports from parent modules are banned".to_string()
@@ -2463,6 +2563,16 @@ impl CheckKind {
CheckKind::SuperfluousElseBreak(branch) => {
format!("Unnecessary `{branch}` after `break` statement")
}
// flake8-implicit-str-concat
CheckKind::SingleLineImplicitStringConcatenation => {
"Implicitly concatenated string literals on one line".to_string()
}
CheckKind::MultiLineImplicitStringConcatenation => {
"Implicitly concatenated string literals over continuation line".to_string()
}
CheckKind::ExplicitStringConcatenation => {
"Explicitly concatenated string should be implicitly concatenated".to_string()
}
// flake8-print
CheckKind::PrintFound => "`print` found".to_string(),
CheckKind::PPrintFound => "`pprint` found".to_string(),
@@ -2568,7 +2678,7 @@ impl CheckKind {
format!("`{alias}` is deprecated, use `{target}`")
}
CheckKind::UselessObjectInheritance(name) => {
format!("Class `{name}` inherits from object")
format!("Class `{name}` inherits from `object`")
}
CheckKind::UsePEP585Annotation(name) => {
format!(
@@ -2594,14 +2704,24 @@ impl CheckKind {
"Unnecessary parameters to `functools.lru_cache`".to_string()
}
CheckKind::UnnecessaryEncodeUTF8 => "Unnecessary call to `encode` as UTF-8".to_string(),
CheckKind::RedundantOpenModes => "Unnecessary open mode parameters".to_string(),
CheckKind::RedundantOpenModes(replacement) => match replacement {
None => "Unnecessary open mode parameters".to_string(),
Some(replacement) => {
format!("Unnecessary open mode parameters, use \"{replacement}\"")
}
},
CheckKind::RemoveSixCompat => "Unnecessary `six` compatibility usage".to_string(),
CheckKind::DatetimeTimezoneUTC => "Use `datetime.UTC` alias".to_string(),
CheckKind::NativeLiterals => "Unnecessary call to `str` and `bytes`".to_string(),
CheckKind::NativeLiterals(literal_type) => {
format!("Unnecessary call to `{literal_type}`")
}
CheckKind::OpenAlias => "Use builtin `open`".to_string(),
CheckKind::ConvertTypedDictFunctionalToClass(name) => {
format!("Convert `{name}` from `TypedDict` functional to class syntax")
}
CheckKind::ConvertNamedTupleFunctionalToClass(name) => {
format!("Convert `{name}` from `NamedTuple` functional to class syntax")
}
CheckKind::ReplaceUniversalNewlines => {
"`universal_newlines` is deprecated, use `text`".to_string()
}
@@ -2611,8 +2731,10 @@ impl CheckKind {
CheckKind::RewriteCElementTree => {
"`cElementTree` is deprecated, use `ElementTree`".to_string()
}
CheckKind::ConvertNamedTupleFunctionalToClass(name) => {
format!("Convert `{name}` from `NamedTuple` functional to class syntax")
CheckKind::OSErrorAlias(..) => "Replace aliased errors with `OSError`".to_string(),
CheckKind::RewriteUnicodeLiteral => "Remove unicode literals from strings".to_string(),
CheckKind::RewriteMockImport(..) => {
"`mock` is deprecated, use `unittest.mock`".to_string()
}
// pydocstyle
CheckKind::FitsOnOneLine => "One-line docstring should fit on one line".to_string(),
@@ -2790,14 +2912,13 @@ impl CheckKind {
CheckKind::HardcodedBindAllInterfaces => {
"Possible binding to all interfaces".to_string()
}
CheckKind::HardcodedPasswordString(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
}
CheckKind::HardcodedPasswordFuncArg(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
}
CheckKind::HardcodedPasswordDefault(string) => {
format!("Possible hardcoded password: `\"{string}\"`")
CheckKind::HardcodedPasswordString(string)
| CheckKind::HardcodedPasswordFuncArg(string)
| CheckKind::HardcodedPasswordDefault(string) => {
format!(
"Possible hardcoded password: `\"{}\"`",
string.escape_debug()
)
}
// flake8-blind-except
CheckKind::BlindExcept(name) => format!("Do not catch blind exception: `{name}`"),
@@ -2816,13 +2937,14 @@ impl CheckKind {
"Boolean positional value in function call".to_string()
}
// pygrep-hooks
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
CheckKind::DeprecatedLogWarn => {
"`warn` is deprecated in favor of `warning`".to_string()
}
CheckKind::BlanketNOQA => "Use specific error codes when using `noqa`".to_string(),
CheckKind::BlanketTypeIgnore => {
"Use specific error codes when ignoring type issues".to_string()
}
CheckKind::DeprecatedLogWarn => {
"`warn` is deprecated in favor of `warning`".to_string()
}
CheckKind::NoEval => "No builtin `eval()` allowed".to_string(),
// flake8-unused-arguments
CheckKind::UnusedFunctionArgument(name) => {
format!("Unused function argument: `{name}`")
@@ -3027,6 +3149,7 @@ impl CheckKind {
matches!(
self,
CheckKind::AmbiguousUnicodeCharacterString(..)
| CheckKind::AmbiguousUnicodeCharacterComment(..)
| CheckKind::AmbiguousUnicodeCharacterDocstring(..)
| CheckKind::BlankLineAfterLastSection(..)
| CheckKind::BlankLineAfterSection(..)
@@ -3040,7 +3163,7 @@ impl CheckKind {
| CheckKind::DatetimeTimezoneUTC
| CheckKind::DeprecatedUnittestAlias(..)
| CheckKind::DoNotAssertFalse
| CheckKind::DoNotAssignLambda
| CheckKind::DoNotAssignLambda(..)
| CheckKind::DuplicateHandlerException(..)
| CheckKind::EndsInPeriod
| CheckKind::EndsInPunctuation
@@ -3048,16 +3171,12 @@ impl CheckKind {
| CheckKind::ImplicitReturn
| CheckKind::ImplicitReturnValue
| CheckKind::InvalidEscapeSequence(..)
| CheckKind::IsLiteral
| CheckKind::IsLiteral(..)
| CheckKind::KeyInDict(..)
| CheckKind::MisplacedComparisonConstant(..)
| CheckKind::MissingReturnTypeSpecialMethod(..)
| CheckKind::NativeLiterals
| CheckKind::OpenAlias
| CheckKind::NativeLiterals(..)
| CheckKind::NewLineAfterLastParagraph
| CheckKind::ReplaceUniversalNewlines
| CheckKind::ReplaceStdoutStderr
| CheckKind::RewriteCElementTree
| CheckKind::NewLineAfterSectionName(..)
| CheckKind::NoBlankLineAfterFunction(..)
| CheckKind::NoBlankLineBeforeClass(..)
@@ -3070,16 +3189,23 @@ impl CheckKind {
| CheckKind::NoneComparison(..)
| CheckKind::NotInTest
| CheckKind::NotIsTest
| CheckKind::OSErrorAlias(..)
| CheckKind::OneBlankLineAfterClass(..)
| CheckKind::OneBlankLineBeforeClass(..)
| CheckKind::OpenAlias
| CheckKind::PEP3120UnnecessaryCodingComment
| CheckKind::PPrintFound
| CheckKind::PercentFormatExtraNamedArguments(..)
| CheckKind::PrintFound
| CheckKind::RaiseNotImplemented
| CheckKind::RedundantOpenModes
| CheckKind::RedundantOpenModes(..)
| CheckKind::RedundantTupleInExceptionHandler(..)
| CheckKind::RemoveSixCompat
| CheckKind::ReplaceStdoutStderr
| CheckKind::ReplaceUniversalNewlines
| CheckKind::RewriteCElementTree
| CheckKind::RewriteMockImport(..)
| CheckKind::RewriteUnicodeLiteral
| CheckKind::SectionNameEndsInColon(..)
| CheckKind::SectionNotOverIndented(..)
| CheckKind::SectionUnderlineAfterName(..)
@@ -3109,7 +3235,7 @@ impl CheckKind {
| CheckKind::UnnecessaryLiteralWithinTupleCall(..)
| CheckKind::UnnecessaryReturnNone
| CheckKind::UnsortedImports
| CheckKind::UnusedImport(_, false)
| CheckKind::UnusedImport(_, false, _)
| CheckKind::UnusedLoopControlVariable(..)
| CheckKind::UnusedNOQA(..)
| CheckKind::UsePEP585Annotation(..)
@@ -3120,6 +3246,243 @@ impl CheckKind {
| CheckKind::UselessObjectInheritance(..)
)
}
/// The message used to describe the fix action for a given `CheckKind`.
pub fn commit(&self) -> Option<String> {
match self {
CheckKind::AmbiguousUnicodeCharacterString(confusable, representant)
| CheckKind::AmbiguousUnicodeCharacterDocstring(confusable, representant)
| CheckKind::AmbiguousUnicodeCharacterComment(confusable, representant) => {
Some(format!("Replace '{confusable}' with '{representant}'"))
}
CheckKind::BlankLineAfterLastSection(name) => {
Some(format!("Add blank line after \"{name}\""))
}
CheckKind::BlankLineAfterSection(name) => {
Some(format!("Add blank line after \"{name}\""))
}
CheckKind::BlankLineAfterSummary => Some("Insert single blank line".to_string()),
CheckKind::BlankLineBeforeSection(name) => {
Some(format!("Add blank line before \"{name}\""))
}
CheckKind::CapitalizeSectionName(name) => Some(format!("Capitalize \"{name}\"")),
CheckKind::CommentedOutCode => Some("Remove commented-out code".to_string()),
CheckKind::ConvertTypedDictFunctionalToClass(name)
| CheckKind::ConvertNamedTupleFunctionalToClass(name) => {
Some(format!("Convert `{name}` to class syntax"))
}
CheckKind::DashedUnderlineAfterSection(name) => {
Some(format!("Add dashed line under \"{name}\""))
}
CheckKind::DatetimeTimezoneUTC => Some("Convert to `datetime.UTC` alias".to_string()),
CheckKind::DeprecatedUnittestAlias(alias, target) => {
Some(format!("Replace `{target}` with `{alias}`"))
}
CheckKind::DoNotAssertFalse => Some("Replace `assert False`".to_string()),
CheckKind::DoNotAssignLambda(name) => Some(format!("Rewrite `{name}` as a `def`")),
CheckKind::DuplicateHandlerException(..) => Some("De-duplicate exceptions".to_string()),
CheckKind::EndsInPeriod => Some("Add period".to_string()),
CheckKind::EndsInPunctuation => Some("Add closing punctuation".to_string()),
CheckKind::GetAttrWithConstant => {
Some("Replace `getattr` with attribute access".to_string())
}
CheckKind::ImplicitReturnValue => Some("Add explicit `None` return value".to_string()),
CheckKind::ImplicitReturn => Some("Add explicit `return` statement".to_string()),
CheckKind::InvalidEscapeSequence(..) => {
Some("Add backslash to escape sequence".to_string())
}
CheckKind::IsLiteral(cmpop) => Some(match cmpop {
IsCmpop::Is => "Replace `is` with `==`".to_string(),
IsCmpop::IsNot => "Replace `is not` with `!=`".to_string(),
}),
CheckKind::KeyInDict(key, dict) => Some(format!("Convert to `{key} in {dict}`")),
CheckKind::MisplacedComparisonConstant(comparison) => {
Some(format!("Replace with {comparison}"))
}
CheckKind::MissingReturnTypeSpecialMethod(..) => {
Some("Add `None` return type".to_string())
}
CheckKind::NativeLiterals(literal_type) => {
Some(format!("Replace with `{literal_type}`"))
}
CheckKind::OpenAlias => Some("Replace with builtin `open`".to_string()),
CheckKind::NewLineAfterLastParagraph => {
Some("Move closing quotes to new line".to_string())
}
CheckKind::ReplaceUniversalNewlines => {
Some("Replace with `text` keyword argument".to_string())
}
CheckKind::ReplaceStdoutStderr => {
Some("Replace with `capture_output` keyword argument".to_string())
}
CheckKind::RewriteCElementTree => Some("Replace with `ElementTree`".to_string()),
CheckKind::RewriteUnicodeLiteral => Some("Remove unicode prefix".to_string()),
CheckKind::RewriteMockImport(reference_type) => Some(match reference_type {
MockReference::Import => "Import from `unittest.mock` instead".to_string(),
MockReference::Attribute => "Replace `mock.mock` with `mock`".to_string(),
}),
CheckKind::NewLineAfterSectionName(name) => {
Some(format!("Add newline after \"{name}\""))
}
CheckKind::NoBlankLineBeforeFunction(..) => {
Some("Remove blank line(s) before function docstring".to_string())
}
CheckKind::NoBlankLineAfterFunction(..) => {
Some("Remove blank line(s) after function docstring".to_string())
}
CheckKind::NoBlankLineBeforeClass(..) => {
Some("Remove blank line(s) before class docstring".to_string())
}
CheckKind::OneBlankLineBeforeClass(..) => {
Some("Insert 1 blank line before class docstring".to_string())
}
CheckKind::OneBlankLineAfterClass(..) => {
Some("Insert 1 blank line after class docstring".to_string())
}
CheckKind::OSErrorAlias(name) => Some(match name {
None => "Replace with builtin `OSError`".to_string(),
Some(name) => format!("Replace `{name}` with builtin `OSError`"),
}),
CheckKind::NoBlankLinesBetweenHeaderAndContent(..) => {
Some("Remove blank line(s)".to_string())
}
CheckKind::NoNewLineAtEndOfFile => Some("Add trailing newline".to_string()),
CheckKind::NoOverIndentation => Some("Remove over-indentation".to_string()),
CheckKind::NoSurroundingWhitespace => Some("Trim surrounding whitespace".to_string()),
CheckKind::NoUnderIndentation => Some("Increase indentation".to_string()),
CheckKind::NoneComparison(op) => Some(match op {
EqCmpop::Eq => "Replace with `cond is None`".to_string(),
EqCmpop::NotEq => "Replace with `cond is not None`".to_string(),
}),
CheckKind::NotInTest => Some("Convert to `not in`".to_string()),
CheckKind::NotIsTest => Some("Convert to `is not`".to_string()),
CheckKind::PEP3120UnnecessaryCodingComment => {
Some("Remove unnecessary coding comment".to_string())
}
CheckKind::PPrintFound => Some("Remove `pprint`".to_string()),
CheckKind::PercentFormatExtraNamedArguments(missing)
| CheckKind::StringDotFormatExtraNamedArguments(missing) => {
let message = missing.join(", ");
Some(format!("Remove extra named arguments: {message}"))
}
CheckKind::PrintFound => Some("Remove `print`".to_string()),
CheckKind::RaiseNotImplemented => Some("Use `raise NotImplementedError`".to_string()),
CheckKind::RedundantOpenModes(replacement) => Some(match replacement {
None => "Remove open mode parameters".to_string(),
Some(replacement) => {
format!("Replace with \"{replacement}\"")
}
}),
CheckKind::RedundantTupleInExceptionHandler(name) => {
Some(format!("Replace with `except {name}`"))
}
CheckKind::RemoveSixCompat => Some("Remove `six` usage".to_string()),
CheckKind::SectionNameEndsInColon(name) => Some(format!("Add colon to \"{name}\"")),
CheckKind::SectionNotOverIndented(name) => {
Some(format!("Remove over-indentation from \"{name}\""))
}
CheckKind::SectionUnderlineAfterName(name) => {
Some(format!("Add underline to \"{name}\""))
}
CheckKind::SectionUnderlineMatchesSectionLength(name) => {
Some(format!("Adjust underline length to match \"{name}\""))
}
CheckKind::SectionUnderlineNotOverIndented(name) => {
Some(format!("Remove over-indentation from \"{name}\" underline"))
}
CheckKind::SetAttrWithConstant => Some("Replace `setattr` with assignment".to_string()),
CheckKind::SuperCallWithParameters => Some("Remove `__super__` parameters".to_string()),
CheckKind::TrueFalseComparison(true, EqCmpop::Eq) => {
Some("Replace with `cond is True`".to_string())
}
CheckKind::TrueFalseComparison(true, EqCmpop::NotEq) => {
Some("Replace with `cond is not True`".to_string())
}
CheckKind::TrueFalseComparison(false, EqCmpop::Eq) => {
Some("Replace with `cond is False`".to_string())
}
CheckKind::TrueFalseComparison(false, EqCmpop::NotEq) => {
Some("Replace with `cond is not False`".to_string())
}
CheckKind::TypeOfPrimitive(primitive) => Some(format!(
"Replace `type(...)` with `{}`",
primitive.builtin()
)),
CheckKind::TypingTextStrAlias => Some("Replace with `str`".to_string()),
CheckKind::UnnecessaryCallAroundSorted(func) => {
Some(format!("Remove unnecessary `{func}` call"))
}
CheckKind::UnnecessaryCollectionCall(..) => Some("Rewrite as a literal".to_string()),
CheckKind::UnnecessaryComprehension(obj_type) => {
Some(format!("Rewrite using `{obj_type}()`"))
}
CheckKind::UnnecessaryEncodeUTF8 => Some("Remove unnecessary `encode`".to_string()),
CheckKind::UnnecessaryFutureImport(..) => {
Some("Remove unnecessary `__future__` import".to_string())
}
CheckKind::UnnecessaryGeneratorDict => {
Some("Rewrite as a `dict` comprehension".to_string())
}
CheckKind::UnnecessaryGeneratorList => {
Some("Rewrite as a `list` comprehension".to_string())
}
CheckKind::UnnecessaryGeneratorSet => {
Some("Rewrite as a `set` comprehension".to_string())
}
CheckKind::UnnecessaryLRUCacheParams => {
Some("Remove unnecessary parameters".to_string())
}
CheckKind::UnnecessaryListCall => Some("Remove outer `list` call".to_string()),
CheckKind::UnnecessaryListComprehensionDict => {
Some("Rewrite as a `dict` comprehension".to_string())
}
CheckKind::UnnecessaryListComprehensionSet => {
Some("Rewrite as a `set` comprehension".to_string())
}
CheckKind::UnnecessaryLiteralDict(..) => {
Some("Rewrite as a `dict` literal".to_string())
}
CheckKind::UnnecessaryLiteralSet(..) => Some("Rewrite as a `set` literal".to_string()),
CheckKind::UnnecessaryLiteralWithinTupleCall(literal) => Some({
if literal == "list" {
"Rewrite as a `tuple` literal".to_string()
} else {
"Remove outer `tuple` call".to_string()
}
}),
CheckKind::UnnecessaryLiteralWithinListCall(literal) => Some({
if literal == "list" {
"Remove outer `list` call".to_string()
} else {
"Rewrite as a `list` literal".to_string()
}
}),
CheckKind::UnnecessaryReturnNone => Some("Remove explicit `return None`".to_string()),
CheckKind::UnsortedImports => Some("Organize imports".to_string()),
CheckKind::UnusedImport(name, false, multiple) => {
if *multiple {
Some("Remove unused import".to_string())
} else {
Some(format!("Remove unused import: `{name}`"))
}
}
CheckKind::UnusedLoopControlVariable(name) => {
Some(format!("Rename unused `{name}` to `_{name}`"))
}
CheckKind::UnusedNOQA(..) => Some("Remove unused `noqa` directive".to_string()),
CheckKind::UsePEP585Annotation(name) => {
Some(format!("Replace `{name}` with `{}`", name.to_lowercase(),))
}
CheckKind::UsePEP604Annotation => Some("Convert to `X | Y`".to_string()),
CheckKind::UseSysExit(name) => Some(format!("Replace `{name}` with `sys.exit()`")),
CheckKind::UselessImportAlias => Some("Remove import alias".to_string()),
CheckKind::UselessMetaclassType => Some("Remove `__metaclass__ = type`".to_string()),
CheckKind::UselessObjectInheritance(..) => {
Some("Remove `object` inheritance".to_string())
}
_ => None,
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -3128,6 +3491,7 @@ pub struct Check {
pub location: Location,
pub end_location: Location,
pub fix: Option<Fix>,
pub parent: Option<Location>,
}
impl Check {
@@ -3137,11 +3501,18 @@ impl Check {
location: range.location,
end_location: range.end_location,
fix: None,
parent: None,
}
}
pub fn amend(&mut self, fix: Fix) {
pub fn amend(&mut self, fix: Fix) -> &mut Self {
self.fix = Some(fix);
self
}
pub fn parent(&mut self, parent: Location) -> &mut Self {
self.parent = Some(parent);
self
}
}
@@ -3292,4 +3663,17 @@ mod tests {
);
}
}
#[test]
fn fixable_codes() {
for check_code in CheckCode::iter() {
let kind = check_code.kind();
if kind.fixable() {
assert!(
kind.commit().is_some(),
"{check_code:?} is fixable but has no commit message."
);
}
}
}
}

View File

@@ -283,8 +283,6 @@ pub enum CheckCodePrefix {
F821,
F822,
F823,
F83,
F831,
F84,
F841,
F842,
@@ -314,6 +312,12 @@ pub enum CheckCodePrefix {
ICN0,
ICN00,
ICN001,
ISC,
ISC0,
ISC00,
ISC001,
ISC002,
ISC003,
M,
M0,
M001,
@@ -377,6 +381,7 @@ pub enum CheckCodePrefix {
PGH001,
PGH002,
PGH003,
PGH004,
PLC,
PLC0,
PLC04,
@@ -485,6 +490,7 @@ pub enum CheckCodePrefix {
TID,
TID2,
TID25,
TID251,
TID252,
U,
U0,
@@ -534,6 +540,9 @@ pub enum CheckCodePrefix {
UP021,
UP022,
UP023,
UP024,
UP025,
UP026,
W,
W2,
W29,
@@ -638,7 +647,6 @@ impl CheckCodePrefix {
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F842,
CheckCode::F901,
@@ -704,6 +712,7 @@ impl CheckCodePrefix {
CheckCode::C417,
CheckCode::T100,
CheckCode::C901,
CheckCode::TID251,
CheckCode::TID252,
CheckCode::RET501,
CheckCode::RET502,
@@ -713,6 +722,9 @@ impl CheckCodePrefix {
CheckCode::RET506,
CheckCode::RET507,
CheckCode::RET508,
CheckCode::ISC001,
CheckCode::ISC002,
CheckCode::ISC003,
CheckCode::T201,
CheckCode::T203,
CheckCode::Q000,
@@ -763,6 +775,9 @@ impl CheckCodePrefix {
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
CheckCode::D100,
CheckCode::D101,
CheckCode::D102,
@@ -857,6 +872,7 @@ impl CheckCodePrefix {
CheckCode::PGH001,
CheckCode::PGH002,
CheckCode::PGH003,
CheckCode::PGH004,
CheckCode::PD002,
CheckCode::PD003,
CheckCode::PD004,
@@ -1491,7 +1507,6 @@ impl CheckCodePrefix {
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F842,
CheckCode::F901,
@@ -1625,7 +1640,6 @@ impl CheckCodePrefix {
CheckCode::F821,
CheckCode::F822,
CheckCode::F823,
CheckCode::F831,
CheckCode::F841,
CheckCode::F842,
],
@@ -1635,8 +1649,6 @@ impl CheckCodePrefix {
CheckCodePrefix::F821 => vec![CheckCode::F821],
CheckCodePrefix::F822 => vec![CheckCode::F822],
CheckCodePrefix::F823 => vec![CheckCode::F823],
CheckCodePrefix::F83 => vec![CheckCode::F831],
CheckCodePrefix::F831 => vec![CheckCode::F831],
CheckCodePrefix::F84 => vec![CheckCode::F841, CheckCode::F842],
CheckCodePrefix::F841 => vec![CheckCode::F841],
CheckCodePrefix::F842 => vec![CheckCode::F842],
@@ -1660,7 +1672,7 @@ impl CheckCodePrefix {
":".bold(),
"`I2` has been remapped to `TID2`".bold()
);
vec![CheckCode::TID252]
vec![CheckCode::TID251, CheckCode::TID252]
}
CheckCodePrefix::I25 => {
one_time_warning!(
@@ -1669,7 +1681,7 @@ impl CheckCodePrefix {
":".bold(),
"`I25` has been remapped to `TID25`".bold()
);
vec![CheckCode::TID252]
vec![CheckCode::TID251, CheckCode::TID252]
}
CheckCodePrefix::I252 => {
one_time_warning!(
@@ -1738,6 +1750,12 @@ impl CheckCodePrefix {
CheckCodePrefix::ICN0 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN00 => vec![CheckCode::ICN001],
CheckCodePrefix::ICN001 => vec![CheckCode::ICN001],
CheckCodePrefix::ISC => vec![CheckCode::ISC001, CheckCode::ISC002, CheckCode::ISC003],
CheckCodePrefix::ISC0 => vec![CheckCode::ISC001, CheckCode::ISC002, CheckCode::ISC003],
CheckCodePrefix::ISC00 => vec![CheckCode::ISC001, CheckCode::ISC002, CheckCode::ISC003],
CheckCodePrefix::ISC001 => vec![CheckCode::ISC001],
CheckCodePrefix::ISC002 => vec![CheckCode::ISC002],
CheckCodePrefix::ISC003 => vec![CheckCode::ISC003],
CheckCodePrefix::M => {
one_time_warning!(
"{}{} {}",
@@ -2073,12 +2091,28 @@ impl CheckCodePrefix {
);
vec![CheckCode::PD901]
}
CheckCodePrefix::PGH => vec![CheckCode::PGH001, CheckCode::PGH002, CheckCode::PGH003],
CheckCodePrefix::PGH0 => vec![CheckCode::PGH001, CheckCode::PGH002, CheckCode::PGH003],
CheckCodePrefix::PGH00 => vec![CheckCode::PGH001, CheckCode::PGH002, CheckCode::PGH003],
CheckCodePrefix::PGH => vec![
CheckCode::PGH001,
CheckCode::PGH002,
CheckCode::PGH003,
CheckCode::PGH004,
],
CheckCodePrefix::PGH0 => vec![
CheckCode::PGH001,
CheckCode::PGH002,
CheckCode::PGH003,
CheckCode::PGH004,
],
CheckCodePrefix::PGH00 => vec![
CheckCode::PGH001,
CheckCode::PGH002,
CheckCode::PGH003,
CheckCode::PGH004,
],
CheckCodePrefix::PGH001 => vec![CheckCode::PGH001],
CheckCodePrefix::PGH002 => vec![CheckCode::PGH002],
CheckCodePrefix::PGH003 => vec![CheckCode::PGH003],
CheckCodePrefix::PGH004 => vec![CheckCode::PGH004],
CheckCodePrefix::PLC => {
vec![CheckCode::PLC0414, CheckCode::PLC2201, CheckCode::PLC3002]
}
@@ -2387,9 +2421,10 @@ impl CheckCodePrefix {
CheckCodePrefix::T20 => vec![CheckCode::T201, CheckCode::T203],
CheckCodePrefix::T201 => vec![CheckCode::T201],
CheckCodePrefix::T203 => vec![CheckCode::T203],
CheckCodePrefix::TID => vec![CheckCode::TID252],
CheckCodePrefix::TID2 => vec![CheckCode::TID252],
CheckCodePrefix::TID25 => vec![CheckCode::TID252],
CheckCodePrefix::TID => vec![CheckCode::TID251, CheckCode::TID252],
CheckCodePrefix::TID2 => vec![CheckCode::TID251, CheckCode::TID252],
CheckCodePrefix::TID25 => vec![CheckCode::TID251, CheckCode::TID252],
CheckCodePrefix::TID251 => vec![CheckCode::TID251],
CheckCodePrefix::TID252 => vec![CheckCode::TID252],
CheckCodePrefix::U => {
one_time_warning!(
@@ -2421,6 +2456,9 @@ impl CheckCodePrefix {
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
]
}
CheckCodePrefix::U0 => {
@@ -2453,6 +2491,9 @@ impl CheckCodePrefix {
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
]
}
CheckCodePrefix::U00 => {
@@ -2669,6 +2710,9 @@ impl CheckCodePrefix {
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
@@ -2693,6 +2737,9 @@ impl CheckCodePrefix {
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
@@ -2739,11 +2786,17 @@ impl CheckCodePrefix {
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP024,
CheckCode::UP025,
CheckCode::UP026,
],
CheckCodePrefix::UP020 => vec![CheckCode::UP020],
CheckCodePrefix::UP021 => vec![CheckCode::UP021],
CheckCodePrefix::UP022 => vec![CheckCode::UP022],
CheckCodePrefix::UP023 => vec![CheckCode::UP023],
CheckCodePrefix::UP024 => vec![CheckCode::UP024],
CheckCodePrefix::UP025 => vec![CheckCode::UP025],
CheckCodePrefix::UP026 => vec![CheckCode::UP026],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -3058,8 +3111,6 @@ impl CheckCodePrefix {
CheckCodePrefix::F821 => SuffixLength::Three,
CheckCodePrefix::F822 => SuffixLength::Three,
CheckCodePrefix::F823 => SuffixLength::Three,
CheckCodePrefix::F83 => SuffixLength::Two,
CheckCodePrefix::F831 => SuffixLength::Three,
CheckCodePrefix::F84 => SuffixLength::Two,
CheckCodePrefix::F841 => SuffixLength::Three,
CheckCodePrefix::F842 => SuffixLength::Three,
@@ -3089,6 +3140,12 @@ impl CheckCodePrefix {
CheckCodePrefix::ICN0 => SuffixLength::One,
CheckCodePrefix::ICN00 => SuffixLength::Two,
CheckCodePrefix::ICN001 => SuffixLength::Three,
CheckCodePrefix::ISC => SuffixLength::Zero,
CheckCodePrefix::ISC0 => SuffixLength::One,
CheckCodePrefix::ISC00 => SuffixLength::Two,
CheckCodePrefix::ISC001 => SuffixLength::Three,
CheckCodePrefix::ISC002 => SuffixLength::Three,
CheckCodePrefix::ISC003 => SuffixLength::Three,
CheckCodePrefix::M => SuffixLength::Zero,
CheckCodePrefix::M0 => SuffixLength::One,
CheckCodePrefix::M001 => SuffixLength::Three,
@@ -3152,6 +3209,7 @@ impl CheckCodePrefix {
CheckCodePrefix::PGH001 => SuffixLength::Three,
CheckCodePrefix::PGH002 => SuffixLength::Three,
CheckCodePrefix::PGH003 => SuffixLength::Three,
CheckCodePrefix::PGH004 => SuffixLength::Three,
CheckCodePrefix::PLC => SuffixLength::Zero,
CheckCodePrefix::PLC0 => SuffixLength::One,
CheckCodePrefix::PLC04 => SuffixLength::Two,
@@ -3260,6 +3318,7 @@ impl CheckCodePrefix {
CheckCodePrefix::TID => SuffixLength::Zero,
CheckCodePrefix::TID2 => SuffixLength::One,
CheckCodePrefix::TID25 => SuffixLength::Two,
CheckCodePrefix::TID251 => SuffixLength::Three,
CheckCodePrefix::TID252 => SuffixLength::Three,
CheckCodePrefix::U => SuffixLength::Zero,
CheckCodePrefix::U0 => SuffixLength::One,
@@ -3309,6 +3368,9 @@ impl CheckCodePrefix {
CheckCodePrefix::UP021 => SuffixLength::Three,
CheckCodePrefix::UP022 => SuffixLength::Three,
CheckCodePrefix::UP023 => SuffixLength::Three,
CheckCodePrefix::UP024 => SuffixLength::Three,
CheckCodePrefix::UP025 => SuffixLength::Three,
CheckCodePrefix::UP026 => SuffixLength::Three,
CheckCodePrefix::W => SuffixLength::Zero,
CheckCodePrefix::W2 => SuffixLength::One,
CheckCodePrefix::W29 => SuffixLength::Two,
@@ -3354,6 +3416,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::ICN,
CheckCodePrefix::ISC,
CheckCodePrefix::N,
CheckCodePrefix::PD,
CheckCodePrefix::PGH,

View File

@@ -17,7 +17,7 @@ use crate::settings::types::{
#[command(version)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
#[arg(required_unless_present_any = ["explain", "generate_shell_completion"])]
#[arg(required_unless_present_any = ["clean", "explain", "generate_shell_completion"])]
pub files: Vec<PathBuf>,
/// Path to the `pyproject.toml` or `ruff.toml` file to use for
/// configuration.
@@ -57,14 +57,15 @@ pub struct Cli {
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
/// List of error codes to enable.
/// 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
/// ones.
#[arg(long, value_delimiter = ',')]
pub extend_select: Option<Vec<CheckCodePrefix>>,
/// List of error codes to ignore.
/// 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
@@ -92,6 +93,12 @@ pub struct Cli {
/// Output serialization format for error messages.
#[arg(long, value_enum)]
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)]
pub cache_dir: Option<PathBuf>,
/// Show violations with source code.
#[arg(long, overrides_with("no_show_source"))]
show_source: bool,
@@ -105,19 +112,15 @@ pub struct Cli {
no_respect_gitignore: bool,
/// Enforce exclusions, even for paths passed to Ruff directly on the
/// command-line.
#[arg(long, overrides_with("no_show_source"))]
#[arg(long, overrides_with("no_force_exclude"))]
force_exclude: bool,
#[clap(long, overrides_with("force_exclude"), hide = true)]
no_force_exclude: bool,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
/// See the settings Ruff will use to check a given Python file.
#[arg(long)]
pub show_settings: bool,
/// Enable automatic additions of noqa directives to failing lines.
#[arg(long)]
pub add_noqa: bool,
/// Enable or disable automatic update checks.
#[arg(long, overrides_with("no_update_check"))]
update_check: bool,
#[clap(long, overrides_with("update_check"), hide = true)]
no_update_check: bool,
/// Regular expression matching the name of dummy variables.
#[arg(long)]
pub dummy_variable_rgx: Option<Regex>,
@@ -128,25 +131,100 @@ pub struct Cli {
/// formatting.
#[arg(long)]
pub line_length: Option<usize>,
/// Max McCabe complexity allowed for a function.
/// Maximum McCabe complexity allowed for a given function.
#[arg(long)]
pub max_complexity: Option<usize>,
/// Round-trip auto-formatting.
// TODO(charlie): This should be a sub-command.
#[arg(long, hide = true)]
pub autoformat: bool,
/// The name of the file when passing it through stdin.
#[arg(long)]
pub stdin_filename: Option<PathBuf>,
/// Enable automatic additions of `noqa` directives to failing lines.
#[arg(
long,
// conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub add_noqa: bool,
/// Clear any caches in the current directory or any subdirectories.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
// conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub clean: bool,
/// Explain a rule.
#[arg(long)]
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
// conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub explain: Option<CheckCode>,
/// Generate shell completion
#[arg(long, hide = true, value_name = "SHELL")]
#[arg(
long,
hide = true,
value_name = "SHELL",
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
// conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub generate_shell_completion: Option<clap_complete_command::Shell>,
/// Path to the cache directory.
#[arg(long)]
pub cache_dir: Option<PathBuf>,
/// See the files Ruff will be run against with the current settings.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
// conflicts_with = "show_files",
conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub show_files: bool,
/// See the settings Ruff will use to check a given Python file.
#[arg(
long,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
conflicts_with = "explain",
conflicts_with = "generate_shell_completion",
conflicts_with = "show_files",
// conflicts_with = "show_settings",
// Unsupported default-command arguments.
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub show_settings: bool,
}
impl Cli {
@@ -156,7 +234,7 @@ impl Cli {
(
Arguments {
add_noqa: self.add_noqa,
autoformat: self.autoformat,
clean: self.clean,
config: self.config,
diff: self.diff,
exit_zero: self.exit_zero,
@@ -192,11 +270,12 @@ impl Cli {
target_version: self.target_version,
unfixable: self.unfixable,
// TODO(charlie): Included in `pyproject.toml`, but not inherited.
cache_dir: self.cache_dir,
fix: resolve_bool_arg(self.fix, self.no_fix),
fix_only: resolve_bool_arg(self.fix_only, self.no_fix_only),
format: self.format,
force_exclude: resolve_bool_arg(self.force_exclude, self.no_force_exclude),
cache_dir: self.cache_dir,
format: self.format,
update_check: resolve_bool_arg(self.update_check, self.no_update_check),
},
)
}
@@ -216,7 +295,7 @@ fn resolve_bool_arg(yes: bool, no: bool) -> Option<bool> {
#[allow(clippy::struct_excessive_bools)]
pub struct Arguments {
pub add_noqa: bool,
pub autoformat: bool,
pub clean: bool,
pub config: Option<PathBuf>,
pub diff: bool,
pub exit_zero: bool,
@@ -253,11 +332,12 @@ pub struct Overrides {
pub target_version: Option<PythonVersion>,
pub unfixable: Option<Vec<CheckCodePrefix>>,
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub cache_dir: Option<PathBuf>,
pub fix: Option<bool>,
pub fix_only: Option<bool>,
pub format: Option<SerializationFormat>,
pub force_exclude: Option<bool>,
pub cache_dir: Option<PathBuf>,
pub format: Option<SerializationFormat>,
pub update_check: Option<bool>,
}
/// Map the CLI settings to a `LogLevel`.

View File

@@ -1,26 +1,32 @@
use std::fs::remove_dir_all;
use std::io::{self, Read};
use std::path::{Path, PathBuf};
use std::time::Instant;
use anyhow::{bail, Result};
use colored::Colorize;
use ignore::Error;
use itertools::Itertools;
use log::{debug, error};
use path_absolutize::path_dedot;
#[cfg(not(target_family = "wasm"))]
use rayon::prelude::*;
use rustpython_ast::Location;
use serde::Serialize;
use walkdir::WalkDir;
use crate::autofix::fixer;
use crate::cache::DEFAULT_CACHE_DIR_NAME;
use crate::checks::{CheckCode, CheckKind};
use crate::cli::Overrides;
use crate::iterators::par_iter;
use crate::linter::{add_noqa_to_path, autoformat_path, lint_path, lint_stdin, Diagnostics};
use crate::linter::{add_noqa_to_path, lint_path, lint_stdin, Diagnostics};
use crate::logging::LogLevel;
use crate::message::Message;
use crate::resolver::{FileDiscovery, PyprojectDiscovery};
use crate::settings::flags;
use crate::settings::types::SerializationFormat;
use crate::{cache, packages, resolver};
use crate::{cache, fs, one_time_warning, packages, resolver};
/// Run the linter over a collection of files.
pub fn run(
@@ -38,6 +44,15 @@ pub fn run(
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
if paths.is_empty() {
one_time_warning!(
"{} {}",
"warning:".yellow().bold(),
"No Python files found under the given path(s)"
);
return Ok(Diagnostics::default());
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
@@ -179,6 +194,15 @@ pub fn add_noqa(
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
if paths.is_empty() {
one_time_warning!(
"{} {}",
"warning:".yellow().bold(),
"No Python files found under the given path(s)"
);
return Ok(0);
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
@@ -204,45 +228,6 @@ pub fn add_noqa(
Ok(modifications)
}
/// Automatically format a collection of files.
pub fn autoformat(
files: &[PathBuf],
pyproject_strategy: &PyprojectDiscovery,
file_strategy: &FileDiscovery,
overrides: &Overrides,
) -> Result<usize> {
// Collect all the files to format.
let start = Instant::now();
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
let duration = start.elapsed();
debug!("Identified files to lint in: {:?}", duration);
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
let start = Instant::now();
let modifications = par_iter(&paths)
.flatten()
.filter_map(|entry| {
let path = entry.path();
let settings = resolver.resolve(path, pyproject_strategy);
match autoformat_path(path, settings) {
Ok(()) => Some(()),
Err(e) => {
error!("Failed to autoformat {}: {e}", path.to_string_lossy());
None
}
}
})
.count();
let duration = start.elapsed();
debug!("Auto-formatted files in: {:?}", duration);
Ok(modifications)
}
/// Print the user-facing configuration settings.
pub fn show_settings(
files: &[PathBuf],
@@ -283,6 +268,15 @@ pub fn show_files(
let (paths, resolver) =
resolver::python_files_in_path(files, pyproject_strategy, file_strategy, overrides)?;
if paths.is_empty() {
one_time_warning!(
"{} {}",
"warning:".yellow().bold(),
"No Python files found under the given path(s)"
);
return Ok(());
}
// Validate the `Settings` and return any errors.
resolver.validate(pyproject_strategy)?;
@@ -338,3 +332,21 @@ pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
};
Ok(())
}
/// Clear any caches in the current directory or any subdirectories.
pub fn clean(level: &LogLevel) -> Result<()> {
for entry in WalkDir::new(&*path_dedot::CWD)
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|entry| entry.file_type().is_dir())
{
let cache = entry.path().join(DEFAULT_CACHE_DIR_NAME);
if cache.is_dir() {
if level >= &LogLevel::Default {
eprintln!("Removing cache at: {}", fs::relativize_path(&cache).bold());
}
remove_dir_all(&cache)?;
}
}
Ok(())
}

View File

@@ -1,5 +1,5 @@
use anyhow::{bail, Result};
use libcst_native::{Expr, Module, SmallStatement, Statement};
use libcst_native::{Expr, Import, ImportFrom, Module, SmallStatement, Statement};
pub fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -19,3 +19,27 @@ pub fn match_expr<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Expr<'b>
bail!("Expected Statement::Simple")
}
}
pub fn match_import<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut Import<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::Import(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
}
} else {
bail!("Expected Statement::Simple")
}
}
pub fn match_import_from<'a, 'b>(module: &'a mut Module<'b>) -> Result<&'a mut ImportFrom<'b>> {
if let Some(Statement::Simple(expr)) = module.body.first_mut() {
if let Some(SmallStatement::ImportFrom(expr)) = expr.body.first_mut() {
Ok(expr)
} else {
bail!("Expected SmallStatement::Expr")
}
} else {
bail!("Expected Statement::Simple")
}
}

View File

@@ -110,10 +110,7 @@ pub fn extract_isort_directives(lxr: &[LexResult], locator: &SourceCodeLocator)
}
// TODO(charlie): Modify RustPython to include the comment text in the token.
let comment_text = locator.slice_source_code_range(&Range {
location: start,
end_location: end,
});
let comment_text = locator.slice_source_code_range(&Range::new(start, end));
if comment_text == "# isort: split" {
splits.push(start.row());

View File

@@ -1,9 +1,9 @@
/// See: <https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals>
pub const TRIPLE_QUOTE_PREFIXES: &[&str] = &[
"ur\"\"\"", "ur'''", "u\"\"\"", "u'''", "r\"\"\"", "r'''", "UR\"\"\"", "UR'''", "Ur\"\"\"",
"Ur'''", "U\"\"\"", "U'''", "uR\"\"\"", "uR'''", "R\"\"\"", "R'''", "\"\"\"", "'''",
"u\"\"\"", "u'''", "r\"\"\"", "r'''", "U\"\"\"", "U'''", "R\"\"\"", "R'''", "\"\"\"", "'''",
];
pub const SINGLE_QUOTE_PREFIXES: &[&str] = &[
"ur\"", "ur'", "u\"", "u'", "r\"", "r'", "ur\"", "ur'", "u\"", "u'", "r\"", "r'", "UR\"",
"UR'", "Ur\"", "Ur'", "U\"", "U'", "uR\"", "uR'", "R\"", "R'", "\"", "'",
"u\"", "u'", "r\"", "r'", "u\"", "u'", "r\"", "r'", "U\"", "U'", "R\"", "R'", "\"", "'",
];

View File

@@ -5,21 +5,21 @@ use crate::docstrings::google::{GOOGLE_SECTION_NAMES, LOWERCASE_GOOGLE_SECTION_N
use crate::docstrings::numpy::{LOWERCASE_NUMPY_SECTION_NAMES, NUMPY_SECTION_NAMES};
pub(crate) enum SectionStyle {
NumPy,
Numpy,
Google,
}
impl SectionStyle {
pub(crate) fn section_names(&self) -> &Lazy<FxHashSet<&'static str>> {
match self {
SectionStyle::NumPy => &NUMPY_SECTION_NAMES,
SectionStyle::Numpy => &NUMPY_SECTION_NAMES,
SectionStyle::Google => &GOOGLE_SECTION_NAMES,
}
}
pub(crate) fn lowercase_section_names(&self) -> &Lazy<FxHashSet<&'static str>> {
match self {
SectionStyle::NumPy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Numpy => &LOWERCASE_NUMPY_SECTION_NAMES,
SectionStyle::Google => &LOWERCASE_GOOGLE_SECTION_NAMES,
}
}

View File

@@ -28,20 +28,11 @@ pub fn commented_out_code(
) -> Option<Check> {
let location = Location::new(start.row(), 0);
let end_location = Location::new(end.row() + 1, 0);
let line = locator.slice_source_code_range(&Range {
location,
end_location,
});
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) {
let mut check = Check::new(
CheckKind::CommentedOutCode,
Range {
location: start,
end_location: end,
},
);
let mut check = Check::new(CheckKind::CommentedOutCode, Range::new(start, end));
if matches!(autofix, flags::Autofix::Enabled)
&& settings.fixable.contains(&CheckCode::ERA001)
{

View File

@@ -16,13 +16,12 @@ mod tests {
#[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 mut checks = test_path(
let checks = test_path(
Path::new("./resources/test/fixtures/eradicate")
.join(path)
.as_path(),
&settings::Settings::for_rule(check_code),
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(snapshot, checks);
Ok(())
}

View File

@@ -17,6 +17,7 @@ expression: checks
end_location:
row: 2
column: 0
parent: ~
- kind: CommentedOutCode
location:
row: 2
@@ -32,6 +33,7 @@ expression: checks
end_location:
row: 3
column: 0
parent: ~
- kind: CommentedOutCode
location:
row: 3
@@ -47,6 +49,7 @@ expression: checks
end_location:
row: 4
column: 0
parent: ~
- kind: CommentedOutCode
location:
row: 5
@@ -62,6 +65,7 @@ expression: checks
end_location:
row: 6
column: 0
parent: ~
- kind: CommentedOutCode
location:
row: 12
@@ -77,4 +81,5 @@ expression: checks
end_location:
row: 13
column: 0
parent: ~

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