Compare commits

...

40 Commits

Author SHA1 Message Date
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
Charlie Marsh
057414ddd4 Bump version to 0.0.199 2022-12-28 20:58:43 -05:00
Charlie Marsh
ca94e9aa26 Warn the user when max iteration count is reached (#1433) 2022-12-28 20:56:43 -05:00
Charlie Marsh
797b5bd261 Split into lint and lint-and-fix methods (#1432) 2022-12-28 20:14:33 -05:00
Charlie Marsh
a64f62f439 Revert setup.py change 2022-12-28 19:34:20 -05:00
Charlie Marsh
058ee8e6bf Add a --diff flag to dry-run autofixes (#1431) 2022-12-28 19:21:29 -05:00
Charlie Marsh
39fc1f0c1b Add a note on autofix settings 2022-12-28 17:26:38 -05:00
Colin Delahunty
34842b4c4b PyUpgrade: Replace pipes with capture_output=True (#1415) 2022-12-28 16:53:35 -05:00
Charlie Marsh
dfa6fa8f83 Check in updated snapshots 2022-12-28 16:42:55 -05:00
Colin Delahunty
6131c819ed Rewrite xml.etree.cElementTree to xml.etree.ElementTree (#1426) 2022-12-28 16:30:36 -05:00
Hannes Käufler
79ba420faa Extract duplicated logic into method (#1428) 2022-12-28 16:10:53 -05:00
Charlie Marsh
d16ba890ae Turn off wasm-pack tests (#1427) 2022-12-28 12:55:25 -05:00
Charlie Marsh
6b6851bf1f Update JSON schema 2022-12-28 12:27:01 -05:00
Charlie Marsh
056718ce75 Remove stray Plugins doc 2022-12-28 12:24:48 -05:00
Charlie Marsh
4521fdf021 Only test --lib for wasm-pack 2022-12-28 10:28:40 -05:00
Maksudul Haque
8e479628f2 Add Support for GitLab CI Code Quality Report Format (#1424) 2022-12-28 10:10:43 -05:00
Charlie Marsh
2a11c4b1f1 Try increasing wasm-bindgen timeout 2022-12-28 07:39:23 -05:00
Anders Kaseorg
a8cde5a936 Check for keyword arguments before the last star argument (#1420) 2022-12-27 23:20:03 -05:00
115 changed files with 5708 additions and 1190 deletions

View File

@@ -121,31 +121,35 @@ jobs:
- run: cargo test --all
- run: cargo test --package ruff --test black_compatibility_test -- --ignored
wasm-pack-test:
name: "wasm-pack test"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
override: true
- uses: actions/cache@v3
env:
cache-name: cache-cargo
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- uses: jetli/wasm-pack-action@v0.4.0
- uses: jetli/wasm-bindgen-action@v0.2.0
- run: wasm-pack test --node
# TODO(charlie): Re-enable the `wasm-pack` tests.
# See: https://github.com/charliermarsh/ruff/issues/1425
# wasm-pack-test:
# name: "wasm-pack test"
# runs-on: ubuntu-latest
# env:
# WASM_BINDGEN_TEST_TIMEOUT: 60
# steps:
# - uses: actions/checkout@v3
# - uses: actions-rs/toolchain@v1
# with:
# profile: minimal
# toolchain: nightly-2022-11-01
# override: true
# - uses: actions/cache@v3
# env:
# cache-name: cache-cargo
# with:
# path: |
# ~/.cargo/registry
# ~/.cargo/git
# key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
# restore-keys: |
# ${{ runner.os }}-build-${{ env.cache-name }}-
# ${{ runner.os }}-build-
# ${{ runner.os }}-
# - uses: jetli/wasm-pack-action@v0.4.0
# - uses: jetli/wasm-bindgen-action@v0.2.0
# - run: wasm-pack test --node
maturin-build:
name: "maturin build"

View File

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

9
Cargo.lock generated
View File

@@ -750,7 +750,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.198-dev.0"
version = "0.0.201-dev.0"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1878,7 +1878,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.198"
version = "0.0.201"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1930,6 +1930,7 @@ dependencies = [
"serde-wasm-bindgen",
"serde_json",
"shellexpand",
"similar",
"strum",
"strum_macros",
"test-case",
@@ -1945,7 +1946,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.198"
version = "0.0.201"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1966,7 +1967,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.198"
version = "0.0.201"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -6,9 +6,15 @@ members = [
[package]
name = "ruff"
version = "0.0.198"
version = "0.0.201"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
documentation = "https://github.com/charliermarsh/ruff"
homepage = "https://github.com/charliermarsh/ruff"
repository = "https://github.com/charliermarsh/ruff"
readme = "README.md"
license = "MIT"
[lib]
name = "ruff"
@@ -45,7 +51,7 @@ path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix
quick-junit = { version = "0.3.2" }
regex = { version = "1.6.0" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.198", path = "ruff_macros" }
ruff_macros = { version = "0.0.201", 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" }
@@ -55,6 +61,7 @@ semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.2.1" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }

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

View File

@@ -44,6 +44,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 +95,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 +169,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.198'
rev: 'v0.0.201'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
@@ -323,6 +325,8 @@ Options:
Attempt to automatically fix lint errors
--fix-only
Fix any fixable lint errors, but don't report on leftover violations. Implies `--fix`
--diff
Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-n, --no-cache
Disable cache reads
--select <SELECT>
@@ -344,13 +348,15 @@ Options:
--per-file-ignores <PER_FILE_IGNORES>
List of mappings from file pattern to code to exclude
--format <FORMAT>
Output serialization format for error messages [possible values: text, json, junit, grouped, github]
Output serialization format for error messages [possible values: text, json, junit, grouped, github, gitlab]
--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
--update-check
Enable or disable automatic update checks
--show-files
See the files Ruff will be run against with the current settings
--show-settings
@@ -671,6 +677,9 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| 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` | 🛠 |
| UP025 | RewriteUnicodeLiteral | Remove unicode literals from strings | 🛠 |
### pep8-naming (N)
@@ -846,6 +855,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 |
@@ -901,6 +920,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)
@@ -967,6 +987,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)
@@ -1264,10 +1285,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/)
@@ -1319,10 +1342,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/)
@@ -1929,8 +1954,9 @@ force-exclude = true
The style in which violation messages should be formatted: `"text"`
(default), `"grouped"` (group messages by file), `"json"`
(machine-readable), `"junit"` (machine-readable XML), or `"github"`
(GitHub Actions annotations).
(machine-readable), `"junit"` (machine-readable XML), `"github"`
(GitHub Actions annotations) or `"gitlab"`
(GitLab CI code quality report).
**Default value**: `"text"`
@@ -2192,6 +2218,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)
@@ -2435,7 +2479,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"`
@@ -2451,6 +2495,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)
@@ -2720,7 +2785,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.198"
version = "0.0.201"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.198"
version = "0.0.201"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.198-dev.0"
version = "0.0.201-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,

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,147 @@
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"}>
<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,101 @@
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",
"bg-ayu-background",
"dark:bg-ayu-background-dark",
)}
>
<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,115 @@
/**
* 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: `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();
};
}, [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

@@ -34,6 +34,7 @@ bindings = "bin"
strip = true
[tool.ruff]
update-check = true
[tool.ruff.isort]
force-wrap-aliases = true

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

@@ -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,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,42 @@
from subprocess import run
import subprocess
output = run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = subprocess.run(["foo"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output = subprocess.run(stdout=subprocess.PIPE, args=["foo"], stderr=subprocess.PIPE)
output = subprocess.run(
["foo"], stdout=subprocess.PIPE, check=True, stderr=subprocess.PIPE
)
output = subprocess.run(
["foo"], stderr=subprocess.PIPE, check=True, stdout=subprocess.PIPE
)
output = subprocess.run(
["foo"],
stdout=subprocess.PIPE,
check=True,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
close_fds=True,
)
if output:
output = subprocess.run(
["foo"],
stdout=subprocess.PIPE,
check=True,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
)
# Examples that should NOT trigger the rule
from foo import PIPE
subprocess.run(["foo"], stdout=PIPE, stderr=PIPE)
run(["foo"], stdout=None, stderr=PIPE)

View File

@@ -0,0 +1,31 @@
# These two imports have something after cElementTree, so they should be fixed.
from xml.etree.cElementTree import XML, Element, SubElement
import xml.etree.cElementTree as ET
# Weird spacing should not cause issues.
from xml.etree.cElementTree import XML
import xml.etree.cElementTree as ET
# Multi line imports should also work fine.
from xml.etree.cElementTree import (
XML,
Element,
SubElement,
)
if True:
import xml.etree.cElementTree as ET
from xml.etree import cElementTree as CET
from xml.etree import cElementTree as ET
import contextlib, xml.etree.cElementTree as ET
# This should fix the second, but not the first invocation.
import xml.etree.cElementTree, xml.etree.cElementTree as ET
# The below items should NOT be changed.
import xml.etree.cElementTree
from .xml.etree.cElementTree import XML
from xml.etree import cElementTree

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

@@ -5,9 +5,11 @@ def f(*args, **kwargs):
a = (1, 2)
b = (3, 4)
c = (5, 6)
d = (7, 8)
f(a, b)
f(a, kw=b)
f(*a, kw=b)
f(kw=a, *b)
f(kw=a, *b, *c)
f(*a, kw=b, *c, kw1=d)

View File

@@ -111,7 +111,7 @@
}
},
"flake8-annotations": {
"description": "Plugins Options for the `flake8-annotations` plugin.",
"description": "Options for the `flake8-annotations` plugin.",
"anyOf": [
{
"$ref": "#/definitions/Flake8AnnotationsOptions"
@@ -195,7 +195,7 @@
]
},
"format": {
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), or `\"github\"` (GitHub Actions annotations).",
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations) or `\"gitlab\"` (GitLab CI code quality report).",
"anyOf": [
{
"$ref": "#/definitions/SerializationFormat"
@@ -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": [
@@ -663,6 +683,12 @@
"ICN0",
"ICN00",
"ICN001",
"ISC",
"ISC0",
"ISC00",
"ISC001",
"ISC002",
"ISC003",
"M",
"M0",
"M001",
@@ -726,6 +752,7 @@
"PGH001",
"PGH002",
"PGH003",
"PGH004",
"PLC",
"PLC0",
"PLC04",
@@ -834,6 +861,7 @@
"TID",
"TID2",
"TID25",
"TID251",
"TID252",
"U",
"U0",
@@ -881,6 +909,9 @@
"UP02",
"UP020",
"UP021",
"UP022",
"UP023",
"UP025",
"W",
"W2",
"W29",
@@ -908,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": {
@@ -1053,9 +1095,12 @@
},
"Flake8TidyImportsOptions": {
"type": "object",
"required": [
"banned-api"
],
"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"
@@ -1064,6 +1109,13 @@
"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",
"additionalProperties": {
"$ref": "#/definitions/BannedApi"
}
}
},
"additionalProperties": false
@@ -1251,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": {
@@ -1264,14 +1327,26 @@
"json",
"junit",
"grouped",
"github"
"github",
"gitlab"
]
},
"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.198"
version = "0.0.201"
edition = "2021"
[dependencies]

View File

@@ -4,8 +4,7 @@ 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_json_schema, generate_options, generate_rules_table,
};
#[derive(Args)]
@@ -28,8 +27,5 @@ 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 {
dry_run: cli.dry_run,
})?;
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

@@ -15,7 +15,6 @@ pub mod generate_all;
pub mod generate_check_code_prefix;
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;

View File

@@ -15,8 +15,7 @@ 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_rules_table, print_ast, print_cst, print_tokens, round_trip,
};
#[derive(Parser)]
@@ -39,9 +38,6 @@ 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),
/// Print the AST for a given Python file.
PrintAST(print_ast::Cli),
/// Print the LibCST CST for a given Python file.
@@ -60,7 +56,6 @@ 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::PrintAST(args) => print_ast::main(args)?,
Commands::PrintCST(args) => print_cst::main(args)?,
Commands::PrintTokens(args) => print_tokens::main(args)?,

View File

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

View File

@@ -14,6 +14,7 @@ use crate::source_code_locator::SourceCodeLocator;
pub enum Mode {
Generate,
Apply,
Diff,
None,
}

View File

@@ -12,7 +12,6 @@ use once_cell::sync::Lazy;
use path_absolutize::Absolutize;
use serde::{Deserialize, Serialize};
use crate::autofix::fixer;
use crate::message::Message;
use crate::settings::{flags, Settings};
@@ -48,7 +47,7 @@ fn content_dir() -> &'static Path {
Path::new("content")
}
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: fixer::Mode) -> u64 {
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: flags::Autofix) -> u64 {
let mut hasher = DefaultHasher::new();
CARGO_PKG_VERSION.hash(&mut hasher);
path.as_ref().absolutize().unwrap().hash(&mut hasher);
@@ -93,13 +92,8 @@ pub fn get<P: AsRef<Path>>(
path: P,
metadata: &Metadata,
settings: &Settings,
autofix: fixer::Mode,
cache: flags::Cache,
autofix: flags::Autofix,
) -> Option<Vec<Message>> {
if matches!(cache, flags::Cache::Disabled) {
return None;
};
let encoded = read_sync(&settings.cache_dir, cache_key(path, settings, autofix)).ok()?;
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
Ok(CheckResult {
@@ -122,14 +116,9 @@ pub fn set<P: AsRef<Path>>(
path: P,
metadata: &Metadata,
settings: &Settings,
autofix: fixer::Mode,
autofix: flags::Autofix,
messages: &[Message],
cache: flags::Cache,
) {
if matches!(cache, flags::Cache::Disabled) {
return;
};
let check_result = CheckResultRef {
metadata: &CacheMetadata {
mtime: FileTime::from_last_modification_time(metadata).unix_seconds(),

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;
@@ -650,6 +650,9 @@ where
));
}
}
if self.settings.enabled.contains(&CheckCode::UP023) {
pyupgrade::plugins::replace_c_element_tree(self, stmt);
}
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
@@ -720,6 +723,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);
@@ -819,6 +833,9 @@ where
} => {
// Track `import from` statements, to ensure that we can correctly attribute
// references like `from typing import Union`.
if self.settings.enabled.contains(&CheckCode::UP023) {
pyupgrade::plugins::replace_c_element_tree(self, stmt);
}
if level.map(|level| level == 0).unwrap_or(true) {
if let Some(module) = module {
self.from_imports
@@ -848,6 +865,27 @@ where
}
}
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() {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
@@ -1552,9 +1590,6 @@ where
pyupgrade::plugins::use_pep585_annotation(self, expr, attr);
}
if self.settings.enabled.contains(&CheckCode::UP019) {
pyupgrade::plugins::typing_text_str_alias(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP016) {
pyupgrade::plugins::remove_six_compat(self, expr);
}
@@ -1564,7 +1599,9 @@ where
{
pyupgrade::plugins::datetime_utc_alias(self, expr);
}
if self.settings.enabled.contains(&CheckCode::UP019) {
pyupgrade::plugins::typing_text_str_alias(self, expr);
}
if self.settings.enabled.contains(&CheckCode::YTT202) {
flake8_2020::plugins::name_or_attribute(self, expr);
}
@@ -1581,6 +1618,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,
@@ -1674,6 +1720,9 @@ where
if self.settings.enabled.contains(&CheckCode::UP021) {
pyupgrade::plugins::replace_universal_newlines(self, expr, keywords);
}
if self.settings.enabled.contains(&CheckCode::UP022) {
pyupgrade::plugins::replace_stdout_stderr(self, expr, keywords);
}
// flake8-print
if self.settings.enabled.contains(&CheckCode::T201)
@@ -2222,6 +2271,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);
@@ -2321,7 +2379,7 @@ where
}
ExprKind::Constant {
value: Constant::Str(value),
..
kind,
} => {
if self.in_type_definition && !self.in_literal {
self.deferred_string_type_definitions.push((
@@ -2339,6 +2397,9 @@ where
self.add_check(check);
}
}
if self.settings.enabled.contains(&CheckCode::UP025) {
pyupgrade::plugins::rewrite_unicode_literal(self, expr, value, kind);
}
}
ExprKind::Lambda { args, body, .. } => {
// Visit the arguments, but avoid the body, which will be deferred.

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

@@ -49,6 +49,9 @@ pub fn check_noqa(
while let Some((index, check)) =
checks_iter.next_if(|(_index, check)| check.location.row() <= *lineno)
{
if check.kind == CheckKind::BlanketNOQA {
continue;
}
// 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.

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

@@ -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,
@@ -229,6 +234,9 @@ pub enum CheckCode {
UP019,
UP020,
UP021,
UP022,
UP023,
UP025,
// pydocstyle
D100,
D101,
@@ -334,6 +342,7 @@ pub enum CheckCode {
PGH001,
PGH002,
PGH003,
PGH004,
// pandas-vet
PD002,
PD003,
@@ -372,6 +381,7 @@ pub enum CheckCategory {
Flake8Comprehensions,
Flake8Debugger,
Flake8ErrMsg,
Flake8ImplicitStrConcat,
Flake8ImportConventions,
Flake8Print,
Flake8Quotes,
@@ -415,6 +425,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",
@@ -448,19 +459,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],
@@ -472,7 +485,6 @@ impl CheckCategory {
CheckCodePrefix::PLW,
],
CheckCategory::Pyupgrade => vec![CheckCodePrefix::UP],
CheckCategory::Flake8ImportConventions => vec![CheckCodePrefix::ICN],
CheckCategory::Ruff => vec![CheckCodePrefix::RUF],
}
}
@@ -522,6 +534,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/",
@@ -780,6 +796,7 @@ pub enum CheckKind {
// flake8-debugger
Debugger(DebuggerUsingType),
// flake8-tidy-imports
BannedApi { name: String, message: String },
BannedRelativeImport(Strictness),
// flake8-return
UnnecessaryReturnNone,
@@ -790,6 +807,10 @@ pub enum CheckKind {
SuperfluousElseRaise(Branch),
SuperfluousElseContinue(Branch),
SuperfluousElseBreak(Branch),
// flake8-implicit-str-concat
SingleLineImplicitStringConcatenation,
MultiLineImplicitStringConcatenation,
ExplicitStringConcatenation,
// flake8-print
PrintFound,
PPrintFound,
@@ -844,6 +865,9 @@ pub enum CheckKind {
NativeLiterals,
OpenAlias,
ReplaceUniversalNewlines,
ReplaceStdoutStderr,
RewriteCElementTree,
RewriteUnicodeLiteral,
// pydocstyle
BlankLineAfterLastSection(String),
BlankLineAfterSection(String),
@@ -927,6 +951,7 @@ pub enum CheckKind {
NoEval,
DeprecatedLogWarn,
BlanketTypeIgnore,
BlanketNOQA,
// flake8-unused-arguments
UnusedFunctionArgument(String),
UnusedMethodArgument(String),
@@ -976,10 +1001,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
@@ -1154,6 +1183,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,
@@ -1164,6 +1197,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,
@@ -1223,6 +1260,9 @@ impl CheckCode {
CheckCode::UP019 => CheckKind::TypingTextStrAlias,
CheckCode::UP020 => CheckKind::OpenAlias,
CheckCode::UP021 => CheckKind::ReplaceUniversalNewlines,
CheckCode::UP022 => CheckKind::ReplaceStdoutStderr,
CheckCode::UP023 => CheckKind::RewriteCElementTree,
CheckCode::UP025 => CheckKind::RewriteUnicodeLiteral,
// pydocstyle
CheckCode::D100 => CheckKind::PublicModule,
CheckCode::D101 => CheckKind::PublicClass,
@@ -1321,6 +1361,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()),
@@ -1556,8 +1597,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,
@@ -1588,6 +1631,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,
@@ -1627,6 +1671,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,
@@ -1647,6 +1693,9 @@ impl CheckCode {
CheckCode::UP019 => CheckCategory::Pyupgrade,
CheckCode::UP020 => CheckCategory::Pyupgrade,
CheckCode::UP021 => CheckCategory::Pyupgrade,
CheckCode::UP022 => CheckCategory::Pyupgrade,
CheckCode::UP023 => CheckCategory::Pyupgrade,
CheckCode::UP025 => CheckCategory::Pyupgrade,
CheckCode::W292 => CheckCategory::Pycodestyle,
CheckCode::W605 => CheckCategory::Pycodestyle,
CheckCode::YTT101 => CheckCategory::Flake82020,
@@ -1798,6 +1847,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,
@@ -1808,6 +1858,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,
@@ -1862,6 +1916,9 @@ impl CheckKind {
CheckKind::TypingTextStrAlias => &CheckCode::UP019,
CheckKind::OpenAlias => &CheckCode::UP020,
CheckKind::ReplaceUniversalNewlines => &CheckCode::UP021,
CheckKind::ReplaceStdoutStderr => &CheckCode::UP022,
CheckKind::RewriteCElementTree => &CheckCode::UP023,
CheckKind::RewriteUnicodeLiteral => &CheckCode::UP025,
// pydocstyle
CheckKind::BlankLineAfterLastSection(..) => &CheckCode::D413,
CheckKind::BlankLineAfterSection(..) => &CheckCode::D410,
@@ -1945,6 +2002,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,
@@ -2422,6 +2480,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()
@@ -2453,6 +2512,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(),
@@ -2595,6 +2664,13 @@ impl CheckKind {
CheckKind::ReplaceUniversalNewlines => {
"`universal_newlines` is deprecated, use `text`".to_string()
}
CheckKind::ReplaceStdoutStderr => {
"Sending stdout and stderr to pipe is deprecated, use `capture_output`".to_string()
}
CheckKind::RewriteCElementTree => {
"`cElementTree` is deprecated, use `ElementTree`".to_string()
}
CheckKind::RewriteUnicodeLiteral => "Remove unicode literals from strings".to_string(),
CheckKind::ConvertNamedTupleFunctionalToClass(name) => {
format!("Convert `{name}` from `NamedTuple` functional to class syntax")
}
@@ -2800,13 +2876,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}`")
@@ -3040,6 +3117,9 @@ impl CheckKind {
| CheckKind::OpenAlias
| CheckKind::NewLineAfterLastParagraph
| CheckKind::ReplaceUniversalNewlines
| CheckKind::ReplaceStdoutStderr
| CheckKind::RewriteCElementTree
| CheckKind::RewriteUnicodeLiteral
| CheckKind::NewLineAfterSectionName(..)
| CheckKind::NoBlankLineAfterFunction(..)
| CheckKind::NoBlankLineBeforeClass(..)

View File

@@ -314,6 +314,12 @@ pub enum CheckCodePrefix {
ICN0,
ICN00,
ICN001,
ISC,
ISC0,
ISC00,
ISC001,
ISC002,
ISC003,
M,
M0,
M001,
@@ -377,6 +383,7 @@ pub enum CheckCodePrefix {
PGH001,
PGH002,
PGH003,
PGH004,
PLC,
PLC0,
PLC04,
@@ -485,6 +492,7 @@ pub enum CheckCodePrefix {
TID,
TID2,
TID25,
TID251,
TID252,
U,
U0,
@@ -532,6 +540,9 @@ pub enum CheckCodePrefix {
UP02,
UP020,
UP021,
UP022,
UP023,
UP025,
W,
W2,
W29,
@@ -702,6 +713,7 @@ impl CheckCodePrefix {
CheckCode::C417,
CheckCode::T100,
CheckCode::C901,
CheckCode::TID251,
CheckCode::TID252,
CheckCode::RET501,
CheckCode::RET502,
@@ -711,6 +723,9 @@ impl CheckCodePrefix {
CheckCode::RET506,
CheckCode::RET507,
CheckCode::RET508,
CheckCode::ISC001,
CheckCode::ISC002,
CheckCode::ISC003,
CheckCode::T201,
CheckCode::T203,
CheckCode::Q000,
@@ -759,6 +774,9 @@ impl CheckCodePrefix {
CheckCode::UP019,
CheckCode::UP020,
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP025,
CheckCode::D100,
CheckCode::D101,
CheckCode::D102,
@@ -853,6 +871,7 @@ impl CheckCodePrefix {
CheckCode::PGH001,
CheckCode::PGH002,
CheckCode::PGH003,
CheckCode::PGH004,
CheckCode::PD002,
CheckCode::PD003,
CheckCode::PD004,
@@ -1656,7 +1675,7 @@ impl CheckCodePrefix {
":".bold(),
"`I2` has been remapped to `TID2`".bold()
);
vec![CheckCode::TID252]
vec![CheckCode::TID251, CheckCode::TID252]
}
CheckCodePrefix::I25 => {
one_time_warning!(
@@ -1665,7 +1684,7 @@ impl CheckCodePrefix {
":".bold(),
"`I25` has been remapped to `TID25`".bold()
);
vec![CheckCode::TID252]
vec![CheckCode::TID251, CheckCode::TID252]
}
CheckCodePrefix::I252 => {
one_time_warning!(
@@ -1734,6 +1753,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!(
"{}{} {}",
@@ -2069,12 +2094,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]
}
@@ -2383,9 +2424,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!(
@@ -2415,6 +2457,9 @@ impl CheckCodePrefix {
CheckCode::UP019,
CheckCode::UP020,
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP025,
]
}
CheckCodePrefix::U0 => {
@@ -2445,6 +2490,9 @@ impl CheckCodePrefix {
CheckCode::UP019,
CheckCode::UP020,
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP025,
]
}
CheckCodePrefix::U00 => {
@@ -2659,6 +2707,9 @@ impl CheckCodePrefix {
CheckCode::UP019,
CheckCode::UP020,
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP025,
],
CheckCodePrefix::UP0 => vec![
CheckCode::UP001,
@@ -2681,6 +2732,9 @@ impl CheckCodePrefix {
CheckCode::UP019,
CheckCode::UP020,
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP025,
],
CheckCodePrefix::UP00 => vec![
CheckCode::UP001,
@@ -2722,9 +2776,18 @@ impl CheckCodePrefix {
CheckCodePrefix::UP017 => vec![CheckCode::UP017],
CheckCodePrefix::UP018 => vec![CheckCode::UP018],
CheckCodePrefix::UP019 => vec![CheckCode::UP019],
CheckCodePrefix::UP02 => vec![CheckCode::UP020, CheckCode::UP021],
CheckCodePrefix::UP02 => vec![
CheckCode::UP020,
CheckCode::UP021,
CheckCode::UP022,
CheckCode::UP023,
CheckCode::UP025,
],
CheckCodePrefix::UP020 => vec![CheckCode::UP020],
CheckCodePrefix::UP021 => vec![CheckCode::UP021],
CheckCodePrefix::UP022 => vec![CheckCode::UP022],
CheckCodePrefix::UP023 => vec![CheckCode::UP023],
CheckCodePrefix::UP025 => vec![CheckCode::UP025],
CheckCodePrefix::W => vec![CheckCode::W292, CheckCode::W605],
CheckCodePrefix::W2 => vec![CheckCode::W292],
CheckCodePrefix::W29 => vec![CheckCode::W292],
@@ -3070,6 +3133,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,
@@ -3133,6 +3202,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,
@@ -3241,6 +3311,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,
@@ -3288,6 +3359,9 @@ impl CheckCodePrefix {
CheckCodePrefix::UP02 => SuffixLength::Two,
CheckCodePrefix::UP020 => SuffixLength::Three,
CheckCodePrefix::UP021 => SuffixLength::Three,
CheckCodePrefix::UP022 => SuffixLength::Three,
CheckCodePrefix::UP023 => SuffixLength::Three,
CheckCodePrefix::UP025 => SuffixLength::Three,
CheckCodePrefix::W => SuffixLength::Zero,
CheckCodePrefix::W2 => SuffixLength::One,
CheckCodePrefix::W29 => SuffixLength::Two,
@@ -3333,6 +3407,7 @@ pub const CATEGORIES: &[CheckCodePrefix] = &[
CheckCodePrefix::FBT,
CheckCodePrefix::I,
CheckCodePrefix::ICN,
CheckCodePrefix::ISC,
CheckCodePrefix::N,
CheckCodePrefix::PD,
CheckCodePrefix::PGH,

View File

@@ -50,6 +50,10 @@ pub struct Cli {
fix_only: bool,
#[clap(long, overrides_with("fix_only"), hide = true)]
no_fix_only: bool,
/// Avoid writing any fixed files back; instead, output a diff for each
/// changed file to stdout.
#[arg(long)]
pub diff: bool,
/// Disable cache reads.
#[arg(short, long)]
pub no_cache: bool,
@@ -101,10 +105,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,
/// 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,
/// See the files Ruff will be run against with the current settings.
#[arg(long)]
pub show_files: bool,
@@ -154,6 +163,7 @@ impl Cli {
add_noqa: self.add_noqa,
autoformat: self.autoformat,
config: self.config,
diff: self.diff,
exit_zero: self.exit_zero,
explain: self.explain,
files: self.files,
@@ -187,11 +197,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),
},
)
}
@@ -213,6 +224,7 @@ pub struct Arguments {
pub add_noqa: bool,
pub autoformat: bool,
pub config: Option<PathBuf>,
pub diff: bool,
pub exit_zero: bool,
pub explain: Option<CheckCode>,
pub files: Vec<PathBuf>,
@@ -247,11 +259,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

@@ -332,6 +332,9 @@ pub fn explain(code: &CheckCode, format: &SerializationFormat) -> Result<()> {
SerializationFormat::Github => {
bail!("`--explain` does not support GitHub format")
}
SerializationFormat::Gitlab => {
bail!("`--explain` does not support GitLab format")
}
};
Ok(())
}

View File

@@ -5,7 +5,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, JsonSchema,
)]
#[serde(
deny_unknown_fields,
@@ -51,7 +51,7 @@ pub struct Options {
pub allow_star_arg_any: Option<bool>,
}
#[derive(Debug, Hash, Default)]
#[derive(Debug, Default, Hash)]
#[allow(clippy::struct_excessive_bools)]
pub struct Settings {
pub mypy_init_return: bool,
@@ -60,14 +60,24 @@ pub struct Settings {
pub allow_star_arg_any: bool,
}
impl Settings {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: Options) -> Self {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
mypy_init_return: options.mypy_init_return.unwrap_or_default(),
suppress_dummy_args: options.suppress_dummy_args.unwrap_or_default(),
suppress_none_returning: options.suppress_none_returning.unwrap_or_default(),
allow_star_arg_any: options.allow_star_arg_any.unwrap_or_default(),
mypy_init_return: options.mypy_init_return.unwrap_or(false),
suppress_dummy_args: options.suppress_dummy_args.unwrap_or(false),
suppress_none_returning: options.suppress_none_returning.unwrap_or(false),
allow_star_arg_any: options.allow_star_arg_any.unwrap_or(false),
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
mypy_init_return: Some(settings.mypy_init_return),
suppress_dummy_args: Some(settings.suppress_dummy_args),
suppress_none_returning: Some(settings.suppress_none_returning),
allow_star_arg_any: Some(settings.allow_star_arg_any),
}
}
}

View File

@@ -5,7 +5,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
Debug, PartialEq, Eq, Default, Serialize, Deserialize, ConfigurationOptions, JsonSchema,
)]
#[serde(
deny_unknown_fields,
@@ -26,15 +26,23 @@ pub struct Options {
pub extend_immutable_calls: Option<Vec<String>>,
}
#[derive(Debug, Hash, Default)]
#[derive(Debug, Default, Hash)]
pub struct Settings {
pub extend_immutable_calls: Vec<String>,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
extend_immutable_calls: options.extend_immutable_calls.unwrap_or_default(),
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
extend_immutable_calls: Some(settings.extend_immutable_calls),
}
}
}

View File

@@ -27,11 +27,18 @@ pub struct Settings {
pub max_string_length: usize,
}
impl Settings {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: Options) -> Self {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
max_string_length: options.max_string_length.unwrap_or_default(),
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
max_string_length: Some(settings.max_string_length),
}
}
}

View File

@@ -0,0 +1,73 @@
use itertools::Itertools;
use rustpython_ast::{Constant, Expr, ExprKind, Location, Operator};
use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::source_code_locator::SourceCodeLocator;
/// ISC001, ISC002
pub fn implicit(tokens: &[LexResult], locator: &SourceCodeLocator) -> Vec<Check> {
let mut checks = vec![];
for ((a_start, a_tok, a_end), (b_start, b_tok, b_end)) in
tokens.iter().flatten().tuple_windows()
{
if matches!(a_tok, Tok::String { .. }) && matches!(b_tok, Tok::String { .. }) {
if a_end.row() == b_start.row() {
checks.push(Check::new(
CheckKind::SingleLineImplicitStringConcatenation,
Range {
location: *a_start,
end_location: *b_end,
},
));
} else {
// TODO(charlie): The RustPython tokenization doesn't differentiate between
// continuations and newlines, so we have to detect them manually.
let contents = locator.slice_source_code_range(&Range {
location: *a_end,
end_location: Location::new(a_end.row() + 1, 0),
});
if contents.trim().ends_with('\\') {
checks.push(Check::new(
CheckKind::MultiLineImplicitStringConcatenation,
Range {
location: *a_start,
end_location: *b_end,
},
));
}
}
}
}
checks
}
/// ISC003
pub fn explicit(expr: &Expr) -> Option<Check> {
if let ExprKind::BinOp { left, op, right } = &expr.node {
if matches!(op, Operator::Add) {
if matches!(
left.node,
ExprKind::JoinedStr { .. }
| ExprKind::Constant {
value: Constant::Str(..) | Constant::Bytes(..),
..
}
) && matches!(
right.node,
ExprKind::JoinedStr { .. }
| ExprKind::Constant {
value: Constant::Str(..) | Constant::Bytes(..),
..
}
) {
return Some(Check::new(
CheckKind::ExplicitStringConcatenation,
Range::from_located(expr),
));
}
}
}
None
}

View File

@@ -0,0 +1,30 @@
pub mod checks;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::checks::CheckCode;
use crate::linter::test_path;
use crate::settings;
#[test_case(CheckCode::ISC001, Path::new("ISC.py"); "ISC001")]
#[test_case(CheckCode::ISC002, Path::new("ISC.py"); "ISC002")]
#[test_case(CheckCode::ISC003, Path::new("ISC.py"); "ISC003")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_implicit_str_concat")
.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

@@ -0,0 +1,21 @@
---
source: src/flake8_implicit_str_concat/mod.rs
expression: checks
---
- kind: SingleLineImplicitStringConcatenation
location:
row: 1
column: 4
end_location:
row: 1
column: 11
fix: ~
- kind: SingleLineImplicitStringConcatenation
location:
row: 1
column: 8
end_location:
row: 1
column: 15
fix: ~

View File

@@ -0,0 +1,13 @@
---
source: src/flake8_implicit_str_concat/mod.rs
expression: checks
---
- kind: MultiLineImplicitStringConcatenation
location:
row: 5
column: 4
end_location:
row: 6
column: 9
fix: ~

View File

@@ -0,0 +1,37 @@
---
source: src/flake8_implicit_str_concat/mod.rs
expression: checks
---
- kind: ExplicitStringConcatenation
location:
row: 3
column: 4
end_location:
row: 3
column: 17
fix: ~
- kind: ExplicitStringConcatenation
location:
row: 9
column: 2
end_location:
row: 10
column: 7
fix: ~
- kind: ExplicitStringConcatenation
location:
row: 14
column: 2
end_location:
row: 15
column: 7
fix: ~
- kind: ExplicitStringConcatenation
location:
row: 19
column: 2
end_location:
row: 20
column: 8
fix: ~

View File

@@ -29,16 +29,14 @@ mod tests {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/custom.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([
("dask.array".to_string(), "da".to_string()),
("dask.dataframe".to_string(), "dd".to_string()),
])),
},
),
flake8_import_conventions: flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([
("dask.array".to_string(), "da".to_string()),
("dask.dataframe".to_string(), "dd".to_string()),
])),
}
.into(),
..Settings::for_rule(CheckCode::ICN001)
},
)?;
@@ -52,18 +50,16 @@ mod tests {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/remove_default.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: Some(FxHashMap::from_iter([
("altair".to_string(), "alt".to_string()),
("matplotlib.pyplot".to_string(), "plt".to_string()),
("pandas".to_string(), "pd".to_string()),
("seaborn".to_string(), "sns".to_string()),
])),
extend_aliases: None,
},
),
flake8_import_conventions: flake8_import_conventions::settings::Options {
aliases: Some(FxHashMap::from_iter([
("altair".to_string(), "alt".to_string()),
("matplotlib.pyplot".to_string(), "plt".to_string()),
("pandas".to_string(), "pd".to_string()),
("seaborn".to_string(), "sns".to_string()),
])),
extend_aliases: None,
}
.into(),
..Settings::for_rule(CheckCode::ICN001)
},
)?;
@@ -77,16 +73,14 @@ mod tests {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_import_conventions/override_default.py"),
&Settings {
flake8_import_conventions:
flake8_import_conventions::settings::Settings::from_options(
flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([(
"numpy".to_string(),
"nmp".to_string(),
)])),
},
),
flake8_import_conventions: flake8_import_conventions::settings::Options {
aliases: None,
extend_aliases: Some(FxHashMap::from_iter([(
"numpy".to_string(),
"nmp".to_string(),
)])),
}
.into(),
..Settings::for_rule(CheckCode::ICN001)
},
)?;

View File

@@ -84,14 +84,6 @@ fn resolve_aliases(options: Options) -> FxHashMap<String, String> {
aliases
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
aliases: resolve_aliases(options),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self {
@@ -99,3 +91,20 @@ impl Default for Settings {
}
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
aliases: resolve_aliases(options),
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
aliases: Some(settings.aliases),
extend_aliases: None,
}
}
}

View File

@@ -7,10 +7,18 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Quote {
/// Use single quotes (`'`).
Single,
/// Use double quotes (`"`).
Double,
}
impl Default for Quote {
fn default() -> Self {
Self::Double
}
}
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
)]
@@ -72,24 +80,35 @@ pub struct Settings {
pub avoid_escape: bool,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
impl Default for Settings {
fn default() -> Self {
Self {
inline_quotes: options.inline_quotes.unwrap_or(Quote::Double),
multiline_quotes: options.multiline_quotes.unwrap_or(Quote::Double),
docstring_quotes: options.docstring_quotes.unwrap_or(Quote::Double),
inline_quotes: Quote::default(),
multiline_quotes: Quote::default(),
docstring_quotes: Quote::default(),
avoid_escape: true,
}
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
inline_quotes: options.inline_quotes.unwrap_or_default(),
multiline_quotes: options.multiline_quotes.unwrap_or_default(),
docstring_quotes: options.docstring_quotes.unwrap_or_default(),
avoid_escape: options.avoid_escape.unwrap_or(true),
}
}
}
impl Default for Settings {
fn default() -> Self {
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
inline_quotes: Quote::Double,
multiline_quotes: Quote::Double,
docstring_quotes: Quote::Double,
avoid_escape: true,
inline_quotes: Some(settings.inline_quotes),
multiline_quotes: Some(settings.multiline_quotes),
docstring_quotes: Some(settings.docstring_quotes),
avoid_escape: Some(settings.avoid_escape),
}
}
}

View File

@@ -1,9 +1,14 @@
use rustpython_ast::Stmt;
use rustc_hash::FxHashMap;
use rustpython_ast::{Alias, Expr, Located, Stmt};
use super::settings::BannedApi;
use crate::ast::helpers::match_call_path;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::checks::{Check, CheckKind};
use crate::flake8_tidy_imports::settings::Strictness;
/// TID252
pub fn banned_relative_import(
stmt: &Stmt,
level: Option<&usize>,
@@ -22,3 +27,71 @@ pub fn banned_relative_import(
None
}
}
/// TID251
pub fn name_is_banned(
module: &str,
name: &Alias,
banned_apis: &FxHashMap<String, BannedApi>,
) -> Option<Check> {
let full_name = format!("{module}.{}", &name.node.name);
if let Some(ban) = banned_apis.get(&full_name) {
return Some(Check::new(
CheckKind::BannedApi {
name: full_name,
message: ban.msg.to_string(),
},
Range::from_located(name),
));
}
None
}
/// TID251
pub fn name_or_parent_is_banned<T>(
located: &Located<T>,
name: &str,
banned_apis: &FxHashMap<String, BannedApi>,
) -> Option<Check> {
let mut name = name;
loop {
if let Some(ban) = banned_apis.get(name) {
return Some(Check::new(
CheckKind::BannedApi {
name: name.to_string(),
message: ban.msg.to_string(),
},
Range::from_located(located),
));
}
match name.rfind('.') {
Some(idx) => {
name = &name[..idx];
}
None => return None,
}
}
}
/// TID251
pub fn banned_attribute_access(
checker: &mut Checker,
call_path: &[&str],
expr: &Expr,
banned_apis: &FxHashMap<String, BannedApi>,
) {
for (banned_path, ban) in banned_apis {
if let Some((module, member)) = banned_path.rsplit_once('.') {
if match_call_path(call_path, module, member, &checker.from_imports) {
checker.add_check(Check::new(
CheckKind::BannedApi {
name: banned_path.to_string(),
message: ban.msg.to_string(),
},
Range::from_located(expr),
));
return;
}
}
}
}

View File

@@ -6,9 +6,10 @@ mod tests {
use std::path::Path;
use anyhow::Result;
use rustc_hash::FxHashMap;
use crate::checks::CheckCode;
use crate::flake8_tidy_imports::settings::Strictness;
use crate::flake8_tidy_imports::settings::{BannedApi, Strictness};
use crate::linter::test_path;
use crate::{flake8_tidy_imports, Settings};
@@ -19,6 +20,7 @@ mod tests {
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::Parents,
..Default::default()
},
..Settings::for_rules(vec![CheckCode::TID252])
},
@@ -35,6 +37,7 @@ mod tests {
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
ban_relative_imports: Strictness::All,
..Default::default()
},
..Settings::for_rules(vec![CheckCode::TID252])
},
@@ -43,4 +46,56 @@ mod tests {
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn banned_api_true_positives() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID251.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
banned_api: FxHashMap::from_iter([
(
"cgi".to_string(),
BannedApi {
msg: "The cgi module is deprecated.".to_string(),
},
),
(
"typing.TypedDict".to_string(),
BannedApi {
msg: "Use typing_extensions.TypedDict instead.".to_string(),
},
),
]),
..Default::default()
},
..Settings::for_rules(vec![CheckCode::TID251])
},
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
#[test]
fn banned_api_false_positives() -> Result<()> {
let mut checks = test_path(
Path::new("./resources/test/fixtures/flake8_tidy_imports/TID251_false_positives.py"),
&Settings {
flake8_tidy_imports: flake8_tidy_imports::settings::Settings {
banned_api: FxHashMap::from_iter([(
"typing.TypedDict".to_string(),
BannedApi {
msg: "Use typing_extensions.TypedDict instead.".to_string(),
},
)]),
..Default::default()
},
..Settings::for_rules(vec![CheckCode::TID251])
},
)?;
checks.sort_by_key(|check| check.location);
insta::assert_yaml_snapshot!(checks);
Ok(())
}
}

View File

@@ -1,16 +1,29 @@
//! Settings for the `flake8-tidy-imports` plugin.
use std::hash::{Hash, Hasher};
use itertools::Itertools;
use ruff_macros::ConfigurationOptions;
use rustc_hash::FxHashMap;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Strictness {
/// Ban imports that extend into the parent module or beyond.
Parents,
/// Ban all relative imports.
All,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub struct BannedApi {
/// The message to display when the API is used.
pub msg: String,
}
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
)]
@@ -29,27 +42,62 @@ pub struct Options {
"#
)]
/// 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"`).
pub ban_relative_imports: Option<Strictness>,
#[option(
default = r#"{}"#,
value_type = "HashMap<String, BannedApi>",
example = r#"
[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."
"#
)]
/// 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`.
pub banned_api: FxHashMap<String, BannedApi>,
}
#[derive(Debug, Hash)]
#[derive(Debug)]
pub struct Settings {
pub ban_relative_imports: Strictness,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
Self {
ban_relative_imports: options.ban_relative_imports.unwrap_or(Strictness::Parents),
}
}
pub banned_api: FxHashMap<String, BannedApi>,
}
impl Default for Settings {
fn default() -> Self {
Self {
ban_relative_imports: Strictness::Parents,
banned_api: FxHashMap::default(),
}
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
ban_relative_imports: options.ban_relative_imports.unwrap_or(Strictness::Parents),
banned_api: options.banned_api,
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
ban_relative_imports: Some(settings.ban_relative_imports),
banned_api: settings.banned_api,
}
}
}
impl Hash for Settings {
fn hash<H: Hasher>(&self, state: &mut H) {
self.ban_relative_imports.hash(state);
for key in self.banned_api.keys().sorted() {
key.hash(state);
self.banned_api[key].hash(state);
}
}
}

View File

@@ -0,0 +1,38 @@
---
source: src/flake8_tidy_imports/mod.rs
expression: checks
---
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 2
column: 7
end_location:
row: 2
column: 23
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 7
column: 0
end_location:
row: 7
column: 16
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 11
column: 4
end_location:
row: 11
column: 20
fix: ~

View File

@@ -0,0 +1,137 @@
---
source: src/flake8_tidy_imports/mod.rs
expression: checks
---
- kind:
BannedApi:
name: cgi
message: The cgi module is deprecated.
location:
row: 2
column: 7
end_location:
row: 2
column: 10
fix: ~
- kind:
BannedApi:
name: cgi
message: The cgi module is deprecated.
location:
row: 4
column: 0
end_location:
row: 4
column: 17
fix: ~
- kind:
BannedApi:
name: cgi
message: The cgi module is deprecated.
location:
row: 6
column: 0
end_location:
row: 6
column: 23
fix: ~
- kind:
BannedApi:
name: cgi
message: The cgi module is deprecated.
location:
row: 9
column: 7
end_location:
row: 9
column: 18
fix: ~
- kind:
BannedApi:
name: cgi
message: The cgi module is deprecated.
location:
row: 11
column: 0
end_location:
row: 11
column: 23
fix: ~
- kind:
BannedApi:
name: cgi
message: The cgi module is deprecated.
location:
row: 13
column: 0
end_location:
row: 13
column: 25
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 17
column: 19
end_location:
row: 17
column: 28
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 22
column: 0
end_location:
row: 22
column: 16
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 24
column: 0
end_location:
row: 24
column: 16
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 27
column: 0
end_location:
row: 27
column: 16
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 29
column: 0
end_location:
row: 29
column: 16
fix: ~
- kind:
BannedApi:
name: typing.TypedDict
message: Use typing_extensions.TypedDict instead.
location:
row: 33
column: 0
end_location:
row: 33
column: 28
fix: ~

View File

@@ -22,16 +22,23 @@ pub struct Options {
pub ignore_variadic_names: Option<bool>,
}
#[derive(Debug, Hash, Default)]
#[derive(Debug, Default, Hash)]
pub struct Settings {
pub ignore_variadic_names: bool,
}
impl Settings {
#[allow(clippy::needless_pass_by_value)]
pub fn from_options(options: Options) -> Self {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
ignore_variadic_names: options.ignore_variadic_names.unwrap_or_default(),
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
ignore_variadic_names: Some(settings.ignore_variadic_names),
}
}
}

View File

@@ -123,8 +123,23 @@ pub struct Settings {
pub extra_standard_library: BTreeSet<String>,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
impl Default for Settings {
fn default() -> Self {
Self {
combine_as_imports: false,
force_wrap_aliases: false,
split_on_trailing_comma: true,
force_single_line: false,
single_line_exclusions: BTreeSet::new(),
known_first_party: BTreeSet::new(),
known_third_party: BTreeSet::new(),
extra_standard_library: BTreeSet::new(),
}
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
combine_as_imports: options.combine_as_imports.unwrap_or(false),
force_wrap_aliases: options.force_wrap_aliases.unwrap_or(false),
@@ -142,17 +157,17 @@ impl Settings {
}
}
impl Default for Settings {
fn default() -> Self {
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
combine_as_imports: false,
force_wrap_aliases: false,
split_on_trailing_comma: true,
force_single_line: false,
single_line_exclusions: BTreeSet::new(),
known_first_party: BTreeSet::new(),
known_third_party: BTreeSet::new(),
extra_standard_library: BTreeSet::new(),
combine_as_imports: Some(settings.combine_as_imports),
force_wrap_aliases: Some(settings.force_wrap_aliases),
split_on_trailing_comma: Some(settings.split_on_trailing_comma),
force_single_line: Some(settings.force_single_line),
single_line_exclusions: Some(settings.single_line_exclusions.into_iter().collect()),
known_first_party: Some(settings.known_first_party.into_iter().collect()),
known_third_party: Some(settings.known_third_party.into_iter().collect()),
extra_standard_library: Some(settings.extra_standard_library.into_iter().collect()),
}
}
}

View File

@@ -39,6 +39,7 @@ mod flake8_comprehensions;
mod flake8_datetimez;
mod flake8_debugger;
pub mod flake8_errmsg;
mod flake8_implicit_str_concat;
mod flake8_import_conventions;
mod flake8_print;
pub mod flake8_quotes;

View File

@@ -7,14 +7,22 @@ use wasm_bindgen::prelude::*;
use crate::autofix::Fix;
use crate::checks::CheckCode;
use crate::directives;
use crate::checks_gen::CheckCodePrefix;
use crate::linter::check_path;
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::options::Options;
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{
directives, flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_import_conventions,
flake8_quotes, flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming,
pydocstyle, pyupgrade,
};
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[wasm_bindgen(typescript_custom_section)]
const TYPES: &'static str = r#"
@@ -60,6 +68,65 @@ pub fn run() {
}
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn currentVersion() -> JsValue {
JsValue::from(VERSION)
}
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn defaultSettings() -> Result<JsValue, JsValue> {
Ok(serde_wasm_bindgen::to_value(&Options {
// Propagate defaults.
allowed_confusables: Some(Vec::default()),
dummy_variable_rgx: Some("^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$".to_string()),
extend_ignore: Some(Vec::default()),
extend_select: Some(Vec::default()),
external: Some(Vec::default()),
ignore: Some(Vec::default()),
line_length: Some(88),
select: Some(vec![CheckCodePrefix::E, CheckCodePrefix::F]),
target_version: Some(PythonVersion::default()),
// Ignore a bunch of options that don't make sense in a single-file editor.
cache_dir: None,
exclude: None,
extend: None,
extend_exclude: None,
fix: None,
fix_only: None,
fixable: None,
force_exclude: None,
format: None,
ignore_init_module_imports: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
show_source: None,
src: None,
unfixable: None,
update_check: None,
// Use default options for all plugins.
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),
flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()),
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
flake8_tidy_imports: Some(flake8_tidy_imports::settings::Settings::default().into()),
flake8_import_conventions: Some(
flake8_import_conventions::settings::Settings::default().into(),
),
flake8_unused_arguments: Some(
flake8_unused_arguments::settings::Settings::default().into(),
),
isort: Some(isort::settings::Settings::default().into()),
mccabe: Some(mccabe::settings::Settings::default().into()),
pep8_naming: Some(pep8_naming::settings::Settings::default().into()),
pydocstyle: Some(pydocstyle::settings::Settings::default().into()),
pyupgrade: Some(pyupgrade::settings::Settings::default().into()),
})?)
}
#[wasm_bindgen]
#[allow(non_snake_case)]
pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
let options: Options = serde_wasm_bindgen::from_value(options).map_err(|e| e.to_string())?;
let configuration =

View File

@@ -5,8 +5,10 @@ use std::ops::AddAssign;
use std::path::Path;
use anyhow::Result;
use colored::Colorize;
use log::debug;
use rustpython_parser::lexer::LexResult;
use similar::TextDiff;
use crate::ast::types::Range;
use crate::autofix::fixer;
@@ -26,6 +28,9 @@ use crate::source_code_locator::SourceCodeLocator;
use crate::source_code_style::SourceCodeStyleDetector;
use crate::{cache, directives, fs, rustpython_helpers};
const CARGO_PKG_NAME: &str = env!("CARGO_PKG_NAME");
const CARGO_PKG_REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY");
#[derive(Debug, Default)]
pub struct Diagnostics {
pub messages: Vec<Message>,
@@ -183,26 +188,54 @@ pub fn lint_path(
// Validate the `Settings` and return any errors.
settings.validate()?;
let metadata = path.metadata()?;
// Check the cache.
if let Some(messages) = cache::get(path, &metadata, settings, autofix, cache) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(Diagnostics::new(messages));
}
// TODO(charlie): `fixer::Mode::Apply` and `fixer::Mode::Diff` both have
// side-effects that aren't captured in the cache. (In practice, it's fine
// to cache `fixer::Mode::Apply`, since a file either has no fixes, or we'll
// write the fixes to disk, thus invalidating the cache. But it's a bit hard
// to reason about. We need to come up with a better solution here.)
let metadata = if matches!(cache, flags::Cache::Enabled)
&& matches!(autofix, fixer::Mode::None | fixer::Mode::Generate)
{
let metadata = path.metadata()?;
if let Some(messages) = cache::get(path, &metadata, settings, autofix.into()) {
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(Diagnostics::new(messages));
}
Some(metadata)
} else {
None
};
// Read the file from disk.
let contents = fs::read_file(path)?;
// Lint the file.
let (contents, fixed, messages) = lint(contents, path, package, settings, autofix)?;
let (messages, fixed) = if matches!(autofix, fixer::Mode::Apply | fixer::Mode::Diff) {
let (transformed, fixed, messages) = lint_fix(&contents, path, package, settings)?;
if fixed > 0 {
if matches!(autofix, fixer::Mode::Apply) {
write(path, transformed)?;
} else if matches!(autofix, fixer::Mode::Diff) {
let mut stdout = io::stdout().lock();
TextDiff::from_lines(&contents, &transformed)
.unified_diff()
.header(&fs::relativize_path(path), &fs::relativize_path(path))
.to_writer(&mut stdout)?;
stdout.write_all(b"\n")?;
stdout.flush()?;
}
}
(messages, fixed)
} else {
let messages = lint_only(&contents, path, package, settings, autofix.into())?;
let fixed = 0;
(messages, fixed)
};
// Re-populate the cache.
cache::set(path, &metadata, settings, autofix, &messages, cache);
// If we applied any fixes, write the contents back to disk.
if fixed > 0 {
write(path, contents)?;
if let Some(metadata) = metadata {
cache::set(path, &metadata, settings, autofix.into(), &messages);
}
Ok(Diagnostics { messages, fixed })
@@ -286,40 +319,121 @@ pub fn autoformat_path(path: &Path, settings: &Settings) -> Result<()> {
pub fn lint_stdin(
path: Option<&Path>,
package: Option<&Path>,
stdin: &str,
contents: &str,
settings: &Settings,
autofix: fixer::Mode,
) -> Result<Diagnostics> {
// Validate the `Settings` and return any errors.
settings.validate()?;
// Read the file from disk.
let contents = stdin.to_string();
// Lint the inputs.
let (messages, fixed) = if matches!(autofix, fixer::Mode::Apply | fixer::Mode::Diff) {
let (transformed, fixed, messages) = lint_fix(
contents,
path.unwrap_or_else(|| Path::new("-")),
package,
settings,
)?;
// Lint the file.
let (contents, fixed, messages) = lint(
contents,
path.unwrap_or_else(|| Path::new("-")),
package,
settings,
autofix,
)?;
if matches!(autofix, fixer::Mode::Apply) {
// Write the contents to stdout, regardless of whether any errors were fixed.
io::stdout().write_all(transformed.as_bytes())?;
} else if matches!(autofix, fixer::Mode::Diff) {
// But only write a diff if it's non-empty.
if fixed > 0 {
let text_diff = TextDiff::from_lines(contents, &transformed);
let mut unified_diff = text_diff.unified_diff();
if let Some(path) = path {
unified_diff.header(&fs::relativize_path(path), &fs::relativize_path(path));
}
// Write the fixed contents to stdout.
if matches!(autofix, fixer::Mode::Apply) {
io::stdout().write_all(contents.as_bytes())?;
}
let mut stdout = io::stdout().lock();
unified_diff.to_writer(&mut stdout)?;
stdout.write_all(b"\n")?;
stdout.flush()?;
}
}
(messages, fixed)
} else {
let messages = lint_only(
contents,
path.unwrap_or_else(|| Path::new("-")),
package,
settings,
autofix.into(),
)?;
let fixed = 0;
(messages, fixed)
};
Ok(Diagnostics { messages, fixed })
}
fn lint(
mut contents: String,
/// Generate a list of `Check` violations (optionally including any autofix
/// patches) from source code content.
fn lint_only(
contents: &str,
path: &Path,
package: Option<&Path>,
settings: &Settings,
autofix: flags::Autofix,
) -> Result<Vec<Message>> {
// Tokenize once.
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(contents);
// Map row and column locations to byte slices (lazily).
let locator = SourceCodeLocator::new(contents);
// Detect the current code style (lazily).
let stylist = SourceCodeStyleDetector::from_contents(contents, &locator);
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives = directives::extract_directives(
&tokens,
&locator,
directives::Flags::from_settings(settings),
);
// Generate checks.
let checks = check_path(
path,
package,
contents,
tokens,
&locator,
&stylist,
&directives,
settings,
autofix,
flags::Noqa::Enabled,
)?;
// Convert from checks to messages.
let path_lossy = path.to_string_lossy();
Ok(checks
.into_iter()
.map(|check| {
let source = if settings.show_source {
Some(Source::from_check(&check, &locator))
} else {
None
};
Message::from_check(check, path_lossy.to_string(), source)
})
.collect())
}
/// Generate a list of `Check` violations from source code content, iteratively
/// autofixing any violations until stable.
fn lint_fix(
contents: &str,
path: &Path,
package: Option<&Path>,
settings: &Settings,
autofix: fixer::Mode,
) -> Result<(String, usize, Vec<Message>)> {
let mut contents = contents.to_string();
// Track the number of fixed errors across iterations.
let mut fixed = 0;
@@ -327,7 +441,7 @@ fn lint(
let mut iterations = 0;
// Continuously autofix until the source code stabilizes.
let messages = loop {
loop {
// Tokenize once.
let tokens: Vec<LexResult> = rustpython_helpers::tokenize(&contents);
@@ -354,13 +468,13 @@ fn lint(
&stylist,
&directives,
settings,
autofix.into(),
flags::Autofix::Enabled,
flags::Noqa::Enabled,
)?;
// Apply autofix.
if matches!(autofix, fixer::Mode::Apply) && iterations < MAX_ITERATIONS {
if let Some((fixed_contents, applied)) = fix_file(&checks, &locator) {
if let Some((fixed_contents, applied)) = fix_file(&checks, &locator) {
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
fixed += applied;
@@ -373,11 +487,29 @@ fn lint(
// Re-run the linter pass (by avoiding the break).
continue;
}
eprintln!(
"
{}: Failed to converge after {} iterations.
This likely indicates a bug in `{}`. If you could open an issue at:
{}/issues
quoting the contents of `{}`, along with the `pyproject.toml` settings and executed command, we'd \
be very appreciative!
",
"warning".yellow().bold(),
MAX_ITERATIONS,
CARGO_PKG_NAME,
CARGO_PKG_REPOSITORY,
fs::relativize_path(path),
);
}
// Convert to messages.
let filename = path.to_string_lossy().to_string();
break checks
let path_lossy = path.to_string_lossy();
let messages = checks
.into_iter()
.map(|check| {
let source = if settings.show_source {
@@ -385,12 +517,11 @@ fn lint(
} else {
None
};
Message::from_check(check, filename.clone(), source)
Message::from_check(check, path_lossy.to_string(), source)
})
.collect();
};
Ok((contents, fixed, messages))
return Ok((contents, fixed, messages));
}
}
#[cfg(test)]

View File

@@ -117,25 +117,20 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
PyprojectDiscovery::Hierarchical(settings) => settings.respect_gitignore,
},
};
let (fix, fix_only, format) = match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => (settings.fix, settings.fix_only, settings.format),
PyprojectDiscovery::Hierarchical(settings) => {
(settings.fix, settings.fix_only, settings.format)
}
let (fix, fix_only, format, update_check) = match &pyproject_strategy {
PyprojectDiscovery::Fixed(settings) => (
settings.fix,
settings.fix_only,
settings.format,
settings.update_check,
),
PyprojectDiscovery::Hierarchical(settings) => (
settings.fix,
settings.fix_only,
settings.format,
settings.update_check,
),
};
let autofix = if fix || fix_only {
fixer::Mode::Apply
} else if matches!(format, SerializationFormat::Json) {
fixer::Mode::Generate
} else {
fixer::Mode::None
};
let violations = if fix_only {
Violations::Hide
} else {
Violations::Show
};
let cache = !cli.no_cache;
if let Some(code) = cli.explain {
commands::explain(&code, &format)?;
@@ -150,9 +145,35 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
return Ok(ExitCode::SUCCESS);
}
// Autofix rules are as follows:
// - If `--fix` or `--fix-only` is set, always apply fixes to the filesystem (or
// print them to stdout, if we're reading from stdin).
// - Otherwise, if `--format json` is set, generate the fixes (so we print them
// out as part of the JSON payload), but don't write them to disk.
// - If `--diff` or `--fix-only` are set, don't print any violations (only
// fixes).
// TODO(charlie): Consider adding ESLint's `--fix-dry-run`, which would generate
// but not apply fixes. That would allow us to avoid special-casing JSON
// here.
let autofix = if cli.diff {
fixer::Mode::Diff
} else if fix || fix_only {
fixer::Mode::Apply
} else if matches!(format, SerializationFormat::Json) {
fixer::Mode::Generate
} else {
fixer::Mode::None
};
let violations = if cli.diff || fix_only {
Violations::Hide
} else {
Violations::Show
};
let cache = !cli.no_cache;
let printer = Printer::new(&format, &log_level, &autofix, &violations);
if cli.watch {
if matches!(autofix, fixer::Mode::Generate | fixer::Mode::Apply) {
if !matches!(autofix, fixer::Mode::None) {
eprintln!("Warning: --fix is not enabled in watch mode.");
}
if cli.add_noqa {
@@ -251,18 +272,28 @@ pub(crate) fn inner_main() -> Result<ExitCode> {
// Always try to print violations (the printer itself may suppress output),
// unless we're writing fixes via stdin (in which case, the transformed
// source code goes to stdout).
if !(is_stdin && matches!(autofix, fixer::Mode::Apply)) {
if !(is_stdin && matches!(autofix, fixer::Mode::Apply | fixer::Mode::Diff)) {
printer.write_once(&diagnostics)?;
}
// Check for updates if we're in a non-silent log level.
#[cfg(feature = "update-informer")]
if !is_stdin && log_level >= LogLevel::Default && atty::is(atty::Stream::Stdout) {
if update_check
&& !is_stdin
&& log_level >= LogLevel::Default
&& atty::is(atty::Stream::Stdout)
{
drop(updates::check_for_updates());
}
if !diagnostics.messages.is_empty() && !cli.exit_zero && !fix_only {
return Ok(ExitCode::FAILURE);
if !cli.exit_zero {
if cli.diff || fix_only {
if diagnostics.fixed > 0 {
return Ok(ExitCode::FAILURE);
}
} else if !diagnostics.messages.is_empty() {
return Ok(ExitCode::FAILURE);
}
}
}

View File

@@ -30,16 +30,24 @@ pub struct Settings {
pub max_complexity: usize,
}
impl Settings {
pub fn from_options(options: &Options) -> Self {
impl Default for Settings {
fn default() -> Self {
Self { max_complexity: 10 }
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
max_complexity: options.max_complexity.unwrap_or_default(),
}
}
}
impl Default for Settings {
fn default() -> Self {
Self { max_complexity: 10 }
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
max_complexity: Some(settings.max_complexity),
}
}
}

View File

@@ -10,7 +10,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use crate::checks::{Check, CheckCode, CODE_REDIRECTS};
static NO_QA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"(?P<spaces>\s*)(?P<noqa>(?i:# noqa)(?::\s?(?P<codes>([A-Z]+[0-9]+(?:[,\s]+)?)+))?)",
)
@@ -39,7 +39,7 @@ pub enum Directive<'a> {
/// Extract the noqa `Directive` from a line of Python source code.
pub fn extract_noqa_directive(line: &str) -> Directive {
match NO_QA_LINE_REGEX.captures(line) {
match NOQA_LINE_REGEX.captures(line) {
Some(caps) => match caps.name("spaces") {
Some(spaces) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
@@ -206,20 +206,20 @@ mod tests {
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
use crate::noqa::{add_noqa_inner, NO_QA_LINE_REGEX};
use crate::noqa::{add_noqa_inner, NOQA_LINE_REGEX};
#[test]
fn regex() {
assert!(NO_QA_LINE_REGEX.is_match("# noqa"));
assert!(NO_QA_LINE_REGEX.is_match("# NoQA"));
assert!(NOQA_LINE_REGEX.is_match("# noqa"));
assert!(NOQA_LINE_REGEX.is_match("# NoQA"));
assert!(NO_QA_LINE_REGEX.is_match("# noqa: F401"));
assert!(NO_QA_LINE_REGEX.is_match("# NoQA: F401"));
assert!(NO_QA_LINE_REGEX.is_match("# noqa: F401, E501"));
assert!(NOQA_LINE_REGEX.is_match("# noqa: F401"));
assert!(NOQA_LINE_REGEX.is_match("# NoQA: F401"));
assert!(NOQA_LINE_REGEX.is_match("# noqa: F401, E501"));
assert!(NO_QA_LINE_REGEX.is_match("# noqa:F401"));
assert!(NO_QA_LINE_REGEX.is_match("# NoQA:F401"));
assert!(NO_QA_LINE_REGEX.is_match("# noqa:F401, E501"));
assert!(NOQA_LINE_REGEX.is_match("# noqa:F401"));
assert!(NOQA_LINE_REGEX.is_match("# NoQA:F401"));
assert!(NOQA_LINE_REGEX.is_match("# noqa:F401, E501"));
}
#[test]

View File

@@ -76,8 +76,18 @@ pub struct Settings {
pub staticmethod_decorators: Vec<String>,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
impl Default for Settings {
fn default() -> Self {
Self {
ignore_names: IGNORE_NAMES.map(String::from).to_vec(),
classmethod_decorators: CLASSMETHOD_DECORATORS.map(String::from).to_vec(),
staticmethod_decorators: STATICMETHOD_DECORATORS.map(String::from).to_vec(),
}
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
ignore_names: options
.ignore_names
@@ -92,12 +102,12 @@ impl Settings {
}
}
impl Default for Settings {
fn default() -> Self {
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
ignore_names: IGNORE_NAMES.map(String::from).to_vec(),
classmethod_decorators: CLASSMETHOD_DECORATORS.map(String::from).to_vec(),
staticmethod_decorators: STATICMETHOD_DECORATORS.map(String::from).to_vec(),
ignore_names: Some(settings.ignore_names),
classmethod_decorators: Some(settings.classmethod_decorators),
staticmethod_decorators: Some(settings.staticmethod_decorators),
}
}
}

View File

@@ -8,6 +8,7 @@ use colored::Colorize;
use itertools::iterate;
use rustpython_parser::ast::Location;
use serde::Serialize;
use serde_json::json;
use crate::autofix::{fixer, Fix};
use crate::checks::CheckCode;
@@ -89,7 +90,11 @@ impl<'a> Printer<'a> {
Violations::Hide => {
let fixed = diagnostics.fixed;
if fixed > 0 {
println!("Fixed {fixed} error(s).");
if matches!(self.autofix, fixer::Mode::Apply) {
println!("Fixed {fixed} error(s).");
} else if matches!(self.autofix, fixer::Mode::Diff) {
println!("Would fix {fixed} error(s).");
}
}
}
}
@@ -134,17 +139,8 @@ impl<'a> Printer<'a> {
SerializationFormat::Junit => {
use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
// Group by filename.
let mut grouped_messages = BTreeMap::default();
for message in &diagnostics.messages {
grouped_messages
.entry(&message.filename)
.or_insert_with(Vec::new)
.push(message);
}
let mut report = Report::new("ruff");
for (filename, messages) in grouped_messages {
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
let mut test_suite = TestSuite::new(filename);
test_suite
.extra
@@ -183,16 +179,7 @@ impl<'a> Printer<'a> {
self.post_text(diagnostics);
}
SerializationFormat::Grouped => {
// Group by filename.
let mut grouped_messages = BTreeMap::default();
for message in &diagnostics.messages {
grouped_messages
.entry(&message.filename)
.or_insert_with(Vec::new)
.push(message);
}
for (filename, messages) in grouped_messages {
for (filename, messages) in group_messages_by_filename(&diagnostics.messages) {
// Compute the maximum number of digits in the row and column, for messages in
// this file.
let row_length = num_digits(
@@ -239,6 +226,34 @@ impl<'a> Printer<'a> {
);
});
}
SerializationFormat::Gitlab => {
// Generate JSON with errors in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implementing-a-custom-tool
println!(
"{}",
serde_json::to_string_pretty(
&diagnostics
.messages
.iter()
.map(|message| {
json!({
"description": format!("({}) {}", message.kind.code(), message.kind.body()),
"severity": "major",
"fingerprint": message.kind.code(),
"location": {
"path": relativize_path(Path::new(&message.filename)),
"lines": {
"begin": message.location.row(),
"end": message.end_location.row()
}
}
})
}
)
.collect::<Vec<_>>()
)?
);
}
}
Ok(())
@@ -275,6 +290,17 @@ impl<'a> Printer<'a> {
}
}
fn group_messages_by_filename(messages: &Vec<Message>) -> BTreeMap<&String, Vec<&Message>> {
let mut grouped_messages = BTreeMap::default();
for message in messages {
grouped_messages
.entry(&message.filename)
.or_insert_with(Vec::new)
.push(message);
}
grouped_messages
}
fn num_digits(n: usize) -> usize {
iterate(n, |&n| n / 10)
.take_while(|&n| n > 0)

View File

@@ -4,10 +4,12 @@ use ruff_macros::ConfigurationOptions;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash, JsonSchema)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
pub enum Convention {
/// Use Google-style docstrings.
Google,
/// Use NumPy-style docstrings.
Numpy,
}
@@ -17,7 +19,7 @@ pub enum Convention {
#[serde(deny_unknown_fields, rename_all = "kebab-case", rename = "Pydocstyle")]
pub struct Options {
#[option(
default = r#""convention""#,
default = r#"None"#,
value_type = "Convention",
example = r#"
# Use Google-style docstrings.
@@ -35,10 +37,18 @@ pub struct Settings {
pub convention: Option<Convention>,
}
impl Settings {
pub fn from_options(options: Options) -> Self {
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
convention: options.convention,
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
convention: settings.convention,
}
}
}

View File

@@ -17,6 +17,7 @@ mod tests {
#[test_case(CheckCode::PGH002, Path::new("PGH002_0.py"); "PGH002_0")]
#[test_case(CheckCode::PGH002, Path::new("PGH002_1.py"); "PGH002_1")]
#[test_case(CheckCode::PGH003, Path::new("PGH003_0.py"); "PGH003_0")]
#[test_case(CheckCode::PGH004, Path::new("PGH004_0.py"); "PGH004_0")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -0,0 +1,22 @@
use once_cell::sync::Lazy;
use regex::Regex;
use rustpython_ast::Location;
use crate::ast::types::Range;
use crate::checks::{Check, CheckKind};
static BLANKET_NOQA_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"(?i)# noqa($|\s|:[^ ])").unwrap());
/// PGH004 - use of blanket noqa comments
pub fn blanket_noqa(lineno: usize, line: &str) -> Option<Check> {
BLANKET_NOQA_REGEX.find(line).map(|m| {
Check::new(
CheckKind::BlanketNOQA,
Range {
location: Location::new(lineno + 1, m.start()),
end_location: Location::new(lineno + 1, m.end()),
},
)
})
}

View File

@@ -1,7 +1,9 @@
pub use blanket_noqa::blanket_noqa;
pub use blanket_type_ignore::blanket_type_ignore;
pub use deprecated_log_warn::deprecated_log_warn;
pub use no_eval::no_eval;
mod blanket_noqa;
mod blanket_type_ignore;
mod deprecated_log_warn;
mod no_eval;

View File

@@ -0,0 +1,53 @@
---
source: src/pygrep_hooks/mod.rs
expression: checks
---
- kind: BlanketNOQA
location:
row: 1
column: 7
end_location:
row: 1
column: 13
fix: ~
- kind: BlanketNOQA
location:
row: 2
column: 7
end_location:
row: 2
column: 15
fix: ~
- kind: BlanketNOQA
location:
row: 3
column: 0
end_location:
row: 3
column: 6
fix: ~
- kind: BlanketNOQA
location:
row: 4
column: 0
end_location:
row: 4
column: 6
fix: ~
- kind: BlanketNOQA
location:
row: 5
column: 0
end_location:
row: 5
column: 8
fix: ~
- kind: BlanketNOQA
location:
row: 6
column: 0
end_location:
row: 6
column: 8
fix: ~

View File

@@ -40,6 +40,9 @@ mod tests {
#[test_case(CheckCode::UP018, Path::new("UP018.py"); "UP018")]
#[test_case(CheckCode::UP019, Path::new("UP019.py"); "UP019")]
#[test_case(CheckCode::UP021, Path::new("UP021.py"); "UP021")]
#[test_case(CheckCode::UP022, Path::new("UP022.py"); "UP022")]
#[test_case(CheckCode::UP023, Path::new("UP023.py"); "UP023")]
#[test_case(CheckCode::UP025, Path::new("UP025.py"); "UP025")]
fn checks(check_code: CheckCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", check_code.as_ref(), path.to_string_lossy());
let mut checks = test_path(

View File

@@ -6,7 +6,10 @@ pub use native_literals::native_literals;
pub use open_alias::open_alias;
pub use redundant_open_modes::redundant_open_modes;
pub use remove_six_compat::remove_six_compat;
pub use replace_stdout_stderr::replace_stdout_stderr;
pub use replace_universal_newlines::replace_universal_newlines;
pub use rewrite_c_element_tree::replace_c_element_tree;
pub use rewrite_unicode_literal::rewrite_unicode_literal;
pub use super_call_with_parameters::super_call_with_parameters;
pub use type_of_primitive::type_of_primitive;
pub use typing_text_str_alias::typing_text_str_alias;
@@ -26,7 +29,10 @@ mod native_literals;
mod open_alias;
mod redundant_open_modes;
mod remove_six_compat;
mod replace_stdout_stderr;
mod replace_universal_newlines;
mod rewrite_c_element_tree;
mod rewrite_unicode_literal;
mod super_call_with_parameters;
mod type_of_primitive;
mod typing_text_str_alias;

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