Compare commits

...

70 Commits

Author SHA1 Message Date
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
Charlie Marsh
23b622943e Bump version to 0.0.230 2023-01-22 13:58:41 -05:00
Harutaka Kawamura
a7ce8621a9 Update RustPython to fix Dict.keys type (#2086)
This PR upgrades RustPython to fix the type of `Dict.keys` to `Vec<Option<Expr>>` (see https://github.com/RustPython/RustPython/pull/4449 for why this change was needed) and unblock #1884.
2023-01-22 13:24:00 -05:00
Shannon Rothe
36fb8f7a63 flake8_to_ruff: support isort options (#2082)
See: https://github.com/charliermarsh/ruff/issues/1749.
2023-01-22 13:18:01 -05:00
alm
e11cf1bf65 Update linters PyPI links to latest version (#2062) 2023-01-22 13:10:22 -05:00
Maksudul Haque
75e16c0ce5 [pep8-naming][N806] Don't mark TypeVar & NewType Assignment as Errors (#2085)
closes https://github.com/charliermarsh/ruff/issues/1985
2023-01-22 12:54:13 -05:00
Martin Fischer
1beedf20f9 fix: add_rule.py for --linter ruff 2023-01-22 11:51:29 -05:00
Martin Fischer
4758ee6ac4 refactor: Generate Linter -> RuleSelector mapping via macro
To enable ruff_dev to automatically generate the rule Markdown tables in
the README the ruff library contained the following function:

    Linter::codes() -> Vec<RuleSelector>

which was slightly changed to `fn prefixes(&self) -> Prefixes` in
9dc66b5a65 to enable ruff_dev to split
up the Markdown tables for linters that have multiple prefixes
(pycodestyle has E & W, Pylint has PLC, PLE, PLR & PLW).

The definition of this method was however largely redundant with the
#[prefix] macro attributes in the Linter enum, which are used to
derive the Linter::parse_code function, used by the --explain command.

This commit removes the redundant Linter::prefixes by introducing a
same-named method with a different signature to the RuleNamespace trait:

     fn prefixes(&self) -> &'static [&'static str];

As well as implementing IntoIterator<Rule> for &Linter. We extend the
extisting RuleNamespace proc macro to automatically derive both
implementations from the Linter enum definition.

To support the previously mentioned Markdown table splitting we
introduce a very simple hand-written method to the Linter impl:

    fn categories(&self) -> Option<&'static [LinterCategory]>;
2023-01-22 11:51:29 -05:00
Martin Fischer
c3dd1b0e3c refactor: Rename ParseCode trait to RuleNamespace
ParseCode was a fitting name since the trait only contained a single
parse_code method ... since we now however want to introduce an
additional `prefixes` method RuleNamespace is more fitting.
2023-01-22 11:51:29 -05:00
Martin Fischer
87443e6301 Support prefix "PL" to select all of Pylint 2023-01-22 11:51:29 -05:00
Martin Fischer
16d2ceba79 refactor: Avoid unnecessary Map indexing 2023-01-22 11:51:29 -05:00
Martin Fischer
aedee7294e refactor: Stop using Ident as BTreeMap key
Using Ident as the key type is inconvenient since creating an Ident
requires the specification of a Span, which isn't actually used by
the Hash implementation of Ident.
2023-01-22 11:51:29 -05:00
Martin Fischer
4f12b31dc8 refactor: Drop RuleSelector::codes in favor of IntoIterator impl 2023-01-22 11:51:29 -05:00
Martin Fischer
9f14e7c830 refactor: Update some variable/field/method names 2023-01-22 11:51:29 -05:00
Martin Fischer
4cc492a17a refactor: Encapsulate PerFileIgnore impl details 2023-01-22 11:51:29 -05:00
Martin Fischer
028436af81 refactor: Group Configuration struct fields 2023-01-22 11:51:29 -05:00
Martin Fischer
da4994aa73 refactor: impl From<&Configuration> for RuleTable 2023-01-22 11:51:29 -05:00
Martin Fischer
4dcb491bec refactor: Avoid some unnecessary allocations 2023-01-22 11:51:29 -05:00
Simon Brugman
6fc6bf0648 feat: enable autofix for TRY004 (#2084)
functionality was already implemented, just the trait needed to be added
2023-01-22 07:18:56 -05:00
Charlie Marsh
c1cb4796f8 Support decorators in source code generator (#2081) 2023-01-21 23:26:32 -05:00
Charlie Marsh
d81620397e Improve generator precedence operations (#2080) 2023-01-21 23:21:37 -05:00
Charlie Marsh
84b1490d03 Base INP check on package inference (#2079)
If a file doesn't have a `package`, then it must both be in a directory that lacks an `__init__.py`, and a directory that _isn't_ marked as a namespace package.

Closes #2075.
2023-01-21 19:49:56 -05:00
Simon Brugman
28f05aa6e7 feat: update scripts to new rules structure (#2078)
- optional `prefix` argument for `add_plugin.py`
- rules directory instead of `rules.rs`
- pathlib syntax
- fix test case where code was added instead of name

Example:
```
python scripts/add_plugin.py --url https://pypi.org/project/example/1.0.0/ example --prefix EXA
python scripts/add_rule.py --name SecondRule --code EXA002 --linter example
python scripts/add_rule.py --name FirstRule --code EXA001 --linter example
python scripts/add_rule.py --name ThirdRule --code EXA003 --linter example
 ```

Note that it breaks compatibility with 'old style' plugins (generation works fine, but namespaces need to be changed):
```
python scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
```
2023-01-21 19:19:58 -05:00
Charlie Marsh
325faa8e18 Include package path in cache key (#2077)
Closes #2075.
2023-01-21 18:33:35 -05:00
Charlie Marsh
6bfa1804de Remove remaining ropey usages (#2076) 2023-01-21 18:24:10 -05:00
Charlie Marsh
4dcf284a04 Index source code upfront to power (row, column) lookups (#1990)
## Summary

The problem: given a (row, column) number (e.g., for a token in the AST), we need to be able to map it to a precise byte index in the source code. A while ago, we moved to `ropey` for this, since it was faster in practice (mostly, I think, because it's able to defer indexing). However, at some threshold of accesses, it becomes faster to index the string in advance, as we're doing here.

## Benchmark

It looks like this is ~3.6% slower for the default rule set, but ~9.3% faster for `--select ALL`.

**I suspect there's a strategy that would be strictly faster in both cases**, based on deferring even more computation (right now, we lazily compute these offsets, but we do it for the entire file at once, even if we only need some slice at the top), or caching the `ropey` lookups in some way.

Before:

![main](https://user-images.githubusercontent.com/1309177/213883581-8f73c61d-2979-4171-88a6-a88d7ff07e40.png)

After:

![48 all](https://user-images.githubusercontent.com/1309177/213883586-3e049680-9ef9-49e2-8f04-fd6ff402eba7.png)

## Alternatives

I tried tweaking the `Vec::with_capacity` hints, and even trying `Vec::with_capacity(str_indices::lines_crlf::count_breaks(contents))` to do a quick scan of the number of lines, but that turned out to be slower.
2023-01-21 17:56:11 -05:00
Zeddicus414
08fc9b8095 ICN001 check from imports that have no alias (#2072)
Add tests.

Ensure that these cases are caught by ICN001:
```python
from xml.dom import minidom
from xml.dom.minidom import parseString
```

with config:
```toml
[tool.ruff.flake8-import-conventions.extend-aliases]
"dask.dataframe" = "dd"
"xml.dom.minidom" = "md"
"xml.dom.minidom.parseString" = "pstr"
```
2023-01-21 17:47:08 -05:00
Cosmo
39aed6f11d Update link to Pylint parity tracking issue (#2074) 2023-01-21 17:46:55 -05:00
Zeddicus414
5726118cfe ICN001 import-alias-is-not-conventional should check "from" imports (#2070)
Closes https://github.com/charliermarsh/ruff/issues/2047.
2023-01-21 15:43:51 -05:00
Simon Brugman
67de8ac85e feat: implementation for TRY004 (#2066)
See: #2056.
2023-01-21 14:58:59 -05:00
figsoda
b1bda0de82 fix: pin rustpython to the same revision to fix cargo vendor (#2069)
I was trying to update ruff in nixpkgs and ran into this error when it was running `cargo vendor`
```
error: failed to sync

Caused by:
  found duplicate version of package `rustpython-ast v0.2.0` vendored from two sources:

        source 1: https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942b
        source 2: https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52
```
2023-01-21 14:40:00 -05:00
Charlie Marsh
84300e00ff Bump version to 0.0.229 2023-01-21 13:18:06 -05:00
Charlie Marsh
fbee95a668 Avoid flagging redefined imports as unused in same-scope (#2065)
This is effectively a revert of #1173, to favor false-negatives over false-positives in the same-scope case.

Closes #2044.
2023-01-21 12:50:21 -05:00
Simon Brugman
afcf5c0ee0 feat: plugin scaffold for tryceratops with TRY300 (#2055)
Renamed to TRY to avoid conflicts, as proposed in https://github.com/guilatrova/tryceratops/pull/55

https://github.com/guilatrova/tryceratops/blob/main/docs/violations/TC300.md

See: #2056
2023-01-21 11:25:10 -05:00
Maksudul Haque
0c30768288 [flake8-builtins] Add builtins-ignorelist Option (#2061)
Closes #2053.
2023-01-21 11:09:04 -05:00
Colin Delahunty
80295f335b Pyupgrade: Printf string formatting (#1803) 2023-01-21 09:37:22 -05:00
Charlie Marsh
465943adf7 Revert "Upgrade to toml v0.5.11" (#2058)
This _did_ fix https://github.com/charliermarsh/ruff/issues/1894, but was a little premature. `toml` doesn't actually depend on `toml-edit` yet, and `v0.5.11` was mostly about deprecations AFAICT. So upgrading might solve that issue, but could introduce other incompatibilities, and I'd like to minimize churn. I expect that `toml` will have a new release soon, so we can revert this revert.

Reverts charliermarsh/ruff#2040.
2023-01-21 07:54:56 -05:00
Charlie Marsh
38eed292e4 Avoid removing comments in RUF005 (#2057)
Closes #2054.
2023-01-21 07:37:25 -05:00
Harutaka Kawamura
883e650a35 Fix S101 range to only highlight assert (#2052)
Fix:

```
resources/test/fixtures/flake8_bandit/S101.py:2:1: S101 Use of `assert` detected
  |
2 | assert True
  | ^^^^^^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:8:5: S101 Use of `assert` detected
  |
8 |     assert x == 1
  |     ^^^^^^^^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:11:5: S101 Use of `assert` detected
   |
11 |     assert x == 2
   |     ^^^^^^^^^^^^^ S101
   |

Found 3 error(s).
```

to:

```
resources/test/fixtures/flake8_bandit/S101.py:2:1: S101 Use of `assert` detected
  |
2 | assert True
  | ^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:8:5: S101 Use of `assert` detected
  |
8 |     assert x == 1
  |     ^^^^^^ S101
  |

resources/test/fixtures/flake8_bandit/S101.py:11:5: S101 Use of `assert` detected
   |
11 |     assert x == 2
   |     ^^^^^^ S101
   |
```
2023-01-21 07:15:00 -05:00
Harutaka Kawamura
eb1b5e5454 De-duplicate SIM102 (#2050)
The idea is the same as #1867. Avoids emitting `SIM102` twice for the following code:

```python
if a:
    if b:
        if c:
            d
```

```
resources/test/fixtures/flake8_simplify/SIM102.py:1:1: SIM102 Use a single `if` statement instead of nested `if` statements
resources/test/fixtures/flake8_simplify/SIM102.py:2:5: SIM102 Use a single `if` statement instead of nested `if` statements
```
2023-01-20 23:38:52 -05:00
Charlie Marsh
8e558a3458 Add scaffolding for flake8-type-checking extension (#2048)
This PR adds the scaffolding files for `flake8-type-checking`, along with the simplest rule (`empty-type-checking-block`), just as an example to get us started.

See: #1785.
2023-01-20 22:41:36 -05:00
Martin Fischer
4e4643aa5d refactor: Decouple Rule from linter prefixes
543865c96b introduced
RuleCode::origin() -> RuleOrigin generation via a macro, while that
signature now has been renamed to Rule::origin() -> Linter we actually
want to get rid of it since rules and linters shouldn't be this tightly
coupled (since one rule can exist in multiple linters).

Another disadvantage of the previous approach was that the prefixes
had to be defined in ruff_macros/src/prefixes.rs, which was easy to
miss when defining new linters in src/*, case in point
INP001 => violations::ImplicitNamespacePackage has in the meantime been
added without ruff_macros/src/prefixes.rs being updated accordingly
which resulted in `ruff --explain INP001` mistakenly reporting that the
rule belongs to isort (since INP001 starts with the isort prefix "I").
The derive proc macro introduced in this commit requires every variant
to have at least one #[prefix = "..."], eliminating such mistakes.
2023-01-20 20:25:57 -05:00
Martin Fischer
b19258a243 refactor: Rename RuleCodePrefix to RuleSelector
More accurate since the enum also encompasses:

* ALL (which isn't a prefix at all)

* fully-qualified rule codes (which aren't prefixes unless you say
  they're a prefix to the empty string but that's not intuitive)
2023-01-20 20:25:57 -05:00
Martin Fischer
7fc42f8f85 refactor: Rename RuleOrigin to Linter
"origin" was accurate since ruff rules are currently always modeled
after one origin (except the Ruff-specific rules).

Since we however want to introduce a many-to-many mapping between codes
and rules, the term "origin" no longer makes much sense. Rules usually
don't have multiple origins but one linter implements a rule first and
then others implement it later (often inspired from another linter).
But we don't actually care much about where a rule originates from when
mapping multiple rule codes to one rule implementation, so renaming
RuleOrigin to Linter is less confusing with the many-to-many system.
2023-01-20 20:25:57 -05:00
Dmitry Dygalo
babe1eb7be perf: Reduce allocations (#2045)
I found a few places where some allocations could be avoided.
2023-01-20 20:06:48 -05:00
Simon Brugman
608b2191aa [flake8-executable] EXE003-005 (#2023)
Tracking issue: https://github.com/charliermarsh/ruff/issues/2024

Implementation for EXE003, EXE004 and EXE005 of `flake8-executable` 
(shebang should contain "python", not have whitespace before, and should be on the first line)

Please take in mind that this is my first rust contribution.

The remaining EXE-rules are a combination of shebang (`lines.rs`), file permissions (`fs.rs`) and if-conditions (`ast.rs`). I was not able to find other rules that have interactions/dependencies in them. Any advice on how this can be best implemented would be very welcome.

For autofixing `EXE005`, I had in mind to _move_  the shebang line to the top op the file. This could be achieved by a combination of `Fix::insert` and `Fix::delete` (multiple fixes per diagnostic), or by implementing a dedicated `Fix::move`, or perhaps in other ways. For now I've left it out, but keen on hearing what you think would be most consistent with the package, and pointer where to start (if at all).

---
If you care about another testimonial:
`ruff` not only helps staying on top of the many excellent flake8 plugins and other Python code quality tools that are available, it also applies them at baffling speed.
(Planning to implement it soon for github.com/pandas-profiling/pandas-profiling (as largest contributor) and github.com/ing-bank/popmon.)
2023-01-20 18:19:07 -05:00
Eric Roberts
3939c2dbf7 Add support for pycodestyle E101 (#2038)
Rule described here: https://www.flake8rules.com/rules/E101.html

I tried to follow contributing guidelines closely, I've never worked with Rust before. Stumbled across Ruff a few days ago and would like to use it in our project, but we use a bunch of flake8 rules that are not yet implemented in ruff, so I decided to give it a go.
2023-01-20 17:24:58 -05:00
Charlie Marsh
20a9252e92 Upgrade to toml v0.5.11 (#2040)
In #1680, we moved over to `toml_edit`. But it looks like `toml` now uses `toml_edit`, and has implemented some improvements (e.g., this closes #1894).
2023-01-20 17:20:45 -05:00
Hugo van Kemenade
a0e3347e43 README: --force-exclude is already set (#2042)
Re: https://github.com/charliermarsh/ruff-pre-commit/issues/19 / https://github.com/charliermarsh/ruff-pre-commit/pull/20

This is now always set, no need to include it in the README example.
2023-01-20 17:20:22 -05:00
249 changed files with 8124 additions and 1583 deletions

View File

@@ -94,9 +94,11 @@ jobs:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v1
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --origin pylint
- run: ./scripts/add_rule.py --name DoTheThing --code PLC999 --linter pylint
- run: cargo check
- run: ./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/
- run: |
./scripts/add_plugin.py test --url https://pypi.org/project/-test/0.1.0/ --prefix TST
./scripts/add_rule.py --name FirstRule --code TST001 --linter test
- run: cargo check
# TODO(charlie): Re-enable the `wasm-pack` tests.

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.228
rev: v0.0.231
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
@@ -69,7 +70,7 @@ At a high level, the steps involved in adding a new lint rule are as follows:
6. Update the generated files (documentation and generated code).
To define the violation, start by creating a dedicated file for your rule under the appropriate
rule origin (e.g., `src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
rule linter (e.g., `src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
contain a struct defined via `define_violation!`, along with a function that creates the violation
based on any required inputs. (Many of the existing examples live in `src/violations.rs`, but we're
looking to place new rules in their own files.)
@@ -81,7 +82,7 @@ collecting diagnostics as it goes.
If you need to inspect the AST, you can run `cargo +nightly dev print-ast` with a Python file. Grep
for the `Check::new` invocations to understand how other, similar rules are implemented.
To add a test fixture, create a file under `resources/test/fixtures/[origin]`, named to match
To add a test fixture, create a file under `resources/test/fixtures/[linter]`, named to match
the code you defined earlier (e.g., `resources/test/fixtures/pycodestyle/E402.py`). This file should
contain a variety of violations and non-violations designed to evaluate and demonstrate the behavior
of your lint rule.
@@ -90,7 +91,7 @@ Run `cargo +nightly dev generate-all` to generate the code for your new fixture.
locally with (e.g.) `cargo run resources/test/fixtures/pycodestyle/E402.py --no-cache --select E402`.
Once you're satisfied with the output, codify the behavior as a snapshot test by adding a new
`test_case` macro in the relevant `src/[origin]/mod.rs` file. Then, run `cargo test --all`.
`test_case` macro in the relevant `src/[linter]/mod.rs` file. Then, run `cargo test --all`.
Your test will fail, but you'll be prompted to follow-up with `cargo insta review`. Accept the
generated snapshot, then commit the snapshot file alongside the rest of your changes.

35
Cargo.lock generated
View File

@@ -719,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.228"
version = "0.0.231"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1826,19 +1826,9 @@ dependencies = [
"winapi",
]
[[package]]
name = "ropey"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd22239fafefc42138ca5da064f3c17726a80d2379d817a3521240e78dd0064"
dependencies = [
"smallvec",
"str_indices",
]
[[package]]
name = "ruff"
version = "0.0.228"
version = "0.0.231"
dependencies = [
"anyhow",
"bitflags",
@@ -1868,7 +1858,6 @@ dependencies = [
"once_cell",
"path-absolutize",
"regex",
"ropey",
"ruff_macros",
"rustc-hash",
"rustpython-ast",
@@ -1893,7 +1882,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.228"
version = "0.0.231"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1930,7 +1919,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.228"
version = "0.0.231"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -1951,7 +1940,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.228"
version = "0.0.231"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2005,7 +1994,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
source = "git+https://github.com/RustPython/RustPython.git?rev=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"num-bigint",
"rustpython-common",
@@ -2015,7 +2004,7 @@ dependencies = [
[[package]]
name = "rustpython-common"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
source = "git+https://github.com/RustPython/RustPython.git?rev=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"ascii",
"bitflags",
@@ -2040,7 +2029,7 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
source = "git+https://github.com/RustPython/RustPython.git?rev=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"bincode",
"bitflags",
@@ -2057,7 +2046,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
source = "git+https://github.com/RustPython/RustPython.git?rev=4f38cb68e4a97aeea9eb19673803a0bd5f655383#4f38cb68e4a97aeea9eb19673803a0bd5f655383"
dependencies = [
"ahash",
"anyhow",
@@ -2256,12 +2245,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str_indices"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f026164926842ec52deb1938fae44f83dfdb82d0a5b0270c5bd5935ab74d6dd"
[[package]]
name = "string_cache"
version = "0.8.4"

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.228"
version = "0.0.231"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -46,12 +46,11 @@ 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" }
ropey = { version = "1.5.0", features = ["cr_lines", "simd"], default-features = false }
ruff_macros = { version = "0.0.228", path = "ruff_macros" }
ruff_macros = { version = "0.0.231", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }
@@ -98,7 +97,3 @@ opt-level = 3
# https://github.com/bytecodealliance/wasm-tools/blob/b5c3d98e40590512a3b12470ef358d5c7b983b15/crates/wasmparser/src/limits.rs#L29
[profile.dev.package.rustpython-parser]
opt-level = 1
[[bench]]
name = "source_code_locator"
harness = false

180
README.md
View File

@@ -63,6 +63,7 @@ Ruff is extremely actively developed and used in major open-source projects like
- [OpenBB](https://github.com/OpenBB-finance/OpenBBTerminal)
- [Cryptography (PyCA)](https://github.com/pyca/cryptography)
- [SnowCLI (Snowflake)](https://github.com/Snowflake-Labs/snowcli)
- [cibuildwheel](https://github.com/pypa/cibuildwheel)
Read the [launch blog post](https://notes.crmarsh.com/python-tooling-could-be-much-much-faster).
@@ -134,10 +135,14 @@ developer of [Zulip](https://github.com/zulip/zulip):
1. [eradicate (ERA)](#eradicate-era)
1. [pandas-vet (PD)](#pandas-vet-pd)
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PLC, PLE, PLR, PLW)](#pylint-plc-ple-plr-plw)
1. [Pylint (PL)](#pylint-pl)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-commas (COM)](#flake8-commas-com)
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
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)
@@ -198,11 +203,9 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.228'
rev: 'v0.0.231'
hooks:
- id: ruff
# Respect `exclude` and `extend-exclude` settings.
args: ["--force-exclude"]
```
## Configuration
@@ -543,7 +546,7 @@ The 🛠 emoji indicates that a rule is automatically fixable by the `--fix` com
<!-- Begin auto-generated sections. -->
### Pyflakes (F)
For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -593,11 +596,12 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/2.5.0/) on PyPI.
### pycodestyle (E, W)
For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI.
For more, see [pycodestyle](https://pypi.org/project/pycodestyle/) on PyPI.
#### Error (E)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| E101 | mixed-spaces-and-tabs | Indentation contains mixed spaces and tabs | |
| E401 | multiple-imports-on-one-line | Multiple imports on one line | |
| E402 | module-import-not-at-top-of-file | Module level import not at top of file | |
| E501 | line-too-long | Line too long ({length} > {limit} characters) | |
@@ -623,7 +627,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/2.9.1/) on PyPI
### mccabe (C90)
For more, see [mccabe](https://pypi.org/project/mccabe/0.7.0/) on PyPI.
For more, see [mccabe](https://pypi.org/project/mccabe/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -631,7 +635,7 @@ For more, see [mccabe](https://pypi.org/project/mccabe/0.7.0/) on PyPI.
### isort (I)
For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
For more, see [isort](https://pypi.org/project/isort/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -640,7 +644,7 @@ For more, see [isort](https://pypi.org/project/isort/5.10.1/) on PyPI.
### pydocstyle (D)
For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -693,7 +697,7 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/6.1.1/) on PyPI.
### pyupgrade (UP)
For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -726,13 +730,14 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/3.2.0/) on PyPI.
| UP028 | rewrite-yield-from | Replace `yield` over `for` loop with `yield from` | 🛠 |
| UP029 | unnecessary-builtin-import | Unnecessary builtin import: `{import}` | 🛠 |
| UP030 | format-literals | Use implicit references for positional format fields | 🛠 |
| UP031 | printf-string-formatting | Use format specifiers instead of percent format | 🛠 |
| UP032 | f-string | Use f-string instead of `format` call | 🛠 |
| UP033 | functools-cache | Use `@functools.cache` instead of `@functools.lru_cache(maxsize=None)` | 🛠 |
| UP034 | extraneous-parentheses | Avoid extraneous parentheses | 🛠 |
### pep8-naming (N)
For more, see [pep8-naming](https://pypi.org/project/pep8-naming/0.13.2/) on PyPI.
For more, see [pep8-naming](https://pypi.org/project/pep8-naming/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -754,7 +759,7 @@ For more, see [pep8-naming](https://pypi.org/project/pep8-naming/0.13.2/) on PyP
### flake8-2020 (YTT)
For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI.
For more, see [flake8-2020](https://pypi.org/project/flake8-2020/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -771,7 +776,7 @@ For more, see [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/) on PyPI
### flake8-annotations (ANN)
For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2.9.1/) on PyPI.
For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -789,7 +794,7 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/2
### flake8-bandit (S)
For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on PyPI.
For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -807,11 +812,12 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/4.1.1/) on
| 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)
For more, see [flake8-blind-except](https://pypi.org/project/flake8-blind-except/0.2.1/) on PyPI.
For more, see [flake8-blind-except](https://pypi.org/project/flake8-blind-except/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -819,7 +825,7 @@ For more, see [flake8-blind-except](https://pypi.org/project/flake8-blind-except
### flake8-boolean-trap (FBT)
For more, see [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/0.1.0/) on PyPI.
For more, see [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -829,7 +835,7 @@ For more, see [flake8-boolean-trap](https://pypi.org/project/flake8-boolean-trap
### flake8-bugbear (B)
For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/) on PyPI.
For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -864,7 +870,7 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/22.10.27/
### flake8-builtins (A)
For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/2.0.1/) on PyPI.
For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -874,7 +880,7 @@ For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/2.0.1/)
### flake8-comprehensions (C4)
For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/3.10.1/) on PyPI.
For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -897,7 +903,7 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
### flake8-debugger (T10)
For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/4.1.2/) on PyPI.
For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -905,7 +911,7 @@ For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/4.1.2/)
### flake8-errmsg (EM)
For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/0.4.0/) on PyPI.
For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -915,7 +921,7 @@ For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/0.4.0/) on
### flake8-implicit-str-concat (ISC)
For more, see [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/0.3.0/) on PyPI.
For more, see [flake8-implicit-str-concat](https://pypi.org/project/flake8-implicit-str-concat/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -933,7 +939,7 @@ For more, see [flake8-import-conventions](https://github.com/joaopalmeiro/flake8
### flake8-print (T20)
For more, see [flake8-print](https://pypi.org/project/flake8-print/5.0.0/) on PyPI.
For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -942,7 +948,7 @@ For more, see [flake8-print](https://pypi.org/project/flake8-print/5.0.0/) on Py
### flake8-pytest-style (PT)
For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/1.6.0/) on PyPI.
For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -974,7 +980,7 @@ For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style
### flake8-quotes (Q)
For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/3.3.1/) on PyPI.
For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -985,7 +991,7 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/3.3.1/) on
### flake8-return (RET)
For more, see [flake8-return](https://pypi.org/project/flake8-return/1.2.0/) on PyPI.
For more, see [flake8-return](https://pypi.org/project/flake8-return/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1000,7 +1006,7 @@ For more, see [flake8-return](https://pypi.org/project/flake8-return/1.2.0/) on
### flake8-simplify (SIM)
For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/) on PyPI.
For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1032,7 +1038,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/0.19.3/
### flake8-tidy-imports (TID)
For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/4.8.0/) on PyPI.
For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1041,7 +1047,7 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
### flake8-unused-arguments (ARG)
For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/0.0.12/) on PyPI.
For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-arguments/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1053,7 +1059,7 @@ For more, see [flake8-unused-arguments](https://pypi.org/project/flake8-unused-a
### flake8-datetimez (DTZ)
For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/20.10.0/) on PyPI.
For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1069,7 +1075,7 @@ For more, see [flake8-datetimez](https://pypi.org/project/flake8-datetimez/20.10
### eradicate (ERA)
For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI.
For more, see [eradicate](https://pypi.org/project/eradicate/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1077,7 +1083,7 @@ For more, see [eradicate](https://pypi.org/project/eradicate/2.1.0/) on PyPI.
### pandas-vet (PD)
For more, see [pandas-vet](https://pypi.org/project/pandas-vet/0.2.3/) on PyPI.
For more, see [pandas-vet](https://pypi.org/project/pandas-vet/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1105,9 +1111,9 @@ For more, see [pygrep-hooks](https://github.com/pre-commit/pygrep-hooks) on GitH
| PGH003 | blanket-type-ignore | Use specific rule codes when ignoring type issues | |
| PGH004 | blanket-noqa | Use specific rule codes when using `noqa` | |
### Pylint (PLC, PLE, PLR, PLW)
### Pylint (PL)
For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
For more, see [Pylint](https://pypi.org/project/pylint/) on PyPI.
#### Convention (PLC)
| Code | Name | Message | Fix |
@@ -1140,18 +1146,20 @@ For more, see [Pylint](https://pypi.org/project/pylint/2.15.7/) on PyPI.
### flake8-pie (PIE)
For more, see [flake8-pie](https://pypi.org/project/flake8-pie/0.16.0/) on PyPI.
For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| 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)
For more, see [flake8-commas](https://pypi.org/project/flake8-commas/2.1.0/) on PyPI.
For more, see [flake8-commas](https://pypi.org/project/flake8-commas/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
@@ -1161,12 +1169,72 @@ For more, see [flake8-commas](https://pypi.org/project/flake8-commas/2.1.0/) on
### flake8-no-pep420 (INP)
For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/2.3.0/) on PyPI.
For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| INP001 | implicit-namespace-package | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
### flake8-executable (EXE)
For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| 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 | |
### flake8-type-checking (TYP)
For more, see [flake8-type-checking](https://pypi.org/project/flake8-type-checking/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TYP005 | empty-type-checking-block | Found empty type-checking block | |
### tryceratops (TRY)
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 | 🛠 |
| TRY201 | verbose-raise | Use `raise` without specifying exception name | |
| TRY300 | try-consider-else | Consider `else` block | |
### 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)
| Code | Name | Message | Fix |
@@ -1459,11 +1527,13 @@ natively, including:
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-eradicate`](https://pypi.org/project/flake8-eradicate/)
- [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/)
- [`flake8-executable`](https://pypi.org/project/flake8-executable/)
- [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-no-pep420`](https://pypi.org/project/flake8-no-pep420)
- [`flake8-pie`](https://pypi.org/project/flake8-pie/) ([#1543](https://github.com/charliermarsh/ruff/issues/1543))
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-pytest-style`](https://pypi.org/project/flake8-pytest-style/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-return`](https://pypi.org/project/flake8-return/)
- [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998))
@@ -1507,7 +1577,7 @@ Like Flake8, Pylint supports plugins (called "checkers"), while Ruff implements
Unlike Pylint, Ruff is capable of automatically fixing its own lint violations.
Pylint parity is being tracked in [#689](https://github.com/charliermarsh/ruff/issues/689).
Pylint parity is being tracked in [#970](https://github.com/charliermarsh/ruff/issues/970).
### Which tools does Ruff replace?
@@ -1527,11 +1597,13 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
- [`flake8-docstrings`](https://pypi.org/project/flake8-docstrings/)
- [`flake8-eradicate`](https://pypi.org/project/flake8-eradicate/)
- [`flake8-errmsg`](https://pypi.org/project/flake8-errmsg/)
- [`flake8-executable`](https://pypi.org/project/flake8-executable/)
- [`flake8-implicit-str-concat`](https://pypi.org/project/flake8-implicit-str-concat/)
- [`flake8-import-conventions`](https://github.com/joaopalmeiro/flake8-import-conventions)
- [`flake8-no-pep420`](https://pypi.org/project/flake8-no-pep420)
- [`flake8-pie`](https://pypi.org/project/flake8-pie/) ([#1543](https://github.com/charliermarsh/ruff/issues/1543))
- [`flake8-print`](https://pypi.org/project/flake8-print/)
- [`flake8-pytest-style`](https://pypi.org/project/flake8-pytest-style/)
- [`flake8-quotes`](https://pypi.org/project/flake8-quotes/)
- [`flake8-return`](https://pypi.org/project/flake8-return/)
- [`flake8-simplify`](https://pypi.org/project/flake8-simplify/) ([#998](https://github.com/charliermarsh/ruff/issues/998))
@@ -1568,12 +1640,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]
@@ -1966,7 +2041,7 @@ recommended to only use `extend-ignore` when extending a
**Default value**: `[]`
**Type**: `Vec<RuleCodePrefix>`
**Type**: `Vec<RuleSelector>`
**Example usage**:
@@ -1991,7 +2066,7 @@ recommended to only use `extend-select` when extending a
**Default value**: `[]`
**Type**: `Vec<RuleCodePrefix>`
**Type**: `Vec<RuleSelector>`
**Example usage**:
@@ -2066,7 +2141,7 @@ A list of rule codes or prefixes to consider autofixable.
**Default value**: `["A", "ANN", "ARG", "B", "BLE", "C", "D", "E", "ERA", "F", "FBT", "I", "ICN", "N", "PGH", "PLC", "PLE", "PLR", "PLW", "Q", "RET", "RUF", "S", "T", "TID", "UP", "W", "YTT"]`
**Type**: `Vec<RuleCodePrefix>`
**Type**: `Vec<RuleSelector>`
**Example usage**:
@@ -2138,7 +2213,7 @@ specific prefixes.
**Default value**: `[]`
**Type**: `Vec<RuleCodePrefix>`
**Type**: `Vec<RuleSelector>`
**Example usage**:
@@ -2153,10 +2228,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`
@@ -2216,7 +2290,7 @@ exclude, when considering any matching files.
**Default value**: `{}`
**Type**: `HashMap<String, Vec<RuleCodePrefix>>`
**Type**: `HashMap<String, Vec<RuleSelector>>`
**Example usage**:
@@ -2280,7 +2354,7 @@ specific prefixes.
**Default value**: `["E", "F"]`
**Type**: `Vec<RuleCodePrefix>`
**Type**: `Vec<RuleSelector>`
**Example usage**:
@@ -2424,7 +2498,7 @@ A list of rule codes or prefixes to consider non-autofix-able.
**Default value**: `[]`
**Type**: `Vec<RuleCodePrefix>`
**Type**: `Vec<RuleSelector>`
**Example usage**:

View File

@@ -1,18 +0,0 @@
use std::fs;
use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use ropey::Rope;
fn criterion_benchmark(c: &mut Criterion) {
let contents = fs::read_to_string(Path::new("resources/test/fixtures/D.py")).unwrap();
c.bench_function("rope", |b| {
b.iter(|| {
let rope = Rope::from_str(black_box(&contents));
rope.line_to_char(black_box(4));
});
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

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_origin_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/2.5.0/).
///
/// This function extracts the link label and url from these comments and
/// generates the `name` and `url` functions for the `RuleOrigin` enum
/// accordingly, so that they can be used by `ruff_dev::generate_rules_table`.
fn generate_origin_name_and_url(out_dir: &Path) {
println!("cargo:rerun-if-changed=src/rules/");
let mut name_match_arms: String = r#"RuleOrigin::Ruff => "Ruff-specific rules","#.into();
let mut url_match_arms: String = r#"RuleOrigin::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#"RuleOrigin::{variant_name} => "{name}","#));
url_match_arms.push_str(&format!(r#"RuleOrigin::{variant_name} => Some("{url}"),"#));
}
write!(
BufWriter::new(fs::File::create(out_dir.join("origin.rs")).unwrap()),
"
impl RuleOrigin {{
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.228"
version = "0.0.231"
dependencies = [
"anyhow",
"clap",
@@ -1975,7 +1975,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.228"
version = "0.0.231"
dependencies = [
"anyhow",
"bincode",

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.228"
version = "0.0.231"
edition = "2021"
[dependencies]

View File

@@ -18,7 +18,7 @@ use std::path::PathBuf;
use anyhow::Result;
use clap::Parser;
use configparser::ini::Ini;
use ruff::flake8_to_ruff;
use ruff::flake8_to_ruff::{self, ExternalConfig};
#[derive(Parser)]
#[command(
@@ -48,14 +48,18 @@ fn main() -> Result<()> {
let config = ini.load(cli.file).map_err(|msg| anyhow::anyhow!(msg))?;
// Read the pyproject.toml file.
let black = cli
.pyproject
.map(flake8_to_ruff::parse_black_options)
.transpose()?
.flatten();
let pyproject = cli.pyproject.map(flake8_to_ruff::parse).transpose()?;
let external_config = pyproject
.as_ref()
.and_then(|pyproject| pyproject.tool.as_ref())
.map(|tool| ExternalConfig {
black: tool.black.as_ref(),
isort: tool.isort.as_ref(),
})
.unwrap_or_default();
// Create Ruff's pyproject.toml section.
let pyproject = flake8_to_ruff::convert(&config, black.as_ref(), cli.plugin)?;
let pyproject = flake8_to_ruff::convert(&config, &external_config, cli.plugin)?;
println!("{}", toml_edit::easy::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.228"
version = "0.0.231"
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

@@ -1,5 +1,6 @@
import some as sum
from some import other as int
from directory import new as dir
print = 1
copyright: 'annotation' = 2
@@ -7,6 +8,8 @@ copyright: 'annotation' = 2
float = object = 4
min, max = 5, 6
id = 4
def bytes():
pass

View File

@@ -5,5 +5,7 @@ def func1(str, /, type, *complex, Exception, **getattr):
async def func2(bytes):
pass
async def func3(id, dir):
pass
map([], lambda float: ...)

View File

@@ -1,8 +1,12 @@
class MyClass:
ImportError = 4
id = 5
dir = "/"
def __init__(self):
self.float = 5 # is fine
self.id = 10
self.dir = "."
def str(self):
pass

View File

@@ -0,0 +1,2 @@
#!/usr/bin/bash
print("hello world")

View File

@@ -0,0 +1 @@
#!/usr/bin/python

View File

@@ -0,0 +1,2 @@
print(" #!/usr/bin/python")
# shebang outside of comments should be ignored

View File

@@ -0,0 +1,2 @@
pass #!/usr/bin/env python

View File

@@ -0,0 +1,3 @@
# A python comment
#!/usr/bin/python

View File

@@ -0,0 +1,4 @@
print("code prior to shebang")
# A python comment
#!/usr/bin/python

View File

@@ -0,0 +1,6 @@
"""
With a docstring
print("commented out code")
"""
# A python comment
#!/usr/bin/python

View File

@@ -0,0 +1,21 @@
# Test absolute imports
# Violation cases
import xml.dom.minidom
import xml.dom.minidom as wrong
from xml.dom import minidom as wrong
from xml.dom import minidom
from xml.dom.minidom import parseString as wrong # Ensure ICN001 throws on function import.
from xml.dom.minidom import parseString
from xml.dom.minidom import parse, parseString
from xml.dom.minidom import parse as ps, parseString as wrong
# No ICN001 violations
import xml.dom.minidom as md
from xml.dom import minidom as md
from xml.dom.minidom import parseString as pstr
from xml.dom.minidom import parse, parseString as pstr
from xml.dom.minidom import parse as ps, parseString as pstr
# Test relative imports
from ..xml.dom import minidom as okay # Ensure config "xml.dom.minidom" doesn't catch relative imports

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

@@ -3,6 +3,12 @@ if a:
if b:
c
# SIM102
if a:
if b:
if c:
d
# SIM102
if a:
pass
@@ -86,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,25 @@
from typing import TYPE_CHECKING, List
if TYPE_CHECKING:
pass # TYP005
def example():
if TYPE_CHECKING:
pass # TYP005
return
class Test:
if TYPE_CHECKING:
pass # TYP005
x = 2
if TYPE_CHECKING:
if 2:
pass
if TYPE_CHECKING:
x: List

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 +0,0 @@
from long_module_name import member_one, member_two, member_three, member_four, member_five

View File

@@ -1,5 +1,7 @@
import collections
from collections import namedtuple
from typing import TypeVar
from typing import NewType
GLOBAL: str = "foo"
@@ -11,5 +13,9 @@ def f():
Camel = 0
CONSTANT = 0
_ = 0
MyObj1 = collections.namedtuple("MyObj1", ["a", "b"])
MyObj2 = namedtuple("MyObj12", ["a", "b"])
T = TypeVar("T")
UserId = NewType('UserId', int)

View File

@@ -0,0 +1,19 @@
def func_all_spaces():
# No error
print("spaces")
def func_tabs():
# No error
print("tabs")
def func_mixed_start_with_tab():
# E101
print("mixed starts with tab")
def func_mixed_start_with_space():
# E101
print("mixed starts with space")
def xyz():
# E101
print("xyz");

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

@@ -1,51 +1,51 @@
if True:
import foo; x = 1
import foo; x = 1
import foo1; x = 1
import foo2; x = 1
if True:
import foo; \
import foo3; \
x = 1
if True:
import foo \
import foo4 \
; x = 1
if True:
x = 1; import foo
x = 1; import foo5
if True:
x = 1; \
import foo
import foo6
if True:
x = 1 \
; import foo
; import foo7
if True:
x = 1; import foo; x = 1
x = 1; import foo; x = 1
x = 1; import foo8; x = 1
x = 1; import foo9; x = 1
if True:
x = 1; \
import foo; \
import foo10; \
x = 1
if True:
x = 1 \
;import foo \
;import foo11 \
;x = 1
# Continuation, but not as the last content in the file.
x = 1; \
import foo
import foo12
# Continuation, followed by end-of-file. (Removing `import foo` would cause a syntax
# error.)
x = 1; \
import foo
import foo13

View File

@@ -12,6 +12,9 @@ per-file-ignores = { "__init__.py" = ["F401"] }
[tool.ruff.flake8-bugbear]
extend-immutable-calls = ["fastapi.Depends", "fastapi.Query"]
[tool.ruff.flake8-builtins]
builtins-ignorelist = ["id", "dir"]
[tool.ruff.flake8-quotes]
inline-quotes = "single"
multiline-quotes = "double"

View File

@@ -31,3 +31,7 @@ MyType10 = TypedDict("MyType10", {"key": Literal["value"]})
# using namespace TypedDict
MyType11 = typing.TypedDict("MyType11", {"key": int})
# unpacking
c = {"c": float}
MyType12 = TypedDict("MyType1", {"a": int, "b": str, **c})

View File

@@ -0,0 +1,75 @@
a, b, x, y = 1, 2, 3, 4
# UP031
print('%s %s' % (a, b))
print('%s%s' % (a, b))
print("trivial" % ())
print("%s" % ("simple",))
print("%s" % ("%s" % ("nested",),))
print("%s%% percent" % (15,))
print("%f" % (15,))
print("%.f" % (15,))
print("%.3f" % (15,))
print("%3f" % (15,))
print("%-5f" % (5,))
print("%9f" % (5,))
print("%#o" % (123,))
print("brace {} %s" % (1,))
print(
"%s" % (
"trailing comma",
)
)
print("foo %s " % (x,))
print("%(k)s" % {"k": "v"})
print("%(k)s" % {
"k": "v",
"i": "j"
})
print("%(to_list)s" % {"to_list": []})
print("%(k)s" % {"k": "v", "i": 1, "j": []})
print("%(ab)s" % {"a" "b": 1})
print("%(a)s" % {"a" : 1})
print((
"foo %s "
"bar %s" % (x, y)
))
print(
"foo %(foo)s "
"bar %(bar)s" % {"foo": x, "bar": y}
)
bar = {"bar": y}
print(
"foo %(foo)s "
"bar %(bar)s" % {"foo": x, **bar}
)
print("%s \N{snowman}" % (a,))
print("%(foo)s \N{snowman}" % {"foo": 1})
print(("foo %s " "bar %s") % (x, y))

View File

@@ -0,0 +1,59 @@
# OK
"%s" % unknown_type
b"%s" % (b"bytestring",)
"%*s" % (5, "hi")
"%d" % (flt,)
"%c" % (some_string,)
"%4%" % ()
"%.2r" % (1.25)
i % 3
"%.*s" % (5, "hi")
"%i" % (flt,)
"%()s" % {"": "empty"}
"%s" % {"k": "v"}
"%(1)s" % {"1": "bar"}
"%(a)s" % {"a": 1, "a": 2}
pytest.param('"%8s" % (None,)', id="unsafe width-string conversion"),
"%()s" % {"": "bar"}
"%(1)s" % {1: 2, "1": 2}
"%(and)s" % {"and": 2}
# OK (arguably false negatives)
(
"foo %s "
"bar %s"
) % (x, y)
(
"foo %(foo)s "
"bar %(bar)s"
) % {"foo": x, "bar": y}
(
"""foo %s"""
% (x,)
)
(
"""
foo %s
"""
% (x,)
)

View File

@@ -20,3 +20,20 @@ astonishment = ("we all feel", ) + Fun.words
chain = ['a', 'b', 'c'] + eggs + list(('yes', 'no', 'pants') + zoob)
baz = () + zoob
first = [
# The order
1, # here
2, # is
# extremely
3, # critical
# to preserve
]
second = first + [
# please
4,
# don't
5,
# touch
6,
]

View File

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

View File

@@ -0,0 +1,296 @@
"""
Violation:
Prefer TypeError when relevant.
"""
def incorrect_basic(some_arg):
if isinstance(some_arg, int):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_type_check(some_arg):
if isinstance(some_arg, (int, str)):
pass
else:
raise Exception("...") # should be typeerror
class MyClass:
pass
def incorrect_with_issubclass(some_arg):
if issubclass(some_arg, MyClass):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_with_callable(some_arg):
if callable(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_ArithmeticError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ArithmeticError("...") # should be typeerror
def incorrect_AssertionError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise AssertionError("...") # should be typeerror
def incorrect_AttributeError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise AttributeError("...") # should be typeerror
def incorrect_BufferError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise BufferError # should be typeerror
def incorrect_EOFError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise EOFError("...") # should be typeerror
def incorrect_ImportError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ImportError("...") # should be typeerror
def incorrect_LookupError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise LookupError("...") # should be typeerror
def incorrect_MemoryError(some_arg):
if isinstance(some_arg, int):
pass
else:
# should be typeerror
# not multiline is on purpose for fix
raise MemoryError(
"..."
)
def incorrect_NameError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise NameError("...") # should be typeerror
def incorrect_ReferenceError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ReferenceError("...") # should be typeerror
def incorrect_RuntimeError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise RuntimeError("...") # should be typeerror
def incorrect_SyntaxError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise SyntaxError("...") # should be typeerror
def incorrect_SystemError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise SystemError("...") # should be typeerror
def incorrect_ValueError(some_arg):
if isinstance(some_arg, int):
pass
else:
raise ValueError("...") # should be typeerror
def incorrect_not_operator_isinstance(some_arg):
if not isinstance(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_and_operator_isinstance(arg1, arg2):
if isinstance(some_arg) and isinstance(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_or_operator_isinstance(arg1, arg2):
if isinstance(some_arg) or isinstance(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_operators_isinstance(arg1, arg2, arg3):
if not isinstance(arg1) and isinstance(arg2) or isinstance(arg3):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_not_operator_callable(some_arg):
if not callable(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_and_operator_callable(arg1, arg2):
if callable(some_arg) and callable(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_or_operator_callable(arg1, arg2):
if callable(some_arg) or callable(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_operators_callable(arg1, arg2, arg3):
if not callable(arg1) and callable(arg2) or callable(arg3):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_not_operator_issubclass(some_arg):
if not issubclass(some_arg):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_and_operator_issubclass(arg1, arg2):
if issubclass(some_arg) and issubclass(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_or_operator_issubclass(arg1, arg2):
if issubclass(some_arg) or issubclass(arg2):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multiple_operators_issubclass(arg1, arg2, arg3):
if not issubclass(arg1) and issubclass(arg2) or issubclass(arg3):
pass
else:
raise Exception("...") # should be typeerror
def incorrect_multi_conditional(arg1, arg2):
if isinstance(arg1, int):
pass
elif isinstance(arg2, int):
raise Exception("...") # should be typeerror
class MyCustomTypeValidation(Exception):
pass
def correct_custom_exception(some_arg):
if isinstance(some_arg, int):
pass
else:
raise MyCustomTypeValidation("...") # that's correct, because it's not vanilla
def correct_complex_conditional(val):
if val is not None and (not isinstance(val, int) or val < 0):
raise ValueError(...) # fine if this is not a TypeError
def correct_multi_conditional(some_arg):
if some_arg == 3:
pass
elif isinstance(some_arg, int):
pass
else:
raise Exception("...") # fine if this is not a TypeError
def correct_should_ignore(some_arg):
if isinstance(some_arg, int):
pass
else:
raise TypeError("...")
def check_body(some_args):
if isinstance(some_args, int):
raise ValueError("...") # should be typeerror
def check_body_correct(some_args):
if isinstance(some_args, int):
raise TypeError("...") # correct
def multiple_elifs(some_args):
if not isinstance(some_args, int):
raise ValueError("...") # should be typerror
elif some_args < 3:
raise ValueError("...") # this is ok
elif some_args > 10:
raise ValueError("...") # this is ok if we don't simplify
else:
pass
def multiple_ifs(some_args):
if not isinstance(some_args, int):
raise ValueError("...") # should be typerror
else:
if some_args < 3:
raise ValueError("...") # this is ok
else:
if some_args > 10:
raise ValueError("...") # this is ok if we don't simplify
else:
pass

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,47 @@
"""
Violation:
Returning a final value inside a try block may indicate you could use an else block
instead to outline the success scenario
"""
import logging
logger = logging.getLogger(__name__)
class MyException(Exception):
pass
def bad():
try:
a = 1
b = process()
return b
except MyException:
logger.exception("process failed")
def good():
try:
a = 1
b = process()
except MyException:
logger.exception("process failed")
else:
return b
def noreturn():
try:
a = 1
b = process()
except MyException:
logger.exception("process failed")
def still_good():
try:
return process()
except MyException:
logger.exception("process failed")

View File

@@ -73,7 +73,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"extend-select": {
@@ -83,7 +83,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"external": {
@@ -117,7 +117,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"flake8-annotations": {
@@ -153,6 +153,17 @@
}
]
},
"flake8-builtins": {
"description": "Options for the `flake8-builtins` plugin.",
"anyOf": [
{
"$ref": "#/definitions/Flake8BuiltinsOptions"
},
{
"type": "null"
}
]
},
"flake8-errmsg": {
"description": "Options for the `flake8-errmsg` plugin.",
"anyOf": [
@@ -244,11 +255,11 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"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"
@@ -315,7 +326,7 @@
"additionalProperties": {
"type": "array",
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
}
},
@@ -388,7 +399,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"show-source": {
@@ -446,7 +457,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"update-check": {
@@ -584,6 +595,22 @@
},
"additionalProperties": false
},
"Flake8BuiltinsOptions": {
"type": "object",
"properties": {
"builtins-ignorelist": {
"description": "Ignore list of builtins.",
"type": [
"array",
"null"
],
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"Flake8ErrMsgOptions": {
"type": "object",
"properties": {
@@ -1134,7 +1161,7 @@
}
]
},
"RuleCodePrefix": {
"RuleSelector": {
"type": "string",
"enum": [
"A",
@@ -1311,6 +1338,9 @@
"DTZ011",
"DTZ012",
"E",
"E1",
"E10",
"E101",
"E4",
"E40",
"E401",
@@ -1348,6 +1378,12 @@
"ERA0",
"ERA00",
"ERA001",
"EXE",
"EXE0",
"EXE00",
"EXE003",
"EXE004",
"EXE005",
"F",
"F4",
"F40",
@@ -1517,7 +1553,10 @@
"PIE796",
"PIE8",
"PIE80",
"PIE800",
"PIE804",
"PIE807",
"PL",
"PLC",
"PLC0",
"PLC04",
@@ -1596,6 +1635,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",
@@ -1658,6 +1727,9 @@
"S506",
"S508",
"S509",
"S6",
"S61",
"S612",
"S7",
"S70",
"S701",
@@ -1711,6 +1783,20 @@
"TID25",
"TID251",
"TID252",
"TRY",
"TRY0",
"TRY00",
"TRY004",
"TRY2",
"TRY20",
"TRY201",
"TRY3",
"TRY30",
"TRY300",
"TYP",
"TYP0",
"TYP00",
"TYP005",
"U",
"U0",
"U00",
@@ -1767,6 +1853,7 @@
"UP029",
"UP03",
"UP030",
"UP031",
"UP032",
"UP033",
"UP034",

View File

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

View File

@@ -35,10 +35,19 @@ fn content_dir() -> &'static Path {
Path::new("content")
}
fn cache_key<P: AsRef<Path>>(path: P, settings: &Settings, autofix: flags::Autofix) -> u64 {
fn cache_key<P: AsRef<Path>>(
path: P,
package: Option<&P>,
settings: &Settings,
autofix: flags::Autofix,
) -> u64 {
let mut hasher = DefaultHasher::new();
CARGO_PKG_VERSION.hash(&mut hasher);
path.as_ref().absolutize().unwrap().hash(&mut hasher);
package
.as_ref()
.map(|path| path.as_ref().absolutize().unwrap())
.hash(&mut hasher);
settings.hash(&mut hasher);
autofix.hash(&mut hasher);
hasher.finish()
@@ -79,13 +88,14 @@ fn read_sync(cache_dir: &Path, key: u64) -> Result<Vec<u8>, std::io::Error> {
/// Get a value from the cache.
pub fn get<P: AsRef<Path>>(
path: P,
package: Option<&P>,
metadata: &fs::Metadata,
settings: &AllSettings,
autofix: flags::Autofix,
) -> Option<Vec<Message>> {
let encoded = read_sync(
&settings.cli.cache_dir,
cache_key(path, &settings.lib, autofix),
cache_key(path, package, &settings.lib, autofix),
)
.ok()?;
let (mtime, messages) = match bincode::deserialize::<CheckResult>(&encoded[..]) {
@@ -107,6 +117,7 @@ pub fn get<P: AsRef<Path>>(
/// Set a value in the cache.
pub fn set<P: AsRef<Path>>(
path: P,
package: Option<&P>,
metadata: &fs::Metadata,
settings: &AllSettings,
autofix: flags::Autofix,
@@ -120,7 +131,7 @@ pub fn set<P: AsRef<Path>>(
};
if let Err(e) = write_sync(
&settings.cli.cache_dir,
cache_key(path, &settings.lib, autofix),
cache_key(path, package, &settings.lib, autofix),
&bincode::serialize(&check_result).unwrap(),
) {
error!("Failed to write to cache: {e:?}");

View File

@@ -1,10 +1,9 @@
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use clap::{command, Parser};
use regex::Regex;
use ruff::fs;
use ruff::logging::LogLevel;
use ruff::registry::{Rule, RuleCodePrefix};
use ruff::registry::{Rule, RuleSelector};
use ruff::resolver::ConfigProcessor;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
@@ -66,18 +65,18 @@ pub struct Cli {
/// Comma-separated list of rule codes to enable (or ALL, to enable all
/// rules).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub select: Option<Vec<RuleCodePrefix>>,
pub select: Option<Vec<RuleSelector>>,
/// Like --select, but adds additional rule codes on top of the selected
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_select: Option<Vec<RuleCodePrefix>>,
pub extend_select: Option<Vec<RuleSelector>>,
/// Comma-separated list of rule codes to disable.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub ignore: Option<Vec<RuleCodePrefix>>,
pub ignore: Option<Vec<RuleSelector>>,
/// Like --ignore, but adds additional rule codes on top of the ignored
/// ones.
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
pub extend_ignore: Option<Vec<RuleSelector>>,
/// List of paths, used to omit files and/or directories from analysis.
#[arg(long, value_delimiter = ',', value_name = "FILE_PATTERN")]
pub exclude: Option<Vec<FilePattern>>,
@@ -88,11 +87,11 @@ pub struct Cli {
/// List of rule codes to treat as eligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub fixable: Option<Vec<RuleCodePrefix>>,
pub fixable: Option<Vec<RuleSelector>>,
/// List of rule codes to treat as ineligible for autofix. Only applicable
/// when autofix itself is enabled (e.g., via `--fix`).
#[arg(long, value_delimiter = ',', value_name = "RULE_CODE")]
pub unfixable: Option<Vec<RuleCodePrefix>>,
pub unfixable: Option<Vec<RuleSelector>>,
/// List of mappings from file pattern to code to exclude
#[arg(long, value_delimiter = ',')]
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
@@ -324,17 +323,17 @@ pub struct Overrides {
pub dummy_variable_rgx: Option<Regex>,
pub exclude: Option<Vec<FilePattern>>,
pub extend_exclude: Option<Vec<FilePattern>>,
pub extend_ignore: Option<Vec<RuleCodePrefix>>,
pub extend_select: Option<Vec<RuleCodePrefix>>,
pub fixable: Option<Vec<RuleCodePrefix>>,
pub ignore: Option<Vec<RuleCodePrefix>>,
pub extend_ignore: Option<Vec<RuleSelector>>,
pub extend_select: Option<Vec<RuleSelector>>,
pub fixable: Option<Vec<RuleSelector>>,
pub ignore: Option<Vec<RuleSelector>>,
pub line_length: Option<usize>,
pub per_file_ignores: Option<Vec<PatternPrefixPair>>,
pub respect_gitignore: Option<bool>,
pub select: Option<Vec<RuleCodePrefix>>,
pub select: Option<Vec<RuleSelector>>,
pub show_source: Option<bool>,
pub target_version: Option<PythonVersion>,
pub unfixable: Option<Vec<RuleCodePrefix>>,
pub unfixable: Option<Vec<RuleSelector>>,
// TODO(charlie): Captured in pyproject.toml as a default, but not part of `Settings`.
pub cache_dir: Option<PathBuf>,
pub fix: Option<bool>,
@@ -435,7 +434,7 @@ pub fn extract_log_level(cli: &Arguments) -> LogLevel {
/// Convert a list of `PatternPrefixPair` structs to `PerFileIgnore`.
pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgnore> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleCodePrefix>> = FxHashMap::default();
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)
@@ -444,9 +443,6 @@ pub fn collect_per_file_ignores(pairs: Vec<PatternPrefixPair>) -> Vec<PerFileIgn
}
per_file_ignores
.into_iter()
.map(|(pattern, prefixes)| {
let absolute = fs::normalize_path(Path::new(&pattern));
PerFileIgnore::new(pattern, absolute, &prefixes)
})
.map(|(pattern, prefixes)| PerFileIgnore::new(pattern, &prefixes, None))
.collect()
}

View File

@@ -15,7 +15,7 @@ use ruff::cache::CACHE_DIR_NAME;
use ruff::linter::add_noqa_to_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
use ruff::registry::Rule;
use ruff::registry::{Linter, Rule, RuleNamespace};
use ruff::resolver::{FileDiscovery, PyprojectDiscovery};
use ruff::settings::flags;
use ruff::settings::types::SerializationFormat;
@@ -285,16 +285,17 @@ pub fn show_files(
#[derive(Serialize)]
struct Explanation<'a> {
code: &'a str,
origin: &'a str,
linter: &'a str,
summary: &'a str,
}
/// Explain a `Rule` to the user.
pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
let (linter, _) = Linter::parse_code(rule.code()).unwrap();
match format {
SerializationFormat::Text | SerializationFormat::Grouped => {
println!("{}\n", rule.as_ref());
println!("Code: {} ({})\n", rule.code(), rule.origin().name());
println!("Code: {} ({})\n", rule.code(), linter.name());
if let Some(autofix) = rule.autofixable() {
println!(
@@ -315,7 +316,7 @@ pub fn explain(rule: &Rule, format: SerializationFormat) -> Result<()> {
"{}",
serde_json::to_string_pretty(&Explanation {
code: rule.code(),
origin: rule.origin().name(),
linter: linter.name(),
summary: rule.message_formats()[0],
})?
);

View File

@@ -55,7 +55,9 @@ pub fn lint_path(
&& matches!(autofix, fix::FixMode::None | fix::FixMode::Generate)
{
let metadata = path.metadata()?;
if let Some(messages) = cache::get(path, &metadata, settings, autofix.into()) {
if let Some(messages) =
cache::get(path, package.as_ref(), &metadata, settings, autofix.into())
{
debug!("Cache hit for: {}", path.to_string_lossy());
return Ok(Diagnostics::new(messages));
}
@@ -92,7 +94,14 @@ pub fn lint_path(
// Re-populate the cache.
if let Some(metadata) = metadata {
cache::set(path, &metadata, settings, autofix.into(), &messages);
cache::set(
path,
package.as_ref(),
&metadata,
settings,
autofix.into(),
&messages,
);
}
Ok(Diagnostics { messages, fixed })

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::RuleOrigin;
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 = RuleOrigin::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(|origin| *origin != RuleOrigin::Ruff)
.map(|origin| origin.prefixes().as_list(","))
.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.228"
version = "0.0.231"
edition = "2021"
[dependencies]
@@ -11,9 +11,9 @@ libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a87
once_cell = { version = "1.16.0" }
ruff = { path = ".." }
ruff_cli = { path = "../ruff_cli" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "ff90fe52eea578c8ebdd9d95e078cc041a5959fa" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "4f38cb68e4a97aeea9eb19673803a0bd5f655383" }
schemars = { version = "0.8.11" }
serde_json = {version="1.0.91"}
strum = { version = "0.24.1", features = ["strum_macros"] }

View File

@@ -2,7 +2,7 @@
use anyhow::Result;
use clap::Args;
use ruff::registry::{Prefixes, RuleCodePrefix, RuleOrigin};
use ruff::registry::{Linter, LinterCategory, Rule, RuleNamespace};
use strum::IntoEnumIterator;
use crate::utils::replace_readme_section;
@@ -20,12 +20,12 @@ pub struct Cli {
pub(crate) dry_run: bool,
}
fn generate_table(table_out: &mut String, prefix: &RuleCodePrefix) {
fn generate_table(table_out: &mut String, rules: impl IntoIterator<Item = Rule>) {
table_out.push_str("| Code | Name | Message | Fix |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | --- |");
table_out.push('\n');
for rule in prefix.codes() {
for rule in rules {
let fix_token = match rule.autofixable() {
None => "",
Some(_) => "🛠",
@@ -47,22 +47,21 @@ pub fn main(cli: &Cli) -> Result<()> {
// Generate the table string.
let mut table_out = String::new();
let mut toc_out = String::new();
for origin in RuleOrigin::iter() {
let prefixes = origin.prefixes();
let codes_csv: String = prefixes.as_list(", ");
table_out.push_str(&format!("### {} ({codes_csv})", origin.name()));
for linter in Linter::iter() {
let codes_csv: String = linter.prefixes().join(", ");
table_out.push_str(&format!("### {} ({codes_csv})", linter.name()));
table_out.push('\n');
table_out.push('\n');
toc_out.push_str(&format!(
" 1. [{} ({})](#{}-{})\n",
origin.name(),
linter.name(),
codes_csv,
origin.name().to_lowercase().replace(' ', "-"),
linter.name().to_lowercase().replace(' ', "-"),
codes_csv.to_lowercase().replace(',', "-").replace(' ', "")
));
if let Some(url) = origin.url() {
if let Some(url) = linter.url() {
let host = url
.trim_start_matches("https://")
.split('/')
@@ -70,7 +69,7 @@ pub fn main(cli: &Cli) -> Result<()> {
.unwrap();
table_out.push_str(&format!(
"For more, see [{}]({}) on {}.",
origin.name(),
linter.name(),
url,
match host {
"pypi.org" => "PyPI",
@@ -78,7 +77,7 @@ pub fn main(cli: &Cli) -> Result<()> {
host => panic!(
"unexpected host in URL of {}, expected pypi.org or github.com but found \
{host}",
origin.name()
linter.name()
),
}
));
@@ -86,15 +85,14 @@ pub fn main(cli: &Cli) -> Result<()> {
table_out.push('\n');
}
match prefixes {
Prefixes::Single(prefix) => generate_table(&mut table_out, &prefix),
Prefixes::Multiple(entries) => {
for (prefix, category) in entries {
table_out.push_str(&format!("#### {category} ({})", prefix.as_ref()));
table_out.push('\n');
generate_table(&mut table_out, &prefix);
}
if let Some(categories) = linter.categories() {
for LinterCategory(prefix, name, selector) in categories {
table_out.push_str(&format!("#### {name} ({prefix})"));
table_out.push('\n');
generate_table(&mut table_out, selector);
}
} else {
generate_table(&mut table_out, &linter);
}
}

View File

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

View File

@@ -10,7 +10,6 @@ pub fn define_rule_mapping(mapping: &Mapping) -> proc_macro2::TokenStream {
let mut diagkind_variants = quote!();
let mut rule_message_formats_match_arms = quote!();
let mut rule_autofixable_match_arms = quote!();
let mut rule_origin_match_arms = quote!();
let mut rule_code_match_arms = quote!();
let mut rule_from_code_match_arms = quote!();
let mut diagkind_code_match_arms = quote!();
@@ -29,8 +28,6 @@ pub fn define_rule_mapping(mapping: &Mapping) -> proc_macro2::TokenStream {
rule_message_formats_match_arms
.extend(quote! {Self::#name => <#path as Violation>::message_formats(),});
rule_autofixable_match_arms.extend(quote! {Self::#name => <#path as Violation>::AUTOFIX,});
let origin = get_origin(code);
rule_origin_match_arms.extend(quote! {Self::#name => RuleOrigin::#origin,});
rule_code_match_arms.extend(quote! {Self::#name => #code_str,});
rule_from_code_match_arms.extend(quote! {#code_str => Ok(&Rule::#name), });
diagkind_code_match_arms.extend(quote! {Self::#name(..) => &Rule::#name, });
@@ -56,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("RuleCodePrefix", Span::call_site()),
&Ident::new("RuleSelector", Span::call_site()),
mapping.entries.iter().map(|(code, ..)| code),
|code| code_to_name[code],
);
@@ -95,10 +92,6 @@ pub fn define_rule_mapping(mapping: &Mapping) -> proc_macro2::TokenStream {
match self { #rule_autofixable_match_arms }
}
pub fn origin(&self) -> RuleOrigin {
match self { #rule_origin_match_arms }
}
pub fn code(&self) -> &'static str {
match self { #rule_code_match_arms }
}
@@ -140,19 +133,6 @@ pub fn define_rule_mapping(mapping: &Mapping) -> proc_macro2::TokenStream {
}
}
fn get_origin(ident: &Ident) -> Ident {
let ident = ident.to_string();
let mut iter = crate::prefixes::PREFIX_TO_ORIGIN.iter();
let origin = loop {
let (prefix, origin) = iter
.next()
.unwrap_or_else(|| panic!("code doesn't start with any recognized prefix: {ident}"));
if ident.starts_with(prefix) {
break origin;
}
};
Ident::new(origin, Span::call_site())
}
pub struct Mapping {
entries: Vec<(Ident, Path, Ident)>,
}

View File

@@ -19,8 +19,8 @@ use syn::{parse_macro_input, DeriveInput, ItemFn};
mod config;
mod define_rule_mapping;
mod derive_message_formats;
mod prefixes;
mod rule_code_prefix;
mod rule_namespace;
#[proc_macro_derive(ConfigurationOptions, attributes(option, doc, option_group))]
pub fn derive_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
@@ -37,6 +37,15 @@ pub fn define_rule_mapping(item: proc_macro::TokenStream) -> proc_macro::TokenSt
define_rule_mapping::define_rule_mapping(&mapping).into()
}
#[proc_macro_derive(RuleNamespace, attributes(prefix))]
pub fn derive_rule_namespace(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
rule_namespace::derive_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro_attribute]
pub fn derive_message_formats(_attr: TokenStream, item: TokenStream) -> TokenStream {
let func = parse_macro_input!(item as ItemFn);

View File

@@ -1,54 +0,0 @@
// Longer prefixes should come first so that you can find an origin for a code
// by simply picking the first entry that starts with the given prefix.
pub const PREFIX_TO_ORIGIN: &[(&str, &str)] = &[
("ANN", "Flake8Annotations"),
("ARG", "Flake8UnusedArguments"),
("A", "Flake8Builtins"),
("BLE", "Flake8BlindExcept"),
("B", "Flake8Bugbear"),
("C4", "Flake8Comprehensions"),
("C9", "McCabe"),
("COM", "Flake8Commas"),
("DTZ", "Flake8Datetimez"),
("D", "Pydocstyle"),
("ERA", "Eradicate"),
("EM", "Flake8ErrMsg"),
("E", "Pycodestyle"),
("FBT", "Flake8BooleanTrap"),
("F", "Pyflakes"),
("ICN", "Flake8ImportConventions"),
("ISC", "Flake8ImplicitStrConcat"),
("I", "Isort"),
("N", "PEP8Naming"),
("PD", "PandasVet"),
("PGH", "PygrepHooks"),
("PL", "Pylint"),
("PT", "Flake8PytestStyle"),
("Q", "Flake8Quotes"),
("RET", "Flake8Return"),
("SIM", "Flake8Simplify"),
("S", "Flake8Bandit"),
("T10", "Flake8Debugger"),
("T20", "Flake8Print"),
("TID", "Flake8TidyImports"),
("UP", "Pyupgrade"),
("W", "Pycodestyle"),
("YTT", "Flake82020"),
("PIE", "Flake8Pie"),
("RUF", "Ruff"),
];
#[cfg(test)]
mod tests {
use super::PREFIX_TO_ORIGIN;
#[test]
fn order() {
for (idx, (prefix, _)) in PREFIX_TO_ORIGIN.iter().enumerate() {
for (prior_prefix, _) in PREFIX_TO_ORIGIN[..idx].iter() {
assert!(!prefix.starts_with(prior_prefix));
}
}
}
}

View File

@@ -7,8 +7,8 @@ use syn::Ident;
const ALL: &str = "ALL";
/// A hash map from deprecated `RuleCodePrefix` to latest
/// `RuleCodePrefix`.
/// 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.
@@ -91,9 +91,12 @@ pub fn expand<'a>(
variant_name: impl Fn(&str) -> &'a Ident,
) -> proc_macro2::TokenStream {
// Build up a map from prefix to matching RuleCodes.
let mut prefix_to_codes: BTreeMap<Ident, BTreeSet<String>> = BTreeMap::default();
let mut prefix_to_codes: BTreeMap<String, BTreeSet<String>> = BTreeMap::default();
let mut all_codes = BTreeSet::new();
let mut pl_codes = BTreeSet::new();
for variant in variants {
let span = variant.span();
let code_str = variant.to_string();
let code_prefix_len = code_str
.chars()
@@ -103,28 +106,32 @@ pub fn expand<'a>(
for i in 0..=code_suffix_len {
let prefix = code_str[..code_prefix_len + i].to_string();
prefix_to_codes
.entry(Ident::new(&prefix, span))
.entry(prefix)
.or_default()
.insert(code_str.clone());
}
prefix_to_codes
.entry(Ident::new(ALL, span))
.or_default()
.insert(code_str.clone());
if code_str.starts_with("PL") {
pl_codes.insert(code_str.to_string());
}
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(
Ident::new(alias, Span::call_site()),
(*alias).to_string(),
prefix_to_codes
.get(&Ident::new(rule_code, Span::call_site()))
.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! {
#prefix
}
@@ -148,6 +155,7 @@ pub fn expand<'a>(
Two,
Three,
Four,
Five,
}
#[derive(
@@ -169,7 +177,7 @@ pub fn expand<'a>(
#prefix_impl
/// A hash map from deprecated `RuleCodePrefix` to latest `RuleCodePrefix`.
/// 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),*
@@ -181,50 +189,51 @@ pub fn expand<'a>(
fn generate_impls<'a>(
rule_type: &Ident,
prefix_ident: &Ident,
prefix_to_codes: &BTreeMap<Ident, BTreeSet<String>>,
prefix_to_codes: &BTreeMap<String, BTreeSet<String>>,
variant_name: impl Fn(&str) -> &'a Ident,
) -> proc_macro2::TokenStream {
let codes_match_arms = prefix_to_codes.iter().map(|(prefix, codes)| {
let into_iter_match_arms = prefix_to_codes.iter().map(|(prefix_str, codes)| {
let codes = codes.iter().map(|code| {
let rule_variant = variant_name(code);
quote! {
#rule_type::#rule_variant
}
});
let prefix_str = prefix.to_string();
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),*]
vec![#(#codes),*].into_iter()
}
}
} else {
quote! {
#prefix_ident::#prefix => vec![#(#codes),*],
#prefix_ident::#prefix => vec![#(#codes),*].into_iter(),
}
}
});
let specificity_match_arms = prefix_to_codes.keys().map(|prefix| {
if *prefix == ALL {
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 num_numeric = prefix
.to_string()
.chars()
.filter(|char| char.is_numeric())
.count();
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! {
@@ -233,11 +242,11 @@ fn generate_impls<'a>(
}
});
let categories = prefix_to_codes.keys().map(|prefix| {
let prefix_str = prefix.to_string();
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,
}
@@ -248,15 +257,6 @@ fn generate_impls<'a>(
quote! {
impl #prefix_ident {
pub fn codes(&self) -> Vec<#rule_type> {
use colored::Colorize;
#[allow(clippy::match_same_arms)]
match self {
#(#codes_match_arms)*
}
}
pub fn specificity(&self) -> SuffixLength {
#[allow(clippy::match_same_arms)]
match self {
@@ -265,6 +265,20 @@ fn generate_impls<'a>(
}
}
impl IntoIterator for &#prefix_ident {
type Item = #rule_type;
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

@@ -0,0 +1,153 @@
use std::collections::HashSet;
use proc_macro2::{Ident, Span};
use quote::quote;
use syn::spanned::Spanned;
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 {
variants, ..
}), .. } = input else {
return Err(Error::new(input.ident.span(), "only named fields are supported"));
};
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 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 prefixes.is_empty() {
return Err(Error::new(
variant.span(),
r#"Missing #[prefix = "..."] attribute"#,
));
}
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 => &[#(#prefixes),*],
});
}
parsed.sort_by_key(|(prefix, _)| prefix.len());
let mut if_statements = quote!();
let mut into_iter_match_arms = quote!();
for (prefix, field) in parsed {
if_statements.extend(quote! {if let Some(rest) = code.strip_prefix(#prefix) {
return Some((#ident::#field, rest));
}});
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(),
});
}
}
into_iter_match_arms.extend(quote! {
#ident::Pycodestyle => {
let rules: Vec<_> = (&RuleSelector::E).into_iter().chain(&RuleSelector::W).collect();
rules.into_iter()
}
});
Ok(quote! {
impl crate::registry::RuleNamespace for #ident {
fn parse_code(code: &str) -> Option<(Self, &str)> {
#if_statements
None
}
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 {
type Item = Rule;
type IntoIter = ::std::vec::IntoIter<Self::Item>;
fn into_iter(self) -> Self::IntoIter {
use colored::Colorize;
match self {
#into_iter_match_arms
}
}
}
})
}
/// 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

@@ -5,13 +5,13 @@ from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(origin: str) -> str:
return origin.replace("-", "_")
def dir_name(linter_name: str) -> str:
return linter_name.replace("-", "_")
def pascal_case(origin: str) -> str:
def pascal_case(linter_name: str) -> str:
"""Convert from snake-case to PascalCase."""
return "".join(word.title() for word in origin.split("-"))
return "".join(word.title() for word in linter_name.split("-"))
def get_indent(line: str) -> str:

View File

@@ -5,7 +5,8 @@ Example usage:
python scripts/add_plugin.py \
flake8-pie \
--url https://pypi.org/project/flake8-pie/0.16.0/
--url https://pypi.org/project/flake8-pie/
--prefix PIE
"""
import argparse
@@ -14,19 +15,18 @@ import os
from _utils import ROOT_DIR, dir_name, get_indent, pascal_case
def main(*, plugin: str, url: str) -> None:
def main(*, plugin: str, url: str, prefix_code: str) -> None:
# Create the test fixture folder.
os.makedirs(
ROOT_DIR / "resources/test/fixtures" / dir_name(plugin),
exist_ok=True,
)
# Create the Rust module.
rust_module = ROOT_DIR / "src/rules" / dir_name(plugin)
os.makedirs(rust_module, exist_ok=True)
with open(rust_module / "rules.rs", "w+") as fp:
fp.write("use crate::checkers::ast::Checker;\n")
with open(rust_module / "mod.rs", "w+") as fp:
# Create the Plugin rules module.
plugin_dir = ROOT_DIR / "src/rules" / dir_name(plugin)
plugin_dir.mkdir(exist_ok=True)
with (plugin_dir / "mod.rs").open("w+") as fp:
fp.write(f"//! Rules from [{plugin}]({url}).\n")
fp.write("pub(crate) mod rules;\n")
fp.write("\n")
@@ -59,45 +59,37 @@ mod tests {
% dir_name(plugin)
)
# Create a subdirectory for rules and create a `mod.rs` placeholder
rules_dir = plugin_dir / "rules"
rules_dir.mkdir(exist_ok=True)
with (rules_dir / "mod.rs").open("w+") as fp:
fp.write("\n\n")
# Create the snapshots subdirectory
(plugin_dir / "snapshots").mkdir(exist_ok=True)
# Add the plugin to `rules/mod.rs`.
with open(ROOT_DIR / "src/rules/mod.rs", "a") as fp:
with (ROOT_DIR / "src/rules/mod.rs").open("a") as fp:
fp.write(f"pub mod {dir_name(plugin)};")
# Add the relevant sections to `src/registry.rs`.
content = (ROOT_DIR / "src/registry.rs").read_text()
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
with (ROOT_DIR / "src/registry.rs").open("w") as fp:
for line in content.splitlines():
indent = get_indent(line)
if line.strip() == "// Ruff":
if line.strip() == "// ruff":
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == "Ruff,":
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")
elif line.strip() == "RuleOrigin::Ruff => Prefixes::Single(RuleCodePrefix::RUF),":
prefix = 'todo!("Fill-in prefix after generating codes")'
fp.write(
f"{indent}RuleOrigin::{pascal_case(plugin)} => Prefixes::Single({prefix}),"
)
fp.write("\n")
fp.write(line)
fp.write("\n")
# Add the relevant section to `src/violations.rs`.
content = (ROOT_DIR / "src/violations.rs").read_text()
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
for line in content.splitlines():
if line.strip() == "// Ruff":
indent = get_indent(line)
fp.write(f"{indent}// {plugin}")
fp.write("\n")
fp.write(line)
fp.write("\n")
@@ -107,7 +99,7 @@ if __name__ == "__main__":
description="Generate boilerplate for a new Flake8 plugin.",
epilog=(
"Example usage: python scripts/add_plugin.py flake8-pie "
"--url https://pypi.org/project/flake8-pie/0.16.0/"
"--url https://pypi.org/project/flake8-pie/"
),
)
parser.add_argument(
@@ -121,6 +113,13 @@ if __name__ == "__main__":
type=str,
help="The URL of the latest release in PyPI.",
)
parser.add_argument(
"--prefix",
required=False,
default="TODO",
type=str,
help="Prefix code for the plugin. Leave empty to manually fill.",
)
args = parser.parse_args()
main(plugin=args.plugin, url=args.url)
main(plugin=args.plugin, url=args.url, prefix_code=args.prefix)

View File

@@ -6,7 +6,7 @@ Example usage:
python scripts/add_rule.py \
--name PreferListBuiltin \
--code PIE807 \
--origin flake8-pie
--linter flake8-pie
"""
import argparse
@@ -19,49 +19,60 @@ def snake_case(name: str) -> str:
return "".join(f"_{word.lower()}" if word.isupper() else word for word in name).lstrip("_")
def main(*, name: str, code: str, origin: str) -> None:
def main(*, name: str, code: str, linter: str) -> None:
# Create a test fixture.
with open(
ROOT_DIR / "resources/test/fixtures" / dir_name(origin) / f"{code}.py",
"a",
):
with (ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py").open("a"):
pass
plugin_module = ROOT_DIR / "src/rules" / dir_name(linter)
rule_name_snake = snake_case(name)
# Add the relevant `#testcase` macro.
mod_rs = ROOT_DIR / "src/rules" / dir_name(origin) / "mod.rs"
mod_rs = plugin_module / "mod.rs"
content = mod_rs.read_text()
with open(mod_rs, "w") as fp:
with mod_rs.open("w") as fp:
for line in content.splitlines():
if line.strip() == "fn rules(rule_code: Rule, path: &Path) -> Result<()> {":
indent = get_indent(line)
fp.write(f'{indent}#[test_case(Rule::{code}, Path::new("{code}.py"); "{code}")]')
fp.write(f'{indent}#[test_case(Rule::{name}, Path::new("{code}.py"); "{code}")]')
fp.write("\n")
fp.write(line)
fp.write("\n")
# Add the relevant rule function.
with open(ROOT_DIR / "src/rules" / dir_name(origin) / (snake_case(name) + ".rs"), "w") as fp:
fp.write(
f"""
/// {code}
pub fn {snake_case(name)}(checker: &mut Checker) {{}}
"""
)
fp.write("\n")
# Add the exports
rules_dir = plugin_module / "rules"
rules_mod = rules_dir / "mod.rs"
# Add the relevant struct to `src/violations.rs`.
content = (ROOT_DIR / "src/violations.rs").read_text()
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
for line in content.splitlines():
fp.write(line)
contents = rules_mod.read_text()
parts = contents.split("\n\n")
if len(parts) == 2:
new_contents = parts[0] + "\n"
new_contents += f"pub use {rule_name_snake}::{{{rule_name_snake}, {name}}};"
new_contents += "\n"
new_contents += "\n"
new_contents += parts[1]
new_contents += f"mod {rule_name_snake};"
new_contents += "\n"
rules_mod.write_text(new_contents)
else:
with rules_mod.open("a") as fp:
fp.write(f"pub use {rule_name_snake}::{{{rule_name_snake}, {name}}};")
fp.write("\n")
fp.write(f"mod {rule_name_snake};")
fp.write("\n")
if line.startswith(f"// {origin}"):
fp.write(
"""define_violation!(
# Add the relevant rule function.
with (rules_dir / f"{rule_name_snake}.rs").open("w") as fp:
fp.write(
"""use ruff_macros::derive_message_formats;
use crate::define_violation;
use crate::violation::Violation;
use crate::checkers::ast::Checker;
define_violation!(
pub struct %s;
);
impl Violation for %s {
@@ -72,16 +83,23 @@ impl Violation for %s {
}
}
"""
% (name, name)
)
fp.write("\n")
% (name, name)
)
fp.write("\n")
fp.write(
f"""
/// {code}
pub fn {rule_name_snake}(checker: &mut Checker) {{}}
"""
)
fp.write("\n")
# Add the relevant code-to-violation pair to `src/registry.rs`.
content = (ROOT_DIR / "src/registry.rs").read_text()
seen_macro = False
has_written = False
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
with (ROOT_DIR / "src/registry.rs").open("w") as fp:
for line in content.splitlines():
fp.write(line)
fp.write("\n")
@@ -96,9 +114,9 @@ impl Violation for %s {
if not seen_macro:
continue
if line.strip() == f"// {origin}":
if line.strip() == f"// {linter}":
indent = get_indent(line)
fp.write(f"{indent}{code} => violations::{name},")
fp.write(f"{indent}{code} => rules::{dir_name(linter)}::rules::{name},")
fp.write("\n")
has_written = True
@@ -108,7 +126,7 @@ impl Violation for %s {
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Generate boilerplate for a new rule.",
epilog="python scripts/add_rule.py --name PreferListBuiltin --code PIE807 --origin flake8-pie",
epilog="python scripts/add_rule.py --name PreferListBuiltin --code PIE807 --linter flake8-pie",
)
parser.add_argument(
"--name",
@@ -123,11 +141,11 @@ if __name__ == "__main__":
help="The code of the check to generate (e.g., 'A001').",
)
parser.add_argument(
"--origin",
"--linter",
type=str,
required=True,
help="The source with which the check originated (e.g., 'flake8-builtins').",
)
args = parser.parse_args()
main(name=args.name, code=args.code, origin=args.origin)
main(name=args.name, code=args.code, linter=args.linter)

View File

@@ -13,6 +13,7 @@ Please use `python -m pip install .` instead.
)
sys.exit(1)
"abc".isidentifier()
# The below code will never execute, however GitHub is particularly
# picky about where it finds Python packaging metadata.

View File

@@ -295,7 +295,7 @@ pub enum ComparableExpr<'a> {
orelse: Box<ComparableExpr<'a>>,
},
Dict {
keys: Vec<ComparableExpr<'a>>,
keys: Vec<Option<ComparableExpr<'a>>>,
values: Vec<ComparableExpr<'a>>,
},
Set {
@@ -424,7 +424,10 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
orelse: orelse.into(),
},
ExprKind::Dict { keys, values } => Self::Dict {
keys: keys.iter().map(std::convert::Into::into).collect(),
keys: keys
.iter()
.map(|expr| expr.as_ref().map(std::convert::Into::into))
.collect(),
values: values.iter().map(std::convert::Into::into).collect(),
},
ExprKind::Set { elts } => Self::Set {

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

@@ -161,7 +161,7 @@ where
}
ExprKind::Dict { keys, values } => values
.iter()
.chain(keys.iter())
.chain(keys.iter().flatten())
.any(|expr| any_over_expr(expr, func)),
ExprKind::Set { elts } | ExprKind::List { elts, .. } | ExprKind::Tuple { elts, .. } => {
elts.iter().any(|expr| any_over_expr(expr, func))
@@ -366,7 +366,7 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
/// Returns `true` if a statement or expression includes at least one comment.
pub fn has_comments_in(range: Range, locator: &Locator) -> bool {
lexer::make_tokenizer(&locator.slice_source_code_range(&range))
lexer::make_tokenizer(locator.slice_source_code_range(&range))
.any(|result| result.map_or(false, |(_, tok, _)| matches!(tok, Tok::Comment(..))))
}
@@ -486,7 +486,7 @@ pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> Range {
| StmtKind::AsyncFunctionDef { .. }
) {
let contents = locator.slice_source_code_range(&Range::from_located(stmt));
for (start, tok, end) in lexer::make_tokenizer_located(&contents, stmt.location).flatten() {
for (start, tok, end) in lexer::make_tokenizer_located(contents, stmt.location).flatten() {
if matches!(tok, Tok::Name { .. }) {
return Range::new(start, end);
}
@@ -515,7 +515,7 @@ pub fn binding_range(binding: &Binding, locator: &Locator) -> Range {
// Return the ranges of `Name` tokens within a specified node.
pub fn find_names<T>(located: &Located<T>, locator: &Locator) -> Vec<Range> {
let contents = locator.slice_source_code_range(&Range::from_located(located));
lexer::make_tokenizer_located(&contents, located.location)
lexer::make_tokenizer_located(contents, located.location)
.flatten()
.filter(|(_, tok, _)| matches!(tok, Tok::Name { .. }))
.map(|(start, _, end)| Range {
@@ -535,7 +535,7 @@ pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> O
let type_end_location = type_.end_location.unwrap();
let contents =
locator.slice_source_code_range(&Range::new(type_end_location, body[0].location));
let range = lexer::make_tokenizer_located(&contents, type_end_location)
let range = lexer::make_tokenizer_located(contents, type_end_location)
.flatten()
.tuple_windows()
.find(|(tok, next_tok)| {
@@ -562,7 +562,7 @@ pub fn except_range(handler: &Excepthandler, locator: &Locator) -> Range {
location: handler.location,
end_location: end,
});
let range = lexer::make_tokenizer_located(&contents, handler.location)
let range = lexer::make_tokenizer_located(contents, handler.location)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Except { .. }))
.map(|(location, _, end_location)| Range {
@@ -576,7 +576,7 @@ pub fn except_range(handler: &Excepthandler, locator: &Locator) -> Range {
/// Find f-strings that don't contain any formatted values in a `JoinedStr`.
pub fn find_useless_f_strings(expr: &Expr, locator: &Locator) -> Vec<(Range, Range)> {
let contents = locator.slice_source_code_range(&Range::from_located(expr));
lexer::make_tokenizer_located(&contents, expr.location)
lexer::make_tokenizer_located(contents, expr.location)
.flatten()
.filter_map(|(location, tok, end_location)| match tok {
Tok::String {
@@ -630,7 +630,7 @@ pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
.expect("Expected orelse to be non-empty")
.location,
});
let range = lexer::make_tokenizer_located(&contents, body_end)
let range = lexer::make_tokenizer_located(contents, body_end)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Else))
.map(|(location, _, end_location)| Range {
@@ -646,7 +646,7 @@ pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
/// Return the `Range` of the first `Tok::Colon` token in a `Range`.
pub fn first_colon_range(range: Range, locator: &Locator) -> Option<Range> {
let contents = locator.slice_source_code_range(&range);
let range = lexer::make_tokenizer_located(&contents, range.location)
let range = lexer::make_tokenizer_located(contents, range.location)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Colon))
.map(|(location, _, end_location)| Range {
@@ -676,7 +676,7 @@ pub fn elif_else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
_ => return None,
};
let contents = locator.slice_source_code_range(&Range::new(start, end));
let range = lexer::make_tokenizer_located(&contents, start)
let range = lexer::make_tokenizer_located(contents, start)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Elif | Tok::Else))
.map(|(location, _, end_location)| Range {

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

@@ -39,7 +39,7 @@ pub fn relocate_expr(expr: &mut Expr, location: Range) {
relocate_expr(orelse, location);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
for expr in keys.iter_mut().flatten() {
relocate_expr(expr, location);
}
for expr in values {

View File

@@ -288,7 +288,7 @@ pub fn walk_expr<'a, V: Visitor<'a> + ?Sized>(visitor: &mut V, expr: &'a Expr) {
visitor.visit_expr(orelse);
}
ExprKind::Dict { keys, values } => {
for expr in keys {
for expr in keys.iter().flatten() {
visitor.visit_expr(expr);
}
for expr in values {

View File

@@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::str::Lines;
use rustpython_ast::{Located, Location};
@@ -7,7 +6,7 @@ use crate::ast::types::Range;
use crate::source_code::Locator;
/// Extract the leading indentation from a line.
pub fn indentation<'a, T>(locator: &'a Locator, located: &'a Located<T>) -> Option<Cow<'a, str>> {
pub fn indentation<'a, T>(locator: &'a Locator, located: &'a Located<T>) -> Option<&'a str> {
let range = Range::from_located(located);
let indentation = locator.slice_source_code_range(&Range::new(
Location::new(range.location.row(), 0),

View File

@@ -80,7 +80,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
let contents = locator.slice_source_code_at(stmt.end_location.unwrap());
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
for (row, line) in LinesWithTrailingNewline::from(contents).enumerate() {
let trimmed = line.trim();
if trimmed.starts_with(';') {
let column = line
@@ -103,7 +103,7 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
let contents = locator.slice_source_code_at(start_location);
for (row, line) in LinesWithTrailingNewline::from(&contents).enumerate() {
for (row, line) in LinesWithTrailingNewline::from(contents).enumerate() {
let trimmed = line.trim();
// Skip past any continuations.
if trimmed.starts_with('\\') {
@@ -202,7 +202,7 @@ pub fn remove_unused_imports<'a>(
indexer: &Indexer,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let mut tree = match_module(&module_text)?;
let mut tree = match_module(module_text)?;
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
bail!("Expected Statement::Simple");

View File

@@ -1,8 +1,6 @@
use std::borrow::Cow;
use std::collections::BTreeSet;
use itertools::Itertools;
use ropey::RopeBuilder;
use rustpython_ast::Location;
use crate::ast::types::Range;
@@ -14,10 +12,7 @@ pub mod fixer;
pub mod helpers;
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub fn fix_file<'a>(
diagnostics: &'a [Diagnostic],
locator: &'a Locator<'a>,
) -> Option<(Cow<'a, str>, usize)> {
pub fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<(String, usize)> {
if diagnostics.iter().all(|check| check.fix.is_none()) {
return None;
}
@@ -32,8 +27,8 @@ pub fn fix_file<'a>(
fn apply_fixes<'a>(
fixes: impl Iterator<Item = &'a Fix>,
locator: &'a Locator<'a>,
) -> (Cow<'a, str>, usize) {
let mut output = RopeBuilder::new();
) -> (String, usize) {
let mut output = String::new();
let mut last_pos: Location = Location::new(1, 0);
let mut applied: BTreeSet<&Fix> = BTreeSet::default();
let mut num_fixed: usize = 0;
@@ -54,10 +49,10 @@ fn apply_fixes<'a>(
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice_source_code_range(&Range::new(last_pos, fix.location));
output.append(&slice);
output.push_str(slice);
// Add the patch itself.
output.append(&fix.content);
output.push_str(&fix.content);
// Track that the fix was applied.
last_pos = fix.end_location;
@@ -67,9 +62,9 @@ fn apply_fixes<'a>(
// Add the remaining content.
let slice = locator.slice_source_code_at(last_pos);
output.append(&slice);
output.push_str(slice);
(Cow::from(output.finish()), num_fixed)
(output, num_fixed)
}
#[cfg(test)]

View File

@@ -35,9 +35,9 @@ use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pie, flake8_print,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports,
flake8_unused_arguments, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff,
flake8_pytest_style, flake8_return, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
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};
@@ -273,7 +273,7 @@ impl<'a> Checker<'a> {
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match noqa::extract_noqa_directive(&line) {
match noqa::extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => noqa::includes(code, &codes),
@@ -1230,6 +1230,28 @@ where
}
}
if self
.settings
.rules
.enabled(&Rule::ImportAliasIsNotConventional)
{
let full_name = helpers::format_import_from_member(
level.as_ref(),
module.as_deref(),
&alias.node.name,
);
if let Some(diagnostic) =
flake8_import_conventions::rules::check_conventional_import(
stmt,
&full_name,
alias.node.asname.as_deref(),
&self.settings.flake8_import_conventions.aliases,
)
{
self.diagnostics.push(diagnostic);
}
}
if let Some(asname) = &alias.node.asname {
if self
.settings
@@ -1355,8 +1377,18 @@ where
if self.settings.rules.enabled(&Rule::IfTuple) {
pyflakes::rules::if_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::EmptyTypeCheckingBlock) {
flake8_type_checking::rules::empty_type_checking_block(self, test, body);
}
if self.settings.rules.enabled(&Rule::NestedIfStatements) {
flake8_simplify::rules::nested_if_statements(self, stmt);
flake8_simplify::rules::nested_if_statements(
self,
stmt,
test,
body,
orelse,
self.current_stmt_parent().map(Into::into),
);
}
if self
.settings
@@ -1382,6 +1414,15 @@ where
self.current_stmt_parent().map(std::convert::Into::into),
);
}
if self.settings.rules.enabled(&Rule::PreferTypeError) {
tryceratops::rules::prefer_type_error(
self,
body,
test,
orelse,
self.current_stmt_parent().map(Into::into),
);
}
}
StmtKind::Assert { test, msg } => {
if self.settings.rules.enabled(&Rule::AssertTuple) {
@@ -1535,6 +1576,12 @@ where
self, body, handlers, finalbody,
);
}
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);
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
@@ -2160,6 +2207,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) {
@@ -2206,6 +2258,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) {
@@ -2502,6 +2563,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
@@ -2515,6 +2605,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) {
@@ -2618,7 +2712,7 @@ where
.enabled(&Rule::PercentFormatUnsupportedFormatCharacter)
{
let location = Range::from_located(expr);
match pyflakes::cformat::CFormatSummary::try_from(value.as_ref()) {
match pyflakes::cformat::CFormatSummary::try_from(value.as_str()) {
Err(CFormatError {
typ: CFormatErrorType::UnsupportedFormatChar(c),
..
@@ -2713,6 +2807,10 @@ where
}
}
}
if self.settings.rules.enabled(&Rule::PrintfStringFormatting) {
pyupgrade::rules::printf_string_formatting(self, expr, left, right);
}
}
}
ExprKind::BinOp {
@@ -3108,7 +3206,7 @@ where
// Ex) TypedDict("a", {"a": int})
if args.len() > 1 {
if let ExprKind::Dict { keys, values } = &args[1].node {
for key in keys {
for key in keys.iter().flatten() {
self.in_type_definition = false;
self.visit_expr(key);
self.in_type_definition = prev_in_type_definition;
@@ -3571,7 +3669,6 @@ impl<'a> Checker<'a> {
{
let binding_index = self.bindings.len();
let mut overridden = None;
if let Some((stack_index, scope_index)) = self
.scope_stack
.iter()
@@ -3603,7 +3700,6 @@ impl<'a> Checker<'a> {
| BindingKind::FutureImportation
);
if matches!(binding.kind, BindingKind::LoopVar) && existing_is_import {
overridden = Some((*scope_index, *existing_binding_index));
if self.settings.rules.enabled(&Rule::ImportShadowedByLoopVar) {
self.diagnostics.push(Diagnostic::new(
violations::ImportShadowedByLoopVar(
@@ -3623,7 +3719,6 @@ impl<'a> Checker<'a> {
cast::decorator_list(existing.source.as_ref().unwrap()),
))
{
overridden = Some((*scope_index, *existing_binding_index));
if self.settings.rules.enabled(&Rule::RedefinedWhileUnused) {
self.diagnostics.push(Diagnostic::new(
violations::RedefinedWhileUnused(
@@ -3643,13 +3738,6 @@ impl<'a> Checker<'a> {
}
}
// If we're about to lose the binding, store it as overridden.
if let Some((scope_index, binding_index)) = overridden {
self.scopes[scope_index]
.overridden
.push((name, binding_index));
}
// Assume the rebound name is used as a global or within a loop.
let scope = self.current_scope();
let binding = match scope.values.get(&name) {
@@ -4573,12 +4661,13 @@ impl<'a> Checker<'a> {
Location::new(expr.location.row(), 0),
Location::new(expr.location.row(), expr.location.column()),
));
let body = pydocstyle::helpers::raw_contents(&contents);
let body = pydocstyle::helpers::raw_contents(contents);
let docstring = Docstring {
kind: definition.kind,
expr,
contents: &contents,
indentation: &indentation,
contents,
indentation,
body,
};
@@ -4722,6 +4811,7 @@ impl<'a> Checker<'a> {
name,
located,
flake8_builtins::types::ShadowingType::Attribute,
&self.settings.flake8_builtins.builtins_ignorelist,
) {
self.diagnostics.push(diagnostic);
}
@@ -4732,6 +4822,7 @@ impl<'a> Checker<'a> {
name,
located,
flake8_builtins::types::ShadowingType::Variable,
&self.settings.flake8_builtins.builtins_ignorelist,
) {
self.diagnostics.push(diagnostic);
}
@@ -4745,6 +4836,7 @@ impl<'a> Checker<'a> {
name,
arg,
flake8_builtins::types::ShadowingType::Argument,
&self.settings.flake8_builtins.builtins_ignorelist,
) {
self.diagnostics.push(diagnostic);
}

View File

@@ -4,12 +4,16 @@ use crate::registry::{Diagnostic, Rule};
use crate::rules::flake8_no_pep420::rules::implicit_namespace_package;
use crate::settings::Settings;
pub fn check_file_path(path: &Path, settings: &Settings) -> Vec<Diagnostic> {
pub fn check_file_path(
path: &Path,
package: Option<&Path>,
settings: &Settings,
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
// flake8-no-pep420
if settings.rules.enabled(&Rule::ImplicitNamespacePackage) {
if let Some(diagnostic) = implicit_namespace_package(path) {
if let Some(diagnostic) = implicit_namespace_package(path, package) {
diagnostics.push(diagnostic);
}
}

View File

@@ -1,8 +1,10 @@
//! Lint rules based on checking raw physical lines.
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::pycodestyle::rules::{
doc_line_too_long, line_too_long, no_newline_at_end_of_file,
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
};
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
@@ -18,6 +20,9 @@ pub fn check_lines(
let mut diagnostics: Vec<Diagnostic> = vec![];
let enforce_blanket_noqa = settings.rules.enabled(&Rule::BlanketNOQA);
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);
let enforce_blanket_type_ignore = settings.rules.enabled(&Rule::BlanketTypeIgnore);
let enforce_doc_line_too_long = settings.rules.enabled(&Rule::DocLineTooLong);
let enforce_line_too_long = settings.rules.enabled(&Rule::LineTooLong);
@@ -25,6 +30,14 @@ pub fn check_lines(
let enforce_unnecessary_coding_comment = settings
.rules
.enabled(&Rule::PEP3120UnnecessaryCodingComment);
let enforce_mixed_spaces_and_tabs = settings.rules.enabled(&Rule::MixedSpacesAndTabs);
let fix_unnecessary_coding_comment = matches!(autofix, flags::Autofix::Enabled)
&& settings
.rules
.should_fix(&Rule::PEP3120UnnecessaryCodingComment);
let fix_shebang_whitespace = matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::ShebangWhitespace);
let mut commented_lines_iter = commented_lines.iter().peekable();
let mut doc_lines_iter = doc_lines.iter().peekable();
@@ -35,14 +48,9 @@ pub fn check_lines(
{
if enforce_unnecessary_coding_comment {
if index < 2 {
if let Some(diagnostic) = unnecessary_coding_comment(
index,
line,
matches!(autofix, flags::Autofix::Enabled)
&& settings
.rules
.should_fix(&Rule::PEP3120UnnecessaryCodingComment),
) {
if let Some(diagnostic) =
unnecessary_coding_comment(index, line, fix_unnecessary_coding_comment)
{
diagnostics.push(diagnostic);
}
}
@@ -59,6 +67,27 @@ pub fn check_lines(
diagnostics.push(diagnostic);
}
}
if enforce_shebang_whitespace || enforce_shebang_newline || enforce_shebang_python {
let shebang = extract_shebang(line);
if enforce_shebang_whitespace {
if let Some(diagnostic) =
shebang_whitespace(index, &shebang, fix_shebang_whitespace)
{
diagnostics.push(diagnostic);
}
}
if enforce_shebang_newline {
if let Some(diagnostic) = shebang_newline(index, &shebang) {
diagnostics.push(diagnostic);
}
}
if enforce_shebang_python {
if let Some(diagnostic) = shebang_python(index, &shebang) {
diagnostics.push(diagnostic);
}
}
}
}
while doc_lines_iter
@@ -72,6 +101,12 @@ pub fn check_lines(
}
}
if enforce_mixed_spaces_and_tabs {
if let Some(diagnostic) = mixed_spaces_and_tabs(index, line) {
diagnostics.push(diagnostic);
}
}
if enforce_line_too_long {
if let Some(diagnostic) = line_too_long(index, line, settings) {
diagnostics.push(diagnostic);

View File

@@ -1,5 +1,3 @@
use std::borrow::Cow;
use rustpython_ast::{Expr, Stmt};
#[derive(Debug, Clone)]
@@ -23,9 +21,9 @@ pub struct Definition<'a> {
pub struct Docstring<'a> {
pub kind: DefinitionKind<'a>,
pub expr: &'a Expr,
pub contents: &'a Cow<'a, str>,
pub contents: &'a str,
pub body: &'a str,
pub indentation: &'a Cow<'a, str>,
pub indentation: &'a str,
}
pub enum Documentable {

View File

@@ -1,8 +1,5 @@
//! Extract Black configuration settings from a pyproject.toml.
use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::settings::types::PythonVersion;
@@ -14,20 +11,3 @@ pub struct Black {
#[serde(alias = "target-version", alias = "target_version")]
pub target_version: Option<Vec<PythonVersion>>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Tools {
black: Option<Black>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
struct Pyproject {
tool: Option<Tools>,
}
pub fn parse_black_options<P: AsRef<Path>>(path: P) -> Result<Option<Black>> {
let contents = std::fs::read_to_string(path)?;
Ok(toml_edit::easy::from_str::<Pyproject>(&contents)?
.tool
.and_then(|tool| tool.black))
}

View File

@@ -3,10 +3,10 @@ use std::collections::{BTreeSet, HashMap};
use anyhow::Result;
use colored::Colorize;
use super::black::Black;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
use crate::registry::RuleCodePrefix;
use crate::registry::RuleSelector;
use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
@@ -14,8 +14,8 @@ use crate::rules::flake8_quotes::settings::Quote;
use crate::rules::flake8_tidy_imports::relative_imports::Strictness;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{
flake8_annotations, flake8_bugbear, flake8_errmsg, flake8_pytest_style, flake8_quotes,
flake8_tidy_imports, mccabe, pep8_naming, pydocstyle,
flake8_annotations, flake8_bugbear, flake8_builtins, flake8_errmsg, flake8_pytest_style,
flake8_quotes, flake8_tidy_imports, mccabe, pep8_naming, pydocstyle,
};
use crate::settings::options::Options;
use crate::settings::pyproject::Pyproject;
@@ -23,7 +23,7 @@ use crate::warn_user;
pub fn convert(
config: &HashMap<String, HashMap<String, Option<String>>>,
black: Option<&Black>,
external_config: &ExternalConfig,
plugins: Option<Vec<Plugin>>,
) -> Result<Pyproject> {
// Extract the Flake8 section.
@@ -32,7 +32,7 @@ pub fn convert(
.expect("Unable to find flake8 section in INI file");
// Extract all referenced rule code prefixes, to power plugin inference.
let mut referenced_codes: BTreeSet<RuleCodePrefix> = BTreeSet::default();
let mut referenced_codes: BTreeSet<RuleSelector> = BTreeSet::default();
for (key, value) in flake8 {
if let Some(value) = value {
match key.as_str() {
@@ -90,6 +90,7 @@ pub fn convert(
let mut options = Options::default();
let mut flake8_annotations = flake8_annotations::settings::Options::default();
let mut flake8_bugbear = flake8_bugbear::settings::Options::default();
let mut flake8_builtins = flake8_builtins::settings::Options::default();
let mut flake8_errmsg = flake8_errmsg::settings::Options::default();
let mut flake8_pytest_style = flake8_pytest_style::settings::Options::default();
let mut flake8_quotes = flake8_quotes::settings::Options::default();
@@ -104,7 +105,7 @@ pub fn convert(
"builtins" => {
options.builtins = Some(parser::parse_strings(value.as_ref()));
}
"max-line-length" | "max_line_length" => match value.clone().parse::<usize>() {
"max-line-length" | "max_line_length" => match value.parse::<usize>() {
Ok(line_length) => options.line_length = Some(line_length),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
@@ -147,6 +148,11 @@ pub fn convert(
flake8_bugbear.extend_immutable_calls =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-builtins
"builtins-ignorelist" | "builtins_ignorelist" => {
flake8_builtins.builtins_ignorelist =
Some(parser::parse_strings(value.as_ref()));
}
// flake8-annotations
"suppress-none-returning" | "suppress_none_returning" => {
match parser::parse_bool(value.as_ref()) {
@@ -241,7 +247,7 @@ pub fn convert(
}
},
// mccabe
"max-complexity" | "max_complexity" => match value.clone().parse::<usize>() {
"max-complexity" | "max_complexity" => match value.parse::<usize>() {
Ok(max_complexity) => mccabe.max_complexity = Some(max_complexity),
Err(e) => {
warn_user!("Unable to parse '{key}' property: {e}");
@@ -249,7 +255,7 @@ pub fn convert(
},
// flake8-errmsg
"errmsg-max-string-length" | "errmsg_max_string_length" => {
match value.clone().parse::<usize>() {
match value.parse::<usize>() {
Ok(max_string_length) => {
flake8_errmsg.max_string_length = Some(max_string_length);
}
@@ -345,6 +351,9 @@ pub fn convert(
if flake8_bugbear != flake8_bugbear::settings::Options::default() {
options.flake8_bugbear = Some(flake8_bugbear);
}
if flake8_builtins != flake8_builtins::settings::Options::default() {
options.flake8_builtins = Some(flake8_builtins);
}
if flake8_errmsg != flake8_errmsg::settings::Options::default() {
options.flake8_errmsg = Some(flake8_errmsg);
}
@@ -368,7 +377,7 @@ pub fn convert(
}
// Extract any settings from the existing `pyproject.toml`.
if let Some(black) = black {
if let Some(black) = &external_config.black {
if let Some(line_length) = &black.line_length {
options.line_length = Some(*line_length);
}
@@ -380,6 +389,19 @@ pub fn convert(
}
}
if let Some(isort) = &external_config.isort {
if let Some(src_paths) = &isort.src_paths {
match options.src.as_mut() {
Some(src) => {
src.extend(src_paths.clone());
}
None => {
options.src = Some(src_paths.clone());
}
}
}
}
// Create the pyproject.toml.
Ok(Pyproject::new(options))
}
@@ -392,7 +414,8 @@ mod tests {
use super::super::plugin::Plugin;
use super::convert;
use crate::registry::RuleCodePrefix;
use crate::flake8_to_ruff::ExternalConfig;
use crate::registry::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options;
@@ -402,7 +425,7 @@ mod tests {
fn it_converts_empty() -> Result<()> {
let actual = convert(
&HashMap::from([("flake8".to_string(), HashMap::default())]),
None,
&ExternalConfig::default(),
None,
)?;
let expected = Pyproject::new(Options {
@@ -428,11 +451,7 @@ mod tests {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::W,
]),
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
@@ -443,6 +462,7 @@ mod tests {
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
@@ -469,7 +489,7 @@ mod tests {
"flake8".to_string(),
HashMap::from([("max-line-length".to_string(), Some("100".to_string()))]),
)]),
None,
&ExternalConfig::default(),
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -495,11 +515,7 @@ mod tests {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::W,
]),
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
@@ -510,6 +526,7 @@ mod tests {
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
@@ -536,7 +553,7 @@ mod tests {
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("100".to_string()))]),
)]),
None,
&ExternalConfig::default(),
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -562,11 +579,7 @@ mod tests {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::W,
]),
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
@@ -577,6 +590,7 @@ mod tests {
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
@@ -603,7 +617,7 @@ mod tests {
"flake8".to_string(),
HashMap::from([("max_line_length".to_string(), Some("abc".to_string()))]),
)]),
None,
&ExternalConfig::default(),
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -629,11 +643,7 @@ mod tests {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::W,
]),
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
@@ -644,6 +654,7 @@ mod tests {
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
@@ -670,7 +681,7 @@ mod tests {
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
None,
&ExternalConfig::default(),
Some(vec![]),
)?;
let expected = Pyproject::new(Options {
@@ -696,11 +707,7 @@ mod tests {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::W,
]),
select: Some(vec![RuleSelector::E, RuleSelector::F, RuleSelector::W]),
show_source: None,
src: None,
target_version: None,
@@ -711,6 +718,7 @@ mod tests {
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 {
@@ -745,7 +753,7 @@ mod tests {
Some("numpy".to_string()),
)]),
)]),
None,
&ExternalConfig::default(),
Some(vec![Plugin::Flake8Docstrings]),
)?;
let expected = Pyproject::new(Options {
@@ -772,10 +780,10 @@ mod tests {
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::D,
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::W,
RuleSelector::D,
RuleSelector::E,
RuleSelector::F,
RuleSelector::W,
]),
show_source: None,
src: None,
@@ -787,6 +795,7 @@ mod tests {
flake8_annotations: None,
flake8_bandit: None,
flake8_bugbear: None,
flake8_builtins: None,
flake8_errmsg: None,
flake8_pytest_style: None,
flake8_quotes: None,
@@ -815,7 +824,7 @@ mod tests {
"flake8".to_string(),
HashMap::from([("inline-quotes".to_string(), Some("single".to_string()))]),
)]),
None,
&ExternalConfig::default(),
None,
)?;
let expected = Pyproject::new(Options {
@@ -842,10 +851,10 @@ mod tests {
required_version: None,
respect_gitignore: None,
select: Some(vec![
RuleCodePrefix::E,
RuleCodePrefix::F,
RuleCodePrefix::Q,
RuleCodePrefix::W,
RuleSelector::E,
RuleSelector::F,
RuleSelector::Q,
RuleSelector::W,
]),
show_source: None,
src: None,
@@ -857,6 +866,7 @@ mod tests {
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 {

View File

@@ -0,0 +1,8 @@
use super::black::Black;
use super::isort::Isort;
#[derive(Default)]
pub struct ExternalConfig<'a> {
pub black: Option<&'a Black>,
pub isort: Option<&'a Isort>,
}

View File

@@ -0,0 +1,10 @@
//! Extract isort configuration settings from a pyproject.toml.
use serde::{Deserialize, Serialize};
/// The [isort configuration](https://pycqa.github.io/isort/docs/configuration/config_files.html).
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Isort {
#[serde(alias = "src-paths", alias = "src_paths")]
pub src_paths: Option<Vec<String>>,
}

View File

@@ -1,8 +1,12 @@
mod black;
mod converter;
mod external_config;
mod isort;
mod parser;
mod plugin;
mod pyproject;
pub use black::parse_black_options;
pub use converter::convert;
pub use external_config::ExternalConfig;
pub use plugin::Plugin;
pub use pyproject::parse;

View File

@@ -6,16 +6,16 @@ use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashMap;
use crate::registry::{RuleCodePrefix, PREFIX_REDIRECTS};
use crate::registry::{RuleSelector, PREFIX_REDIRECTS};
use crate::settings::types::PatternPrefixPair;
use crate::warn_user;
static COMMA_SEPARATED_LIST_RE: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
/// Parse a comma-separated list of `RuleCodePrefix` values (e.g.,
/// Parse a comma-separated list of `RuleSelector` values (e.g.,
/// "F401,E501").
pub fn parse_prefix_codes(value: &str) -> Vec<RuleCodePrefix> {
let mut codes: Vec<RuleCodePrefix> = vec![];
pub fn parse_prefix_codes(value: &str) -> Vec<RuleSelector> {
let mut codes: Vec<RuleSelector> = vec![];
for code in COMMA_SEPARATED_LIST_RE.split(value) {
let code = code.trim();
if code.is_empty() {
@@ -23,7 +23,7 @@ pub fn parse_prefix_codes(value: &str) -> Vec<RuleCodePrefix> {
}
if let Some(code) = PREFIX_REDIRECTS.get(code) {
codes.push(code.clone());
} else if let Ok(code) = RuleCodePrefix::from_str(code) {
} else if let Ok(code) = RuleSelector::from_str(code) {
codes.push(code);
} else {
warn_user!("Unsupported prefix code: {code}");
@@ -96,7 +96,7 @@ impl State {
prefix: code.clone(),
});
}
} else if let Ok(code) = RuleCodePrefix::from_str(code) {
} else if let Ok(code) = RuleSelector::from_str(code) {
for filename in &self.filenames {
codes.push(PatternPrefixPair {
pattern: filename.clone(),
@@ -190,8 +190,8 @@ pub fn parse_files_to_codes_mapping(value: &str) -> Result<Vec<PatternPrefixPair
/// Collect a list of `PatternPrefixPair` structs as a `BTreeMap`.
pub fn collect_per_file_ignores(
pairs: Vec<PatternPrefixPair>,
) -> FxHashMap<String, Vec<RuleCodePrefix>> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleCodePrefix>> = FxHashMap::default();
) -> FxHashMap<String, Vec<RuleSelector>> {
let mut per_file_ignores: FxHashMap<String, Vec<RuleSelector>> = FxHashMap::default();
for pair in pairs {
per_file_ignores
.entry(pair.pattern)
@@ -206,33 +206,33 @@ mod tests {
use anyhow::Result;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
use crate::registry::RuleCodePrefix;
use crate::registry::RuleSelector;
use crate::settings::types::PatternPrefixPair;
#[test]
fn it_parses_prefix_codes() {
let actual = parse_prefix_codes("");
let expected: Vec<RuleCodePrefix> = vec![];
let expected: Vec<RuleSelector> = vec![];
assert_eq!(actual, expected);
let actual = parse_prefix_codes(" ");
let expected: Vec<RuleCodePrefix> = vec![];
let expected: Vec<RuleSelector> = vec![];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401");
let expected = vec![RuleCodePrefix::F401];
let expected = vec![RuleSelector::F401];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,");
let expected = vec![RuleCodePrefix::F401];
let expected = vec![RuleSelector::F401];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,E501");
let expected = vec![RuleCodePrefix::F401, RuleCodePrefix::E501];
let expected = vec![RuleSelector::F401, RuleSelector::E501];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401, E501");
let expected = vec![RuleCodePrefix::F401, RuleCodePrefix::E501];
let expected = vec![RuleSelector::F401, RuleSelector::E501];
assert_eq!(actual, expected);
}
@@ -285,11 +285,11 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "locust/test/*".to_string(),
prefix: RuleCodePrefix::F841,
prefix: RuleSelector::F841,
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: RuleCodePrefix::F841,
prefix: RuleSelector::F841,
},
];
assert_eq!(actual, expected);
@@ -305,23 +305,23 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "t/*".to_string(),
prefix: RuleCodePrefix::D,
prefix: RuleSelector::D,
},
PatternPrefixPair {
pattern: "setup.py".to_string(),
prefix: RuleCodePrefix::D,
prefix: RuleSelector::D,
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: RuleCodePrefix::D,
prefix: RuleSelector::D,
},
PatternPrefixPair {
pattern: "docs/*".to_string(),
prefix: RuleCodePrefix::D,
prefix: RuleSelector::D,
},
PatternPrefixPair {
pattern: "extra/*".to_string(),
prefix: RuleCodePrefix::D,
prefix: RuleSelector::D,
},
];
assert_eq!(actual, expected);
@@ -343,47 +343,47 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "scrapy/__init__.py".to_string(),
prefix: RuleCodePrefix::E402,
prefix: RuleSelector::E402,
},
PatternPrefixPair {
pattern: "scrapy/core/downloader/handlers/http.py".to_string(),
prefix: RuleCodePrefix::F401,
prefix: RuleSelector::F401,
},
PatternPrefixPair {
pattern: "scrapy/http/__init__.py".to_string(),
prefix: RuleCodePrefix::F401,
prefix: RuleSelector::F401,
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: RuleCodePrefix::E402,
prefix: RuleSelector::E402,
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: RuleCodePrefix::F401,
prefix: RuleSelector::F401,
},
PatternPrefixPair {
pattern: "scrapy/selector/__init__.py".to_string(),
prefix: RuleCodePrefix::F401,
prefix: RuleSelector::F401,
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: RuleCodePrefix::E402,
prefix: RuleSelector::E402,
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: RuleCodePrefix::F401,
prefix: RuleSelector::F401,
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: RuleCodePrefix::F403,
prefix: RuleSelector::F403,
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: RuleCodePrefix::F405,
prefix: RuleSelector::F405,
},
PatternPrefixPair {
pattern: "tests/test_loader.py".to_string(),
prefix: RuleCodePrefix::E741,
prefix: RuleSelector::E741,
},
];
assert_eq!(actual, expected);

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::anyhow;
use crate::registry::RuleCodePrefix;
use crate::registry::RuleSelector;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum Plugin {
@@ -98,32 +98,32 @@ impl fmt::Debug for Plugin {
}
impl Plugin {
pub fn prefix(&self) -> RuleCodePrefix {
pub fn selector(&self) -> RuleSelector {
match self {
Plugin::Flake8Annotations => RuleCodePrefix::ANN,
Plugin::Flake8Bandit => RuleCodePrefix::S,
Plugin::Flake8Annotations => RuleSelector::ANN,
Plugin::Flake8Bandit => RuleSelector::S,
// TODO(charlie): Handle rename of `B` to `BLE`.
Plugin::Flake8BlindExcept => RuleCodePrefix::BLE,
Plugin::Flake8Bugbear => RuleCodePrefix::B,
Plugin::Flake8Builtins => RuleCodePrefix::A,
Plugin::Flake8Comprehensions => RuleCodePrefix::C4,
Plugin::Flake8Datetimez => RuleCodePrefix::DTZ,
Plugin::Flake8Debugger => RuleCodePrefix::T1,
Plugin::Flake8Docstrings => RuleCodePrefix::D,
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 => RuleCodePrefix::ERA,
Plugin::Flake8ErrMsg => RuleCodePrefix::EM,
Plugin::Flake8ImplicitStrConcat => RuleCodePrefix::ISC,
Plugin::Flake8Print => RuleCodePrefix::T2,
Plugin::Flake8PytestStyle => RuleCodePrefix::PT,
Plugin::Flake8Quotes => RuleCodePrefix::Q,
Plugin::Flake8Return => RuleCodePrefix::RET,
Plugin::Flake8Simplify => RuleCodePrefix::SIM,
Plugin::Flake8TidyImports => RuleCodePrefix::TID25,
Plugin::McCabe => RuleCodePrefix::C9,
Plugin::PandasVet => RuleCodePrefix::PD,
Plugin::PEP8Naming => RuleCodePrefix::N,
Plugin::Pyupgrade => RuleCodePrefix::UP,
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,
}
}
}
@@ -249,7 +249,7 @@ pub fn infer_plugins_from_options(flake8: &HashMap<String, Option<String>>) -> V
///
/// For example, if the user ignores `ANN101`, we should infer that
/// `flake8-annotations` is active.
pub fn infer_plugins_from_codes(codes: &BTreeSet<RuleCodePrefix>) -> Vec<Plugin> {
pub fn infer_plugins_from_codes(selectors: &BTreeSet<RuleSelector>) -> Vec<Plugin> {
[
Plugin::Flake8Annotations,
Plugin::Flake8Bandit,
@@ -273,11 +273,10 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<RuleCodePrefix>) -> Vec<Plugin>
]
.into_iter()
.filter(|plugin| {
for prefix in codes {
if prefix
.codes()
.iter()
.any(|code| plugin.prefix().codes().contains(code))
for selector in selectors {
if selector
.into_iter()
.any(|rule| plugin.selector().into_iter().any(|r| r == rule))
{
return true;
}
@@ -287,11 +286,11 @@ pub fn infer_plugins_from_codes(codes: &BTreeSet<RuleCodePrefix>) -> Vec<Plugin>
.collect()
}
/// Resolve the set of enabled `RuleCodePrefix` values for the given
/// Resolve the set of enabled `RuleSelector` values for the given
/// plugins.
pub fn resolve_select(plugins: &[Plugin]) -> BTreeSet<RuleCodePrefix> {
let mut select = BTreeSet::from([RuleCodePrefix::F, RuleCodePrefix::E, RuleCodePrefix::W]);
select.extend(plugins.iter().map(Plugin::prefix));
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
}

View File

@@ -0,0 +1,24 @@
use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use super::black::Black;
use super::isort::Isort;
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tools {
pub black: Option<Black>,
pub isort: Option<Isort>,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pyproject {
pub tool: Option<Tools>,
}
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)?;
Ok(pyproject)
}

View File

@@ -9,9 +9,10 @@ use crate::directives;
use crate::linter::check_path;
use crate::registry::Rule;
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, 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_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,
};
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
@@ -136,6 +137,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
flake8_annotations: Some(flake8_annotations::settings::Settings::default().into()),
flake8_bandit: Some(flake8_bandit::settings::Settings::default().into()),
flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()),
flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()),
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),

View File

@@ -16,6 +16,8 @@ use crate::directives::Directives;
use crate::doc_lines::{doc_lines_from_ast, doc_lines_from_tokens};
use crate::message::{Message, Source};
use crate::noqa::add_noqa;
#[cfg(test)]
use crate::packaging::detect_package_root;
use crate::registry::{Diagnostic, LintSource, Rule};
use crate::settings::{flags, Settings};
use crate::source_code::{Indexer, Locator, Stylist};
@@ -69,7 +71,7 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| matches!(rule_code.lint_source(), LintSource::Filesystem))
{
diagnostics.extend(check_file_path(path, settings));
diagnostics.extend(check_file_path(path, package, settings));
}
// Run the AST-based rules.
@@ -395,7 +397,8 @@ pub fn test_path(path: &Path, settings: &Settings) -> Result<Vec<Diagnostic>> {
directives::extract_directives(&tokens, directives::Flags::from_settings(settings));
let mut diagnostics = check_path(
path,
None,
path.parent()
.and_then(|parent| detect_package_root(parent, &settings.namespace_packages)),
&contents,
tokens,
&locator,

View File

@@ -63,9 +63,9 @@ pub struct Source {
impl Source {
pub fn from_diagnostic(diagnostic: &Diagnostic, locator: &Locator) -> Self {
let location = Location::new(diagnostic.location.row(), 0);
// Diagnostics can already extend one-past-the-end per Ropey's semantics. If
// they do, though, then they'll end at the start of a line. We need to
// avoid extending by yet another line past-the-end.
// Diagnostics can already extend one-past-the-end. If they do, though, then
// they'll end at the start of a line. We need to avoid extending by yet another
// line past-the-end.
let end_location = if diagnostic.end_location.column() == 0 {
diagnostic.end_location
} else {

View File

@@ -1,5 +1,15 @@
use once_cell::sync::Lazy;
use regex::Regex;
/// Returns `true` if a string is a valid Python identifier (e.g., variable
/// name).
pub fn is_identifier(s: &str) -> bool {
// Is the first character a letter or underscore?
if !s
.chars()
.next()
.map_or(false, |c| c.is_alphabetic() || c == '_')
{
return false;
}
pub static IDENTIFIER_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap());
// Are the rest of the characters letters, digits, or underscores?
s.chars().skip(1).all(|c| c.is_alphanumeric() || c == '_')
}

View File

@@ -1,7 +1,7 @@
//! Registry of [`Rule`] to [`DiagnosticKind`] mappings.
use itertools::Itertools;
use once_cell::sync::Lazy;
use ruff_macros::RuleNamespace;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
@@ -14,6 +14,7 @@ use crate::{rules, violations};
ruff_macros::define_rule_mapping!(
// pycodestyle errors
E101 => violations::MixedSpacesAndTabs,
E401 => violations::MultipleImportsOnOneLine,
E402 => violations::ModuleImportNotAtTopOfFile,
E501 => violations::LineTooLong,
@@ -250,6 +251,7 @@ ruff_macros::define_rule_mapping!(
UP028 => violations::RewriteYieldFrom,
UP029 => violations::UnnecessaryBuiltinImport,
UP030 => violations::FormatLiterals,
UP031 => violations::PrintfStringFormatting,
UP032 => violations::FString,
UP033 => violations::FunctoolsCache,
UP034 => violations::ExtraneousParentheses,
@@ -336,6 +338,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,
@@ -411,6 +414,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,
@@ -418,7 +423,43 @@ ruff_macros::define_rule_mapping!(
COM819 => violations::TrailingCommaProhibited,
// flake8-no-pep420
INP001 => violations::ImplicitNamespacePackage,
// Ruff
// flake8-executable
EXE003 => rules::flake8_executable::rules::ShebangPython,
EXE004 => rules::flake8_executable::rules::ShebangWhitespace,
EXE005 => rules::flake8_executable::rules::ShebangNewline,
// flake8-type-checking
TYP005 => rules::flake8_type_checking::rules::EmptyTypeCheckingBlock,
// tryceratops
TRY004 => rules::tryceratops::rules::PreferTypeError,
TRY201 => rules::tryceratops::rules::VerboseRaise,
TRY300 => rules::tryceratops::rules::TryConsiderElse,
// 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,
RUF003 => violations::AmbiguousUnicodeCharacterComment,
@@ -427,110 +468,155 @@ ruff_macros::define_rule_mapping!(
RUF100 => violations::UnusedNOQA,
);
#[derive(EnumIter, Debug, PartialEq, Eq)]
pub enum RuleOrigin {
#[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,
}
pub enum Prefixes {
Single(RuleCodePrefix),
Multiple(Vec<(RuleCodePrefix, &'static str)>),
pub trait RuleNamespace: Sized {
fn parse_code(code: &str) -> Option<(Self, &str)>;
fn prefixes(&self) -> &'static [&'static str];
fn name(&self) -> &'static str;
fn url(&self) -> Option<&'static str>;
}
impl Prefixes {
pub fn as_list(&self, separator: &str) -> String {
match self {
Prefixes::Single(prefix) => prefix.as_ref().to_string(),
Prefixes::Multiple(entries) => entries
.iter()
.map(|(prefix, _)| prefix.as_ref())
.join(separator),
}
}
}
/// The prefix, name and selector for an upstream linter category.
pub struct LinterCategory(pub &'static str, pub &'static str, pub RuleSelector);
include!(concat!(env!("OUT_DIR"), "/origin.rs"));
impl RuleOrigin {
pub fn prefixes(&self) -> Prefixes {
impl Linter {
pub fn categories(&self) -> Option<&'static [LinterCategory]> {
match self {
RuleOrigin::Eradicate => Prefixes::Single(RuleCodePrefix::ERA),
RuleOrigin::Flake82020 => Prefixes::Single(RuleCodePrefix::YTT),
RuleOrigin::Flake8Annotations => Prefixes::Single(RuleCodePrefix::ANN),
RuleOrigin::Flake8Bandit => Prefixes::Single(RuleCodePrefix::S),
RuleOrigin::Flake8BlindExcept => Prefixes::Single(RuleCodePrefix::BLE),
RuleOrigin::Flake8BooleanTrap => Prefixes::Single(RuleCodePrefix::FBT),
RuleOrigin::Flake8Bugbear => Prefixes::Single(RuleCodePrefix::B),
RuleOrigin::Flake8Builtins => Prefixes::Single(RuleCodePrefix::A),
RuleOrigin::Flake8Comprehensions => Prefixes::Single(RuleCodePrefix::C4),
RuleOrigin::Flake8Datetimez => Prefixes::Single(RuleCodePrefix::DTZ),
RuleOrigin::Flake8Debugger => Prefixes::Single(RuleCodePrefix::T10),
RuleOrigin::Flake8ErrMsg => Prefixes::Single(RuleCodePrefix::EM),
RuleOrigin::Flake8ImplicitStrConcat => Prefixes::Single(RuleCodePrefix::ISC),
RuleOrigin::Flake8ImportConventions => Prefixes::Single(RuleCodePrefix::ICN),
RuleOrigin::Flake8Print => Prefixes::Single(RuleCodePrefix::T20),
RuleOrigin::Flake8PytestStyle => Prefixes::Single(RuleCodePrefix::PT),
RuleOrigin::Flake8Quotes => Prefixes::Single(RuleCodePrefix::Q),
RuleOrigin::Flake8Return => Prefixes::Single(RuleCodePrefix::RET),
RuleOrigin::Flake8Simplify => Prefixes::Single(RuleCodePrefix::SIM),
RuleOrigin::Flake8TidyImports => Prefixes::Single(RuleCodePrefix::TID),
RuleOrigin::Flake8UnusedArguments => Prefixes::Single(RuleCodePrefix::ARG),
RuleOrigin::Isort => Prefixes::Single(RuleCodePrefix::I),
RuleOrigin::McCabe => Prefixes::Single(RuleCodePrefix::C90),
RuleOrigin::PEP8Naming => Prefixes::Single(RuleCodePrefix::N),
RuleOrigin::PandasVet => Prefixes::Single(RuleCodePrefix::PD),
RuleOrigin::Pycodestyle => Prefixes::Multiple(vec![
(RuleCodePrefix::E, "Error"),
(RuleCodePrefix::W, "Warning"),
Linter::Pycodestyle => Some(&[
LinterCategory("E", "Error", RuleSelector::E),
LinterCategory("W", "Warning", RuleSelector::W),
]),
RuleOrigin::Pydocstyle => Prefixes::Single(RuleCodePrefix::D),
RuleOrigin::Pyflakes => Prefixes::Single(RuleCodePrefix::F),
RuleOrigin::PygrepHooks => Prefixes::Single(RuleCodePrefix::PGH),
RuleOrigin::Pylint => Prefixes::Multiple(vec![
(RuleCodePrefix::PLC, "Convention"),
(RuleCodePrefix::PLE, "Error"),
(RuleCodePrefix::PLR, "Refactor"),
(RuleCodePrefix::PLW, "Warning"),
Linter::Pylint => Some(&[
LinterCategory("PLC", "Convention", RuleSelector::PLC),
LinterCategory("PLE", "Error", RuleSelector::PLE),
LinterCategory("PLR", "Refactor", RuleSelector::PLR),
LinterCategory("PLW", "Warning", RuleSelector::PLW),
]),
RuleOrigin::Pyupgrade => Prefixes::Single(RuleCodePrefix::UP),
RuleOrigin::Flake8Pie => Prefixes::Single(RuleCodePrefix::PIE),
RuleOrigin::Flake8Commas => Prefixes::Single(RuleCodePrefix::COM),
RuleOrigin::Flake8NoPep420 => Prefixes::Single(RuleCodePrefix::INP),
RuleOrigin::Ruff => Prefixes::Single(RuleCodePrefix::RUF),
_ => None,
}
}
}
@@ -551,12 +637,16 @@ impl Rule {
pub fn lint_source(&self) -> &'static LintSource {
match self {
Rule::UnusedNOQA => &LintSource::NoQa,
Rule::LineTooLong
| Rule::NoNewLineAtEndOfFile
| Rule::DocLineTooLong
| Rule::PEP3120UnnecessaryCodingComment
Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
| Rule::BlanketNOQA => &LintSource::Lines,
| Rule::DocLineTooLong
| Rule::LineTooLong
| Rule::MixedSpacesAndTabs
| Rule::NoNewLineAtEndOfFile
| Rule::PEP3120UnnecessaryCodingComment
| Rule::ShebangNewline
| Rule::ShebangPython
| Rule::ShebangWhitespace => &LintSource::Lines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
@@ -677,7 +767,7 @@ pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, Rule>> = Lazy::new(|| {
mod tests {
use strum::IntoEnumIterator;
use crate::registry::Rule;
use super::{Linter, Rule, RuleNamespace};
#[test]
fn check_code_serialization() {
@@ -688,4 +778,12 @@ mod tests {
);
}
}
#[test]
fn test_linter_prefixes() {
for rule in Rule::iter() {
Linter::parse_code(rule.code())
.unwrap_or_else(|| panic!("couldn't parse {:?}", rule.code()));
}
}
}

View File

@@ -1,4 +1,4 @@
//! Rules from [eradicate](https://pypi.org/project/eradicate/2.1.0/).
//! Rules from [eradicate](https://pypi.org/project/eradicate/).
pub(crate) mod detection;
pub(crate) mod rules;

View File

@@ -32,7 +32,7 @@ pub fn commented_out_code(
let line = locator.slice_source_code_range(&Range::new(location, end_location));
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(&line) && comment_contains_code(&line, &settings.task_tags[..]) {
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(violations::CommentedOutCode, Range::new(start, end));
if matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::CommentedOutCode)

View File

@@ -1,4 +1,4 @@
//! Rules from [flake8-2020](https://pypi.org/project/flake8-2020/1.7.0/).
//! Rules from [flake8-2020](https://pypi.org/project/flake8-2020/).
pub(crate) mod rules;
#[cfg(test)]

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