Compare commits

...

52 Commits

Author SHA1 Message Date
Charlie Marsh
605416922d Bump version to 0.0.233 2023-01-24 10:46:49 -05:00
Charlie Marsh
7b81f36e54 Enable executable checks on Windows (#2133) 2023-01-24 10:46:27 -05:00
Eric Roberts
ff63da9f52 Move compare to helpers file (#2131)
From discussion on https://github.com/charliermarsh/ruff/pull/2123

I didn't originally have a helpers file so I put the function in both
places but now that a helpers file exists it seems logical for it to be
there.
2023-01-24 10:30:56 -05:00
Charlie Marsh
d645a19e0a Bump version to 0.0.232 2023-01-24 09:49:07 -05:00
Charlie Marsh
30ae0d3723 Add Dagger and Great Expectations (#2130) 2023-01-24 09:48:00 -05:00
Charlie Marsh
3fb9e76012 Remove unnecessary manual Generator invocations (#2129) 2023-01-24 09:38:12 -05:00
Eric Roberts
0f283ae98c Move pycodestyle rules into individual files (#2123) 2023-01-24 09:27:26 -05:00
Martin Fischer
269926cec4 refactor: Move redirects out of RuleCodePrefix
This commit removes rule redirects such as ("U" -> "UP") from the
RuleCodePrefix enum because they complicated the generation of that enum
(which we want to change to be prefix-agnostic in the future).

To preserve backwards compatibility redirects are now resolved
before the strum-generated RuleCodePrefix::from_str is invoked.

This change also brings two other advantages:

* Redirects are now only defined once
  (previously they had to be defined twice:
  once in ruff_macros/src/rule_code_prefix.rs
  and a second time in src/registry.rs).

* The deprecated redirects will no longer be suggested in IDE
  autocompletion within pyproject.toml since they are now no
  longer part of the ruff.schema.json.
2023-01-24 09:26:19 -05:00
Martin Fischer
28018442f6 refactor: Move ALL from RuleCodePrefix to RuleSelector 2023-01-24 09:26:19 -05:00
Martin Fischer
abc9810e2b refactor: Turn RuleSelector into a newtype around RuleCodePrefix
Yet another refactor to let us implement the many-to-many mapping
between codes and rules in a prefix-agnostic way.

We want to break up the RuleCodePrefix[1] enum into smaller enums.
To facilitate that this commit  introduces a new wrapping type around
RuleCodePrefix so that we can start breaking it apart.

[1]: Actually `RuleCodePrefix` is the previous name of the autogenerated
enum ... I renamed it in b19258a243 to
RuleSelector since `ALL` isn't a prefix. This commit now renames it back
but only because the new `RuleSelector` wrapper type, introduced in this
commit, will let us move the `ALL` variant from `RuleCodePrefix` to
`RuleSelector` in the next commit.
2023-01-24 09:26:19 -05:00
Charlie Marsh
a20482961b Add tryceratops to flake8-to-ruff 2023-01-24 08:41:18 -05:00
Charlie Marsh
d97c07818e Update flake8-to-ruff to include latest plugins (#2127)
Closes #2124 (along with a release).
2023-01-24 08:39:58 -05:00
Ville Skyttä
7e92485f43 feat: autofix multi-line-summary-*-line (#2093) 2023-01-24 08:17:13 -05:00
Charlie Marsh
930c3be69d Ignore generators in flake8-return rules (#2126)
We could do a better job of handling them, but they cause too many false-positives right now.

Closes #2119.
2023-01-24 08:15:26 -05:00
Aarni Koskela
24d0a980c5 flake8-annotations: deduplicate code between functions and methods (#2125) 2023-01-24 08:03:33 -05:00
Edgar R. M
f5f0ed280a Implement EXE001 and EXE002 from flake8-executable (#2118) 2023-01-24 08:02:47 -05:00
Martin Fischer
ca58c72fc9 refactor: Convention::codes to rules_to_be_ignored 2023-01-24 07:37:34 -05:00
Martin Fischer
c40f14620a refactor: Get rid of registry::CATEGORIES 2023-01-24 07:37:34 -05:00
Martin Fischer
04300ce258 refactor: Rename SuffixLength enum to Specificity 2023-01-24 07:37:34 -05:00
Martin Fischer
ead5f948d3 refactor: Move Colorize imports where they're used 2023-01-24 07:37:34 -05:00
Martin Fischer
e93e9fae82 refactor: Make flake8_to_ruff tests even more DRY 2023-01-24 07:37:34 -05:00
Martin Fischer
f5ddec0fb3 refactor: Move resolve_select to converter module
The function is only used there and is not plugin-specific
since it also specifies the default rule selectors (F, E, W).
2023-01-24 07:37:34 -05:00
Martin Fischer
3de2a57416 refactor: Use ..Options::default() for tests 2023-01-24 07:37:34 -05:00
Hugo
b29b4084ff Add apk instructions to README (#2121) 2023-01-24 07:29:03 -05:00
Aarni Koskela
c61ca4a953 Add Home Assistant to Readme (#2120) 2023-01-24 07:27:45 -05:00
Denis Gavrilyuk
58d5ac08a8 feat: implement TRY301 (#2113) 2023-01-24 07:25:26 -05:00
Charlie Marsh
cc63a4be6a Allow flagging of multiline implicit string concatenations (#2117)
At present, `ISC001` and `ISC002` flag concatenations like the following:

```py
"a" "b"  # ISC001
"a" \
  "b"  # ISC002
```

However, multiline concatenations are allowed.

This PR adds a setting:

```toml
[tool.ruff.flake8-implicit-str-concat]
allow-multiline = false
```

Which extends `ISC002` to _also_ flag multiline concatenations, like:

```py
(
  "a"  # ISC002
  "b"
)
```

Note that this is backwards compatible, as `allow-multiline` defaults to `true`.
2023-01-24 00:01:01 -05:00
Charlie Marsh
549a5d44bc Upgrade to toml v0.6.0 (#2116)
Closes #1894.
2023-01-23 19:22:42 -05:00
Denis Gavrilyuk
d65ce6308b feat: implement TRY200 (#2087)
#2056
2023-01-23 14:12:42 -05:00
Charlie Marsh
b988a268e4 Escape curly braces when converting .format() strings (#2112)
Closes #2111.
2023-01-23 14:11:24 -05:00
Charlie Marsh
1c3265ef98 Bump version to 0.0.231 2023-01-23 12:51:09 -05:00
Maksudul Haque
8001a1639c [flake8-bandit] Added Rule S612 (Use of insecure logging.config.listen) (#2108)
ref: https://github.com/charliermarsh/ruff/issues/1646
2023-01-23 12:37:33 -05:00
Charlie Marsh
7d9c1d7a5a Add a note on some isort incompatibilities 2023-01-23 12:32:35 -05:00
Thomas MK
c5cebb106e Fix outdated description of ruff's support of isort settings (#2106)
Ruff supports more than `known-first-party`, `known-third-party`, `extra-standard-library`, and `src` nowadays.

Not sure if this is the best wording. Suggestions welcome!
2023-01-23 12:29:44 -05:00
Martin Fischer
8c61e8a1ef Improve #[derive(RuleNamespace)] error handling 2023-01-23 12:20:10 -05:00
Martin Fischer
4f338273a5 refactor: Simplify test_ruff_black_compatibility 2023-01-23 12:20:10 -05:00
Martin Fischer
648191652d refactor: Collect into Result<Vec<_>, _> 2023-01-23 12:20:10 -05:00
Martin Fischer
90558609c3 refactor: Introduce parse_doc_attr helper function 2023-01-23 12:20:10 -05:00
Martin Fischer
991d3c1ef6 refactor: Move Linter::url and Linter::name generation to proc macro
This lets us get rid of the build.rs script and results
in more developer-friendly compile error messages.
2023-01-23 12:20:10 -05:00
Simon Brugman
f472fbc6d4 docs(readme): add pypa cibuildwheel (#2107) 2023-01-23 11:39:23 -05:00
Charlie Marsh
09b65a6449 Remove some usages of default format for expressions (#2100) 2023-01-22 23:15:43 -05:00
Charlie Marsh
9d2eced941 Add flake8-simplify to CONTRIBUTING.md 2023-01-22 21:46:52 -05:00
Charlie Marsh
be0f6acb40 Change contributing to point to tryceratops 2023-01-22 21:45:20 -05:00
Steve Dignam
0c624af036 Add flake8-pie PIE800: no-unnecessary-spread (#1881)
Checks for unnecessary spreads, like `{**foo, **{"bar": True}}`
rel: https://github.com/charliermarsh/ruff/issues/1879
rel: https://github.com/charliermarsh/ruff/issues/1543
2023-01-22 21:43:34 -05:00
Steve Dignam
4ca328f964 Add flake8-pie PIE804: no-unnecessary-dict-kwargs (#1884)
Warn about things like `foo(**{"bar": True})` which is equivalent to `foo(bar=True)`

rel: https://github.com/charliermarsh/ruff/issues/1879
rel: https://github.com/charliermarsh/ruff/issues/1543
2023-01-22 21:32:45 -05:00
Charlie Marsh
07b5bf7030 Remove misleading emoji comment 2023-01-22 21:23:55 -05:00
Charlie Marsh
f40ae943a7 Fix bad documentation message for init option 2023-01-22 19:25:23 -05:00
Charlie Marsh
8d46d3bfa6 Avoid nested-if violations when outer-if has else clause (#2095)
It looks like we need `do`-`while`-like semantics here with an additional outer check.

Closes #2094.
2023-01-22 17:40:56 -05:00
alm
4fb0c6e3ad feat: Implement TRY201 (#2073) 2023-01-22 17:08:57 -05:00
Simon Brugman
ebfdefd110 refactor: remove redundant enum (#2091) 2023-01-22 15:27:08 -05:00
Simon Brugman
11f06055a0 feat: flake8-use-pathlib PTH100-124 (#2090) 2023-01-22 15:17:25 -05:00
Ville Skyttä
6a6a792562 fix: issue D401 only for non-test/property functions and methods (#2071)
Extend test fixture to verify the targeting.

Includes two "attribute docstrings" which per PEP 257 are not recognized by the Python bytecode compiler or available as runtime object attributes. They are not available for us either at time of writing, but include them for completeness anyway in case they one day are.
2023-01-22 14:24:59 -05:00
164 changed files with 5203 additions and 2293 deletions

View File

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

View File

@@ -14,9 +14,10 @@ If you're looking for a place to start, we recommend implementing a new lint rul
pattern-match against the examples in the existing codebase. Many lint rules are inspired by
existing Python plugins, which can be used as a reference implementation.
As a concrete example: consider taking on one of the rules in [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998),
and looking to the originating [Python source](https://github.com/MartinThoma/flake8-simplify) for
guidance.
As a concrete example: consider taking on one of the rules from the [`tryceratops`](https://github.com/charliermarsh/ruff/issues/2056)
plugin, and looking to the originating [Python source](https://github.com/guilatrova/tryceratops)
for guidance. [`flake8-simplify`](https://github.com/charliermarsh/ruff/issues/998) has a few rules
left too.
### Prerequisites

63
Cargo.lock generated
View File

@@ -719,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.230"
version = "0.0.233"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -733,7 +733,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"toml_edit",
"toml 0.6.0",
]
[[package]]
@@ -984,6 +984,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "is_executable"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8"
dependencies = [
"winapi",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1633,7 +1642,7 @@ checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
dependencies = [
"once_cell",
"thiserror",
"toml",
"toml 0.5.11",
]
[[package]]
@@ -1828,7 +1837,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.230"
version = "0.0.233"
dependencies = [
"anyhow",
"bitflags",
@@ -1847,6 +1856,7 @@ dependencies = [
"ignore",
"imperative",
"insta",
"is_executable",
"itertools",
"js-sys",
"libcst",
@@ -1875,14 +1885,14 @@ dependencies = [
"textwrap",
"thiserror",
"titlecase",
"toml_edit",
"toml 0.6.0",
"wasm-bindgen",
"wasm-bindgen-test",
]
[[package]]
name = "ruff_cli"
version = "0.0.230"
version = "0.0.233"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1919,7 +1929,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.230"
version = "0.0.233"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1940,7 +1950,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.230"
version = "0.0.233"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2200,6 +2210,15 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c68e921cef53841b8925c2abadd27c9b891d9613bdc43d6b823062866df38e8"
dependencies = [
"serde",
]
[[package]]
name = "shellexpand"
version = "3.0.0"
@@ -2480,32 +2499,44 @@ dependencies = [
[[package]]
name = "toml"
version = "0.5.10"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml_datetime"
version = "0.5.0"
name = "toml"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808b51e57d0ef8f71115d8f3a01e7d3750d01c79cac4b3eda910f4389fdf92fd"
checksum = "4fb9d890e4dc9298b70f740f615f2e05b9db37dce531f6b24fb77ac993f9f217"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4553f467ac8e3d374bc9a177a26801e5d0f9b211aa1673fb137a403afd1c9cf5"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.17.1"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34cc558345efd7e88b9eda9626df2138b80bb46a7606f695e751c892bc7dac6"
checksum = "729bfd096e40da9c001f778f5cdecbd2957929a24e10e5883d9392220a751581"
dependencies = [
"indexmap",
"itertools",
"nom8",
"serde",
"serde_spanned",
"toml_datetime",
]

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.230"
version = "0.0.233"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -46,7 +46,7 @@ num-traits = "0.2.15"
once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ruff_macros = { version = "0.0.230", path = "ruff_macros" }
ruff_macros = { version = "0.0.233", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
@@ -61,7 +61,7 @@ strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml_edit = { version = "0.17.1", features = ["easy"] }
toml = { version = "0.6.0" }
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
# For (future) wasm-pack support
@@ -73,6 +73,9 @@ serde-wasm-bindgen = { version = "0.4" }
js-sys = { version = "0.3.60" }
wasm-bindgen = { version = "0.2.83" }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
is_executable = "1.0.1"
[dev-dependencies]
insta = { version = "1.19.1", features = ["yaml"] }
test-case = { version = "2.2.2" }

107
README.md
View File

@@ -51,18 +51,22 @@ Ruff is extremely actively developed and used in major open-source projects like
- [Apache Airflow](https://github.com/apache/airflow)
- [Bokeh](https://github.com/bokeh/bokeh)
- [Zulip](https://github.com/zulip/zulip)
- [Dagster](https://github.com/dagster-io/dagster)
- [Pydantic](https://github.com/pydantic/pydantic)
- [Dagster](https://github.com/dagster-io/dagster)
- [Dagger](https://github.com/dagger/dagger)
- [Sphinx](https://github.com/sphinx-doc/sphinx)
- [Hatch](https://github.com/pypa/hatch)
- [Jupyter](https://github.com/jupyter-server/jupyter_server)
- [Synapse (Matrix)](https://github.com/matrix-org/synapse)
- [Saleor](https://github.com/saleor/saleor)
- [Great Expectations](https://github.com/great-expectations/great_expectations)
- [Polars](https://github.com/pola-rs/polars)
- [Ibis](https://github.com/ibis-project/ibis)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [Synapse (Matrix)](https://github.com/matrix-org/synapse)
- [SnowCLI (Snowflake)](https://github.com/Snowflake-Labs/snowcli)
- [Saleor](https://github.com/saleor/saleor)
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [Home Assistant](https://github.com/home-assistant/core)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [cibuildwheel (PyPA)](https://github.com/pypa/cibuildwheel)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -141,6 +145,7 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-type-checking (TYP)](#flake8-type-checking-typ)
1. [tryceratops (TRY)](#tryceratops-try)
1. [flake8-use-pathlib (PTH)](#flake8-use-pathlib-pth)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
@@ -178,6 +183,12 @@ For **Arch Linux** users, Ruff is also available as [`ruff`](https://archlinux.o
pacman -S ruff
```
For **Alpine** users, Ruff is also available as [`ruff`](https://pkgs.alpinelinux.org/package/edge/testing/x86_64/ruff) on the testing repositories:
```shell
apk add ruff
```
[![Packaging status](https://repology.org/badge/vertical-allrepos/ruff-python-linter.svg?exclude_unsupported=1)](https://repology.org/project/ruff-python-linter/versions)
### Usage
@@ -201,7 +212,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.230'
rev: 'v0.0.233'
hooks:
- id: ruff
```
@@ -666,8 +677,8 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| D209 | new-line-after-last-paragraph | Multi-line docstring closing quotes should be on a separate line | 🛠 |
| D210 | no-surrounding-whitespace | No whitespaces allowed surrounding docstring text | 🛠 |
| D211 | no-blank-line-before-class | No blank lines allowed before class docstring | 🛠 |
| D212 | multi-line-summary-first-line | Multi-line docstring summary should start at the first line | |
| D213 | multi-line-summary-second-line | Multi-line docstring summary should start at the second line | |
| D212 | multi-line-summary-first-line | Multi-line docstring summary should start at the first line | 🛠 |
| D213 | multi-line-summary-second-line | Multi-line docstring summary should start at the second line | 🛠 |
| D214 | section-not-over-indented | Section is over-indented ("{name}") | 🛠 |
| D215 | section-underline-not-over-indented | Section underline is over-indented ("{name}") | 🛠 |
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
@@ -810,6 +821,7 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| S506 | unsafe-yaml-load | Probable use of unsafe loader `{name}` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
| S508 | snmp-insecure-version | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
| S509 | snmp-weak-cryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
| S612 | logging-config-insecure-listen | Use of insecure `logging.config.listen` detected | |
| S701 | jinja2-autoescape-false | Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function. | |
### flake8-blind-except (BLE)
@@ -1150,7 +1162,9 @@ For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| PIE790 | no-unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE794 | dupe-class-field-definitions | Class field `{name}` is defined multiple times | 🛠 |
| PIE796 | prefer-unique-enums | Enum contains duplicate value: `{value}` | |
| PIE807 | prefer-list-builtin | Prefer `list()` over useless lambda | 🛠 |
| PIE800 | no-unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | no-unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE807 | prefer-list-builtin | Prefer `list` over useless lambda | 🛠 |
### flake8-commas (COM)
@@ -1176,6 +1190,8 @@ For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) o
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| EXE001 | shebang-not-executable | Shebang is present but file is not executable | |
| EXE002 | shebang-missing-executable-file | The file is executable but no shebang is present | |
| EXE003 | shebang-python | Shebang should contain "python" | |
| EXE004 | shebang-whitespace | Avoid whitespace before shebang | 🛠 |
| EXE005 | shebang-newline | Shebang should be at the beginning of the file | |
@@ -1195,7 +1211,42 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | 🛠 |
| TRY200 | reraise-no-cause | Use `raise from` to specify exception cause | |
| TRY201 | verbose-raise | Use `raise` without specifying exception name | |
| TRY300 | try-consider-else | Consider `else` block | |
| TRY301 | raise-within-try | Abstract `raise` to an inner function | |
### flake8-use-pathlib (PTH)
For more, see [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PTH100 | pathlib-abspath | `os.path.abspath` should be replaced by `.resolve()` | |
| PTH101 | pathlib-chmod | `os.chmod` should be replaced by `.chmod()` | |
| PTH102 | pathlib-mkdir | `os.mkdir` should be replaced by `.mkdir()` | |
| PTH103 | pathlib-makedirs | `os.makedirs` should be replaced by `.mkdir(parents=True)` | |
| PTH104 | pathlib-rename | `os.rename` should be replaced by `.rename()` | |
| PTH105 | pathlib-replace | `os.replace`should be replaced by `.replace()` | |
| PTH106 | pathlib-rmdir | `os.rmdir` should be replaced by `.rmdir()` | |
| PTH107 | pathlib-remove | `os.remove` should be replaced by `.unlink()` | |
| PTH108 | pathlib-unlink | `os.unlink` should be replaced by `.unlink()` | |
| PTH109 | pathlib-getcwd | `os.getcwd()` should be replaced by `Path.cwd()` | |
| PTH110 | pathlib-exists | `os.path.exists` should be replaced by `.exists()` | |
| PTH111 | pathlib-expanduser | `os.path.expanduser` should be replaced by `.expanduser()` | |
| PTH112 | pathlib-is-dir | `os.path.isdir` should be replaced by `.is_dir()` | |
| PTH113 | pathlib-is-file | `os.path.isfile` should be replaced by `.is_file()` | |
| PTH114 | pathlib-is-link | `os.path.islink` should be replaced by `.is_symlink()` | |
| PTH115 | pathlib-readlink | `os.readlink(` should be replaced by `.readlink()` | |
| PTH116 | pathlib-stat | `os.stat` should be replaced by `.stat()` or `.owner()` or `.group()` | |
| PTH117 | pathlib-is-abs | `os.path.isabs` should be replaced by `.is_absolute()` | |
| PTH118 | pathlib-join | `os.path.join` should be replaced by foo_path / "bar" | |
| PTH119 | pathlib-basename | `os.path.basename` should be replaced by `.name` | |
| PTH120 | pathlib-dirname | `os.path.dirname` should be replaced by `.parent` | |
| PTH121 | pathlib-samefile | `os.path.samefile` should be replaced by `.samefile()` | |
| PTH122 | pathlib-splitext | `os.path.splitext` should be replaced by `.suffix` | |
| PTH123 | pathlib-open | `open("foo")` should be replaced by`Path("foo").open()` | |
| PTH124 | pathlib-py-path | `py.path` is in maintenance mode, use `pathlib` instead | |
### Ruff-specific rules (RUF)
@@ -1602,12 +1653,15 @@ project. See [#283](https://github.com/charliermarsh/ruff/issues/283) for more.
### How does Ruff's import sorting compare to [`isort`](https://pypi.org/project/isort/)?
Ruff's import sorting is intended to be nearly equivalent to `isort` when used `profile = "black"`.
(There are some minor differences in how Ruff and isort break ties between similar imports.)
There are a few known, minor differences in how Ruff and isort break ties between similar imports,
and in how Ruff and isort treat inline comments in some cases (see: [#1381](https://github.com/charliermarsh/ruff/issues/1381),
[#2104](https://github.com/charliermarsh/ruff/issues/2104)).
Like `isort`, Ruff's import sorting is compatible with Black.
Ruff is less configurable than `isort`, but supports the `known-first-party`, `known-third-party`,
`extra-standard-library`, and `src` settings, like so:
Ruff does not yet support all of `isort`'s configuration options, though it does support many of
them. You can find the supported settings in the [API reference](#isort). For example, you can set
`known-first-party` like so:
```toml
[tool.ruff]
@@ -2187,10 +2241,9 @@ ignore = ["F841"]
#### [`ignore-init-module-imports`](#ignore-init-module-imports)
Avoid automatically removing unused imports in `__init__.py` files. Such
imports will still be +flagged, but with a dedicated message
suggesting that the import is either added to the module' +`__all__`
symbol, or re-exported with a redundant alias (e.g., `import os as
os`).
imports will still be flagged, but with a dedicated message suggesting
that the import is either added to the module's `__all__` symbol, or
re-exported with a redundant alias (e.g., `import os as os`).
**Default value**: `false`
@@ -2643,6 +2696,28 @@ max-string-length = 20
---
### `flake8-implicit-str-concat`
#### [`allow-multiline`](#allow-multiline)
Whether to allow implicit string concatenations for multiline strings.
By default, implicit concatenations of multiline strings are
allowed (but continuation lines, delimited with a backslash, are
prohibited).
**Default value**: `true`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff.flake8-implicit-str-concat]
allow-multiline = false
```
---
### `flake8-import-conventions`
#### [`aliases`](#aliases)

View File

@@ -1,84 +0,0 @@
use std::fs;
use std::io::{BufRead, BufReader, BufWriter, Write};
use std::path::{Path, PathBuf};
fn main() {
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
generate_linter_name_and_url(&out_dir);
}
const RULES_SUBMODULE_DOC_PREFIX: &str = "//! Rules from ";
/// The `src/rules/*/mod.rs` files are expected to have a first line such as the
/// following:
///
/// //! Rules from [Pyflakes](https://pypi.org/project/pyflakes/).
///
/// This function extracts the link label and url from these comments and
/// generates the `name` and `url` functions for the `Linter` enum
/// accordingly, so that they can be used by `ruff_dev::generate_rules_table`.
fn generate_linter_name_and_url(out_dir: &Path) {
println!("cargo:rerun-if-changed=src/rules/");
let mut name_match_arms: String = r#"Linter::Ruff => "Ruff-specific rules","#.into();
let mut url_match_arms: String = r#"Linter::Ruff => None,"#.into();
for file in fs::read_dir("src/rules/")
.unwrap()
.flatten()
.filter(|f| f.file_type().unwrap().is_dir() && f.file_name() != "ruff")
{
let mod_rs_path = file.path().join("mod.rs");
let mod_rs_path = mod_rs_path.to_str().unwrap();
let first_line = BufReader::new(fs::File::open(mod_rs_path).unwrap())
.lines()
.next()
.unwrap()
.unwrap();
let Some(comment) = first_line.strip_prefix(RULES_SUBMODULE_DOC_PREFIX) else {
panic!("expected first line in {mod_rs_path} to start with `{RULES_SUBMODULE_DOC_PREFIX}`")
};
let md_link = comment.trim_end_matches('.');
let (name, url) = md_link
.strip_prefix('[')
.unwrap()
.strip_suffix(')')
.unwrap()
.split_once("](")
.unwrap();
let dirname = file.file_name();
let dirname = dirname.to_str().unwrap();
let variant_name = dirname
.split('_')
.map(|part| match part {
"errmsg" => "ErrMsg".to_string(),
"mccabe" => "McCabe".to_string(),
"pep8" => "PEP8".to_string(),
_ => format!("{}{}", part[..1].to_uppercase(), &part[1..]),
})
.collect::<String>();
name_match_arms.push_str(&format!(r#"Linter::{variant_name} => "{name}","#));
url_match_arms.push_str(&format!(r#"Linter::{variant_name} => Some("{url}"),"#));
}
write!(
BufWriter::new(fs::File::create(out_dir.join("linter.rs")).unwrap()),
"
impl Linter {{
pub fn name(&self) -> &'static str {{
match self {{ {name_match_arms} }}
}}
pub fn url(&self) -> Option<&'static str> {{
match self {{ {url_match_arms} }}
}}
}}
"
)
.unwrap();
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.230"
version = "0.0.233"
edition = "2021"
[dependencies]
@@ -16,7 +16,7 @@ serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
toml_edit = { version = "0.17.1", features = ["easy"] }
toml = { version = "0.6.0" }
[dev-dependencies]

View File

@@ -60,7 +60,7 @@ fn main() -> Result<()> {
// Create Ruff's pyproject.toml section.
let pyproject = flake8_to_ruff::convert(&config, &external_config, cli.plugin)?;
println!("{}", toml_edit::easy::to_string_pretty(&pyproject)?);
println!("{}", toml::to_string_pretty(&pyproject)?);
Ok(())
}

View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2021 Rodolphe Pelloux-Prayer
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.

View File

@@ -7,7 +7,7 @@ build-backend = "maturin"
[project]
name = "ruff"
version = "0.0.230"
version = "0.0.233"
description = "An extremely fast Python linter, written in Rust."
authors = [
{ name = "Charlie Marsh", email = "charlie.r.marsh@gmail.com" },

View File

@@ -0,0 +1,8 @@
import logging.config
t = logging.config.listen(9999)
def verify_func():
pass
l = logging.config.listen(9999, verify=verify_func)

View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
if __name__ == '__main__':
print('I should be executable.')

View File

@@ -0,0 +1,2 @@
if __name__ == '__main__':
print('I should be executable.')

View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
if __name__ == '__main__':
print('I should be executable.')

View File

@@ -0,0 +1,2 @@
if __name__ == '__main__':
print('I should be executable.')

View File

@@ -0,0 +1,2 @@
if __name__ == '__main__':
print('I should be executable.')

View File

@@ -0,0 +1,4 @@
#!/usr/bin/python
if __name__ == '__main__':
print('I should be executable.')

0
resources/test/fixtures/flake8_executable/EXE003.py vendored Normal file → Executable file
View File

0
resources/test/fixtures/flake8_executable/EXE004_1.py vendored Normal file → Executable file
View File

0
resources/test/fixtures/flake8_executable/EXE004_3.py vendored Normal file → Executable file
View File

0
resources/test/fixtures/flake8_executable/EXE005_1.py vendored Normal file → Executable file
View File

0
resources/test/fixtures/flake8_executable/EXE005_2.py vendored Normal file → Executable file
View File

0
resources/test/fixtures/flake8_executable/EXE005_3.py vendored Normal file → Executable file
View File

View File

@@ -0,0 +1,17 @@
{"foo": 1, **{"bar": 1}} # PIE800
foo({**foo, **{"bar": True}}) # PIE800
{**foo, **{"bar": 10}} # PIE800
{**foo, **buzz, **{bar: 10}} # PIE800
{**foo, "bar": True } # OK
{"foo": 1, "buzz": {"bar": 1}} # OK
{**foo, "bar": True } # OK
Table.objects.filter(inst=inst, **{f"foo__{bar}__exists": True}) # OK
buzz = {**foo, "bar": { 1: 2 }} # OK

View File

@@ -0,0 +1,19 @@
foo(**{"bar": True}) # PIE804
foo(**{"r2d2": True}) # PIE804
Foo.objects.create(**{"bar": True}) # PIE804
Foo.objects.create(**{"_id": some_id}) # PIE804
Foo.objects.create(**{**bar}) # PIE804
foo(**{**data, "foo": "buzz"})
foo(**buzz)
foo(**{"bar-foo": True})
foo(**{"bar foo": True})
foo(**{"1foo": True})
foo(**{buzz: True})
foo(**{"": True})
foo(**{f"buzz__{bar}": True})

View File

@@ -114,3 +114,12 @@ def bar3(x, y, z):
else:
return z
return None
def prompts(self, foo):
if not foo:
return []
for x in foo:
yield x
yield x + 1

View File

@@ -92,3 +92,40 @@ if node.module:
"multiprocessing."
):
print("Bad module!")
# OK
if a:
if b:
print("foo")
else:
print("bar")
# OK
if a:
if b:
if c:
print("foo")
else:
print("bar")
else:
print("bar")
# OK
if a:
# SIM 102
if b:
if c:
print("foo")
else:
print("bar")
# OK
if a:
if b:
if c:
print("foo")
print("baz")
else:
print("bar")

View File

@@ -0,0 +1,31 @@
import os
import os.path
p = "/foo"
a = os.path.abspath(p)
aa = os.chmod(p)
aaa = os.mkdir(p)
os.makedirs(p)
os.rename(p)
os.replace(p)
os.rmdir(p)
os.remove(p)
os.unlink(p)
os.getcwd(p)
b = os.path.exists(p)
bb = os.path.expanduser(p)
bbb = os.path.isdir(p)
bbbb = os.path.isfile(p)
bbbbb = os.path.islink(p)
os.readlink(p)
os.stat(p)
os.path.isabs(p)
os.path.join(p)
os.path.basename(p)
os.path.dirname(p)
os.path.samefile(p)
os.path.splitext(p)
with open(p) as fp:
fp.read()
open(p).close()

View File

@@ -0,0 +1,28 @@
import os as foo
import os.path as foo_p
p = "/foo"
a = foo_p.abspath(p)
aa = foo.chmod(p)
aaa = foo.mkdir(p)
foo.makedirs(p)
foo.rename(p)
foo.replace(p)
foo.rmdir(p)
foo.remove(p)
foo.unlink(p)
foo.getcwd(p)
b = foo_p.exists(p)
bb = foo_p.expanduser(p)
bbb = foo_p.isdir(p)
bbbb = foo_p.isfile(p)
bbbbb = foo_p.islink(p)
foo.readlink(p)
foo.stat(p)
foo_p.isabs(p)
foo_p.join(p)
foo_p.basename(p)
foo_p.dirname(p)
foo_p.samefile(p)
foo_p.splitext(p)

View File

@@ -0,0 +1,33 @@
from os import chmod, mkdir, makedirs, rename, replace, rmdir
from os import remove, unlink, getcwd, readlink, stat
from os.path import abspath, exists, expanduser, isdir, isfile, islink
from os.path import isabs, join, basename, dirname, samefile, splitext
p = "/foo"
a = abspath(p)
aa = chmod(p)
aaa = mkdir(p)
makedirs(p)
rename(p)
replace(p)
rmdir(p)
remove(p)
unlink(p)
getcwd(p)
b = exists(p)
bb = expanduser(p)
bbb = isdir(p)
bbbb = isfile(p)
bbbbb = islink(p)
readlink(p)
stat(p)
isabs(p)
join(p)
basename(p)
dirname(p)
samefile(p)
splitext(p)
with open(p) as fp:
fp.read()
open(p).close()

View File

@@ -0,0 +1,35 @@
from os import chmod as xchmod, mkdir as xmkdir
from os import makedirs as xmakedirs, rename as xrename, replace as xreplace
from os import rmdir as xrmdir, remove as xremove, unlink as xunlink
from os import getcwd as xgetcwd, readlink as xreadlink, stat as xstat
from os.path import abspath as xabspath, exists as xexists
from os.path import expanduser as xexpanduser, isdir as xisdir
from os.path import isfile as xisfile, islink as xislink, isabs as xisabs
from os.path import join as xjoin, basename as xbasename, dirname as xdirname
from os.path import samefile as xsamefile, splitext as xsplitext
p = "/foo"
a = xabspath(p)
aa = xchmod(p)
aaa = xmkdir(p)
xmakedirs(p)
xrename(p)
xreplace(p)
xrmdir(p)
xremove(p)
xunlink(p)
xgetcwd(p)
b = xexists(p)
bb = xexpanduser(p)
bbb = xisdir(p)
bbbb = xisfile(p)
bbbbb = xislink(p)
xreadlink(p)
xstat(p)
xisabs(p)
xjoin(p)
xbasename(p)
xdirname(p)
xsamefile(p)
xsplitext(p)

View File

@@ -0,0 +1,3 @@
import py
p = py.path.local("../foo")

View File

@@ -0,0 +1,3 @@
from py.path import local as path
p = path("/foo")

View File

@@ -1,3 +1,7 @@
"""This module docstring does not need to be written in imperative mood."""
from functools import cached_property
# Bad examples
def bad_liouiwnlkjl():
@@ -18,7 +22,11 @@ def bad_sdgfsdg23245777():
def bad_run_something():
"""Runs something"""
pass
def bad_nested():
"""Runs other things, nested"""
bad_nested()
def multi_line():
@@ -32,6 +40,11 @@ def multi_line():
def good_run_something():
"""Run away."""
def good_nested():
"""Run to the hills."""
good_nested()
def good_construct():
"""Construct a beautiful house."""
@@ -41,3 +54,48 @@ def good_multi_line():
"""Write a logical line that
extends to two physical lines.
"""
good_top_level_var = False
"""This top level assignment attribute docstring does not need to be written in imperative mood."""
# Classes and their members
class Thingy:
"""This class docstring does not need to be written in imperative mood."""
_beep = "boop"
"""This class attribute docstring does not need to be written in imperative mood."""
def bad_method(self):
"""This method docstring should be written in imperative mood."""
@property
def good_property(self):
"""This property method docstring does not need to be written in imperative mood."""
return self._beep
@cached_property
def good_cached_property(self):
"""This property method docstring does not need to be written in imperative mood."""
return 42 * 42
class NestedThingy:
"""This nested class docstring does not need to be written in imperative mood."""
# Test functions
def test_something():
"""This test function does not need to be written in imperative mood.
pydocstyle's rationale:
We exclude tests from the imperative mood check, because to phrase
their docstring in the imperative mood, they would have to start with
a highly redundant "Test that ..."
"""
def runTest():
"""This test function does not need to be written in imperative mood, either."""

View File

@@ -44,6 +44,8 @@ print("foo {} ".format(x))
"{}".format(a)
'({}={{0!e}})'.format(a)
###
# Non-errors
###

View File

@@ -0,0 +1,27 @@
class MyException(Exception):
pass
class MainFunctionFailed(Exception):
pass
def process():
raise MyException
def bad():
try:
process()
except MyException:
raise MainFunctionFailed()
if True:
raise MainFunctionFailed()
def good():
try:
process()
except MyException as ex:
raise MainFunctionFailed() from ex

View File

@@ -0,0 +1,54 @@
"""
Violation:
Raising an exception using its assigned name is verbose and unrequired
"""
import logging
logger = logging.getLogger(__name__)
class MyException(Exception):
pass
def bad():
try:
process()
except MyException as e:
logger.exception("process failed")
raise e
def good():
try:
process()
except MyException:
logger.exception("process failed")
raise
def still_good():
try:
process()
except MyException as e:
print(e)
raise
def bad_that_needs_recursion():
try:
process()
except MyException as e:
logger.exception("process failed")
if True:
raise e
def bad_that_needs_recursion_2():
try:
process()
except MyException as e:
logger.exception("process failed")
if True:
def foo():
raise e

View File

@@ -0,0 +1,29 @@
class MyException(Exception):
pass
def bad():
try:
a = process()
if not a:
raise MyException(a)
raise MyException(a)
try:
b = process()
if not b:
raise MyException(b)
except Exception:
logger.exception("something failed")
except Exception:
logger.exception("something failed")
def good():
try:
a = process() # This throws the exception now
except MyException:
logger.exception("a failed")
except Exception:
logger.exception("something failed")

View File

@@ -175,6 +175,17 @@
}
]
},
"flake8-implicit-str-concat": {
"description": "Options for the `flake8-implicit-str-concat` plugin.",
"anyOf": [
{
"$ref": "#/definitions/Flake8ImplicitStrConcatOptions"
},
{
"type": "null"
}
]
},
"flake8-import-conventions": {
"description": "Options for the `flake8-import-conventions` plugin.",
"anyOf": [
@@ -259,7 +270,7 @@
}
},
"ignore-init-module-imports": {
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be +flagged, but with a dedicated message suggesting that the import is either added to the module' +`__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
"description": "Avoid automatically removing unused imports in `__init__.py` files. Such imports will still be flagged, but with a dedicated message suggesting that the import is either added to the module's `__all__` symbol, or re-exported with a redundant alias (e.g., `import os as os`).",
"type": [
"boolean",
"null"
@@ -626,6 +637,19 @@
},
"additionalProperties": false
},
"Flake8ImplicitStrConcatOptions": {
"type": "object",
"properties": {
"allow-multiline": {
"description": "Whether to allow implicit string concatenations for multiline strings. By default, implicit concatenations of multiline strings are allowed (but continuation lines, delimited with a backslash, are prohibited).",
"type": [
"boolean",
"null"
]
}
},
"additionalProperties": false
},
"Flake8ImportConventionsOptions": {
"type": "object",
"properties": {
@@ -1164,13 +1188,13 @@
"RuleSelector": {
"type": "string",
"enum": [
"ALL",
"A",
"A0",
"A00",
"A001",
"A002",
"A003",
"ALL",
"ANN",
"ANN0",
"ANN00",
@@ -1381,6 +1405,8 @@
"EXE",
"EXE0",
"EXE00",
"EXE001",
"EXE002",
"EXE003",
"EXE004",
"EXE005",
@@ -1458,15 +1484,6 @@
"I00",
"I001",
"I002",
"I2",
"I25",
"I252",
"IC",
"IC0",
"IC001",
"IC002",
"IC003",
"IC004",
"ICN",
"ICN0",
"ICN00",
@@ -1481,9 +1498,6 @@
"ISC001",
"ISC002",
"ISC003",
"M",
"M0",
"M001",
"N",
"N8",
"N80",
@@ -1521,23 +1535,6 @@
"PD9",
"PD90",
"PD901",
"PDV",
"PDV0",
"PDV002",
"PDV003",
"PDV004",
"PDV007",
"PDV008",
"PDV009",
"PDV01",
"PDV010",
"PDV011",
"PDV012",
"PDV013",
"PDV015",
"PDV9",
"PDV90",
"PDV901",
"PGH",
"PGH0",
"PGH00",
@@ -1553,6 +1550,8 @@
"PIE796",
"PIE8",
"PIE80",
"PIE800",
"PIE804",
"PIE807",
"PL",
"PLC",
@@ -1633,6 +1632,36 @@
"PT024",
"PT025",
"PT026",
"PTH",
"PTH1",
"PTH10",
"PTH100",
"PTH101",
"PTH102",
"PTH103",
"PTH104",
"PTH105",
"PTH106",
"PTH107",
"PTH108",
"PTH109",
"PTH11",
"PTH110",
"PTH111",
"PTH112",
"PTH113",
"PTH114",
"PTH115",
"PTH116",
"PTH117",
"PTH118",
"PTH119",
"PTH12",
"PTH120",
"PTH121",
"PTH122",
"PTH123",
"PTH124",
"Q",
"Q0",
"Q00",
@@ -1640,17 +1669,6 @@
"Q001",
"Q002",
"Q003",
"R",
"R5",
"R50",
"R501",
"R502",
"R503",
"R504",
"R505",
"R506",
"R507",
"R508",
"RET",
"RET5",
"RET50",
@@ -1695,6 +1713,9 @@
"S506",
"S508",
"S509",
"S6",
"S61",
"S612",
"S7",
"S70",
"S701",
@@ -1752,34 +1773,18 @@
"TRY0",
"TRY00",
"TRY004",
"TRY2",
"TRY20",
"TRY200",
"TRY201",
"TRY3",
"TRY30",
"TRY300",
"TRY301",
"TYP",
"TYP0",
"TYP00",
"TYP005",
"U",
"U0",
"U00",
"U001",
"U003",
"U004",
"U005",
"U006",
"U007",
"U008",
"U009",
"U01",
"U010",
"U011",
"U012",
"U013",
"U014",
"U015",
"U016",
"U017",
"U019",
"UP",
"UP0",
"UP00",

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.0.230"
version = "0.0.233"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"

View File

@@ -3,11 +3,12 @@ use std::path::PathBuf;
use clap::{command, Parser};
use regex::Regex;
use ruff::logging::LogLevel;
use ruff::registry::{Rule, RuleSelector};
use ruff::registry::Rule;
use ruff::resolver::ConfigProcessor;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
};
use ruff::RuleSelector;
use rustc_hash::FxHashMap;
#[derive(Debug, Parser)]

View File

@@ -10,11 +10,8 @@ use std::{fs, process, str};
use anyhow::{anyhow, Context, Result};
use assert_cmd::Command;
use itertools::Itertools;
use log::info;
use ruff::logging::{set_up_logging, LogLevel};
use ruff::registry::{Linter, RuleNamespace};
use strum::IntoEnumIterator;
use walkdir::WalkDir;
/// Handles `blackd` process and allows submitting code to it for formatting.
@@ -175,13 +172,6 @@ fn test_ruff_black_compatibility() -> Result<()> {
.filter_map(Result::ok)
.collect();
let codes = Linter::iter()
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
.filter(|linter| *linter != Linter::Ruff)
.map(|linter| linter.prefixes().join(","))
.join(",");
let ruff_args = [
"-",
"--silent",
@@ -189,8 +179,11 @@ fn test_ruff_black_compatibility() -> Result<()> {
"--fix",
"--line-length",
"88",
"--select",
&codes,
"--select ALL",
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
"--ignore RUF",
];
for entry in paths {

View File

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

View File

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

View File

@@ -53,7 +53,7 @@ pub fn define_rule_mapping(mapping: &Mapping) -> proc_macro2::TokenStream {
let rulecodeprefix = super::rule_code_prefix::expand(
&Ident::new("Rule", Span::call_site()),
&Ident::new("RuleSelector", Span::call_site()),
&Ident::new("RuleCodePrefix", Span::call_site()),
mapping.entries.iter().map(|(code, ..)| code),
|code| code_to_name[code],
);

View File

@@ -1,89 +1,9 @@
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::collections::{BTreeMap, BTreeSet};
use once_cell::sync::Lazy;
use proc_macro2::Span;
use quote::quote;
use syn::Ident;
const ALL: &str = "ALL";
/// A hash map from deprecated `RuleSelector` to latest
/// `RuleSelector`.
pub static PREFIX_REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
HashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", "UP001"),
("U003", "UP003"),
("U004", "UP004"),
("U005", "UP005"),
("U006", "UP006"),
("U007", "UP007"),
("U008", "UP008"),
("U009", "UP009"),
("U010", "UP010"),
("U011", "UP011"),
("U012", "UP012"),
("U013", "UP013"),
("U014", "UP014"),
("U015", "UP015"),
("U016", "UP016"),
("U017", "UP017"),
("U019", "UP019"),
// TODO(charlie): Remove by 2023-02-01.
("I252", "TID252"),
("M001", "RUF100"),
// TODO(charlie): Remove by 2023-02-01.
("PDV002", "PD002"),
("PDV003", "PD003"),
("PDV004", "PD004"),
("PDV007", "PD007"),
("PDV008", "PD008"),
("PDV009", "PD009"),
("PDV010", "PD010"),
("PDV011", "PD011"),
("PDV012", "PD012"),
("PDV013", "PD013"),
("PDV015", "PD015"),
("PDV901", "PD901"),
// TODO(charlie): Remove by 2023-02-01.
("R501", "RET501"),
("R502", "RET502"),
("R503", "RET503"),
("R504", "RET504"),
("R505", "RET505"),
("R506", "RET506"),
("R507", "RET507"),
("R508", "RET508"),
("IC001", "ICN001"),
("IC002", "ICN001"),
("IC003", "ICN001"),
("IC004", "ICN001"),
// TODO(charlie): Remove by 2023-01-01.
("U", "UP"),
("U0", "UP0"),
("U00", "UP00"),
("U01", "UP01"),
// TODO(charlie): Remove by 2023-02-01.
("I2", "TID2"),
("I25", "TID25"),
("M", "RUF100"),
("M0", "RUF100"),
// TODO(charlie): Remove by 2023-02-01.
("PDV", "PD"),
("PDV0", "PD0"),
("PDV01", "PD01"),
("PDV9", "PD9"),
("PDV90", "PD90"),
// TODO(charlie): Remove by 2023-02-01.
("R", "RET"),
("R5", "RET5"),
("R50", "RET50"),
// TODO(charlie): Remove by 2023-02-01.
("IC", "ICN"),
("IC0", "ICN0"),
])
});
pub fn expand<'a>(
rule_type: &Ident,
prefix_ident: &Ident,
@@ -116,20 +36,8 @@ pub fn expand<'a>(
all_codes.insert(code_str);
}
prefix_to_codes.insert(ALL.to_string(), all_codes);
prefix_to_codes.insert("PL".to_string(), pl_codes);
// Add any prefix aliases (e.g., "U" to "UP").
for (alias, rule_code) in PREFIX_REDIRECTS.iter() {
prefix_to_codes.insert(
(*alias).to_string(),
prefix_to_codes
.get(*rule_code)
.unwrap_or_else(|| panic!("Unknown RuleCode: {alias:?}"))
.clone(),
);
}
let prefix_variants = prefix_to_codes.keys().map(|prefix| {
let prefix = Ident::new(prefix, Span::call_site());
quote! {
@@ -139,26 +47,9 @@ pub fn expand<'a>(
let prefix_impl = generate_impls(rule_type, prefix_ident, &prefix_to_codes, variant_name);
let prefix_redirects = PREFIX_REDIRECTS.iter().map(|(alias, rule_code)| {
let code = Ident::new(rule_code, Span::call_site());
quote! {
(#alias, #prefix_ident::#code)
}
});
quote! {
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum SuffixLength {
None,
Zero,
One,
Two,
Three,
Four,
Five,
}
#[derive(
::strum_macros::EnumIter,
::strum_macros::EnumString,
::strum_macros::AsRefStr,
Debug,
@@ -167,22 +58,15 @@ pub fn expand<'a>(
PartialOrd,
Ord,
Clone,
Hash,
::serde::Serialize,
::serde::Deserialize,
::schemars::JsonSchema,
)]
pub enum #prefix_ident {
#(#prefix_variants,)*
}
#prefix_impl
/// A hash map from deprecated `RuleSelector` to latest `RuleSelector`.
pub static PREFIX_REDIRECTS: ::once_cell::sync::Lazy<::rustc_hash::FxHashMap<&'static str, #prefix_ident>> = ::once_cell::sync::Lazy::new(|| {
::rustc_hash::FxHashMap::from_iter([
#(#prefix_redirects),*
])
});
}
}
@@ -200,64 +84,37 @@ fn generate_impls<'a>(
}
});
let prefix = Ident::new(prefix_str, Span::call_site());
if let Some(target) = PREFIX_REDIRECTS.get(prefix_str.as_str()) {
quote! {
#prefix_ident::#prefix => {
crate::warn_user_once!(
"`{}` has been remapped to `{}`", #prefix_str, #target
);
vec![#(#codes),*].into_iter()
}
}
} else {
quote! {
#prefix_ident::#prefix => vec![#(#codes),*].into_iter(),
}
quote! {
#prefix_ident::#prefix => vec![#(#codes),*].into_iter(),
}
});
let specificity_match_arms = prefix_to_codes.keys().map(|prefix_str| {
let prefix = Ident::new(prefix_str, Span::call_site());
if prefix_str == ALL {
quote! {
#prefix_ident::#prefix => SuffixLength::None,
}
} else {
let mut num_numeric = prefix_str.chars().filter(|char| char.is_numeric()).count();
if prefix_str != "PL" && prefix_str.starts_with("PL") {
num_numeric += 1;
}
let suffix_len = match num_numeric {
0 => quote! { SuffixLength::Zero },
1 => quote! { SuffixLength::One },
2 => quote! { SuffixLength::Two },
3 => quote! { SuffixLength::Three },
4 => quote! { SuffixLength::Four },
5 => quote! { SuffixLength::Five },
_ => panic!("Invalid prefix: {prefix}"),
};
quote! {
#prefix_ident::#prefix => #suffix_len,
}
let mut num_numeric = prefix_str.chars().filter(|char| char.is_numeric()).count();
if prefix_str != "PL" && prefix_str.starts_with("PL") {
num_numeric += 1;
}
});
let categories = prefix_to_codes.keys().map(|prefix_str| {
if prefix_str.chars().all(char::is_alphabetic)
&& !PREFIX_REDIRECTS.contains_key(&prefix_str.as_str())
{
let prefix = Ident::new(prefix_str, Span::call_site());
quote! {
#prefix_ident::#prefix,
}
} else {
quote! {}
let suffix_len = match num_numeric {
0 => quote! { Specificity::Linter },
1 => quote! { Specificity::Code1Char },
2 => quote! { Specificity::Code2Chars },
3 => quote! { Specificity::Code3Chars },
4 => quote! { Specificity::Code4Chars },
5 => quote! { Specificity::Code5Chars },
_ => panic!("Invalid prefix: {prefix}"),
};
quote! {
#prefix_ident::#prefix => #suffix_len,
}
});
quote! {
impl #prefix_ident {
pub fn specificity(&self) -> SuffixLength {
pub(crate) fn specificity(&self) -> crate::rule_selector::Specificity {
use crate::rule_selector::Specificity;
#[allow(clippy::match_same_arms)]
match self {
#(#specificity_match_arms)*
@@ -270,15 +127,11 @@ fn generate_impls<'a>(
type IntoIter = ::std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
use colored::Colorize;
#[allow(clippy::match_same_arms)]
match self {
#(#into_iter_match_arms)*
}
}
}
pub const CATEGORIES: &[#prefix_ident] = &[#(#categories)*];
}
}

View File

@@ -1,7 +1,9 @@
use std::collections::HashSet;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::spanned::Spanned;
use syn::{Data, DataEnum, DeriveInput, Error, Lit, Meta, MetaNameValue};
use syn::{Attribute, Data, DataEnum, DeriveInput, Error, Lit, Meta, MetaNameValue};
pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput { ident, data: Data::Enum(DataEnum {
@@ -13,39 +15,62 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
let mut parsed = Vec::new();
let mut prefix_match_arms = quote!();
let mut name_match_arms = quote!(Self::Ruff => "Ruff-specific rules",);
let mut url_match_arms = quote!(Self::Ruff => None,);
let mut all_prefixes = HashSet::new();
for variant in variants {
let prefix_attrs: Vec<_> = variant
let prefixes: Result<Vec<_>, _> = variant
.attrs
.iter()
.filter(|a| a.path.is_ident("prefix"))
.map(|attr| {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(lit), ..})) = attr.parse_meta() else {
return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#));
};
let str = lit.value();
match str.chars().next() {
None => return Err(Error::new(lit.span(), "expected prefix string to be non-empty")),
Some(_) => {},
}
if !all_prefixes.insert(str.clone()) {
return Err(Error::new(lit.span(), "prefix has already been defined before"));
}
Ok(str)
})
.collect();
let prefixes = prefixes?;
if prefix_attrs.is_empty() {
if prefixes.is_empty() {
return Err(Error::new(
variant.span(),
r#"Missing [#prefix = "..."] attribute"#,
r#"Missing #[prefix = "..."] attribute"#,
));
}
let mut prefix_literals = Vec::new();
for attr in prefix_attrs {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(lit), ..})) = attr.parse_meta() else {
return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#))
};
parsed.push((lit.clone(), variant.ident.clone()));
prefix_literals.push(lit);
}
let Some(doc_attr) = variant.attrs.iter().find(|a| a.path.is_ident("doc")) else {
return Err(Error::new(variant.span(), r#"expected a doc comment"#))
};
let variant_ident = variant.ident;
if variant_ident != "Ruff" {
let (name, url) = parse_doc_attr(doc_attr)?;
name_match_arms.extend(quote! {Self::#variant_ident => #name,});
url_match_arms.extend(quote! {Self::#variant_ident => Some(#url),});
}
for lit in &prefixes {
parsed.push((lit.clone(), variant_ident.clone()));
}
prefix_match_arms.extend(quote! {
Self::#variant_ident => &[#(#prefix_literals),*],
Self::#variant_ident => &[#(#prefixes),*],
});
}
parsed.sort_by_key(|(prefix, _)| prefix.value().len());
parsed.sort_by_key(|(prefix, _)| prefix.len());
let mut if_statements = quote!();
let mut into_iter_match_arms = quote!();
@@ -55,18 +80,18 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
return Some((#ident::#field, rest));
}});
let prefix_ident = Ident::new(&prefix.value(), Span::call_site());
let prefix_ident = Ident::new(&prefix, Span::call_site());
if field != "Pycodestyle" {
into_iter_match_arms.extend(quote! {
#ident::#field => RuleSelector::#prefix_ident.into_iter(),
#ident::#field => RuleCodePrefix::#prefix_ident.into_iter(),
});
}
}
into_iter_match_arms.extend(quote! {
#ident::Pycodestyle => {
let rules: Vec<_> = (&RuleSelector::E).into_iter().chain(&RuleSelector::W).collect();
let rules: Vec<_> = (&RuleCodePrefix::E).into_iter().chain(&RuleCodePrefix::W).collect();
rules.into_iter()
}
});
@@ -82,6 +107,14 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
fn prefixes(&self) -> &'static [&'static str] {
match self { #prefix_match_arms }
}
fn name(&self) -> &'static str {
match self { #name_match_arms }
}
fn url(&self) -> Option<&'static str> {
match self { #url_match_arms }
}
}
impl IntoIterator for &#ident {
@@ -98,3 +131,23 @@ pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream>
}
})
}
/// Parses an attribute in the form of `#[doc = " [name](https://example.com/)"]`
/// into a tuple of link label and URL.
fn parse_doc_attr(doc_attr: &Attribute) -> syn::Result<(String, String)> {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(doc_lit), ..})) = doc_attr.parse_meta() else {
return Err(Error::new(doc_attr.span(), r#"expected doc attribute to be in the form of #[doc = "..."]"#))
};
parse_markdown_link(doc_lit.value().trim())
.map(|(name, url)| (name.to_string(), url.to_string()))
.ok_or_else(|| {
Error::new(
doc_lit.span(),
r#"expected doc comment to be in the form of `/// [name](https://example.com/)`"#,
)
})
}
fn parse_markdown_link(link: &str) -> Option<(&str, &str)> {
link.strip_prefix('[')?.strip_suffix(')')?.split_once("](")
}

View File

@@ -84,7 +84,8 @@ mod tests {
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == '#[prefix = "RUF"]':
elif line.strip() == '/// Ruff-specific rules':
fp.write(f"/// [{plugin}]({url})\n")
fp.write(f'{indent}#[prefix = "{prefix_code}"]\n')
fp.write(f"{indent}{pascal_case(plugin)},")
fp.write("\n")

View File

@@ -116,7 +116,7 @@ pub fn {rule_name_snake}(checker: &mut Checker) {{}}
if line.strip() == f"// {linter}":
indent = get_indent(line)
fp.write(f"{indent}{code} => rules::{linter}::rules::{name},")
fp.write(f"{indent}{code} => rules::{dir_name(linter)}::rules::{name},")
fp.write("\n")
has_written = True

40
src/ast/hashable.rs Normal file
View File

@@ -0,0 +1,40 @@
use std::hash::Hash;
use rustpython_ast::Expr;
use crate::ast::comparable::ComparableExpr;
/// Wrapper around `Expr` that implements `Hash` and `PartialEq`.
pub struct HashableExpr<'a>(&'a Expr);
impl Hash for HashableExpr<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let comparable = ComparableExpr::from(self.0);
comparable.hash(state);
}
}
impl PartialEq<Self> for HashableExpr<'_> {
fn eq(&self, other: &Self) -> bool {
let comparable = ComparableExpr::from(self.0);
comparable == ComparableExpr::from(other.0)
}
}
impl Eq for HashableExpr<'_> {}
impl<'a> From<&'a Expr> for HashableExpr<'a> {
fn from(expr: &'a Expr) -> Self {
Self(expr)
}
}
impl<'a> HashableExpr<'a> {
pub(crate) fn from_expr(expr: &'a Expr) -> Self {
Self(expr)
}
pub(crate) fn as_expr(&self) -> &'a Expr {
self.0
}
}

View File

@@ -2,6 +2,7 @@ pub mod branch_detection;
pub mod cast;
pub mod comparable;
pub mod function_type;
pub mod hashable;
pub mod helpers;
pub mod operations;
pub mod relocate;

View File

@@ -36,8 +36,8 @@ use crate::rules::{
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pie, flake8_print,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_unused_arguments, flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle,
pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -1386,6 +1386,7 @@ where
stmt,
test,
body,
orelse,
self.current_stmt_parent().map(Into::into),
);
}
@@ -1578,6 +1579,12 @@ where
if self.settings.rules.enabled(&Rule::TryConsiderElse) {
tryceratops::rules::try_consider_else(self, body, orelse);
}
if self.settings.rules.enabled(&Rule::VerboseRaise) {
tryceratops::rules::verbose_raise(self, handlers);
}
if self.settings.rules.enabled(&Rule::RaiseWithinTry) {
tryceratops::rules::raise_within_try(self, body);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
@@ -2203,6 +2210,11 @@ where
flake8_bugbear::rules::zip_without_explicit_strict(self, expr, func, keywords);
}
// flake8-pie
if self.settings.rules.enabled(&Rule::NoUnnecessaryDictKwargs) {
flake8_pie::rules::no_unnecessary_dict_kwargs(self, expr, keywords);
}
// flake8-bandit
if self.settings.rules.enabled(&Rule::ExecUsed) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
@@ -2249,6 +2261,15 @@ where
if self.settings.rules.enabled(&Rule::RequestWithoutTimeout) {
flake8_bandit::rules::request_without_timeout(self, func, args, keywords);
}
if self
.settings
.rules
.enabled(&Rule::LoggingConfigInsecureListen)
{
flake8_bandit::rules::logging_config_insecure_listen(
self, func, args, keywords,
);
}
// flake8-comprehensions
if self.settings.rules.enabled(&Rule::UnnecessaryGeneratorList) {
@@ -2545,6 +2566,35 @@ where
{
flake8_simplify::rules::open_file_with_context_handler(self, func);
}
// flake8-use-pathlib
if self.settings.rules.enabled(&Rule::PathlibAbspath)
|| self.settings.rules.enabled(&Rule::PathlibChmod)
|| self.settings.rules.enabled(&Rule::PathlibMkdir)
|| self.settings.rules.enabled(&Rule::PathlibMakedirs)
|| self.settings.rules.enabled(&Rule::PathlibRename)
|| self.settings.rules.enabled(&Rule::PathlibReplace)
|| self.settings.rules.enabled(&Rule::PathlibRmdir)
|| self.settings.rules.enabled(&Rule::PathlibRemove)
|| self.settings.rules.enabled(&Rule::PathlibUnlink)
|| self.settings.rules.enabled(&Rule::PathlibGetcwd)
|| self.settings.rules.enabled(&Rule::PathlibExists)
|| self.settings.rules.enabled(&Rule::PathlibExpanduser)
|| self.settings.rules.enabled(&Rule::PathlibIsDir)
|| self.settings.rules.enabled(&Rule::PathlibIsFile)
|| self.settings.rules.enabled(&Rule::PathlibIsLink)
|| self.settings.rules.enabled(&Rule::PathlibReadlink)
|| self.settings.rules.enabled(&Rule::PathlibStat)
|| self.settings.rules.enabled(&Rule::PathlibIsAbs)
|| self.settings.rules.enabled(&Rule::PathlibJoin)
|| self.settings.rules.enabled(&Rule::PathlibBasename)
|| self.settings.rules.enabled(&Rule::PathlibSamefile)
|| self.settings.rules.enabled(&Rule::PathlibSplitext)
|| self.settings.rules.enabled(&Rule::PathlibOpen)
|| self.settings.rules.enabled(&Rule::PathlibPyPath)
{
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}
}
ExprKind::Dict { keys, values } => {
if self
@@ -2558,6 +2608,10 @@ where
{
pyflakes::rules::repeated_keys(self, keys, values);
}
if self.settings.rules.enabled(&Rule::NoUnnecessarySpread) {
flake8_pie::rules::no_unnecessary_spread(self, keys, values);
}
}
ExprKind::Yield { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
@@ -3306,6 +3360,9 @@ where
body,
);
}
if self.settings.rules.enabled(&Rule::ReraiseNoCause) {
tryceratops::rules::reraise_no_cause(self, body);
}
match name {
Some(name) => {
if self.settings.rules.enabled(&Rule::AmbiguousVariableName) {

View File

@@ -1,8 +1,12 @@
//! Lint rules based on checking raw physical lines.
use std::path::Path;
use crate::registry::{Diagnostic, Rule};
use crate::rules::flake8_executable::helpers::extract_shebang;
use crate::rules::flake8_executable::rules::{shebang_newline, shebang_python, shebang_whitespace};
use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective};
use crate::rules::flake8_executable::rules::{
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,
};
use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
};
@@ -11,6 +15,7 @@ use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
pub fn check_lines(
path: &Path,
contents: &str,
commented_lines: &[usize],
doc_lines: &[usize],
@@ -18,8 +23,11 @@ pub fn check_lines(
autofix: flags::Autofix,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
let mut has_any_shebang = false;
let enforce_blanket_noqa = settings.rules.enabled(&Rule::BlanketNOQA);
let enforce_shebang_not_executable = settings.rules.enabled(&Rule::ShebangNotExecutable);
let enforce_shebang_missing = settings.rules.enabled(&Rule::ShebangMissingExecutableFile);
let enforce_shebang_whitespace = settings.rules.enabled(&Rule::ShebangWhitespace);
let enforce_shebang_newline = settings.rules.enabled(&Rule::ShebangNewline);
let enforce_shebang_python = settings.rules.enabled(&Rule::ShebangPython);
@@ -68,8 +76,23 @@ pub fn check_lines(
}
}
if enforce_shebang_whitespace || enforce_shebang_newline || enforce_shebang_python {
if enforce_shebang_missing
|| enforce_shebang_not_executable
|| enforce_shebang_whitespace
|| enforce_shebang_newline
|| enforce_shebang_python
{
let shebang = extract_shebang(line);
if enforce_shebang_not_executable {
if let Some(diagnostic) = shebang_not_executable(path, index, &shebang) {
diagnostics.push(diagnostic);
}
}
if enforce_shebang_missing {
if !has_any_shebang && matches!(shebang, ShebangDirective::Match(_, _, _, _)) {
has_any_shebang = true;
}
}
if enforce_shebang_whitespace {
if let Some(diagnostic) =
shebang_whitespace(index, &shebang, fix_shebang_whitespace)
@@ -124,12 +147,20 @@ pub fn check_lines(
}
}
if enforce_shebang_missing && !has_any_shebang {
if let Some(diagnostic) = shebang_missing(path) {
diagnostics.push(diagnostic);
}
}
diagnostics
}
#[cfg(test)]
mod tests {
use std::path::Path;
use super::check_lines;
use crate::registry::Rule;
use crate::settings::{flags, Settings};
@@ -139,6 +170,7 @@ mod tests {
let line = "'\u{4e9c}' * 2"; // 7 in UTF-32, 9 in UTF-8.
let check_with_max_line_length = |line_length: usize| {
check_lines(
Path::new("foo.py"),
line,
&[],
&[],

View File

@@ -6,7 +6,8 @@ use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::noqa::{is_file_exempt, Directive};
use crate::registry::{Diagnostic, DiagnosticKind, Rule, CODE_REDIRECTS};
use crate::registry::{Diagnostic, DiagnosticKind, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::settings::{flags, Settings};
use crate::violations::UnusedCodes;
use crate::{noqa, violations};
@@ -123,7 +124,7 @@ pub fn check_noqa(
let mut valid_codes = vec![];
let mut self_ignore = false;
for code in codes {
let code = CODE_REDIRECTS.get(code).map_or(code, |r| r.code());
let code = get_redirect_target(code).unwrap_or(code);
if code == Rule::UnusedNOQA.code() {
self_ignore = true;
break;

View File

@@ -124,9 +124,12 @@ pub fn check_tokens(
// ISC001, ISC002
if enforce_implicit_string_concatenation {
diagnostics.extend(
flake8_implicit_str_concat::rules::implicit(tokens)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
flake8_implicit_str_concat::rules::implicit(
tokens,
&settings.flake8_implicit_str_concat,
)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
}

View File

@@ -1,12 +1,12 @@
use std::collections::{BTreeSet, HashMap};
use anyhow::Result;
use colored::Colorize;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
use crate::registry::RuleSelector;
use crate::registry::RuleCodePrefix;
use crate::rule_selector::{prefix_to_selector, RuleSelector};
use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
@@ -21,6 +21,12 @@ use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
use crate::warn_user;
const DEFAULT_SELECTORS: &[RuleSelector] = &[
prefix_to_selector(RuleCodePrefix::F),
prefix_to_selector(RuleCodePrefix::E),
prefix_to_selector(RuleCodePrefix::W),
];
pub fn convert(
config: &HashMap<String, HashMap<String, Option<String>>>,
external_config: &ExternalConfig,
@@ -76,7 +82,7 @@ pub fn convert(
.as_ref()
.map(|value| BTreeSet::from_iter(parser::parse_prefix_codes(value)))
})
.unwrap_or_else(|| plugin::resolve_select(&plugins));
.unwrap_or_else(|| resolve_select(&plugins));
let mut ignore = flake8
.get("ignore")
.and_then(|value| {
@@ -406,21 +412,47 @@ pub fn convert(
Ok(Pyproject::new(options))
}
/// Resolve the set of enabled `RuleSelector` values for the given
/// plugins.
fn resolve_select(plugins: &[Plugin]) -> BTreeSet<RuleSelector> {
let mut select: BTreeSet<_> = DEFAULT_SELECTORS.iter().cloned().collect();
select.extend(plugins.iter().map(Plugin::selector));
select
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use anyhow::Result;
use itertools::Itertools;
use super::super::plugin::Plugin;
use super::convert;
use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS;
use crate::flake8_to_ruff::ExternalConfig;
use crate::registry::RuleSelector;
use crate::registry::RuleCodePrefix;
use crate::rule_selector::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
fn default_options(plugins: impl IntoIterator<Item = RuleSelector>) -> Options {
Options {
ignore: Some(vec![]),
select: Some(
DEFAULT_SELECTORS
.iter()
.cloned()
.chain(plugins)
.sorted()
.collect(),
),
..Options::default()
}
}
#[test]
fn it_converts_empty() -> Result<()> {
let actual = convert(
@@ -428,55 +460,7 @@ mod tests {
&ExternalConfig::default(),
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: None,
pylint: None,
pyupgrade: None,
});
let expected = Pyproject::new(default_options([]));
assert_eq!(actual, expected);
Ok(())
@@ -493,53 +477,8 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: None,
pylint: None,
pyupgrade: None,
..default_options([])
});
assert_eq!(actual, expected);
@@ -557,53 +496,8 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: Some(100),
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: None,
pylint: None,
pyupgrade: None,
..default_options([])
});
assert_eq!(actual, expected);
@@ -620,55 +514,7 @@ mod tests {
&ExternalConfig::default(),
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: None,
pylint: None,
pyupgrade: None,
});
let expected = Pyproject::new(default_options([]));
assert_eq!(actual, expected);
Ok(())
@@ -685,58 +531,13 @@ mod tests {
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: None,
pylint: None,
pyupgrade: None,
..default_options([])
});
assert_eq!(actual, expected);
@@ -757,60 +558,10 @@ mod tests {
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleSelector::D,
RuleSelector::E,
RuleSelector::F,
RuleSelector::W,
]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: Some(pydocstyle::settings::Options {
convention: Some(Convention::Numpy),
}),
pylint: None,
pyupgrade: None,
..default_options([RuleCodePrefix::D.into()])
});
assert_eq!(actual, expected);
@@ -828,63 +579,13 @@ mod tests {
None,
)?;
let expected = Pyproject::new(Options {
allowed_confusables: None,
builtins: None,
cache_dir: None,
dummy_variable_rgx: None,
exclude: None,
extend: None,
extend_exclude: None,
extend_ignore: None,
extend_select: None,
external: None,
fix: None,
fix_only: None,
fixable: None,
format: None,
force_exclude: None,
ignore: Some(vec![]),
ignore_init_module_imports: None,
line_length: None,
namespace_packages: None,
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleSelector::E,
RuleSelector::F,
RuleSelector::Q,
RuleSelector::W,
]),
show_source: None,
src: None,
target_version: None,
unfixable: None,
typing_modules: None,
task_tags: None,
update_check: None,
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: Some(flake8_quotes::settings::Options {
inline_quotes: Some(flake8_quotes::settings::Quote::Single),
multiline_quotes: None,
docstring_quotes: None,
avoid_escape: None,
}),
flake8_tidy_imports: None,
flake8_import_conventions: None,
flake8_unused_arguments: None,
isort: None,
mccabe: None,
pep8_naming: None,
pycodestyle: None,
pydocstyle: None,
pylint: None,
pyupgrade: None,
..default_options([RuleCodePrefix::Q.into()])
});
assert_eq!(actual, expected);

View File

@@ -1,12 +1,11 @@
use std::str::FromStr;
use anyhow::{bail, Result};
use colored::Colorize;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashMap;
use crate::registry::{RuleSelector, PREFIX_REDIRECTS};
use crate::rule_selector::RuleSelector;
use crate::settings::types::PatternPrefixPair;
use crate::warn_user;
@@ -21,9 +20,7 @@ pub fn parse_prefix_codes(value: &str) -> Vec<RuleSelector> {
if code.is_empty() {
continue;
}
if let Some(code) = PREFIX_REDIRECTS.get(code) {
codes.push(code.clone());
} else if let Ok(code) = RuleSelector::from_str(code) {
if let Ok(code) = RuleSelector::from_str(code) {
codes.push(code);
} else {
warn_user!("Unsupported prefix code: {code}");
@@ -89,14 +86,7 @@ impl State {
fn parse(&self) -> Vec<PatternPrefixPair> {
let mut codes: Vec<PatternPrefixPair> = vec![];
for code in &self.codes {
if let Some(code) = PREFIX_REDIRECTS.get(code.as_str()) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
prefix: code.clone(),
});
}
} else if let Ok(code) = RuleSelector::from_str(code) {
if let Ok(code) = RuleSelector::from_str(code) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
@@ -206,7 +196,8 @@ mod tests {
use anyhow::Result;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
use crate::registry::RuleSelector;
use crate::registry::RuleCodePrefix;
use crate::rule_selector::RuleSelector;
use crate::settings::types::PatternPrefixPair;
#[test]
@@ -220,19 +211,19 @@ mod tests {
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401");
let expected = vec![RuleSelector::F401];
let expected = vec![RuleCodePrefix::F401.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,");
let expected = vec![RuleSelector::F401];
let expected = vec![RuleCodePrefix::F401.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,E501");
let expected = vec![RuleSelector::F401, RuleSelector::E501];
let expected = vec![RuleCodePrefix::F401.into(), RuleCodePrefix::E501.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401, E501");
let expected = vec![RuleSelector::F401, RuleSelector::E501];
let expected = vec![RuleCodePrefix::F401.into(), RuleCodePrefix::E501.into()];
assert_eq!(actual, expected);
}
@@ -285,11 +276,11 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "locust/test/*".to_string(),
prefix: RuleSelector::F841,
prefix: RuleCodePrefix::F841.into(),
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: RuleSelector::F841,
prefix: RuleCodePrefix::F841.into(),
},
];
assert_eq!(actual, expected);
@@ -305,23 +296,23 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "t/*".to_string(),
prefix: RuleSelector::D,
prefix: RuleCodePrefix::D.into(),
},
PatternPrefixPair {
pattern: "setup.py".to_string(),
prefix: RuleSelector::D,
prefix: RuleCodePrefix::D.into(),
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: RuleSelector::D,
prefix: RuleCodePrefix::D.into(),
},
PatternPrefixPair {
pattern: "docs/*".to_string(),
prefix: RuleSelector::D,
prefix: RuleCodePrefix::D.into(),
},
PatternPrefixPair {
pattern: "extra/*".to_string(),
prefix: RuleSelector::D,
prefix: RuleCodePrefix::D.into(),
},
];
assert_eq!(actual, expected);
@@ -343,47 +334,47 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "scrapy/__init__.py".to_string(),
prefix: RuleSelector::E402,
prefix: RuleCodePrefix::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/core/downloader/handlers/http.py".to_string(),
prefix: RuleSelector::F401,
prefix: RuleCodePrefix::F401.into(),
},
PatternPrefixPair {
pattern: "scrapy/http/__init__.py".to_string(),
prefix: RuleSelector::F401,
prefix: RuleCodePrefix::F401.into(),
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: RuleSelector::E402,
prefix: RuleCodePrefix::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: RuleSelector::F401,
prefix: RuleCodePrefix::F401.into(),
},
PatternPrefixPair {
pattern: "scrapy/selector/__init__.py".to_string(),
prefix: RuleSelector::F401,
prefix: RuleCodePrefix::F401.into(),
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: RuleSelector::E402,
prefix: RuleCodePrefix::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: RuleSelector::F401,
prefix: RuleCodePrefix::F401.into(),
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: RuleSelector::F403,
prefix: RuleCodePrefix::F403.into(),
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: RuleSelector::F405,
prefix: RuleCodePrefix::F405.into(),
},
PatternPrefixPair {
pattern: "tests/test_loader.py".to_string(),
prefix: RuleSelector::E741,
prefix: RuleCodePrefix::E741.into(),
},
];
assert_eq!(actual, expected);

View File

@@ -4,32 +4,44 @@ use std::str::FromStr;
use anyhow::anyhow;
use crate::registry::RuleSelector;
use crate::registry::RuleCodePrefix;
use crate::rule_selector::RuleSelector;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
Flake82020,
Flake8Annotations,
Flake8Bandit,
Flake8BlindExcept,
Flake8BooleanTrap,
Flake8Bugbear,
Flake8Builtins,
Flake8Commas,
Flake8Comprehensions,
Flake8Datetimez,
Flake8Debugger,
Flake8Docstrings,
Flake8Eradicate,
Flake8ErrMsg,
Flake8Executable,
Flake8ImplicitStrConcat,
Flake8ImportConventions,
Flake8NoPep420,
Flake8Pie,
Flake8Print,
Flake8PytestStyle,
Flake8Quotes,
Flake8Return,
Flake8Simplify,
Flake8TidyImports,
Flake8TypeChecking,
Flake8UnusedArguments,
Flake8UsePathlib,
McCabe,
PEP8Naming,
PandasVet,
Pyupgrade,
Tryceratops,
}
impl FromStr for Plugin {
@@ -37,28 +49,39 @@ impl FromStr for Plugin {
fn from_str(string: &str) -> Result<Self, Self::Err> {
match string {
"flake8-2020" => Ok(Plugin::Flake82020),
"flake8-annotations" => Ok(Plugin::Flake8Annotations),
"flake8-bandit" => Ok(Plugin::Flake8Bandit),
"flake8-blind-except" => Ok(Plugin::Flake8BlindExcept),
"flake8-boolean-trap" => Ok(Plugin::Flake8BooleanTrap),
"flake8-bugbear" => Ok(Plugin::Flake8Bugbear),
"flake8-builtins" => Ok(Plugin::Flake8Builtins),
"flake8-commas" => Ok(Plugin::Flake8Commas),
"flake8-comprehensions" => Ok(Plugin::Flake8Comprehensions),
"flake8-datetimez" => Ok(Plugin::Flake8Datetimez),
"flake8-debugger" => Ok(Plugin::Flake8Debugger),
"flake8-docstrings" => Ok(Plugin::Flake8Docstrings),
"flake8-eradicate" => Ok(Plugin::Flake8Eradicate),
"flake8-errmsg" => Ok(Plugin::Flake8ErrMsg),
"flake8-executable" => Ok(Plugin::Flake8Executable),
"flake8-implicit-str-concat" => Ok(Plugin::Flake8ImplicitStrConcat),
"flake8-import-conventions" => Ok(Plugin::Flake8ImportConventions),
"flake8-no-pep420" => Ok(Plugin::Flake8NoPep420),
"flake8-pie" => Ok(Plugin::Flake8Pie),
"flake8-print" => Ok(Plugin::Flake8Print),
"flake8-pytest-style" => Ok(Plugin::Flake8PytestStyle),
"flake8-quotes" => Ok(Plugin::Flake8Quotes),
"flake8-return" => Ok(Plugin::Flake8Return),
"flake8-simplify" => Ok(Plugin::Flake8Simplify),
"flake8-tidy-imports" => Ok(Plugin::Flake8TidyImports),
"flake8-type-checking" => Ok(Plugin::Flake8TypeChecking),
"flake8-unused-arguments" => Ok(Plugin::Flake8UnusedArguments),
"flake8-use-pathlib" => Ok(Plugin::Flake8UsePathlib),
"mccabe" => Ok(Plugin::McCabe),
"pandas-vet" => Ok(Plugin::PandasVet),
"pep8-naming" => Ok(Plugin::PEP8Naming),
"pandas-vet" => Ok(Plugin::PandasVet),
"pyupgrade" => Ok(Plugin::Pyupgrade),
"tryceratops" => Ok(Plugin::Tryceratops),
_ => Err(anyhow!("Unknown plugin: {string}")),
}
}
@@ -70,60 +93,81 @@ impl fmt::Debug for Plugin {
f,
"{}",
match self {
Plugin::Flake82020 => "flake8-2020",
Plugin::Flake8Annotations => "flake8-annotations",
Plugin::Flake8Bandit => "flake8-bandit",
Plugin::Flake8BlindExcept => "flake8-blind-except",
Plugin::Flake8BooleanTrap => "flake8-boolean-trap",
Plugin::Flake8Bugbear => "flake8-bugbear",
Plugin::Flake8Builtins => "flake8-builtins",
Plugin::Flake8Commas => "flake8-commas",
Plugin::Flake8Comprehensions => "flake8-comprehensions",
Plugin::Flake8Datetimez => "flake8-datetimez",
Plugin::Flake8Debugger => "flake8-debugger",
Plugin::Flake8Docstrings => "flake8-docstrings",
Plugin::Flake8Eradicate => "flake8-eradicate",
Plugin::Flake8ErrMsg => "flake8-errmsg",
Plugin::Flake8Executable => "flake8-executable",
Plugin::Flake8ImplicitStrConcat => "flake8-implicit-str-concat",
Plugin::Flake8ImportConventions => "flake8-import-conventions",
Plugin::Flake8NoPep420 => "flake8-no-pep420",
Plugin::Flake8Pie => "flake8-pie",
Plugin::Flake8Print => "flake8-print",
Plugin::Flake8PytestStyle => "flake8-pytest-style",
Plugin::Flake8Quotes => "flake8-quotes",
Plugin::Flake8Return => "flake8-return",
Plugin::Flake8Simplify => "flake8-simplify",
Plugin::Flake8TidyImports => "flake8-tidy-imports",
Plugin::Flake8TypeChecking => "flake8-type-checking",
Plugin::Flake8UnusedArguments => "flake8-unused-arguments",
Plugin::Flake8UsePathlib => "flake8-use-pathlib",
Plugin::McCabe => "mccabe",
Plugin::PEP8Naming => "pep8-naming",
Plugin::PandasVet => "pandas-vet",
Plugin::Pyupgrade => "pyupgrade",
Plugin::Tryceratops => "tryceratops",
}
)
}
}
// TODO(martin): Convert into `impl From<Plugin> for Linter`
impl Plugin {
pub fn selector(&self) -> RuleSelector {
match self {
Plugin::Flake8Annotations => RuleSelector::ANN,
Plugin::Flake8Bandit => RuleSelector::S,
// TODO(charlie): Handle rename of `B` to `BLE`.
Plugin::Flake8BlindExcept => RuleSelector::BLE,
Plugin::Flake8Bugbear => RuleSelector::B,
Plugin::Flake8Builtins => RuleSelector::A,
Plugin::Flake8Comprehensions => RuleSelector::C4,
Plugin::Flake8Datetimez => RuleSelector::DTZ,
Plugin::Flake8Debugger => RuleSelector::T1,
Plugin::Flake8Docstrings => RuleSelector::D,
// TODO(charlie): Handle rename of `E` to `ERA`.
Plugin::Flake8Eradicate => RuleSelector::ERA,
Plugin::Flake8ErrMsg => RuleSelector::EM,
Plugin::Flake8ImplicitStrConcat => RuleSelector::ISC,
Plugin::Flake8Print => RuleSelector::T2,
Plugin::Flake8PytestStyle => RuleSelector::PT,
Plugin::Flake8Quotes => RuleSelector::Q,
Plugin::Flake8Return => RuleSelector::RET,
Plugin::Flake8Simplify => RuleSelector::SIM,
Plugin::Flake8TidyImports => RuleSelector::TID25,
Plugin::McCabe => RuleSelector::C9,
Plugin::PandasVet => RuleSelector::PD,
Plugin::PEP8Naming => RuleSelector::N,
Plugin::Pyupgrade => RuleSelector::UP,
Plugin::Flake82020 => RuleCodePrefix::YTT.into(),
Plugin::Flake8Annotations => RuleCodePrefix::ANN.into(),
Plugin::Flake8Bandit => RuleCodePrefix::S.into(),
Plugin::Flake8BlindExcept => RuleCodePrefix::BLE.into(),
Plugin::Flake8BooleanTrap => RuleCodePrefix::FBT.into(),
Plugin::Flake8Bugbear => RuleCodePrefix::B.into(),
Plugin::Flake8Builtins => RuleCodePrefix::A.into(),
Plugin::Flake8Commas => RuleCodePrefix::COM.into(),
Plugin::Flake8Comprehensions => RuleCodePrefix::C4.into(),
Plugin::Flake8Datetimez => RuleCodePrefix::DTZ.into(),
Plugin::Flake8Debugger => RuleCodePrefix::T1.into(),
Plugin::Flake8Docstrings => RuleCodePrefix::D.into(),
Plugin::Flake8Eradicate => RuleCodePrefix::ERA.into(),
Plugin::Flake8ErrMsg => RuleCodePrefix::EM.into(),
Plugin::Flake8Executable => RuleCodePrefix::EXE.into(),
Plugin::Flake8ImplicitStrConcat => RuleCodePrefix::ISC.into(),
Plugin::Flake8ImportConventions => RuleCodePrefix::ICN.into(),
Plugin::Flake8NoPep420 => RuleCodePrefix::INP.into(),
Plugin::Flake8Pie => RuleCodePrefix::PIE.into(),
Plugin::Flake8Print => RuleCodePrefix::T2.into(),
Plugin::Flake8PytestStyle => RuleCodePrefix::PT.into(),
Plugin::Flake8Quotes => RuleCodePrefix::Q.into(),
Plugin::Flake8Return => RuleCodePrefix::RET.into(),
Plugin::Flake8Simplify => RuleCodePrefix::SIM.into(),
Plugin::Flake8TidyImports => RuleCodePrefix::TID.into(),
Plugin::Flake8TypeChecking => RuleCodePrefix::TYP.into(),
Plugin::Flake8UnusedArguments => RuleCodePrefix::ARG.into(),
Plugin::Flake8UsePathlib => RuleCodePrefix::PTH.into(),
Plugin::McCabe => RuleCodePrefix::C9.into(),
Plugin::PEP8Naming => RuleCodePrefix::N.into(),
Plugin::PandasVet => RuleCodePrefix::PD.into(),
Plugin::Pyupgrade => RuleCodePrefix::UP.into(),
Plugin::Tryceratops => RuleCodePrefix::TRY.into(),
}
}
}
@@ -250,26 +294,40 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
/// For example, if the user ignores `ANN101`, we should infer that
/// `flake8-annotations` is active.
pub fn infer_plugins_from_codes(selectors: &BTreeSet<RuleSelector>) -> Vec<Plugin> {
// Ignore cases in which we've knowingly changed rule prefixes.
[
Plugin::Flake82020,
Plugin::Flake8Annotations,
Plugin::Flake8Bandit,
Plugin::Flake8BlindExcept,
// Plugin::Flake8BlindExcept,
Plugin::Flake8BooleanTrap,
Plugin::Flake8Bugbear,
Plugin::Flake8Builtins,
// Plugin::Flake8Commas,
Plugin::Flake8Comprehensions,
Plugin::Flake8Datetimez,
Plugin::Flake8Debugger,
Plugin::Flake8Docstrings,
Plugin::Flake8Eradicate,
// Plugin::Flake8Eradicate,
Plugin::Flake8ErrMsg,
Plugin::Flake8Executable,
Plugin::Flake8ImplicitStrConcat,
// Plugin::Flake8ImportConventions,
Plugin::Flake8NoPep420,
Plugin::Flake8Pie,
Plugin::Flake8Print,
Plugin::Flake8PytestStyle,
Plugin::Flake8Quotes,
Plugin::Flake8Return,
Plugin::Flake8Simplify,
Plugin::Flake8TidyImports,
Plugin::PandasVet,
// Plugin::Flake8TidyImports,
// Plugin::Flake8TypeChecking,
Plugin::Flake8UnusedArguments,
// Plugin::Flake8UsePathlib,
Plugin::McCabe,
Plugin::PEP8Naming,
Plugin::PandasVet,
Plugin::Tryceratops,
]
.into_iter()
.filter(|plugin| {
@@ -286,14 +344,6 @@ pub fn infer_plugins_from_codes(selectors: &BTreeSet<RuleSelector>) -> Vec<Plugi
.collect()
}
/// Resolve the set of enabled `RuleSelector` values for the given
/// plugins.
pub fn resolve_select(plugins: &[Plugin]) -> BTreeSet<RuleSelector> {
let mut select = BTreeSet::from([RuleSelector::F, RuleSelector::E, RuleSelector::W]);
select.extend(plugins.iter().map(Plugin::selector));
select
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;

View File

@@ -19,6 +19,6 @@ pub struct Pyproject {
pub fn parse<P: AsRef<Path>>(path: P) -> Result<Pyproject> {
let contents = std::fs::read_to_string(path)?;
let pyproject = toml_edit::easy::from_str::<Pyproject>(&contents)?;
let pyproject = toml::from_str::<Pyproject>(&contents)?;
Ok(pyproject)
}

View File

@@ -37,6 +37,8 @@ mod noqa;
mod python;
pub mod registry;
pub mod resolver;
mod rule_redirects;
mod rule_selector;
mod rules;
mod rustpython_helpers;
pub mod settings;
@@ -47,6 +49,7 @@ mod violations;
mod visibility;
use cfg_if::cfg_if;
pub use rule_selector::RuleSelector;
pub use violation::{AutofixKind, Availability as AutofixAvailability};
pub use violations::IOError;

View File

@@ -10,9 +10,9 @@ use crate::linter::check_path;
use crate::registry::Rule;
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_errmsg,
flake8_import_conventions, flake8_pytest_style, flake8_quotes, flake8_tidy_imports,
flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint,
pyupgrade,
flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes,
flake8_tidy_imports, flake8_unused_arguments, isort, mccabe, pep8_naming, pycodestyle,
pydocstyle, pylint, pyupgrade,
};
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
@@ -142,6 +142,9 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
flake8_tidy_imports: Some(flake8_tidy_imports::Settings::default().into()),
flake8_implicit_str_concat: Some(
flake8_implicit_str_concat::settings::Settings::default().into(),
),
flake8_import_conventions: Some(
flake8_import_conventions::settings::Settings::default().into(),
),

View File

@@ -141,6 +141,7 @@ pub fn check_path(
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::Lines))
{
diagnostics.extend(check_lines(
path,
contents,
indexer.commented_lines(),
&doc_lines,

View File

@@ -4,6 +4,7 @@ use fern;
#[macro_export]
macro_rules! warn_user_once {
($($arg:tt)*) => {
use colored::Colorize;
static WARNED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
if !WARNED.swap(true, std::sync::atomic::Ordering::SeqCst) {
let message = format!("{}", format_args!($($arg)*));
@@ -20,6 +21,7 @@ macro_rules! warn_user_once {
#[macro_export]
macro_rules! warn_user {
($($arg:tt)*) => {
use colored::Colorize;
let message = format!("{}", format_args!($($arg)*));
eprintln!(
"{}{} {}",

View File

@@ -8,7 +8,8 @@ use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::registry::{Diagnostic, Rule, CODE_REDIRECTS};
use crate::registry::{Diagnostic, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
@@ -71,13 +72,9 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
/// thereof).
pub fn includes(needle: &Rule, haystack: &[&str]) -> bool {
let needle: &str = needle.code();
haystack.iter().any(|candidate| {
if let Some(candidate) = CODE_REDIRECTS.get(candidate) {
needle == candidate.code()
} else {
&needle == candidate
}
})
haystack
.iter()
.any(|candidate| needle == get_redirect_target(candidate).unwrap_or(candidate))
}
pub fn add_noqa(

View File

@@ -1,14 +1,13 @@
//! Registry of [`Rule`] to [`DiagnosticKind`] mappings.
use once_cell::sync::Lazy;
use ruff_macros::RuleNamespace;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, EnumIter};
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::rule_selector::{prefix_to_selector, RuleSelector};
use crate::violation::Violation;
use crate::{rules, violations};
@@ -338,6 +337,7 @@ ruff_macros::define_rule_mapping!(
S506 => violations::UnsafeYAMLLoad,
S508 => violations::SnmpInsecureVersion,
S509 => violations::SnmpWeakCryptography,
S612 => rules::flake8_bandit::rules::LoggingConfigInsecureListen,
S701 => violations::Jinja2AutoescapeFalse,
// flake8-boolean-trap
FBT001 => violations::BooleanPositionalArgInFunctionDefinition,
@@ -413,6 +413,8 @@ ruff_macros::define_rule_mapping!(
PIE790 => violations::NoUnnecessaryPass,
PIE794 => violations::DupeClassFieldDefinitions,
PIE796 => violations::PreferUniqueEnums,
PIE800 => violations::NoUnnecessarySpread,
PIE804 => violations::NoUnnecessaryDictKwargs,
PIE807 => violations::PreferListBuiltin,
// flake8-commas
COM812 => violations::TrailingCommaMissing,
@@ -421,6 +423,8 @@ ruff_macros::define_rule_mapping!(
// flake8-no-pep420
INP001 => violations::ImplicitNamespacePackage,
// flake8-executable
EXE001 => rules::flake8_executable::rules::ShebangNotExecutable,
EXE002 => rules::flake8_executable::rules::ShebangMissingExecutableFile,
EXE003 => rules::flake8_executable::rules::ShebangPython,
EXE004 => rules::flake8_executable::rules::ShebangWhitespace,
EXE005 => rules::flake8_executable::rules::ShebangNewline,
@@ -428,7 +432,36 @@ ruff_macros::define_rule_mapping!(
TYP005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
// tryceratops
TRY004 => rules::tryceratops::rules::PreferTypeError,
TRY200 => rules::tryceratops::rules::ReraiseNoCause,
TRY201 => rules::tryceratops::rules::VerboseRaise,
TRY300 => rules::tryceratops::rules::TryConsiderElse,
TRY301 => rules::tryceratops::rules::RaiseWithinTry,
// flake8-use-pathlib
PTH100 => rules::flake8_use_pathlib::violations::PathlibAbspath,
PTH101 => rules::flake8_use_pathlib::violations::PathlibChmod,
PTH102 => rules::flake8_use_pathlib::violations::PathlibMkdir,
PTH103 => rules::flake8_use_pathlib::violations::PathlibMakedirs,
PTH104 => rules::flake8_use_pathlib::violations::PathlibRename,
PTH105 => rules::flake8_use_pathlib::violations::PathlibReplace,
PTH106 => rules::flake8_use_pathlib::violations::PathlibRmdir,
PTH107 => rules::flake8_use_pathlib::violations::PathlibRemove,
PTH108 => rules::flake8_use_pathlib::violations::PathlibUnlink,
PTH109 => rules::flake8_use_pathlib::violations::PathlibGetcwd,
PTH110 => rules::flake8_use_pathlib::violations::PathlibExists,
PTH111 => rules::flake8_use_pathlib::violations::PathlibExpanduser,
PTH112 => rules::flake8_use_pathlib::violations::PathlibIsDir,
PTH113 => rules::flake8_use_pathlib::violations::PathlibIsFile,
PTH114 => rules::flake8_use_pathlib::violations::PathlibIsLink,
PTH115 => rules::flake8_use_pathlib::violations::PathlibReadlink,
PTH116 => rules::flake8_use_pathlib::violations::PathlibStat,
PTH117 => rules::flake8_use_pathlib::violations::PathlibIsAbs,
PTH118 => rules::flake8_use_pathlib::violations::PathlibJoin,
PTH119 => rules::flake8_use_pathlib::violations::PathlibBasename,
PTH120 => rules::flake8_use_pathlib::violations::PathlibDirname,
PTH121 => rules::flake8_use_pathlib::violations::PathlibSamefile,
PTH122 => rules::flake8_use_pathlib::violations::PathlibSplitext,
PTH123 => rules::flake8_use_pathlib::violations::PathlibOpen,
PTH124 => rules::flake8_use_pathlib::violations::PathlibPyPath,
// ruff
RUF001 => violations::AmbiguousUnicodeCharacterString,
RUF002 => violations::AmbiguousUnicodeCharacterDocstring,
@@ -440,81 +473,122 @@ ruff_macros::define_rule_mapping!(
#[derive(EnumIter, Debug, PartialEq, Eq, RuleNamespace)]
pub enum Linter {
/// [Pyflakes](https://pypi.org/project/pyflakes/)
#[prefix = "F"]
Pyflakes,
/// [pycodestyle](https://pypi.org/project/pycodestyle/)
#[prefix = "E"]
#[prefix = "W"]
Pycodestyle,
/// [mccabe](https://pypi.org/project/mccabe/)
#[prefix = "C90"]
McCabe,
/// [isort](https://pypi.org/project/isort/)
#[prefix = "I"]
Isort,
/// [pydocstyle](https://pypi.org/project/pydocstyle/)
#[prefix = "D"]
Pydocstyle,
/// [pyupgrade](https://pypi.org/project/pyupgrade/)
#[prefix = "UP"]
Pyupgrade,
/// [pep8-naming](https://pypi.org/project/pep8-naming/)
#[prefix = "N"]
PEP8Naming,
/// [flake8-2020](https://pypi.org/project/flake8-2020/)
#[prefix = "YTT"]
Flake82020,
/// [flake8-annotations](https://pypi.org/project/flake8-annotations/)
#[prefix = "ANN"]
Flake8Annotations,
/// [flake8-bandit](https://pypi.org/project/flake8-bandit/)
#[prefix = "S"]
Flake8Bandit,
/// [flake8-blind-except](https://pypi.org/project/flake8-blind-except/)
#[prefix = "BLE"]
Flake8BlindExcept,
/// [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/)
#[prefix = "FBT"]
Flake8BooleanTrap,
/// [flake8-bugbear](https://pypi.org/project/flake8-bugbear/)
#[prefix = "B"]
Flake8Bugbear,
/// [flake8-builtins](https://pypi.org/project/flake8-builtins/)
#[prefix = "A"]
Flake8Builtins,
/// [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
#[prefix = "C4"]
Flake8Comprehensions,
/// [flake8-debugger](https://pypi.org/project/flake8-debugger/)
#[prefix = "T10"]
Flake8Debugger,
/// [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
#[prefix = "EM"]
Flake8ErrMsg,
/// [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/)
#[prefix = "ISC"]
Flake8ImplicitStrConcat,
/// [flake8-import-conventions](https://github.com/joaopalmeiro/flake8-import-conventions)
#[prefix = "ICN"]
Flake8ImportConventions,
/// [flake8-print](https://pypi.org/project/flake8-print/)
#[prefix = "T20"]
Flake8Print,
/// [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
#[prefix = "PT"]
Flake8PytestStyle,
/// [flake8-quotes](https://pypi.org/project/flake8-quotes/)
#[prefix = "Q"]
Flake8Quotes,
/// [flake8-return](https://pypi.org/project/flake8-return/)
#[prefix = "RET"]
Flake8Return,
/// [flake8-simplify](https://pypi.org/project/flake8-simplify/)
#[prefix = "SIM"]
Flake8Simplify,
/// [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/)
#[prefix = "TID"]
Flake8TidyImports,
/// [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/)
#[prefix = "ARG"]
Flake8UnusedArguments,
/// [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
#[prefix = "DTZ"]
Flake8Datetimez,
/// [eradicate](https://pypi.org/project/eradicate/)
#[prefix = "ERA"]
Eradicate,
/// [pandas-vet](https://pypi.org/project/pandas-vet/)
#[prefix = "PD"]
PandasVet,
/// [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks)
#[prefix = "PGH"]
PygrepHooks,
/// [Pylint](https://pypi.org/project/pylint/)
#[prefix = "PL"]
Pylint,
/// [flake8-pie](https://pypi.org/project/flake8-pie/)
#[prefix = "PIE"]
Flake8Pie,
/// [flake8-commas](https://pypi.org/project/flake8-commas/)
#[prefix = "COM"]
Flake8Commas,
/// [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/)
#[prefix = "INP"]
Flake8NoPep420,
/// [flake8-executable](https://pypi.org/project/flake8-executable/)
#[prefix = "EXE"]
Flake8Executable,
/// [flake8-type-checking](https://pypi.org/project/flake8-type-checking/)
#[prefix = "TYP"]
Flake8TypeChecking,
/// [tryceratops](https://pypi.org/project/tryceratops/1.1.0/)
#[prefix = "TRY"]
Tryceratops,
/// [flake8-use-pathlib](https://pypi.org/project/flake8-use-pathlib/)
#[prefix = "PTH"]
Flake8UsePathlib,
/// Ruff-specific rules
#[prefix = "RUF"]
Ruff,
}
@@ -523,26 +597,34 @@ pub trait RuleNamespace: Sized {
fn parse_code(code: &str) -> Option<(Self, &str)>;
fn prefixes(&self) -> &'static [&'static str];
}
include!(concat!(env!("OUT_DIR"), "/linter.rs"));
fn name(&self) -> &'static str;
fn url(&self) -> Option<&'static str>;
}
/// The prefix, name and selector for an upstream linter category.
pub struct LinterCategory(pub &'static str, pub &'static str, pub RuleSelector);
// TODO(martin): Move these constant definitions back to Linter::categories impl
// once RuleSelector is an enum with a Linter variant
const PYCODESTYLE_CATEGORIES: &[LinterCategory] = &[
LinterCategory("E", "Error", prefix_to_selector(RuleCodePrefix::E)),
LinterCategory("W", "Warning", prefix_to_selector(RuleCodePrefix::W)),
];
const PYLINT_CATEGORIES: &[LinterCategory] = &[
LinterCategory("PLC", "Convention", prefix_to_selector(RuleCodePrefix::PLC)),
LinterCategory("PLE", "Error", prefix_to_selector(RuleCodePrefix::PLE)),
LinterCategory("PLR", "Refactor", prefix_to_selector(RuleCodePrefix::PLR)),
LinterCategory("PLW", "Warning", prefix_to_selector(RuleCodePrefix::PLW)),
];
impl Linter {
pub fn categories(&self) -> Option<&'static [LinterCategory]> {
match self {
Linter::Pycodestyle => Some(&[
LinterCategory("E", "Error", RuleSelector::E),
LinterCategory("W", "Warning", RuleSelector::W),
]),
Linter::Pylint => Some(&[
LinterCategory("PLC", "Convention", RuleSelector::PLC),
LinterCategory("PLE", "Error", RuleSelector::PLE),
LinterCategory("PLR", "Refactor", RuleSelector::PLR),
LinterCategory("PLW", "Warning", RuleSelector::PLW),
]),
Linter::Pycodestyle => Some(PYCODESTYLE_CATEGORIES),
Linter::Pylint => Some(PYLINT_CATEGORIES),
_ => None,
}
}
@@ -571,6 +653,8 @@ impl Rule {
| Rule::MixedSpacesAndTabs
| Rule::NoNewLineAtEndOfFile
| Rule::PEP3120UnnecessaryCodingComment
| Rule::ShebangMissingExecutableFile
| Rule::ShebangNotExecutable
| Rule::ShebangNewline
| Rule::ShebangPython
| Rule::ShebangWhitespace => &LintSource::Lines,
@@ -582,9 +666,9 @@ impl Rule {
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
| Rule::CommentedOutCode
| Rule::MultiLineImplicitStringConcatenation
| Rule::ExtraneousParentheses
| Rule::InvalidEscapeSequence
| Rule::MultiLineImplicitStringConcatenation
| Rule::SingleLineImplicitStringConcatenation
| Rule::TrailingCommaMissing
| Rule::TrailingCommaOnBareTupleProhibited
@@ -636,60 +720,6 @@ pub const INCOMPATIBLE_CODES: &[(Rule, Rule, &str)] = &[(
Consider adding `D203` to `ignore`.",
)];
/// A hash map from deprecated to latest `Rule`.
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, Rule>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", Rule::UselessMetaclassType),
("U003", Rule::TypeOfPrimitive),
("U004", Rule::UselessObjectInheritance),
("U005", Rule::DeprecatedUnittestAlias),
("U006", Rule::UsePEP585Annotation),
("U007", Rule::UsePEP604Annotation),
("U008", Rule::SuperCallWithParameters),
("U009", Rule::PEP3120UnnecessaryCodingComment),
("U010", Rule::UnnecessaryFutureImport),
("U011", Rule::LRUCacheWithoutParameters),
("U012", Rule::UnnecessaryEncodeUTF8),
("U013", Rule::ConvertTypedDictFunctionalToClass),
("U014", Rule::ConvertNamedTupleFunctionalToClass),
("U015", Rule::RedundantOpenModes),
("U016", Rule::RemoveSixCompat),
("U017", Rule::DatetimeTimezoneUTC),
("U019", Rule::TypingTextStrAlias),
// TODO(charlie): Remove by 2023-02-01.
("I252", Rule::RelativeImports),
("M001", Rule::UnusedNOQA),
// TODO(charlie): Remove by 2023-02-01.
("PDV002", Rule::UseOfInplaceArgument),
("PDV003", Rule::UseOfDotIsNull),
("PDV004", Rule::UseOfDotNotNull),
("PDV007", Rule::UseOfDotIx),
("PDV008", Rule::UseOfDotAt),
("PDV009", Rule::UseOfDotIat),
("PDV010", Rule::UseOfDotPivotOrUnstack),
("PDV011", Rule::UseOfDotValues),
("PDV012", Rule::UseOfDotReadTable),
("PDV013", Rule::UseOfDotStack),
("PDV015", Rule::UseOfPdMerge),
("PDV901", Rule::DfIsABadVariableName),
// TODO(charlie): Remove by 2023-02-01.
("R501", Rule::UnnecessaryReturnNone),
("R502", Rule::ImplicitReturnValue),
("R503", Rule::ImplicitReturn),
("R504", Rule::UnnecessaryAssign),
("R505", Rule::SuperfluousElseReturn),
("R506", Rule::SuperfluousElseRaise),
("R507", Rule::SuperfluousElseContinue),
("R508", Rule::SuperfluousElseBreak),
// TODO(charlie): Remove by 2023-02-01.
("IC001", Rule::ImportAliasIsNotConventional),
("IC002", Rule::ImportAliasIsNotConventional),
("IC003", Rule::ImportAliasIsNotConventional),
("IC004", Rule::ImportAliasIsNotConventional),
])
});
#[cfg(test)]
mod tests {
use strum::IntoEnumIterator;

85
src/rule_redirects.rs Normal file
View File

@@ -0,0 +1,85 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
/// Returns the redirect target for the given code.
pub(crate) fn get_redirect_target(code: &str) -> Option<&'static str> {
REDIRECTS.get(code).copied()
}
/// Returns the code and the redirect target if the given code is a redirect.
/// (The same code is returned to obtain it with a static lifetime).
pub(crate) fn get_redirect(code: &str) -> Option<(&'static str, &'static str)> {
REDIRECTS.get_key_value(code).map(|(k, v)| (*k, *v))
}
static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
HashMap::from_iter([
// TODO(charlie): Remove by 2023-02-01.
("R", "RET"),
("R5", "RET5"),
("R50", "RET50"),
("R501", "RET501"),
("R502", "RET502"),
("R503", "RET503"),
("R504", "RET504"),
("R505", "RET505"),
("R506", "RET506"),
("R507", "RET507"),
("R508", "RET508"),
("IC", "ICN"),
("IC0", "ICN0"),
("IC00", "ICN00"),
("IC001", "ICN001"),
("IC002", "ICN001"),
("IC003", "ICN001"),
("IC004", "ICN001"),
// TODO(charlie): Remove by 2023-01-01.
("U", "UP"),
("U0", "UP0"),
("U00", "UP00"),
("U001", "UP001"),
("U003", "UP003"),
("U004", "UP004"),
("U005", "UP005"),
("U006", "UP006"),
("U007", "UP007"),
("U008", "UP008"),
("U009", "UP009"),
("U01", "UP01"),
("U010", "UP010"),
("U011", "UP011"),
("U012", "UP012"),
("U013", "UP013"),
("U014", "UP014"),
("U015", "UP015"),
("U016", "UP016"),
("U017", "UP017"),
("U019", "UP019"),
// TODO(charlie): Remove by 2023-02-01.
("I2", "TID2"),
("I25", "TID25"),
("I252", "TID252"),
("M", "RUF100"),
("M0", "RUF100"),
("M001", "RUF100"),
// TODO(charlie): Remove by 2023-02-01.
("PDV", "PD"),
("PDV0", "PD0"),
("PDV002", "PD002"),
("PDV003", "PD003"),
("PDV004", "PD004"),
("PDV007", "PD007"),
("PDV008", "PD008"),
("PDV009", "PD009"),
("PDV01", "PD01"),
("PDV010", "PD010"),
("PDV011", "PD011"),
("PDV012", "PD012"),
("PDV013", "PD013"),
("PDV015", "PD015"),
("PDV9", "PD9"),
("PDV90", "PD90"),
("PDV901", "PD901"),
])
});

183
src/rule_selector.rs Normal file
View File

@@ -0,0 +1,183 @@
use std::str::FromStr;
use schemars::_serde_json::Value;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::JsonSchema;
use serde::de::{self, Visitor};
use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use crate::registry::{Rule, RuleCodePrefix, RuleIter};
use crate::rule_redirects::get_redirect;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum RuleSelector {
/// All rules
All,
Prefix {
prefix: RuleCodePrefix,
redirected_from: Option<&'static str>,
},
}
impl FromStr for RuleSelector {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "ALL" {
Ok(Self::All)
} else {
let (s, redirected_from) = match get_redirect(s) {
Some((from, target)) => (target, Some(from)),
None => (s, None),
};
Ok(Self::Prefix {
prefix: RuleCodePrefix::from_str(s)
.map_err(|_| ParseError::Unknown(s.to_string()))?,
redirected_from,
})
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("Unknown rule selector `{0}`")]
// TODO(martin): tell the user how to discover rule codes via the CLI once such a command is
// implemented (but that should of course be done only in ruff_cli and not here)
Unknown(String),
}
impl Serialize for RuleSelector {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
RuleSelector::All => serializer.serialize_str("ALL"),
RuleSelector::Prefix { prefix, .. } => prefix.serialize(serializer),
}
}
}
impl<'de> Deserialize<'de> for RuleSelector {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
// We are not simply doing:
// let s: &str = Deserialize::deserialize(deserializer)?;
// FromStr::from_str(s).map_err(de::Error::custom)
// here because the toml crate apparently doesn't support that
// (as of toml v0.6.0 running `cargo test` failed with the above two lines)
deserializer.deserialize_str(SelectorVisitor)
}
}
struct SelectorVisitor;
impl Visitor<'_> for SelectorVisitor {
type Value = RuleSelector;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str(
"expected a string code identifying a linter or specific rule, or a partial rule code \
or ALL to refer to all rules",
)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
FromStr::from_str(v).map_err(de::Error::custom)
}
}
impl From<RuleCodePrefix> for RuleSelector {
fn from(prefix: RuleCodePrefix) -> Self {
Self::Prefix {
prefix,
redirected_from: None,
}
}
}
impl IntoIterator for &RuleSelector {
type IntoIter = RuleSelectorIter;
type Item = Rule;
fn into_iter(self) -> Self::IntoIter {
match self {
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Prefix(prefix.into_iter()),
}
}
}
pub enum RuleSelectorIter {
All(RuleIter),
Prefix(std::vec::IntoIter<Rule>),
}
impl Iterator for RuleSelectorIter {
type Item = Rule;
fn next(&mut self) -> Option<Self::Item> {
match self {
RuleSelectorIter::All(iter) => iter.next(),
RuleSelectorIter::Prefix(iter) => iter.next(),
}
}
}
/// A const alternative to the `impl From<RuleCodePrefix> for RuleSelector`
// to let us keep the fields of RuleSelector private.
// Note that Rust doesn't yet support `impl const From<RuleCodePrefix> for
// RuleSelector` (see https://github.com/rust-lang/rust/issues/67792).
// TODO(martin): Remove once RuleSelector is an enum with Linter & Rule variants
pub(crate) const fn prefix_to_selector(prefix: RuleCodePrefix) -> RuleSelector {
RuleSelector::Prefix {
prefix,
redirected_from: None,
}
}
impl JsonSchema for RuleSelector {
fn schema_name() -> String {
"RuleSelector".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
Schema::Object(SchemaObject {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(
std::iter::once("ALL".to_string())
.chain(RuleCodePrefix::iter().map(|s| s.as_ref().to_string()))
.map(Value::String)
.collect(),
),
..SchemaObject::default()
})
}
}
impl RuleSelector {
pub(crate) fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
RuleSelector::Prefix { prefix, .. } => prefix.specificity(),
}
}
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum Specificity {
All,
Linter,
Code1Char,
Code2Chars,
Code3Chars,
Code4Chars,
Code5Chars,
}

View File

@@ -74,147 +74,14 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
DefinitionKind::Package => {}
DefinitionKind::Class(_) => {}
DefinitionKind::NestedClass(_) => {}
DefinitionKind::Function(stmt) | DefinitionKind::NestedFunction(stmt) => {
DefinitionKind::Function(stmt)
| DefinitionKind::NestedFunction(stmt)
| DefinitionKind::Method(stmt) => {
let is_method = matches!(definition.kind, DefinitionKind::Method(_));
let (name, args, returns, body) = match_function_def(stmt);
let mut has_any_typed_arg = false;
// ANN001, ANN401
for arg in args
.args
.iter()
.chain(args.posonlyargs.iter())
.chain(args.kwonlyargs.iter())
{
if let Some(expr) = &arg.node.annotation {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
check_dynamically_typed(checker, expr, || arg.node.arg.to_string());
};
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker
.settings
.rules
.enabled(&Rule::MissingTypeFunctionArgument)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeFunctionArgument(arg.node.arg.to_string()),
Range::from_located(arg),
));
}
}
}
}
// ANN002, ANN401
if let Some(arg) = &args.vararg {
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&Rule::MissingTypeArgs) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeArgs(arg.node.arg.to_string()),
Range::from_located(arg),
));
}
}
}
}
// ANN003, ANN401
if let Some(arg) = &args.kwarg {
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&Rule::MissingTypeKwargs) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeKwargs(arg.node.arg.to_string()),
Range::from_located(arg),
));
}
}
}
}
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
check_dynamically_typed(checker, expr, || name.to_string());
};
} else {
// Allow omission of return annotation in `__init__` functions, if the function
// only returns `None` (explicitly or implicitly).
if checker.settings.flake8_annotations.suppress_none_returning
&& is_none_returning(body)
{
return;
}
match visibility {
Visibility::Public => {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePublicFunction)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypePublicFunction(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
));
}
}
Visibility::Private => {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePrivateFunction)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypePrivateFunction(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
));
}
}
}
}
}
DefinitionKind::Method(stmt) => {
let (name, args, returns, body) = match_function_def(stmt);
let mut has_any_typed_arg = false;
// ANN001
for arg in args
.args
.iter()
@@ -222,10 +89,10 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(!visibility::is_staticmethod(
checker,
cast::decorator_list(stmt),
)),
usize::from(
is_method
&& !visibility::is_staticmethod(checker, cast::decorator_list(stmt)),
),
)
{
// ANN401 for dynamically typed arguments
@@ -313,7 +180,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
// ANN101, ANN102
if !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if is_method && !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
@@ -335,7 +202,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
// ANN201, ANN202
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
if checker
.settings
@@ -353,7 +220,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
return;
}
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if is_method && visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker
.settings
.rules
@@ -364,7 +231,9 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
helpers::identifier_range(stmt, checker.locator),
));
}
} else if visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
} else if is_method
&& visibility::is_staticmethod(checker, cast::decorator_list(stmt))
{
if checker
.settings
.rules
@@ -375,7 +244,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
helpers::identifier_range(stmt, checker.locator),
));
}
} else if visibility::is_init(cast::name(stmt)) {
} else if is_method && visibility::is_init(cast::name(stmt)) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker
@@ -401,7 +270,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
checker.diagnostics.push(diagnostic);
}
}
} else if visibility::is_magic(cast::name(stmt)) {
} else if is_method && visibility::is_magic(cast::name(stmt)) {
if checker
.settings
.rules

View File

@@ -28,6 +28,7 @@ mod tests {
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"); "S508")]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"); "S509")]
#[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"); "S612")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());

View File

@@ -0,0 +1,40 @@
use ruff_macros::derive_message_formats;
use rustpython_ast::{Expr, Keyword};
use crate::ast::helpers::SimpleCallArgs;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct LoggingConfigInsecureListen;
);
impl Violation for LoggingConfigInsecureListen {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of insecure `logging.config.listen` detected")
}
}
/// S612
pub fn logging_config_insecure_listen(
checker: &mut Checker,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
) {
if checker.resolve_call_path(func).map_or(false, |call_path| {
call_path.as_slice() == ["logging", "config", "listen"]
}) {
let call_args = SimpleCallArgs::new(args, keywords);
if call_args.get_argument("verify", None).is_none() {
checker.diagnostics.push(Diagnostic::new(
LoggingConfigInsecureListen,
Range::from_located(func),
));
}
}
}

View File

@@ -10,6 +10,9 @@ pub use hardcoded_password_string::{
pub use hardcoded_tmp_directory::hardcoded_tmp_directory;
pub use hashlib_insecure_hash_functions::hashlib_insecure_hash_functions;
pub use jinja2_autoescape_false::jinja2_autoescape_false;
pub use logging_config_insecure_listen::{
logging_config_insecure_listen, LoggingConfigInsecureListen,
};
pub use request_with_no_cert_validation::request_with_no_cert_validation;
pub use request_without_timeout::request_without_timeout;
pub use snmp_insecure_version::snmp_insecure_version;
@@ -26,6 +29,7 @@ mod hardcoded_password_string;
mod hardcoded_tmp_directory;
mod hashlib_insecure_hash_functions;
mod jinja2_autoescape_false;
mod logging_config_insecure_listen;
mod request_with_no_cert_validation;
mod request_without_timeout;
mod snmp_insecure_version;

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
LoggingConfigInsecureListen: ~
location:
row: 3
column: 4
end_location:
row: 3
column: 25
fix: ~
parent: ~

View File

@@ -1,10 +1,10 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind};
use crate::ast::helpers::unparse_stmt;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::Generator;
use crate::violations;
fn assertion_error(msg: Option<&Expr>) -> Stmt {
@@ -48,10 +48,8 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option
let mut diagnostic = Diagnostic::new(violations::DoNotAssertFalse, Range::from_located(test));
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_stmt(&assertion_error(msg));
diagnostic.amend(Fix::replacement(
generator.generate(),
unparse_stmt(&assertion_error(msg), checker.stylist),
stmt.location,
stmt.end_location.unwrap(),
));

View File

@@ -3,11 +3,11 @@ use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_ast::{Excepthandler, ExcepthandlerKind, Expr, ExprContext, ExprKind, Location};
use crate::ast::helpers;
use crate::ast::helpers::unparse_expr;
use crate::ast::types::{CallPath, Range};
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::source_code::Generator;
use crate::violations;
fn type_pattern(elts: Vec<&Expr>) -> Expr {
@@ -59,14 +59,12 @@ fn duplicate_handler_exceptions<'a>(
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
if unique_elts.len() == 1 {
generator.unparse_expr(unique_elts[0], 0);
} else {
generator.unparse_expr(&type_pattern(unique_elts), 0);
}
diagnostic.amend(Fix::replacement(
generator.generate(),
if unique_elts.len() == 1 {
unparse_expr(unique_elts[0], checker.stylist)
} else {
unparse_expr(&type_pattern(unique_elts), checker.stylist)
},
expr.location,
expr.end_location.unwrap(),
));

View File

@@ -1,12 +1,12 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location};
use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::is_identifier;
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::source_code::Generator;
use crate::violations;
fn attribute(value: &Expr, attr: &str) -> Expr {
@@ -48,10 +48,8 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
let mut diagnostic =
Diagnostic::new(violations::GetAttrWithConstant, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(&attribute(obj, value), 0);
diagnostic.amend(Fix::replacement(
generator.generate(),
unparse_expr(&attribute(obj, value), checker.stylist),
expr.location,
expr.end_location.unwrap(),
));

View File

@@ -1,10 +1,10 @@
use rustpython_ast::{Excepthandler, ExcepthandlerKind, ExprKind};
use crate::ast::helpers::unparse_expr;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::source_code::Generator;
use crate::violations;
/// B013
@@ -20,14 +20,12 @@ pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[E
continue;
};
let mut diagnostic = Diagnostic::new(
violations::RedundantTupleInExceptionHandler(elt.to_string()),
violations::RedundantTupleInExceptionHandler(unparse_expr(elt, checker.stylist)),
Range::from_located(type_),
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(elt, 0);
diagnostic.amend(Fix::replacement(
generator.generate(),
unparse_expr(elt, checker.stylist),
type_.location,
type_.end_location.unwrap(),
));

View File

@@ -1,12 +1,13 @@
use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind};
use crate::ast::helpers::unparse_stmt;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::is_identifier;
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::source_code::{Generator, Stylist};
use crate::source_code::Stylist;
use crate::violations;
fn assignment(obj: &Expr, name: &str, value: &Expr, stylist: &Stylist) -> String {
@@ -27,9 +28,7 @@ fn assignment(obj: &Expr, name: &str, value: &Expr, stylist: &Stylist) -> String
type_comment: None,
},
);
let mut generator: Generator = stylist.into();
generator.unparse_stmt(&stmt);
generator.generate()
unparse_stmt(&stmt, stylist)
}
/// B010

View File

@@ -13,6 +13,12 @@ mod tests {
use crate::registry::Rule;
use crate::settings;
#[test_case(Path::new("EXE001_1.py"); "EXE001_1")]
#[test_case(Path::new("EXE001_2.py"); "EXE001_2")]
#[test_case(Path::new("EXE001_3.py"); "EXE001_3")]
#[test_case(Path::new("EXE002_1.py"); "EXE002_1")]
#[test_case(Path::new("EXE002_2.py"); "EXE002_2")]
#[test_case(Path::new("EXE002_3.py"); "EXE002_3")]
#[test_case(Path::new("EXE003.py"); "EXE003")]
#[test_case(Path::new("EXE004_1.py"); "EXE004_1")]
#[test_case(Path::new("EXE004_2.py"); "EXE004_2")]
@@ -27,6 +33,8 @@ mod tests {
.join(path)
.as_path(),
&settings::Settings::for_rules(vec![
Rule::ShebangNotExecutable,
Rule::ShebangMissingExecutableFile,
Rule::ShebangWhitespace,
Rule::ShebangNewline,
Rule::ShebangPython,

View File

@@ -1,7 +1,11 @@
pub use shebang_missing::{shebang_missing, ShebangMissingExecutableFile};
pub use shebang_newline::{shebang_newline, ShebangNewline};
pub use shebang_not_executable::{shebang_not_executable, ShebangNotExecutable};
pub use shebang_python::{shebang_python, ShebangPython};
pub use shebang_whitespace::{shebang_whitespace, ShebangWhitespace};
mod shebang_missing;
mod shebang_newline;
mod shebang_not_executable;
mod shebang_python;
mod shebang_whitespace;

View File

@@ -0,0 +1,37 @@
use std::path::Path;
#[cfg(not(target_family = "wasm"))]
use is_executable::IsExecutable;
use ruff_macros::derive_message_formats;
#[cfg(not(target_family = "wasm"))]
use crate::ast::types::Range;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct ShebangMissingExecutableFile;
);
impl Violation for ShebangMissingExecutableFile {
#[derive_message_formats]
fn message(&self) -> String {
format!("The file is executable but no shebang is present")
}
}
/// EXE002
#[cfg(not(target_family = "wasm"))]
pub fn shebang_missing(filepath: &Path) -> Option<Diagnostic> {
if filepath.is_executable() {
let diagnostic = Diagnostic::new(ShebangMissingExecutableFile, Range::default());
Some(diagnostic)
} else {
None
}
}
#[cfg(target_family = "wasm")]
pub fn shebang_missing(_filepath: &Path) -> Option<Diagnostic> {
None
}

View File

@@ -0,0 +1,58 @@
use std::path::Path;
#[cfg(not(target_family = "wasm"))]
use is_executable::IsExecutable;
use ruff_macros::derive_message_formats;
#[cfg(not(target_family = "wasm"))]
use rustpython_ast::Location;
#[cfg(not(target_family = "wasm"))]
use crate::ast::types::Range;
use crate::define_violation;
use crate::registry::Diagnostic;
use crate::rules::flake8_executable::helpers::ShebangDirective;
use crate::violation::Violation;
define_violation!(
pub struct ShebangNotExecutable;
);
impl Violation for ShebangNotExecutable {
#[derive_message_formats]
fn message(&self) -> String {
format!("Shebang is present but file is not executable")
}
}
/// EXE001
#[cfg(not(target_family = "wasm"))]
pub fn shebang_not_executable(
filepath: &Path,
lineno: usize,
shebang: &ShebangDirective,
) -> Option<Diagnostic> {
if let ShebangDirective::Match(_, start, end, _) = shebang {
if filepath.is_executable() {
None
} else {
let diagnostic = Diagnostic::new(
ShebangNotExecutable,
Range::new(
Location::new(lineno + 1, *start),
Location::new(lineno + 1, *end),
),
);
Some(diagnostic)
}
} else {
None
}
}
#[cfg(target_family = "wasm")]
pub fn shebang_not_executable(
_filepath: &Path,
_lineno: usize,
_shebang: &ShebangDirective,
) -> Option<Diagnostic> {
None
}

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
- kind:
ShebangNotExecutable: ~
location:
row: 1
column: 2
end_location:
row: 1
column: 17
fix: ~
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,15 @@
---
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
- kind:
ShebangMissingExecutableFile: ~
location:
row: 1
column: 0
end_location:
row: 1
column: 0
fix: ~
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,6 @@
---
source: src/rules/flake8_executable/mod.rs
expression: diagnostics
---
[]

View File

@@ -1,5 +1,6 @@
//! Rules from [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/).
pub(crate) mod rules;
pub mod settings;
#[cfg(test)]
mod tests {
@@ -26,4 +27,24 @@ mod tests {
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::SingleLineImplicitStringConcatenation, Path::new("ISC.py"); "ISC001")]
#[test_case(Rule::MultiLineImplicitStringConcatenation, Path::new("ISC.py"); "ISC002")]
#[test_case(Rule::ExplicitStringConcatenation, Path::new("ISC.py"); "ISC003")]
fn multiline(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("multiline_{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_implicit_str_concat")
.join(path)
.as_path(),
&settings::Settings {
flake8_implicit_str_concat: super::settings::Settings {
allow_multiline: false,
},
..settings::Settings::for_rule(rule_code)
},
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -4,13 +4,20 @@ use rustpython_parser::lexer::{LexResult, Tok};
use crate::ast::types::Range;
use crate::registry::Diagnostic;
use crate::rules::flake8_implicit_str_concat::settings::Settings;
use crate::violations;
/// ISC001, ISC002
pub fn implicit(tokens: &[LexResult]) -> Vec<Diagnostic> {
pub fn implicit(tokens: &[LexResult], settings: &Settings) -> Vec<Diagnostic> {
let mut diagnostics = vec![];
for ((a_start, a_tok, a_end), (b_start, b_tok, b_end)) in
tokens.iter().flatten().tuple_windows()
for ((a_start, a_tok, a_end), (b_start, b_tok, b_end)) in tokens
.iter()
.flatten()
.filter(|(_, tok, _)| {
!matches!(tok, Tok::Comment(..))
&& (settings.allow_multiline || !matches!(tok, Tok::NonLogicalNewline))
})
.tuple_windows()
{
if matches!(a_tok, Tok::String { .. }) && matches!(b_tok, Tok::String { .. }) {
if a_end.row() == b_start.row() {
@@ -22,8 +29,6 @@ pub fn implicit(tokens: &[LexResult]) -> Vec<Diagnostic> {
},
));
} else {
// Not on the same line, and no NonLogicalNewline between a and b =>
// concatantion over a continuation line.
diagnostics.push(Diagnostic::new(
violations::MultiLineImplicitStringConcatenation,
Range {

View File

@@ -0,0 +1,57 @@
//! Settings for the `flake8-implicit-str-concat` plugin.
use ruff_macros::ConfigurationOptions;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(
Debug, PartialEq, Eq, Serialize, Deserialize, Default, ConfigurationOptions, JsonSchema,
)]
#[serde(
deny_unknown_fields,
rename_all = "kebab-case",
rename = "Flake8ImplicitStrConcatOptions"
)]
pub struct Options {
#[option(
default = r#"true"#,
value_type = "bool",
example = r#"
allow-multiline = false
"#
)]
/// Whether to allow implicit string concatenations for multiline strings.
/// By default, implicit concatenations of multiline strings are
/// allowed (but continuation lines, delimited with a backslash, are
/// prohibited).
pub allow_multiline: Option<bool>,
}
#[derive(Debug, Hash)]
pub struct Settings {
pub allow_multiline: bool,
}
impl Default for Settings {
fn default() -> Self {
Self {
allow_multiline: true,
}
}
}
impl From<Options> for Settings {
fn from(options: Options) -> Self {
Self {
allow_multiline: options.allow_multiline.unwrap_or(true),
}
}
}
impl From<Settings> for Options {
fn from(settings: Settings) -> Self {
Self {
allow_multiline: Some(settings.allow_multiline),
}
}
}

View File

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

View File

@@ -0,0 +1,45 @@
---
source: src/rules/flake8_implicit_str_concat/mod.rs
expression: diagnostics
---
- kind:
MultiLineImplicitStringConcatenation: ~
location:
row: 5
column: 4
end_location:
row: 6
column: 9
fix: ~
parent: ~
- kind:
MultiLineImplicitStringConcatenation: ~
location:
row: 24
column: 2
end_location:
row: 25
column: 7
fix: ~
parent: ~
- kind:
MultiLineImplicitStringConcatenation: ~
location:
row: 29
column: 2
end_location:
row: 30
column: 7
fix: ~
parent: ~
- kind:
MultiLineImplicitStringConcatenation: ~
location:
row: 34
column: 2
end_location:
row: 35
column: 8
fix: ~
parent: ~

View File

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

View File

@@ -12,10 +12,12 @@ mod tests {
use crate::registry::Rule;
use crate::settings;
#[test_case(Rule::NoUnnecessaryPass, Path::new("PIE790.py"); "PIE790")]
#[test_case(Rule::DupeClassFieldDefinitions, Path::new("PIE794.py"); "PIE794")]
#[test_case(Rule::PreferUniqueEnums, Path::new("PIE796.py"); "PIE796")]
#[test_case(Rule::NoUnnecessaryDictKwargs, Path::new("PIE804.py"); "PIE804")]
#[test_case(Rule::NoUnnecessaryPass, Path::new("PIE790.py"); "PIE790")]
#[test_case(Rule::NoUnnecessarySpread, Path::new("PIE800.py"); "PIE800")]
#[test_case(Rule::PreferListBuiltin, Path::new("PIE807.py"); "PIE807")]
#[test_case(Rule::PreferUniqueEnums, Path::new("PIE796.py"); "PIE796")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,6 +1,6 @@
use log::error;
use rustc_hash::FxHashSet;
use rustpython_ast::{Constant, Expr, ExprKind, Stmt, StmtKind};
use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::comparable::ComparableExpr;
use crate::ast::helpers::unparse_expr;
@@ -8,6 +8,7 @@ use crate::ast::types::{Range, RefEquality};
use crate::autofix::helpers::delete_stmt;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::is_identifier;
use crate::registry::{Diagnostic, Rule};
use crate::violations;
@@ -152,6 +153,56 @@ where
}
}
/// PIE800
pub fn no_unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], values: &[Expr]) {
for item in keys.iter().zip(values.iter()) {
if let (None, value) = item {
// We only care about when the key is None which indicates a spread `**`
// inside a dict.
if let ExprKind::Dict { .. } = value.node {
let diagnostic =
Diagnostic::new(violations::NoUnnecessarySpread, Range::from_located(value));
checker.diagnostics.push(diagnostic);
}
}
}
}
/// Return `true` if a key is a valid keyword argument name.
fn is_valid_kwarg_name(key: &Expr) -> bool {
if let ExprKind::Constant {
value: Constant::Str(value),
..
} = &key.node
{
is_identifier(value)
} else {
false
}
}
/// PIE804
pub fn no_unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[Keyword]) {
for kw in kwargs {
// keyword is a spread operator (indicated by None)
if kw.node.arg.is_none() {
if let ExprKind::Dict { keys, .. } = &kw.node.value.node {
// ensure foo(**{"bar-bar": 1}) doesn't error
if keys.iter().all(|expr| expr.as_ref().map_or(false, is_valid_kwarg_name)) ||
// handle case of foo(**{**bar})
(keys.len() == 1 && keys[0].is_none())
{
let diagnostic = Diagnostic::new(
violations::NoUnnecessaryDictKwargs,
Range::from_located(expr),
);
checker.diagnostics.push(diagnostic);
}
}
}
}
}
/// PIE807
pub fn prefer_list_builtin(checker: &mut Checker, expr: &Expr) {
let ExprKind::Lambda { args, body } = &expr.node else {

View File

@@ -0,0 +1,45 @@
---
source: src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessarySpread: ~
location:
row: 1
column: 13
end_location:
row: 1
column: 23
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
location:
row: 3
column: 14
end_location:
row: 3
column: 27
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
location:
row: 5
column: 10
end_location:
row: 5
column: 21
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
location:
row: 7
column: 18
end_location:
row: 7
column: 27
fix: ~
parent: ~

View File

@@ -0,0 +1,55 @@
---
source: src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 1
column: 0
end_location:
row: 1
column: 20
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 3
column: 0
end_location:
row: 3
column: 21
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 5
column: 0
end_location:
row: 5
column: 35
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 7
column: 0
end_location:
row: 7
column: 38
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
location:
row: 9
column: 0
end_location:
row: 9
column: 29
fix: ~
parent: ~

View File

@@ -2,7 +2,7 @@ use rustpython_ast::{Constant, Expr, ExprContext, ExprKind};
use super::super::types;
use super::helpers::{is_pytest_parametrize, split_names};
use crate::ast::helpers::create_expr;
use crate::ast::helpers::{create_expr, unparse_expr};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
@@ -31,8 +31,7 @@ fn elts_to_csv(elts: &[Expr], checker: &Checker) -> Option<String> {
return None;
}
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(
Some(unparse_expr(
&create_expr(ExprKind::Constant {
value: Constant::Str(elts.iter().fold(String::new(), |mut acc, elt| {
if let ExprKind::Constant {
@@ -49,9 +48,8 @@ fn elts_to_csv(elts: &[Expr], checker: &Checker) -> Option<String> {
})),
kind: None,
}),
0,
);
Some(generator.generate())
checker.stylist,
))
}
/// PT006
@@ -102,24 +100,22 @@ fn check_names(checker: &mut Checker, expr: &Expr) {
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(
&create_expr(ExprKind::List {
elts: names
.iter()
.map(|&name| {
create_expr(ExprKind::Constant {
value: Constant::Str(name.to_string()),
kind: None,
})
})
.collect(),
ctx: ExprContext::Load,
}),
0,
);
diagnostic.amend(Fix::replacement(
generator.generate(),
unparse_expr(
&create_expr(ExprKind::List {
elts: names
.iter()
.map(|&name| {
create_expr(ExprKind::Constant {
value: Constant::Str(name.to_string()),
kind: None,
})
})
.collect(),
ctx: ExprContext::Load,
}),
checker.stylist,
),
expr.location,
expr.end_location.unwrap(),
));
@@ -144,16 +140,14 @@ fn check_names(checker: &mut Checker, expr: &Expr) {
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(
&create_expr(ExprKind::List {
elts: elts.clone(),
ctx: ExprContext::Load,
}),
0,
);
diagnostic.amend(Fix::replacement(
generator.generate(),
unparse_expr(
&create_expr(ExprKind::List {
elts: elts.clone(),
ctx: ExprContext::Load,
}),
checker.stylist,
),
expr.location,
expr.end_location.unwrap(),
));
@@ -285,10 +279,8 @@ fn handle_single_name(checker: &mut Checker, expr: &Expr, value: &Expr) {
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(&create_expr(value.node.clone()), 0);
diagnostic.amend(Fix::replacement(
generator.generate(),
unparse_expr(&create_expr(value.node.clone()), checker.stylist),
expr.location,
expr.end_location.unwrap(),
));

View File

@@ -325,6 +325,11 @@ pub fn function(checker: &mut Checker, body: &[Stmt]) {
visitor.stack
};
// Avoid false positives for generators.
if !stack.yields.is_empty() {
return;
}
if checker.settings.rules.enabled(&Rule::SuperfluousElseReturn)
|| checker.settings.rules.enabled(&Rule::SuperfluousElseRaise)
|| checker

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