Compare commits

...

73 Commits

Author SHA1 Message Date
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
Charlie Marsh
9e704a7c63 Only fix true-false returns for return-bool-condition-directly (#2037)
Closes #2035.
2023-01-20 13:17:19 -05:00
Zeddicus414
c9da98e0b7 Fix D404 NoThisPrefix not working with whitespace. (#2036)
D404 should trigger for """ This is a docstring."""

Add a few tests to ensure the fix worked.
2023-01-20 13:01:31 -05:00
Charlie Marsh
5377d24507 Bump version to 0.0.228 2023-01-20 09:58:56 -05:00
Florian Best
db8e4500ee fix(pydocstyle): Avoid trimming docstring if starts with leading quote (#2027)
Fixes: #2017

looks like the other way round is also possible to break:

```""" "foo"""`
2023-01-20 09:57:48 -05:00
Aarni Koskela
bd2de5624e Move readme dev details to CONTRIBUTING.md and fix contradictions (#2030)
Following up on #2018/#2019 discussion, this moves the readme's development-related bits to `CONTRIBUTING.md` to avoid duplication, and fixes up the commands accordingly 😄
2023-01-20 09:23:28 -05:00
Aarni Koskela
3a81f893cc Bump terminfo to remove a whole bunch of unnecessary dependencies (#2022)
See 6281c6b8f7

```
$ cargo update -p terminfo
    Updating crates.io index
    Removing cfg-if v0.1.10
    Removing dirs v2.0.2
    Removing getrandom v0.1.16
    Removing phf v0.8.0
    Updating phf_codegen v0.8.0 -> v0.11.1
    Updating phf_generator v0.8.0 -> v0.11.1
    Removing phf_shared v0.8.0
    Removing rand v0.7.3
    Removing rand_chacha v0.2.2
    Removing rand_core v0.5.1
    Removing rand_hc v0.2.0
    Removing rand_pcg v0.2.1
    Updating terminfo v0.7.3 -> v0.7.5
    Removing wasi v0.9.0+wasi-snapshot-preview1
```
2023-01-20 09:09:02 -05:00
Charlie Marsh
fd6dc2a343 Use platform-appropriate newline character for LibCST embedding (#2028)
Closes #2026.
2023-01-20 09:08:04 -05:00
Martin Fischer
8693236f9e Make CI test add_*.py scripts 2023-01-20 08:09:54 -05:00
Martin Fischer
44e2b6208a fix: Update add_rule.py to create new files for rules 2023-01-20 08:09:54 -05:00
Martin Fischer
16c81f75c2 fix: Update add_rule.py to account for 16e79c8d 2023-01-20 08:09:54 -05:00
Martin Fischer
e1d6ac3265 fix: Update add_plugin.py to account for 9dc66b5a 2023-01-20 08:09:54 -05:00
Martin Fischer
3aec1100f5 fix: Update add_plugin.py to account for b78b6f27 2023-01-20 08:09:54 -05:00
Martin Fischer
c00df647e1 fix: Update add_rule.py to account for 81996f1bc 2023-01-20 08:09:54 -05:00
Martin Fischer
f012877be1 Add scripts/pyproject.toml to use ruff for ruff :) 2023-01-20 08:09:54 -05:00
Martin Fischer
ff6defc988 refactor: Introduce get_indent helper for scripts 2023-01-20 08:09:54 -05:00
Martin Fischer
67ca50e9f2 refactor: Reduce code duplication in scripts/ 2023-01-20 08:09:54 -05:00
Martin Fischer
6cc160bc2b Mark scripts/add_*.py as executable 2023-01-20 08:09:54 -05:00
Ville Skyttä
4bdf506d80 Grammar fixes (#2014) 2023-01-20 07:44:23 -05:00
Charlie Marsh
4af2353ef9 Avoid trimming docstring if ends in trailing quote (#2025)
Closes #2017.
2023-01-20 07:41:58 -05:00
Ville Skyttä
6072edf5bf Note .astimezone() in call-datetime-strptime-without-zone message (#2015) 2023-01-20 07:40:34 -05:00
Martin Fischer
4061eeeb32 Update CI to use MSRV for cargo test and build
As per Cargo.toml our minimal supported Rust version is 1.65.0, so we
should be using that version in our CI for cargo test and cargo build.

This was apparently accidentally changed in
79ca66ace5.
2023-01-20 07:39:40 -05:00
Aarni Koskela
bea6deb0c3 Port pydocstyle code 401 (ImperativeMood) (#1999)
This adds support for pydocstyle code D401 using the `imperative` crate.
2023-01-20 07:18:27 -05:00
Colin Delahunty
81db00a3c4 Pyupgrade: Extraneous parenthesis (#1926) 2023-01-20 00:04:07 -05:00
Charlie Marsh
cf56955ba6 Bump version to 0.0.227 2023-01-19 23:24:52 -05:00
Charlie Marsh
8a8939afd8 Avoid checking row types for single-name @parametrize decorators (#2013)
Closes #2008.
2023-01-19 22:13:17 -05:00
Martin Fischer
6acf2accc6 Improve --explain output
Previous output for `ruff --explain E711`:

    E711 (pycodestyle): Comparison to `None` should be `cond is None`

New output:

    none-comparison

    Code: E711 (pycodestyle)

    Autofix is always available.

    Message formats:

    * Comparison to `None` should be `cond is None`
    * Comparison to `None` should be `cond is not None`
2023-01-19 22:08:00 -05:00
Charlie Marsh
ec0c7647ab Avoid SIM401 in elif blocks (#2012)
For now, we're just gonna avoid flagging this for `elif` blocks, following the same reasoning as for ternaries. We can handle all of these cases, but we'll knock out the TODOs as a pair, and this avoids broken code.

Closes #2007.
2023-01-19 21:57:18 -05:00
Charlie Marsh
045229630e Upgrade RustPython (#2011)
This lets us revert the "manual" fix introduced in #1944.
2023-01-19 21:49:12 -05:00
Martin Fischer
c600991905 Change AsRef<str> impl for Rule to kebab-case
As we surface rule names more to users we want
them to be easier to type than PascalCase.

Prior art:

Pylint and ESLint also use kebab-case for their rule names.
Clippy uses snake_case but only for syntactical reasons
(so that the argument to e.g. #![allow(clippy::some_lint)]
can be parsed as a path[1]).

[1]: https://doc.rust-lang.org/reference/paths.html
2023-01-19 21:37:11 -05:00
Charlie Marsh
f6a93a4c3d Enable autofix for FitsOnOneLine (D200) (#2006)
Closes #1965.
2023-01-19 19:24:50 -05:00
Aarni Koskela
de54ff114e Add RUF005 "unpack instead of concatenating" check (#1957)
This PR adds a new check that turns expressions such as `[1, 2, 3] + foo` into `[1, 2, 3, *foo]`, since the latter is easier to read and faster:

```
~ $ python3.11 -m timeit -s 'b = [6, 5, 4]' '[1, 2, 3] + b'
5000000 loops, best of 5: 81.4 nsec per loop
~ $ python3.11 -m timeit -s 'b = [6, 5, 4]' '[1, 2, 3, *b]'
5000000 loops, best of 5: 66.2 nsec per loop
```

However there's a couple of gotchas:

* This felt like a `simplify` rule, so I borrowed an unused `SIM` code even if the upstream `flake8-simplify` doesn't do this transform. If it should be assigned some other code, let me know 😄 
* **More importantly** this transform could be unsafe if the other operand of the `+` operation has overridden `__add__` to do something else. What's the `ruff` policy around potentially unsafe operations? (I think some of the suggestions other ported rules give could be semantically different from the original code, but I'm not sure.)
* I'm not a very established Rustacean, so there's no doubt my code isn't quite idiomatic. (For instance, is there a neater way to write that four-way `match` statement?)

Thanks for `ruff`, by the way! :)
2023-01-19 17:38:17 -05:00
Charlie Marsh
64b398c72b Tweak some instructions in CONTRIBUTING.md 2023-01-19 17:17:39 -05:00
Aarni Koskela
c99bd3fa60 Split up pydocstyle rules (#2003)
As per @not-mAs per @not-my-profile's [comment](https://github.com/charliermarsh/ruff/pull/1999#discussion_r1081579337):

> we actually want to break up such rules.rs files into smaller files

this breaks up `pydocstyle/rules.rs` into a directory.y-profile's [comment](https://github.com/charliermarsh/ruff/pull/1999#discussion_r1081579337):

> we actually want to break up such rules.rs files into smaller files

this breaks up `pydocstyle/rules.rs` into a directory.
2023-01-19 13:17:25 -05:00
Martin Fischer
8ac930f886 Fix that --explain panics
This commit fixes a bug accidentally introduced in
6cf770a692,
which resulted every `ruff --explain <code>` invocation to fail with:

    thread 'main' panicked at 'Mismatch between definition and access of `explain`.
    Could not downcast to ruff::registry::Rule, need to downcast to &ruff::registry::Rule',
    ruff_cli/src/cli.rs:184:18

We also add an integration test for --explain to prevent such bugs from
going by unnoticed in the future.
2023-01-19 12:58:44 -05:00
Charlie Marsh
ad80fdc2cd Avoid SIM201 and SIM202 errors in __ne__ et al (#2001)
Closes #1986.
2023-01-19 11:27:27 -05:00
Aarni Koskela
a0ea8fe22f Apply #[derive(Default)] fixes suggested by Clippy (#2000)
These were bugging me every time I ran `clippy` 😁
2023-01-19 11:04:43 -05:00
Martin Fischer
3c3da8a88c derive-msg-formats 5/5: Remove placeholder implementations
# This commit has been generated via the following Python script:
# (followed by `cargo +nightly fmt` and `cargo dev generate-all`)
# For the reasoning see the previous commit(s).

import re
import sys

for path in (
    'src/violations.rs',
    'src/rules/flake8_tidy_imports/banned_api.rs',
    'src/rules/flake8_tidy_imports/relative_imports.rs',
):
    with open(path) as f:
        text = ''

        while line := next(f, None):

            if line.strip() != 'fn message(&self) -> String {':
                text += line
                continue

            text += '    #[derive_message_formats]\n' + line

            body = next(f)
            while (line := next(f)) != '    }\n':
                body += line

            # body = re.sub(r'(?<!code\| |\.push\()format!', 'format!', body)
            body = re.sub(
                r'("[^"]+")\s*\.to_string\(\)', r'format!(\1)', body, re.DOTALL
            )
            body = re.sub(
                r'(r#".+?"#)\s*\.to_string\(\)', r'format!(\1)', body, re.DOTALL
            )

            text += body + '    }\n'

            while (line := next(f)).strip() != 'fn placeholder() -> Self {':
                text += line
            while (line := next(f)) != '    }\n':
                pass

    with open(path, 'w') as f:
        f.write(text)
2023-01-19 11:03:32 -05:00
Martin Fischer
16e79c8db6 derive-msg-formats 4/5: Implement #[derive_message_formats]
The idea is nice and simple we replace:

    fn placeholder() -> Self;

with

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

So e.g. if a Violation implementation defines:

    fn message(&self) -> String {
        format!("Local variable `{name}` is assigned to but never used")
    }

it would also have to define:

   fn message_formats() -> &'static [&'static str] {
       &["Local variable `{name}` is assigned to but never used"]
   }

Since we however obviously do not want to duplicate all of our format
strings we simply introduce a new procedural macro attribute
#[derive_message_formats] that can be added to the message method
declaration in order to automatically derive the message_formats
implementation.

This commit implements the macro. The following and final commit
updates violations.rs to use the macro. (The changes have been separated
because the next commit is autogenerated via a Python script.)
2023-01-19 11:03:32 -05:00
Martin Fischer
8f6d8e215c derive-msg-formats 3/5: Introduce Violation::AUTOFIX associated constant
ruff_dev::generate_rules_table previously documented which rules are
autofixable via DiagnosticKind::fixable ... since the DiagnosticKind was
obtained via Rule::kind (and Violation::placeholder) which we both want
to get rid of we have to obtain the autofixability via another way.

This commit implements such another way by adding an AUTOFIX
associated constant to the Violation trait. The constant is of the type
Option<AutoFixkind>, AutofixKind is a new struct containing an
Availability enum { Sometimes, Always}, letting us additionally document
that some autofixes are only available sometimes (which previously
wasn't documented). We intentionally introduce this information in a
struct so that we can easily introduce further autofix metadata in the
future such as autofix applicability[1].

[1]: https://doc.rust-lang.org/stable/nightly-rustc/rustc_errors/enum.Applicability.html
2023-01-19 11:03:32 -05:00
Martin Fischer
8993baab01 derive-msg-formats 2/5: Remove DiagnosticKind::summary
While ruff displays the string returned by Violation::message in its
output for detected violations the messages displayed in the README
and in the `--explain <code>` output previously used the
DiagnosticKind::summary() function which for some verbose messages
provided shorter descriptions.

This commit removes DiagnosticKind::summary, and moves the more
extensive documentation into doc comments ... these are not displayed
yet to the user but doing that is very much planned.
2023-01-19 11:03:32 -05:00
Martin Fischer
2568627c4c derive-msg-formats 1/5: Remove unnecessary usages of Rule::kind
This commit series removes the following associated
function from the Violation trait:

    fn placeholder() -> Self;

ruff previously used this placeholder approach for the messages it
listed in the README and displayed when invoked with --explain <code>.

This approach is suboptimal for three reasons:

1. The placeholder implementations are completely boring code since they
   just initialize the struct with some dummy values.

2. Displaying concrete error messages with arbitrary interpolated values
   can be confusing for the user since they might not recognize that the
   values are interpolated.

3. Some violations have varying format strings depending on the
   violation which could not be documented with the previous approach
   (while we could have changed the signature to return Vec<Self> this
   would still very much suffer from the previous two points).

We therefore drop Violation::placeholder in favor of a new macro-based
approach, explained in commit 4/5.

Violation::placeholder is only invoked via Rule::kind, so we firstly
have to get rid of all Rule::kind invocations ... this commit starts
removing the trivial cases.
2023-01-19 11:03:32 -05:00
Martin Fischer
9603a024b3 refactor: Move a bunch of pandas-vet logic to rules::pandas_vet 2023-01-19 11:03:32 -05:00
Charlie Marsh
a122d95ef5 Preserve unmatched comparators in SIM109 (#1998)
Closes #1993.
2023-01-19 10:23:20 -05:00
Damien Allen
6ddfe50ac4 Added pylint formatter (#1995)
Fixes: #1953

@charliermarsh thank you for the tips in the issue.

I'm not very familiar with Rust, so please excuse if my string formatting syntax is messy.

In terms of testing, I compared output of `flake8 --format=pylint ` and `cargo run --format=pylint` on the same code and the output syntax seems to check out.
2023-01-19 08:01:27 -05:00
Martin Fischer
26901a78c9 Make define_rule_mapping! set rule code as doc comment of variants
Since the UI still relies on the rule codes this improves the developer
experience by letting developers view the code of a Rule enum variant by
hovering over it.
2023-01-19 07:37:16 -05:00
Martin Fischer
6649225167 rule 8/8: Automatically rewrite RuleCode to Rule
# This commit was automatically generated by running the following
# script (followed by `cargo +nightly fmt`):

import glob
import re
from typing import NamedTuple

class Rule(NamedTuple):
    code: str
    name: str
    path: str

def rules() -> list[Rule]:
    """Returns all the rules defined in `src/registry.rs`."""
    file = open('src/registry.rs')

    rules = []

    while next(file) != 'ruff_macros::define_rule_mapping!(\n':
        continue

    while (line := next(file)) != ');\n':
        line = line.strip().rstrip(',')
        if line.startswith('//'):
            continue
        code, path = line.split(' => ')
        name = path.rsplit('::')[-1]
        rules.append(Rule(code, name, path))

    return rules

code2name = {r.code: r.name for r in rules()}

for pattern in ('src/**/*.rs', 'ruff_cli/**/*.rs', 'ruff_dev/**/*.rs', 'scripts/add_*.py'):
    for name in glob.glob(pattern, recursive=True):
        with open(name) as f:
            text = f.read()

        text = re.sub('Rule(?:Code)?::([A-Z]\w+)', lambda m: 'Rule::' + code2name[m.group(1)], text)
        text = re.sub(r'(?<!"<FilePattern>:<)RuleCode\b', 'Rule', text)
        text = re.sub('(use crate::registry::{.*, Rule), Rule(.*)', r'\1\2', text) # fix duplicate import

        with open(name, 'w') as f:
            f.write(text)
2023-01-18 23:51:48 -05:00
Martin Fischer
9e3083aa2c rule 7/8: Change Rule enum definition 2023-01-18 23:51:48 -05:00
Martin Fischer
6d11ff3822 rule 6/8: Remove Serialize & Deserialize impls for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
6cf770a692 rule 5/8: Remove FromStr impl for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
3534e370e1 rule 4/8: Remove Display impl for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
dbcab5128c rule 3/8: Remove AsRef<str> impl for Rule 2023-01-18 23:51:48 -05:00
Martin Fischer
3810250bb6 rule 2/8: Rename DiagnosticKind::code to rule 2023-01-18 23:51:48 -05:00
Martin Fischer
3c1c1e1dd3 rule 1/8: Rename RuleCode to Rule (backwards-compatible for now)
This commit series refactors ruff to decouple "rules" from "rule codes",
in order to:

1. Make our code more readable by changing e.g.
   RuleCode::UP004 to Rule::UselessObjectInheritance.

2. Let us cleanly map multiple codes to one rule, for example:

   [UP004] in pyupgrade, [R0205] in pylint and [PIE792] in flake8-pie
   all refer to the rule UselessObjectInheritance but ruff currently
   only associates that rule with the UP004 code (since the
   implementation was initially modeled after pyupgrade).

3. Let us cleanly map one code to multiple rules, for example:

   [C0103] from pylint encompasses N801, N802 and N803 from pep8-naming.

The latter two steps are not yet implemented by this commit series
but this refactoring enables us to introduce such a mapping.  Such a
mapping would also let us expand flake8_to_ruff to support e.g. pylint.

After the next commit which just does some renaming the following four
commits remove all trait derivations from the Rule (previously RuleCode)
enum that depend on the variant names to guarantee that they are not
used anywhere anymore so that we can rename all of these variants in the
eigth and final commit without breaking anything.

While the plan very much is to also surface these human-friendly names
more in the user interface this is not yet done in this commit series,
which does not change anything about the UI: it's purely a refactor.

[UP004]: pyupgrade doesn't actually assign codes to its messages.
[R0205]: https://pylint.pycqa.org/en/latest/user_guide/messages/refactor/useless-object-inheritance.html
[PIE792]: https://github.com/sbdchd/flake8-pie#pie792-no-inherit-object
[C0103]: https://pylint.pycqa.org/en/latest/user_guide/messages/convention/invalid-name.html
2023-01-18 23:51:48 -05:00
Martin Fischer
5b7bd93b91 refactor: Use Self:: in match arms 2023-01-18 23:51:48 -05:00
Martin Fischer
9e096b4a4c refactor: Make define_rule_mapping! generate RuleCodePrefix directly 2023-01-18 23:51:48 -05:00
296 changed files with 11173 additions and 6968 deletions

View File

@@ -70,7 +70,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
toolchain: 1.65.0
override: true
- uses: Swatinem/rust-cache@v1
- run: cargo install cargo-insta
@@ -84,6 +84,21 @@ jobs:
# Setting RUSTDOCFLAGS because `cargo doc --check` isn't yet implemented (https://github.com/rust-lang/cargo/issues/10025).
- run: RUSTDOCFLAGS="-D warnings" cargo doc --all --no-deps
scripts:
name: "test scripts"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions-rs/toolchain@v1
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v1
- 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: cargo check
# TODO(charlie): Re-enable the `wasm-pack` tests.
# See: https://github.com/charliermarsh/ruff/issues/1425
# wasm-pack-test:
@@ -122,7 +137,7 @@ jobs:
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly-2022-11-01
toolchain: 1.65.0
override: true
- uses: Swatinem/rust-cache@v1
- uses: actions/setup-python@v4

View File

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

View File

@@ -37,13 +37,16 @@ After cloning the repository, run Ruff locally with:
cargo run resources/test/fixtures --no-cache
```
Prior to opening a pull request, ensure that your code has been auto-formatted, and that it passes
both the lint and test validation checks:
Prior to opening a pull request, ensure that your code has been auto-formatted,
and that it passes both the lint and test validation checks.
For rustfmt and Clippy, we use [nightly Rust][nightly], as it is stricter than stable Rust.
(However, tests and builds use stable Rust.)
```shell
cargo +nightly fmt --all # Auto-formatting...
cargo +nightly clippy --all # Linting...
cargo +nightly test --all # Testing...
cargo +nightly clippy --fix --workspace --all-targets --all-features -- -W clippy::pedantic # Linting...
cargo test --all # Testing...
```
These checks will run on GitHub Actions when you open your Pull Request, but running them locally
@@ -54,18 +57,22 @@ prior to merging.
### Example: Adding a new lint rule
There are four phases to adding a new lint rule:
At a high level, the steps involved in adding a new lint rule are as follows:
1. Define the violation struct in `src/violations.rs` (e.g., `ModuleImportNotAtTopOfFile`).
2. Map the violation struct to a rule code in `src/registry.rs` (e.g., `E402`).
3. Define the logic for triggering the violation in `src/checkers/ast.rs` (for AST-based checks),
`src/checkers/tokens.rs` (for token-based checks), `src/checkers/lines.rs` (for text-based checks) or `src/checkers/filesystem.rs` (for filesystem-based checks).
4. Add a test fixture.
5. Update the generated files (documentation and generated code).
1. Create a file for your rule (e.g., `src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
2. In that file, define a violation struct. You can grep for `define_violation!` to see examples.
3. Map the violation struct to a rule code in `src/registry.rs` (e.g., `E402`).
4. Define the logic for triggering the violation in `src/checkers/ast.rs` (for AST-based checks),
`src/checkers/tokens.rs` (for token-based checks), `src/checkers/lines.rs` (for text-based
checks), or `src/checkers/filesystem.rs` (for filesystem-based checks).
5. Add a test fixture.
6. Update the generated files (documentation and generated code).
To define the violation, open up `src/violations.rs`, and define a new struct using the
`define_violation!` macro. There are plenty of examples in that file, so feel free to pattern-match
against the existing structs.
To define the violation, start by creating a dedicated file for your rule under the appropriate
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.)
To trigger the violation, you'll likely want to augment the logic in `src/checkers/ast.rs`, which
defines the Python AST visitor, responsible for iterating over the abstract syntax tree and
@@ -74,7 +81,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.
@@ -83,7 +90,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.
@@ -123,3 +130,5 @@ them to [PyPI](https://pypi.org/project/ruff/).
Ruff follows the [semver](https://semver.org/) versioning standard. However, as pre-1.0 software,
even patch releases may contain [non-backwards-compatible changes](https://semver.org/#spec-item-4).
[nightly]: https://rust-lang.github.io/rustup/concepts/channels.html#working-with-nightly-rust

339
Cargo.lock generated
View File

@@ -14,7 +14,7 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
dependencies = [
"getrandom 0.2.8",
"getrandom",
"once_cell",
"version_check",
]
@@ -197,12 +197,6 @@ version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d"
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -413,7 +407,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"wasm-bindgen",
]
@@ -439,7 +433,7 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -484,7 +478,7 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-utils",
]
@@ -494,7 +488,7 @@ version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-epoch",
"crossbeam-utils",
]
@@ -506,7 +500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
dependencies = [
"autocfg",
"cfg-if 1.0.0",
"cfg-if",
"crossbeam-utils",
"memoffset",
"scopeguard",
@@ -518,7 +512,7 @@ version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -592,16 +586,6 @@ dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3"
dependencies = [
"cfg-if 0.1.10",
"dirs-sys",
]
[[package]]
name = "dirs"
version = "4.0.0"
@@ -617,7 +601,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"dirs-sys-next",
]
@@ -721,7 +705,7 @@ version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"windows-sys",
@@ -735,7 +719,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.226"
version = "0.0.229"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -786,24 +770,13 @@ dependencies = [
"libc",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
[[package]]
name = "getrandom"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
@@ -926,6 +899,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "imperative"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f92123bf2fe0d9f1b5df1964727b970ca3b2d0203d47cf97fb1f36d856b6398"
dependencies = [
"phf 0.11.1",
"rust-stemmers",
]
[[package]]
name = "indexmap"
version = "1.9.2"
@@ -976,7 +959,7 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1186,7 +1169,7 @@ version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
]
[[package]]
@@ -1265,7 +1248,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"libc",
"static_assertions",
]
@@ -1419,7 +1402,7 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
@@ -1493,15 +1476,6 @@ dependencies = [
"indexmap",
]
[[package]]
name = "phf"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12"
dependencies = [
"phf_shared 0.8.0",
]
[[package]]
name = "phf"
version = "0.10.1"
@@ -1512,13 +1486,12 @@ dependencies = [
]
[[package]]
name = "phf_codegen"
version = "0.8.0"
name = "phf"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815"
checksum = "928c6535de93548188ef63bb7c4036bd415cd8f36ad25af44b9789b2ee72a48c"
dependencies = [
"phf_generator 0.8.0",
"phf_shared 0.8.0",
"phf_shared 0.11.1",
]
[[package]]
@@ -1532,13 +1505,13 @@ dependencies = [
]
[[package]]
name = "phf_generator"
version = "0.8.0"
name = "phf_codegen"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526"
checksum = "a56ac890c5e3ca598bbdeaa99964edb5b0258a583a9eb6ef4e89fc85d9224770"
dependencies = [
"phf_shared 0.8.0",
"rand 0.7.3",
"phf_generator 0.11.1",
"phf_shared 0.11.1",
]
[[package]]
@@ -1548,16 +1521,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
dependencies = [
"phf_shared 0.10.0",
"rand 0.8.5",
"rand",
]
[[package]]
name = "phf_shared"
version = "0.8.0"
name = "phf_generator"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7"
checksum = "b1181c94580fa345f50f19d738aaa39c0ed30a600d95cb2d3e23f94266f14fbf"
dependencies = [
"siphasher",
"phf_shared 0.11.1",
"rand",
]
[[package]]
@@ -1569,6 +1543,15 @@ dependencies = [
"siphasher",
]
[[package]]
name = "phf_shared"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1fb5f6f826b772a8d4c0394209441e7d37cbbb967ae9c7e0e8134365c9ee676"
dependencies = [
"siphasher",
]
[[package]]
name = "pico-args"
version = "0.4.2"
@@ -1724,20 +1707,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09"
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
"rand_pcg",
]
[[package]]
name = "rand"
version = "0.8.5"
@@ -1745,18 +1714,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
"rand_chacha",
"rand_core",
]
[[package]]
@@ -1766,16 +1725,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core 0.6.4",
]
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
"rand_core",
]
[[package]]
@@ -1784,25 +1734,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.8",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429"
dependencies = [
"rand_core 0.5.1",
"getrandom",
]
[[package]]
@@ -1842,7 +1774,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.8",
"getrandom",
"redox_syscall",
"thiserror",
]
@@ -1906,23 +1838,24 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.226"
version = "0.0.229"
dependencies = [
"anyhow",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"chrono",
"clap 4.0.32",
"colored",
"console_error_panic_hook",
"console_log",
"criterion",
"dirs 4.0.0",
"dirs",
"fern",
"getrandom 0.2.8",
"getrandom",
"glob",
"globset",
"ignore",
"imperative",
"insta",
"itertools",
"js-sys",
@@ -1938,9 +1871,9 @@ dependencies = [
"ropey",
"ruff_macros",
"rustc-hash",
"rustpython-ast",
"rustpython-common",
"rustpython-parser",
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
"rustpython-parser 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
"schemars",
"semver",
"serde",
@@ -1951,6 +1884,7 @@ dependencies = [
"strum_macros",
"test-case",
"textwrap",
"thiserror",
"titlecase",
"toml_edit",
"wasm-bindgen",
@@ -1959,7 +1893,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.226"
version = "0.0.229"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1996,7 +1930,7 @@ dependencies = [
[[package]]
name = "ruff_dev"
version = "0.0.226"
version = "0.0.229"
dependencies = [
"anyhow",
"clap 4.0.32",
@@ -2005,9 +1939,9 @@ dependencies = [
"once_cell",
"ruff",
"ruff_cli",
"rustpython-ast",
"rustpython-common",
"rustpython-parser",
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
"rustpython-parser 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
"schemars",
"serde_json",
"strum",
@@ -2017,7 +1951,7 @@ dependencies = [
[[package]]
name = "ruff_macros"
version = "0.0.226"
version = "0.0.229"
dependencies = [
"once_cell",
"proc-macro2",
@@ -2026,6 +1960,16 @@ dependencies = [
"textwrap",
]
[[package]]
name = "rust-stemmers"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54"
dependencies = [
"serde",
"serde_derive",
]
[[package]]
name = "rustc-hash"
version = "1.1.0"
@@ -2061,21 +2005,31 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
dependencies = [
"num-bigint",
"rustpython-common",
"rustpython-compiler-core",
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
]
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
dependencies = [
"num-bigint",
"rustpython-common 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
]
[[package]]
name = "rustpython-common"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
dependencies = [
"ascii",
"bitflags",
"cfg-if 1.0.0",
"cfg-if",
"hexf-parse",
"itertools",
"lexical-parse-float",
@@ -2086,7 +2040,32 @@ dependencies = [
"num-traits",
"once_cell",
"radium",
"rand 0.8.5",
"rand",
"siphasher",
"unic-ucd-category",
"volatile",
"widestring",
]
[[package]]
name = "rustpython-common"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
dependencies = [
"ascii",
"bitflags",
"cfg-if",
"hexf-parse",
"itertools",
"lexical-parse-float",
"libc",
"lock_api",
"num-bigint",
"num-complex",
"num-traits",
"once_cell",
"radium",
"rand",
"siphasher",
"unic-ucd-category",
"volatile",
@@ -2096,7 +2075,24 @@ dependencies = [
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
dependencies = [
"bincode",
"bitflags",
"bstr 0.2.17",
"itertools",
"lz4_flex",
"num-bigint",
"num-complex",
"num_enum",
"serde",
"thiserror",
]
[[package]]
name = "rustpython-compiler-core"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
dependencies = [
"bincode",
"bitflags",
@@ -2113,7 +2109,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=acbc517b55406c76da83d7b2711941d8d3f65b87#acbc517b55406c76da83d7b2711941d8d3f65b87"
source = "git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c#62aa942bf506ea3d41ed0503b947b84141fdaa3c"
dependencies = [
"ahash",
"anyhow",
@@ -2126,8 +2122,33 @@ dependencies = [
"phf 0.10.1",
"phf_codegen 0.10.0",
"rustc-hash",
"rustpython-ast",
"rustpython-compiler-core",
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=62aa942bf506ea3d41ed0503b947b84141fdaa3c)",
"thiserror",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",
"unicode_names2",
]
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa#ff90fe52eea578c8ebdd9d95e078cc041a5959fa"
dependencies = [
"ahash",
"anyhow",
"itertools",
"lalrpop",
"lalrpop-util",
"log",
"num-bigint",
"num-traits",
"phf 0.10.1",
"phf_codegen 0.10.0",
"rustc-hash",
"rustpython-ast 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
"rustpython-compiler-core 0.2.0 (git+https://github.com/RustPython/RustPython.git?rev=ff90fe52eea578c8ebdd9d95e078cc041a5959fa)",
"thiserror",
"tiny-keccak",
"unic-emoji-char",
@@ -2273,7 +2294,7 @@ version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd1c7ddea665294d484c39fd0c0d2b7e35bbfe10035c5fe1854741a57f6880e1"
dependencies = [
"dirs 4.0.0",
"dirs",
]
[[package]]
@@ -2376,7 +2397,7 @@ version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"fastrand",
"libc",
"redox_syscall",
@@ -2416,15 +2437,15 @@ dependencies = [
[[package]]
name = "terminfo"
version = "0.7.3"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76971977e6121664ec1b960d1313aacfa75642adc93b9d4d53b247bd4cb1747e"
checksum = "da31aef70da0f6352dbcb462683eb4dd2bfad01cf3fc96cf204547b9a839a585"
dependencies = [
"dirs 2.0.2",
"dirs",
"fnv",
"nom",
"phf 0.8.0",
"phf_codegen 0.8.0",
"phf 0.11.1",
"phf_codegen 0.11.1",
]
[[package]]
@@ -2448,7 +2469,7 @@ version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45b7bf6e19353ddd832745c8fcf77a17a93171df7151187f26623f2b75b5b26"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"proc-macro-error",
"proc-macro2",
"quote",
@@ -2588,7 +2609,7 @@ version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"static_assertions",
]
@@ -2792,12 +2813,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@@ -2816,7 +2831,7 @@ version = "0.2.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"wasm-bindgen-macro",
]
@@ -2841,7 +2856,7 @@ version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
dependencies = [
"cfg-if 1.0.0",
"cfg-if",
"js-sys",
"wasm-bindgen",
"web-sys",

View File

@@ -8,7 +8,7 @@ default-members = [".", "ruff_cli"]
[package]
name = "ruff"
version = "0.0.226"
version = "0.0.229"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -35,6 +35,7 @@ fern = { version = "0.6.1" }
glob = { version = "0.3.0" }
globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
imperative = { version = "1.0.3" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
log = { version = "0.4.17" }
@@ -46,11 +47,11 @@ 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.226", path = "ruff_macros" }
ruff_macros = { version = "0.0.229", path = "ruff_macros" }
rustc-hash = { version = "1.1.0" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-ast = { features = ["unparse"], git = "https://github.com/RustPython/RustPython.git", rev = "62aa942bf506ea3d41ed0503b947b84141fdaa3c" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "62aa942bf506ea3d41ed0503b947b84141fdaa3c" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "62aa942bf506ea3d41ed0503b947b84141fdaa3c" }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }
@@ -59,6 +60,7 @@ smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
textwrap = { version = "0.16.0" }
thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml_edit = { version = "0.17.1", features = ["easy"] }

838
README.md

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,7 @@ 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);
generate_linter_name_and_url(&out_dir);
}
const RULES_SUBMODULE_DOC_PREFIX: &str = "//! Rules from ";
@@ -15,13 +15,13 @@ const RULES_SUBMODULE_DOC_PREFIX: &str = "//! Rules from ";
/// //! 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
/// generates the `name` and `url` functions for the `Linter` enum
/// accordingly, so that they can be used by `ruff_dev::generate_rules_table`.
fn generate_origin_name_and_url(out_dir: &Path) {
fn generate_linter_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();
let mut name_match_arms: String = r#"Linter::Ruff => "Ruff-specific rules","#.into();
let mut url_match_arms: String = r#"Linter::Ruff => None,"#.into();
for file in fs::read_dir("src/rules/")
.unwrap()
@@ -62,14 +62,14 @@ fn generate_origin_name_and_url(out_dir: &Path) {
})
.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}"),"#));
name_match_arms.push_str(&format!(r#"Linter::{variant_name} => "{name}","#));
url_match_arms.push_str(&format!(r#"Linter::{variant_name} => Some("{url}"),"#));
}
write!(
BufWriter::new(fs::File::create(out_dir.join("origin.rs")).unwrap()),
BufWriter::new(fs::File::create(out_dir.join("linter.rs")).unwrap()),
"
impl RuleOrigin {{
impl Linter {{
pub fn name(&self) -> &'static str {{
match self {{ {name_match_arms} }}
}}

View File

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

View File

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

View File

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

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

@@ -383,7 +383,7 @@ s = (
s2 = (
'this'
'is a also a string'
'is also a string'
)
t = (

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

@@ -53,3 +53,25 @@ def test_list_of_tuples(param1, param2):
)
def test_list_of_lists(param1, param2):
...
@pytest.mark.parametrize(
"param1,param2",
[
[1, 2],
[3, 4],
],
)
def test_csv_name_list_of_lists(param1, param2):
...
@pytest.mark.parametrize(
"param",
[
[1, 2],
[3, 4],
],
)
def test_single_list_of_lists(param):
...

View File

@@ -3,6 +3,12 @@ if a:
if b:
c
# SIM102
if a:
if b:
if c:
d
# SIM102
if a:
pass

View File

@@ -42,3 +42,11 @@ def f():
return "foo"
else:
return False
def f():
# SIM103 (but not fixable)
if a:
return False
else:
return True

View File

@@ -1,7 +1,31 @@
# Bad
# SIM109
if a == b or a == c:
d
# Good
# SIM109
if (a == b or a == c) and None:
d
# SIM109
if a == b or a == c or None:
d
# SIM109
if a == b or None or a == c:
d
# OK
if a in (b, c):
d
d
# OK
if a == b or a == c():
d
# OK
if (
a == b
# This comment prevents us from raising SIM109
or a == c
):
d

View File

@@ -1,17 +1,27 @@
if not a == b: # SIM201
# SIM201
if not a == b:
pass
if not a == (b + c): # SIM201
# SIM201
if not a == (b + c):
pass
if not (a + b) == c: # SIM201
# SIM201
if not (a + b) == c:
pass
if not a != b: # OK
# OK
if not a != b:
pass
if a == b: # OK
# OK
if a == b:
pass
if not a == b: # OK
# OK
if not a == b:
raise ValueError()
# OK
def __ne__(self, other):
return not self == other

View File

@@ -1,14 +1,27 @@
if not a != b: # SIM202
# SIM202
if not a != b:
pass
if not a != (b + c): # SIM202
# SIM202
if not a != (b + c):
pass
if not (a + b) != c: # SIM202
# SIM202
if not (a + b) != c:
pass
if not a == b: # OK
# OK
if not a == b:
pass
if a != b: # OK
# OK
if a != b:
pass
# OK
if not a != b:
raise ValueError()
# OK
def __eq__(self, other):
return not self != other

View File

@@ -91,3 +91,20 @@ if key in a_dict:
var = a_dict[key]
else:
var = a_dict["fallback"]
# OK (false negative for elif)
if foo():
pass
elif key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"
# OK (false negative for nested else)
if foo():
pass
else:
if key in a_dict:
vars[idx] = a_dict[key]
else:
vars[idx] = "default"

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,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

@@ -588,3 +588,49 @@ def asdfljdjgf24():
"""Summary.
Description. """
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""
Wrong."""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
r"""Wrong.
"""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""Wrong."
"""
@expect('D200: One-line docstring should fit on one line with quotes '
'(found 3)')
@expect('D212: Multi-line docstring summary should start at the first line')
def one_liner():
"""
"Wrong."""
@expect('D404: First word of the docstring should not be "This"')
def starts_with_this():
"""This is a docstring."""
@expect('D404: First word of the docstring should not be "This"')
def starts_with_space_then_this():
""" This is a docstring that starts with a space.""" # noqa: D210

View File

@@ -0,0 +1,43 @@
# Bad examples
def bad_liouiwnlkjl():
"""Returns foo."""
def bad_sdgfsdg23245():
"""Constructor for a foo."""
def bad_sdgfsdg23245777():
"""
Constructor for a boa.
"""
def bad_run_something():
"""Runs something"""
pass
def multi_line():
"""Writes a logical line that
extends to two physical lines.
"""
# Good examples
def good_run_something():
"""Run away."""
def good_construct():
"""Construct a beautiful house."""
def good_multi_line():
"""Write a logical line that
extends to two physical lines.
"""

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

@@ -0,0 +1,69 @@
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}
)
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

@@ -0,0 +1,61 @@
# UP034
print(("foo"))
# UP034
print(("hell((goodybe))o"))
# UP034
print((("foo")))
# UP034
print((((1))))
# UP034
print(("foo{}".format(1)))
# UP034
print(
("foo{}".format(1))
)
# UP034
print(
(
"foo"
)
)
# UP034
def f():
x = int(((yield 1)))
# UP034
if True:
print(
("foo{}".format(1))
)
# UP034
print((x for x in range(3)))
# OK
print("foo")
# OK
print((1, 2, 3))
# OK
print(())
# OK
print((1,))
# OK
sum((block.code for block in blocks), [])
# OK
def f():
x = int((yield 1))
# OK
sum((i for i in range(3)), [])

39
resources/test/fixtures/ruff/RUF005.py vendored Normal file
View File

@@ -0,0 +1,39 @@
class Fun:
words = ("how", "fun!")
def yay(self):
return self.words
yay = Fun().yay
foo = [4, 5, 6]
bar = [1, 2, 3] + foo
zoob = tuple(bar)
quux = (7, 8, 9) + zoob
spam = quux + (10, 11, 12)
spom = list(spam)
eggs = spom + [13, 14, 15]
elatement = ("we all say", ) + yay()
excitement = ("we all think", ) + Fun().yay()
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,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": [
@@ -227,7 +238,7 @@
]
},
"format": {
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations) or `\"gitlab\"` (GitLab CI code quality report).",
"description": "The style in which violation messages should be formatted: `\"text\"` (default), `\"grouped\"` (group messages by file), `\"json\"` (machine-readable), `\"junit\"` (machine-readable XML), `\"github\"` (GitHub Actions annotations), `\"gitlab\"` (GitLab CI code quality report), or `\"pylint\"` (Pylint text format).",
"anyOf": [
{
"$ref": "#/definitions/SerializationFormat"
@@ -244,7 +255,7 @@
"null"
],
"items": {
"$ref": "#/definitions/RuleCodePrefix"
"$ref": "#/definitions/RuleSelector"
}
},
"ignore-init-module-imports": {
@@ -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",
@@ -1277,6 +1304,7 @@
"D4",
"D40",
"D400",
"D401",
"D402",
"D403",
"D404",
@@ -1310,6 +1338,9 @@
"DTZ011",
"DTZ012",
"E",
"E1",
"E10",
"E101",
"E4",
"E40",
"E401",
@@ -1347,6 +1378,12 @@
"ERA0",
"ERA00",
"ERA001",
"EXE",
"EXE0",
"EXE00",
"EXE003",
"EXE004",
"EXE005",
"F",
"F4",
"F40",
@@ -1631,6 +1668,7 @@
"RUF002",
"RUF003",
"RUF004",
"RUF005",
"RUF1",
"RUF10",
"RUF100",
@@ -1709,6 +1747,14 @@
"TID25",
"TID251",
"TID252",
"TRY",
"TRY3",
"TRY30",
"TRY300",
"TYP",
"TYP0",
"TYP00",
"TYP005",
"U",
"U0",
"U00",
@@ -1765,8 +1811,10 @@
"UP029",
"UP03",
"UP030",
"UP031",
"UP032",
"UP033",
"UP034",
"W",
"W2",
"W29",
@@ -1804,7 +1852,8 @@
"junit",
"grouped",
"github",
"gitlab"
"gitlab",
"pylint"
]
},
"Strictness": {

View File

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

View File

@@ -4,7 +4,7 @@ use clap::{command, Parser};
use regex::Regex;
use ruff::fs;
use ruff::logging::LogLevel;
use ruff::registry::{RuleCode, RuleCodePrefix};
use ruff::registry::{Rule, RuleSelector};
use ruff::resolver::ConfigProcessor;
use ruff::settings::types::{
FilePattern, PatternPrefixPair, PerFileIgnore, PythonVersion, SerializationFormat,
@@ -66,18 +66,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 +88,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>>,
@@ -169,6 +169,7 @@ pub struct Cli {
/// Explain a rule.
#[arg(
long,
value_parser=Rule::from_code,
// Fake subcommands.
conflicts_with = "add_noqa",
conflicts_with = "clean",
@@ -180,7 +181,7 @@ pub struct Cli {
conflicts_with = "stdin_filename",
conflicts_with = "watch",
)]
pub explain: Option<RuleCode>,
pub explain: Option<&'static Rule>,
/// Generate shell completion
#[arg(
long,
@@ -302,7 +303,7 @@ pub struct Arguments {
pub config: Option<PathBuf>,
pub diff: bool,
pub exit_zero: bool,
pub explain: Option<RuleCode>,
pub explain: Option<&'static Rule>,
pub files: Vec<PathBuf>,
pub generate_shell_completion: Option<clap_complete_command::Shell>,
pub isolated: bool,
@@ -323,17 +324,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>,
@@ -434,7 +435,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)

View File

@@ -15,11 +15,11 @@ 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::RuleCode;
use ruff::registry::{Linter, ParseCode, Rule};
use ruff::resolver::{FileDiscovery, PyprojectDiscovery};
use ruff::settings::flags;
use ruff::settings::types::SerializationFormat;
use ruff::{fix, fs, packaging, resolver, warn_user_once, IOError};
use ruff::{fix, fs, packaging, resolver, warn_user_once, AutofixAvailability, IOError};
use serde::Serialize;
use walkdir::WalkDir;
@@ -114,7 +114,7 @@ pub fn run(
.unwrap_or_else(|(path, message)| {
if let Some(path) = &path {
let settings = resolver.resolve(path, pyproject_strategy);
if settings.rules.enabled(&RuleCode::E902) {
if settings.rules.enabled(&Rule::IOError) {
Diagnostics::new(vec![Message {
kind: IOError(message).into(),
location: Location::default(),
@@ -285,28 +285,39 @@ pub fn show_files(
#[derive(Serialize)]
struct Explanation<'a> {
code: &'a str,
origin: &'a str,
linter: &'a str,
summary: &'a str,
}
/// Explain a `RuleCode` to the user.
pub fn explain(code: &RuleCode, format: SerializationFormat) -> Result<()> {
/// 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!(
"{} ({}): {}",
code.as_ref(),
code.origin().name(),
code.kind().summary()
);
println!("{}\n", rule.as_ref());
println!("Code: {} ({})\n", rule.code(), linter.name());
if let Some(autofix) = rule.autofixable() {
println!(
"{}",
match autofix.available {
AutofixAvailability::Sometimes => "Autofix is sometimes available.\n",
AutofixAvailability::Always => "Autofix is always available.\n",
}
);
}
println!("Message formats:\n");
for format in rule.message_formats() {
println!("* {format}");
}
}
SerializationFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&Explanation {
code: code.as_ref(),
origin: code.origin().name(),
summary: &code.kind().summary(),
code: rule.code(),
linter: linter.name(),
summary: rule.message_formats()[0],
})?
);
}
@@ -319,6 +330,9 @@ pub fn explain(code: &RuleCode, format: SerializationFormat) -> Result<()> {
SerializationFormat::Gitlab => {
bail!("`--explain` does not support GitLab format")
}
SerializationFormat::Pylint => {
bail!("`--explain` does not support pylint format")
}
};
Ok(())
}

View File

@@ -157,8 +157,8 @@ quoting the executed command, along with the relevant file contents and `pyproje
PyprojectDiscovery::Hierarchical(settings) => settings.cli.clone(),
};
if let Some(code) = cli.explain {
commands::explain(&code, format)?;
if let Some(rule) = cli.explain {
commands::explain(rule, format)?;
return Ok(ExitCode::SUCCESS);
}
if cli.show_settings {

View File

@@ -11,7 +11,7 @@ use itertools::iterate;
use ruff::fs::relativize_path;
use ruff::logging::LogLevel;
use ruff::message::{Location, Message};
use ruff::registry::RuleCode;
use ruff::registry::Rule;
use ruff::settings::types::SerializationFormat;
use ruff::{fix, notify_user};
use serde::Serialize;
@@ -35,7 +35,7 @@ struct ExpandedFix<'a> {
#[derive(Serialize)]
struct ExpandedMessage<'a> {
code: &'a RuleCode,
code: SerializeRuleAsCode<'a>,
message: String,
fix: Option<ExpandedFix<'a>>,
location: Location,
@@ -43,6 +43,23 @@ struct ExpandedMessage<'a> {
filename: &'a str,
}
struct SerializeRuleAsCode<'a>(&'a Rule);
impl Serialize for SerializeRuleAsCode<'_> {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.code())
}
}
impl<'a> From<&'a Rule> for SerializeRuleAsCode<'a> {
fn from(rule: &'a Rule) -> Self {
Self(rule)
}
}
pub struct Printer<'a> {
format: &'a SerializationFormat,
log_level: &'a LogLevel,
@@ -143,7 +160,7 @@ impl<'a> Printer<'a> {
.messages
.iter()
.map(|message| ExpandedMessage {
code: message.kind.code(),
code: message.kind.rule().into(),
message: message.kind.body(),
fix: message.fix.as_ref().map(|fix| ExpandedFix {
content: &fix.content,
@@ -177,8 +194,10 @@ impl<'a> Printer<'a> {
message.location.column(),
message.kind.body()
));
let mut case =
TestCase::new(format!("org.ruff.{}", message.kind.code()), status);
let mut case = TestCase::new(
format!("org.ruff.{}", message.kind.rule().code()),
status,
);
let file_path = Path::new(filename);
let file_stem = file_path.file_stem().unwrap().to_str().unwrap();
let classname = file_path.parent().unwrap().join(file_stem);
@@ -248,14 +267,14 @@ impl<'a> Printer<'a> {
":",
message.location.column(),
":",
message.kind.code().as_ref(),
message.kind.rule().code(),
message.kind.body(),
);
writeln!(
stdout,
"::error title=Ruff \
({}),file={},line={},col={},endLine={},endColumn={}::{}",
message.kind.code(),
message.kind.rule().code(),
message.filename,
message.location.row(),
message.location.column(),
@@ -266,7 +285,7 @@ impl<'a> Printer<'a> {
}
}
SerializationFormat::Gitlab => {
// Generate JSON with errors in GitLab CI format
// Generate JSON with violations in GitLab CI format
// https://docs.gitlab.com/ee/ci/testing/code_quality.html#implementing-a-custom-tool
writeln!(stdout,
"{}",
@@ -276,9 +295,9 @@ impl<'a> Printer<'a> {
.iter()
.map(|message| {
json!({
"description": format!("({}) {}", message.kind.code(), message.kind.body()),
"description": format!("({}) {}", message.kind.rule().code(), message.kind.body()),
"severity": "major",
"fingerprint": message.kind.code(),
"fingerprint": message.kind.rule().code(),
"location": {
"path": message.filename,
"lines": {
@@ -293,6 +312,20 @@ impl<'a> Printer<'a> {
)?
)?;
}
SerializationFormat::Pylint => {
// Generate violations in Pylint format.
// See: https://flake8.pycqa.org/en/latest/internal/formatters.html#pylint-formatter
for message in &diagnostics.messages {
let label = format!(
"{}:{}: [{}] {}",
relativize_path(Path::new(&message.filename)),
message.location.row(),
message.kind.rule().code(),
message.kind.body(),
);
writeln!(stdout, "{label}")?;
}
}
}
stdout.flush()?;
@@ -361,7 +394,7 @@ fn print_message<T: Write>(stdout: &mut T, message: &Message) -> Result<()> {
":".cyan(),
message.location.column(),
":".cyan(),
message.kind.code().as_ref().red().bold(),
message.kind.rule().code().red().bold(),
message.kind.body(),
);
writeln!(stdout, "{label}")?;
@@ -388,7 +421,7 @@ fn print_message<T: Write>(stdout: &mut T, message: &Message) -> Result<()> {
source: &source.contents,
line_start: message.location.row(),
annotations: vec![SourceAnnotation {
label: message.kind.code().as_ref(),
label: message.kind.rule().code(),
annotation_type: AnnotationType::Error,
range: source.range,
}],
@@ -425,7 +458,7 @@ fn print_grouped_message<T: Write>(
":".cyan(),
message.location.column(),
" ".repeat(column_length - num_digits(message.location.column())),
message.kind.code().as_ref().red().bold(),
message.kind.rule().code().red().bold(),
message.kind.body(),
);
writeln!(stdout, "{label}")?;
@@ -452,7 +485,7 @@ fn print_grouped_message<T: Write>(
source: &source.contents,
line_start: message.location.row(),
annotations: vec![SourceAnnotation {
label: message.kind.code().as_ref(),
label: message.kind.rule().code(),
annotation_type: AnnotationType::Error,
range: source.range,
}],

View File

@@ -13,7 +13,7 @@ use assert_cmd::Command;
use itertools::Itertools;
use log::info;
use ruff::logging::{set_up_logging, LogLevel};
use ruff::registry::RuleOrigin;
use ruff::registry::Linter;
use strum::IntoEnumIterator;
use walkdir::WalkDir;
@@ -175,12 +175,12 @@ fn test_ruff_black_compatibility() -> Result<()> {
.filter_map(Result::ok)
.collect();
let codes = RuleOrigin::iter()
let codes = Linter::iter()
// Exclude ruff codes, specifically RUF100, because it causes differences that are not a
// problem. Ruff would add a `# noqa: W292` after the first run, black introduces a
// newline, and ruff removes the `# noqa: W292` again.
.filter(|origin| *origin != RuleOrigin::Ruff)
.map(|origin| origin.prefixes().as_list(","))
.filter(|linter| *linter != Linter::Ruff)
.map(|linter| linter.prefixes().as_list(","))
.join(",");
let ruff_args = [
"-",

View File

@@ -151,3 +151,12 @@ fn test_show_source() -> Result<()> {
assert!(str::from_utf8(&output.get_output().stdout)?.contains("l = 1"));
Ok(())
}
#[test]
fn explain_status_codes() -> Result<()> {
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--explain", "F401"]).assert().success();
let mut cmd = Command::cargo_bin(BIN_NAME)?;
cmd.args(["-", "--explain", "RUF404"]).assert().failure();
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_dev"
version = "0.0.226"
version = "0.0.229"
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 = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "acbc517b55406c76da83d7b2711941d8d3f65b87" }
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" }
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, Prefixes, RuleSelector};
use strum::IntoEnumIterator;
use crate::utils::replace_readme_section;
@@ -20,19 +20,22 @@ pub struct Cli {
pub(crate) dry_run: bool,
}
fn generate_table(table_out: &mut String, prefix: &RuleCodePrefix) {
fn generate_table(table_out: &mut String, prefix: &RuleSelector) {
table_out.push_str("| Code | Name | Message | Fix |");
table_out.push('\n');
table_out.push_str("| ---- | ---- | ------- | --- |");
table_out.push('\n');
for rule_code in prefix.codes() {
let kind = rule_code.kind();
let fix_token = if kind.fixable() { "🛠" } else { "" };
for rule in prefix.codes() {
let fix_token = match rule.autofixable() {
None => "",
Some(_) => "🛠",
};
table_out.push_str(&format!(
"| {} | {} | {} | {} |",
kind.code().as_ref(),
kind.as_ref(),
kind.summary().replace('|', r"\|"),
rule.code(),
rule.as_ref(),
rule.message_formats()[0].replace('|', r"\|"),
fix_token
));
table_out.push('\n');
@@ -44,22 +47,22 @@ 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();
for linter in Linter::iter() {
let prefixes = linter.prefixes();
let codes_csv: String = prefixes.as_list(", ");
table_out.push_str(&format!("### {} ({codes_csv})", origin.name()));
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('/')
@@ -67,7 +70,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",
@@ -75,7 +78,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()
),
}
));

View File

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

View File

@@ -1,35 +1,41 @@
use std::collections::HashMap;
use proc_macro2::Span;
use quote::quote;
use syn::parse::Parse;
use syn::{Ident, Path, Token};
use syn::{Ident, LitStr, Path, Token};
pub fn define_rule_mapping(mapping: Mapping) -> proc_macro2::TokenStream {
let mut rulecode_variants = quote!();
pub fn define_rule_mapping(mapping: &Mapping) -> proc_macro2::TokenStream {
let mut rule_variants = quote!();
let mut diagkind_variants = quote!();
let mut rulecode_kind_match_arms = quote!();
let mut rulecode_origin_match_arms = quote!();
let mut rule_message_formats_match_arms = quote!();
let mut rule_autofixable_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!();
let mut diagkind_body_match_arms = quote!();
let mut diagkind_fixable_match_arms = quote!();
let mut diagkind_commit_match_arms = quote!();
let mut from_impls_for_diagkind = quote!();
for (code, path, name) in mapping.entries {
rulecode_variants.extend(quote! {#code,});
for (code, path, name) in &mapping.entries {
let code_str = LitStr::new(&code.to_string(), Span::call_site());
rule_variants.extend(quote! {
#[doc = #code_str]
#name,
});
diagkind_variants.extend(quote! {#name(#path),});
rulecode_kind_match_arms.extend(
quote! {RuleCode::#code => DiagnosticKind::#name(<#path as Violation>::placeholder()),},
);
let origin = get_origin(&code);
rulecode_origin_match_arms.extend(quote! {RuleCode::#code => RuleOrigin::#origin,});
diagkind_code_match_arms.extend(quote! {DiagnosticKind::#name(..) => &RuleCode::#code, });
diagkind_body_match_arms
.extend(quote! {DiagnosticKind::#name(x) => Violation::message(x), });
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,});
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, });
diagkind_body_match_arms.extend(quote! {Self::#name(x) => Violation::message(x), });
diagkind_fixable_match_arms
.extend(quote! {DiagnosticKind::#name(x) => x.autofix_title_formatter().is_some(),});
diagkind_commit_match_arms.extend(
quote! {DiagnosticKind::#name(x) => x.autofix_title_formatter().map(|f| f(x)), },
);
.extend(quote! {Self::#name(x) => x.autofix_title_formatter().is_some(),});
diagkind_commit_match_arms
.extend(quote! {Self::#name(x) => x.autofix_title_formatter().map(|f| f(x)), });
from_impls_for_diagkind.extend(quote! {
impl From<#path> for DiagnosticKind {
fn from(x: #path) -> Self {
@@ -39,44 +45,69 @@ pub fn define_rule_mapping(mapping: Mapping) -> proc_macro2::TokenStream {
});
}
let code_to_name: HashMap<_, _> = mapping
.entries
.iter()
.map(|(code, _, name)| (code.to_string(), name))
.collect();
let rulecodeprefix = super::rule_code_prefix::expand(
&Ident::new("Rule", Span::call_site()),
&Ident::new("RuleSelector", Span::call_site()),
mapping.entries.iter().map(|(code, ..)| code),
|code| code_to_name[code],
);
quote! {
#[derive(
AsRefStr,
RuleCodePrefix,
EnumIter,
EnumString,
Debug,
Display,
PartialEq,
Eq,
Clone,
Serialize,
Deserialize,
Hash,
PartialOrd,
Ord,
AsRefStr,
)]
pub enum RuleCode { #rulecode_variants }
#[strum(serialize_all = "kebab-case")]
pub enum Rule { #rule_variants }
#[derive(AsRefStr, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum DiagnosticKind { #diagkind_variants }
#[derive(thiserror::Error, Debug)]
pub enum FromCodeError {
#[error("unknown rule code")]
Unknown,
}
impl RuleCode {
/// A placeholder representation of the `DiagnosticKind` for the diagnostic.
pub fn kind(&self) -> DiagnosticKind {
match self { #rulecode_kind_match_arms }
impl Rule {
/// Returns the format strings used to report violations of this rule.
pub fn message_formats(&self) -> &'static [&'static str] {
match self { #rule_message_formats_match_arms }
}
pub fn origin(&self) -> RuleOrigin {
match self { #rulecode_origin_match_arms }
pub fn autofixable(&self) -> Option<crate::violation::AutofixKind> {
match self { #rule_autofixable_match_arms }
}
pub fn code(&self) -> &'static str {
match self { #rule_code_match_arms }
}
pub fn from_code(code: &str) -> Result<&'static Self, FromCodeError> {
match code {
#rule_from_code_match_arms
_ => Err(FromCodeError::Unknown),
}
}
}
impl DiagnosticKind {
/// A four-letter shorthand code for the diagnostic.
pub fn code(&self) -> &'static RuleCode {
/// The rule of the diagnostic.
pub fn rule(&self) -> &'static Rule {
match self { #diagkind_code_match_arms }
}
@@ -97,22 +128,11 @@ pub fn define_rule_mapping(mapping: Mapping) -> proc_macro2::TokenStream {
}
#from_impls_for_diagkind
#rulecodeprefix
}
}
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

@@ -0,0 +1,55 @@
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{Block, Expr, ItemFn, Stmt};
pub fn derive_message_formats(func: &ItemFn) -> proc_macro2::TokenStream {
let mut strings = quote!();
if let Err(err) = parse_block(&func.block, &mut strings) {
return err;
}
quote! {
#func
fn message_formats() -> &'static [&'static str] {
&[#strings]
}
}
}
fn parse_block(block: &Block, strings: &mut TokenStream) -> Result<(), TokenStream> {
let Some(Stmt::Expr(last)) = block.stmts.last() else {panic!("expected last statement in block to be an expression")};
parse_expr(last, strings)?;
Ok(())
}
fn parse_expr(expr: &Expr, strings: &mut TokenStream) -> Result<(), TokenStream> {
match expr {
Expr::Macro(mac) if mac.mac.path.is_ident("format") => {
let Some(first_token) = mac.mac.tokens.to_token_stream().into_iter().next() else {
return Err(quote_spanned!(expr.span() => compile_error!("expected format! to have an argument")))
};
strings.extend(quote! {#first_token,});
Ok(())
}
Expr::Block(block) => parse_block(&block.block, strings),
Expr::If(expr) => {
parse_block(&expr.then_branch, strings)?;
if let Some((_, then)) = &expr.else_branch {
parse_expr(then, strings)?;
}
Ok(())
}
Expr::Match(block) => {
for arm in &block.arms {
parse_expr(&arm.body, strings)?;
}
Ok(())
}
_ => Err(quote_spanned!(
expr.span() =>
compile_error!("expected last expression to be a format! macro or a match block")
)),
}
}

View File

@@ -13,11 +13,13 @@
)]
#![forbid(unsafe_code)]
use syn::{parse_macro_input, DeriveInput};
use proc_macro::TokenStream;
use syn::{parse_macro_input, DeriveInput, ItemFn};
mod config;
mod define_rule_mapping;
mod prefixes;
mod derive_message_formats;
mod parse_code;
mod rule_code_prefix;
#[proc_macro_derive(ConfigurationOptions, attributes(option, doc, option_group))]
@@ -29,17 +31,23 @@ pub fn derive_config(input: proc_macro::TokenStream) -> proc_macro::TokenStream
.into()
}
#[proc_macro_derive(RuleCodePrefix)]
#[proc_macro]
pub fn define_rule_mapping(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mapping = parse_macro_input!(item as define_rule_mapping::Mapping);
define_rule_mapping::define_rule_mapping(&mapping).into()
}
#[proc_macro_derive(ParseCode, attributes(prefix))]
pub fn derive_rule_code_prefix(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
rule_code_prefix::derive_impl(input)
parse_code::derive_impl(input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
#[proc_macro]
pub fn define_rule_mapping(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
let mapping = parse_macro_input!(item as define_rule_mapping::Mapping);
define_rule_mapping::define_rule_mapping(mapping).into()
#[proc_macro_attribute]
pub fn derive_message_formats(_attr: TokenStream, item: TokenStream) -> TokenStream {
let func = parse_macro_input!(item as ItemFn);
derive_message_formats::derive_message_formats(&func).into()
}

View File

@@ -0,0 +1,54 @@
use quote::quote;
use syn::spanned::Spanned;
use syn::{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();
for variant in variants {
let prefix_attrs: Vec<_> = variant
.attrs
.iter()
.filter(|a| a.path.is_ident("prefix"))
.collect();
if prefix_attrs.is_empty() {
return Err(Error::new(
variant.span(),
r#"Missing [#prefix = "..."] attribute"#,
));
}
for attr in prefix_attrs {
let Ok(Meta::NameValue(MetaNameValue{lit: Lit::Str(lit), ..})) = attr.parse_meta() else {
return Err(Error::new(attr.span(), r#"expected attribute to be in the form of [#prefix = "..."]"#))
};
parsed.push((lit, variant.ident.clone()));
}
}
parsed.sort_by_key(|(prefix, _)| prefix.value().len());
let mut if_statements = quote!();
for (prefix, field) in parsed {
if_statements.extend(quote! {if let Some(rest) = code.strip_prefix(#prefix) {
return Some((#ident::#field, rest));
}});
}
Ok(quote! {
impl crate::registry::ParseCode for #ident {
fn parse_code(code: &str) -> Option<(Self, &str)> {
#if_statements
None
}
}
})
}

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

@@ -3,14 +3,12 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use once_cell::sync::Lazy;
use proc_macro2::Span;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::token::Comma;
use syn::{DataEnum, DeriveInput, Ident, Variant};
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.
@@ -86,43 +84,17 @@ pub static PREFIX_REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::n
])
});
pub fn derive_impl(input: DeriveInput) -> syn::Result<proc_macro2::TokenStream> {
let DeriveInput { ident, data, .. } = input;
let syn::Data::Enum(DataEnum { variants, .. }) = data else {
return Err(syn::Error::new(
ident.span(),
"Can only derive `RuleCodePrefix` from enums.",
));
};
let prefix_ident = Ident::new(&format!("{ident}Prefix"), ident.span());
let prefix = expand(&ident, &prefix_ident, &variants);
let expanded = quote! {
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum SuffixLength {
None,
Zero,
One,
Two,
Three,
Four,
}
#prefix
};
Ok(expanded)
}
fn expand(
ident: &Ident,
pub fn expand<'a>(
rule_type: &Ident,
prefix_ident: &Ident,
variants: &Punctuated<Variant, Comma>,
variants: impl Iterator<Item = &'a Ident>,
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();
for variant in variants {
let span = variant.ident.span();
let code_str = variant.ident.to_string();
let span = variant.span();
let code_str = variant.to_string();
let code_prefix_len = code_str
.chars()
.take_while(|char| char.is_alphabetic())
@@ -158,7 +130,7 @@ fn expand(
}
});
let prefix_impl = generate_impls(ident, prefix_ident, &prefix_to_codes);
let prefix_impl = generate_impls(rule_type, prefix_ident, &prefix_to_codes, variant_name);
let prefix_redirects = PREFIX_REDIRECTS.iter().map(|(alias, rule_code)| {
let code = Ident::new(rule_code, Span::call_site());
@@ -168,6 +140,16 @@ fn expand(
});
quote! {
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub enum SuffixLength {
None,
Zero,
One,
Two,
Three,
Four,
}
#[derive(
::strum_macros::EnumString,
::strum_macros::AsRefStr,
@@ -187,7 +169,7 @@ fn expand(
#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),*
@@ -196,16 +178,17 @@ fn expand(
}
}
fn generate_impls(
ident: &Ident,
fn generate_impls<'a>(
rule_type: &Ident,
prefix_ident: &Ident,
prefix_to_codes: &BTreeMap<Ident, BTreeSet<String>>,
variant_name: impl Fn(&str) -> &'a Ident,
) -> proc_macro2::TokenStream {
let codes_match_arms = prefix_to_codes.iter().map(|(prefix, codes)| {
let codes = codes.iter().map(|code| {
let code = Ident::new(code, Span::call_site());
let rule_variant = variant_name(code);
quote! {
#ident::#code
#rule_type::#rule_variant
}
});
let prefix_str = prefix.to_string();
@@ -265,7 +248,7 @@ fn generate_impls(
quote! {
impl #prefix_ident {
pub fn codes(&self) -> Vec<#ident> {
pub fn codes(&self) -> Vec<#rule_type> {
use colored::Colorize;
#[allow(clippy::match_same_arms)]

18
scripts/_utils.py Normal file
View File

@@ -0,0 +1,18 @@
import os
import re
from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(linter_name: str) -> str:
return linter_name.replace("-", "_")
def pascal_case(linter_name: str) -> str:
"""Convert from snake-case to PascalCase."""
return "".join(word.title() for word in linter_name.split("-"))
def get_indent(line: str) -> str:
return re.match(r"^\s*", line).group() # pyright: ignore[reportOptionalMemberAccess]

43
scripts/add_plugin.py Normal file → Executable file
View File

@@ -10,17 +10,8 @@ Example usage:
import argparse
import os
from pathlib import Path
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def dir_name(plugin: str) -> str:
return plugin.replace("-", "_")
def pascal_case(plugin: str) -> str:
return "".join(word.title() for word in plugin.split("-"))
from _utils import ROOT_DIR, dir_name, get_indent, pascal_case
def main(*, plugin: str, url: str) -> None:
@@ -36,6 +27,7 @@ def main(*, plugin: str, url: str) -> None:
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:
fp.write(f"//! Rules from [{plugin}]({url}).\n")
fp.write("pub(crate) mod rules;\n")
fp.write("\n")
fp.write(
@@ -47,11 +39,11 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::linter::test_path;
use crate::settings;
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics =test_path(
Path::new("./resources/test/fixtures/%s")
@@ -76,35 +68,24 @@ mod tests {
with open(ROOT_DIR / "src/registry.rs", "w") as fp:
for line in content.splitlines():
indent = get_indent(line)
if line.strip() == "// Ruff":
indent = line.split("// Ruff")[0]
fp.write(f"{indent}// {plugin}")
fp.write("\n")
elif line.strip() == "Ruff,":
indent = line.split("Ruff,")[0]
elif line.strip() == '#[prefix = "RUF"]':
fp.write(f'{indent}#[prefix = "TODO"]\n')
fp.write(f"{indent}{pascal_case(plugin)},")
fp.write("\n")
elif line.strip() == 'RuleOrigin::Ruff => "Ruff-specific rules",':
indent = line.split('RuleOrigin::Ruff => "Ruff-specific rules",')[0]
fp.write(f'{indent}RuleOrigin::{pascal_case(plugin)} => "{plugin}",')
fp.write("\n")
elif line.strip() == "RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],":
indent = line.split("RuleOrigin::Ruff => vec![RuleCodePrefix::RUF],")[0]
elif line.strip() == "Linter::Ruff => Prefixes::Single(RuleSelector::RUF),":
prefix = 'todo!("Fill-in prefix after generating codes")'
fp.write(
f"{indent}RuleOrigin::{pascal_case(plugin)} => vec![\n"
f'{indent} todo!("Fill-in prefix after generating codes")\n'
f"{indent}],"
f"{indent}Linter::{pascal_case(plugin)} => Prefixes::Single({prefix}),"
)
fp.write("\n")
elif line.strip() == "RuleOrigin::Ruff => None,":
indent = line.split("RuleOrigin::Ruff => None,")[0]
fp.write(f"{indent}RuleOrigin::{pascal_case(plugin)} => " f'Some(("{url}", &Platform::PyPI)),')
fp.write("\n")
fp.write(line)
fp.write("\n")
@@ -114,7 +95,7 @@ mod tests {
with open(ROOT_DIR / "src/violations.rs", "w") as fp:
for line in content.splitlines():
if line.strip() == "// Ruff":
indent = line.split("// Ruff")[0]
indent = get_indent(line)
fp.write(f"{indent}// {plugin}")
fp.write("\n")

55
scripts/add_rule.py Normal file → Executable file
View File

@@ -6,23 +6,12 @@ Example usage:
python scripts/add_rule.py \
--name PreferListBuiltin \
--code PIE807 \
--origin flake8-pie
--linter flake8-pie
"""
import argparse
import os
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 pascal_case(origin: str) -> str:
"""Convert from snake-case to PascalCase."""
return "".join(word.title() for word in origin.split("-"))
from _utils import ROOT_DIR, dir_name, get_indent
def snake_case(name: str) -> str:
@@ -30,30 +19,30 @@ 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",
ROOT_DIR / "resources/test/fixtures" / dir_name(linter) / f"{code}.py",
"a",
):
pass
# Add the relevant `#testcase` macro.
mod_rs = ROOT_DIR / "src/rules" / dir_name(origin) / "mod.rs"
mod_rs = ROOT_DIR / "src/rules" / dir_name(linter) / "mod.rs"
content = mod_rs.read_text()
with open(mod_rs, "w") as fp:
for line in content.splitlines():
if line.strip() == "fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {":
indent = line.split("fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {")[0]
fp.write(f'{indent}#[test_case(RuleCode::{code}, Path::new("{code}.py"); "{code}")]')
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("\n")
fp.write(line)
fp.write("\n")
# Add the relevant rule function.
with open(ROOT_DIR / "src/rules" / dir_name(origin) / "rules.rs", "a") as fp:
with open(ROOT_DIR / "src/rules" / dir_name(linter) / (snake_case(name) + ".rs"), "w") as fp:
fp.write(
f"""
/// {code}
@@ -70,22 +59,20 @@ pub fn {snake_case(name)}(checker: &mut Checker) {{}}
fp.write(line)
fp.write("\n")
if line.startswith(f"// {origin}"):
if line.startswith(f"// {linter}"):
fp.write(
"""define_violation!(
pub struct %s;
);
impl Violation for %s {
#[derive_message_formats]
fn message(&self) -> String {
todo!("Implement message")
}
fn placeholder() -> Self {
%s
todo!("implement message");
format!("TODO: write message")
}
}
"""
% (name, name, name)
% (name, name)
)
fp.write("\n")
@@ -102,24 +89,26 @@ impl Violation for %s {
if has_written:
continue
if line.startswith("define_rule_mapping!"):
if line.startswith("ruff_macros::define_rule_mapping!"):
seen_macro = True
continue
if not seen_macro:
continue
if line.strip() == f"// {origin}":
indent = line.split("//")[0]
if line.strip() == f"// {linter}":
indent = get_indent(line)
fp.write(f"{indent}{code} => violations::{name},")
fp.write("\n")
has_written = True
assert has_written
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",
@@ -134,11 +123,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)

9
scripts/pyproject.toml Normal file
View File

@@ -0,0 +1,9 @@
[tool.ruff]
select = ["ALL"]
ignore = [
"S101", # assert-used
"PLR2004", # magic-value-comparison
]
[tool.ruff.pydocstyle]
convention = "pep257"

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.

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -6,7 +6,7 @@ use rustpython_parser::ast::Suite;
use crate::ast::visitor::Visitor;
use crate::directives::IsortDirectives;
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::rules::isort;
use crate::rules::isort::track::{Block, ImportTracker};
use crate::settings::{flags, Settings};
@@ -36,7 +36,7 @@ pub fn check_imports(
// Enforce import rules.
let mut diagnostics = vec![];
if settings.rules.enabled(&RuleCode::I001) {
if settings.rules.enabled(&Rule::UnsortedImports) {
for block in &blocks {
if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports(
@@ -47,7 +47,7 @@ pub fn check_imports(
}
}
}
if settings.rules.enabled(&RuleCode::I002) {
if settings.rules.enabled(&Rule::MissingRequiredImport) {
diagnostics.extend(isort::rules::add_required_imports(
&blocks, python_ast, locator, settings, autofix,
));

View File

@@ -1,8 +1,10 @@
//! Lint rules based on checking raw physical lines.
use crate::registry::{Diagnostic, RuleCode};
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;
@@ -17,12 +19,25 @@ pub fn check_lines(
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
let enforce_blanket_noqa = settings.rules.enabled(&RuleCode::PGH004);
let enforce_blanket_type_ignore = settings.rules.enabled(&RuleCode::PGH003);
let enforce_doc_line_too_long = settings.rules.enabled(&RuleCode::W505);
let enforce_line_too_long = settings.rules.enabled(&RuleCode::E501);
let enforce_no_newline_at_end_of_file = settings.rules.enabled(&RuleCode::W292);
let enforce_unnecessary_coding_comment = settings.rules.enabled(&RuleCode::UP009);
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);
let enforce_no_newline_at_end_of_file = settings.rules.enabled(&Rule::NoNewLineAtEndOfFile);
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();
@@ -33,12 +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(&RuleCode::UP009),
) {
if let Some(diagnostic) =
unnecessary_coding_comment(index, line, fix_unnecessary_coding_comment)
{
diagnostics.push(diagnostic);
}
}
@@ -55,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
@@ -68,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);
@@ -79,7 +118,7 @@ pub fn check_lines(
if let Some(diagnostic) = no_newline_at_end_of_file(
contents,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&RuleCode::W292),
&& settings.rules.should_fix(&Rule::NoNewLineAtEndOfFile),
) {
diagnostics.push(diagnostic);
}
@@ -92,7 +131,7 @@ pub fn check_lines(
mod tests {
use super::check_lines;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings::{flags, Settings};
#[test]
@@ -105,7 +144,7 @@ mod tests {
&[],
&Settings {
line_length,
..Settings::for_rule(RuleCode::E501)
..Settings::for_rule(Rule::LineTooLong)
},
flags::Autofix::Enabled,
)

View File

@@ -1,14 +1,12 @@
//! `NoQA` enforcement and validation.
use std::str::FromStr;
use nohash_hasher::IntMap;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::noqa::{is_file_exempt, Directive};
use crate::registry::{Diagnostic, DiagnosticKind, RuleCode, CODE_REDIRECTS};
use crate::registry::{Diagnostic, DiagnosticKind, Rule, CODE_REDIRECTS};
use crate::settings::{flags, Settings};
use crate::violations::UnusedCodes;
use crate::{noqa, violations};
@@ -24,7 +22,7 @@ pub fn check_noqa(
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
let mut ignored = vec![];
let enforce_noqa = settings.rules.enabled(&RuleCode::RUF100);
let enforce_noqa = settings.rules.enabled(&Rule::UnusedNOQA);
let lines: Vec<&str> = contents.lines().collect();
for lineno in commented_lines {
@@ -56,13 +54,13 @@ pub fn check_noqa(
});
match noqa {
(Directive::All(..), matches) => {
matches.push(diagnostic.kind.code().as_ref());
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
continue;
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(diagnostic.kind.code(), codes) {
matches.push(diagnostic.kind.code().as_ref());
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
continue;
}
@@ -83,12 +81,12 @@ pub fn check_noqa(
.or_insert_with(|| (noqa::extract_noqa_directive(lines[noqa_lineno - 1]), vec![]));
match noqa {
(Directive::All(..), matches) => {
matches.push(diagnostic.kind.code().as_ref());
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(diagnostic.kind.code(), codes) {
matches.push(diagnostic.kind.code().as_ref());
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
}
}
@@ -108,7 +106,7 @@ pub fn check_noqa(
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
);
if matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(diagnostic.kind.code())
&& settings.rules.should_fix(diagnostic.kind.rule())
{
diagnostic.amend(Fix::deletion(
Location::new(row + 1, start - spaces),
@@ -125,8 +123,8 @@ pub fn check_noqa(
let mut valid_codes = vec![];
let mut self_ignore = false;
for code in codes {
let code = CODE_REDIRECTS.get(code).map_or(code, AsRef::as_ref);
if code == RuleCode::RUF100.as_ref() {
let code = CODE_REDIRECTS.get(code).map_or(code, |r| r.code());
if code == Rule::UnusedNOQA.code() {
self_ignore = true;
break;
}
@@ -134,8 +132,8 @@ pub fn check_noqa(
if matches.contains(&code) || settings.external.contains(code) {
valid_codes.push(code);
} else {
if let Ok(rule_code) = RuleCode::from_str(code) {
if settings.rules.enabled(&rule_code) {
if let Ok(rule) = Rule::from_code(code) {
if settings.rules.enabled(rule) {
unmatched_codes.push(code);
} else {
disabled_codes.push(code);
@@ -172,7 +170,7 @@ pub fn check_noqa(
Range::new(Location::new(row + 1, start), Location::new(row + 1, end)),
);
if matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(diagnostic.kind.code())
&& settings.rules.should_fix(diagnostic.kind.rule())
{
if valid_codes.is_empty() {
diagnostic.amend(Fix::deletion(

View File

@@ -3,10 +3,11 @@
use rustpython_parser::lexer::{LexResult, Tok};
use crate::lex::docstring_detection::StateMachine;
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::rules::ruff::rules::Context;
use crate::rules::{
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, ruff,
eradicate, flake8_commas, flake8_implicit_str_concat, flake8_quotes, pycodestyle, pyupgrade,
ruff,
};
use crate::settings::{flags, Settings};
use crate::source_code::Locator;
@@ -19,20 +20,33 @@ pub fn check_tokens(
) -> Vec<Diagnostic> {
let mut diagnostics: Vec<Diagnostic> = vec![];
let enforce_ambiguous_unicode_character = settings.rules.enabled(&RuleCode::RUF001)
|| settings.rules.enabled(&RuleCode::RUF002)
|| settings.rules.enabled(&RuleCode::RUF003);
let enforce_quotes = settings.rules.enabled(&RuleCode::Q000)
|| settings.rules.enabled(&RuleCode::Q001)
|| settings.rules.enabled(&RuleCode::Q002)
|| settings.rules.enabled(&RuleCode::Q003);
let enforce_commented_out_code = settings.rules.enabled(&RuleCode::ERA001);
let enforce_invalid_escape_sequence = settings.rules.enabled(&RuleCode::W605);
let enforce_implicit_string_concatenation =
settings.rules.enabled(&RuleCode::ISC001) || settings.rules.enabled(&RuleCode::ISC002);
let enforce_trailing_comma = settings.rules.enabled(&RuleCode::COM812)
|| settings.rules.enabled(&RuleCode::COM818)
|| settings.rules.enabled(&RuleCode::COM819);
let enforce_ambiguous_unicode_character = settings
.rules
.enabled(&Rule::AmbiguousUnicodeCharacterString)
|| settings
.rules
.enabled(&Rule::AmbiguousUnicodeCharacterDocstring)
|| settings
.rules
.enabled(&Rule::AmbiguousUnicodeCharacterComment);
let enforce_quotes = settings.rules.enabled(&Rule::BadQuotesInlineString)
|| settings.rules.enabled(&Rule::BadQuotesMultilineString)
|| settings.rules.enabled(&Rule::BadQuotesDocstring)
|| settings.rules.enabled(&Rule::AvoidQuoteEscape);
let enforce_commented_out_code = settings.rules.enabled(&Rule::CommentedOutCode);
let enforce_invalid_escape_sequence = settings.rules.enabled(&Rule::InvalidEscapeSequence);
let enforce_implicit_string_concatenation = settings
.rules
.enabled(&Rule::SingleLineImplicitStringConcatenation)
|| settings
.rules
.enabled(&Rule::MultiLineImplicitStringConcatenation);
let enforce_trailing_comma = settings.rules.enabled(&Rule::TrailingCommaMissing)
|| settings
.rules
.enabled(&Rule::TrailingCommaOnBareTupleProhibited)
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
@@ -75,7 +89,7 @@ pub fn check_tokens(
settings,
autofix,
) {
if settings.rules.enabled(diagnostic.kind.code()) {
if settings.rules.enabled(diagnostic.kind.rule()) {
diagnostics.push(diagnostic);
}
}
@@ -101,7 +115,7 @@ pub fn check_tokens(
start,
end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&RuleCode::W605),
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
}
}
@@ -112,7 +126,7 @@ pub fn check_tokens(
diagnostics.extend(
flake8_implicit_str_concat::rules::implicit(tokens)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.code())),
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
}
@@ -121,7 +135,15 @@ pub fn check_tokens(
diagnostics.extend(
flake8_commas::rules::trailing_commas(tokens, settings, autofix)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.code())),
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
}
// UP034
if enforce_extraneous_parenthesis {
diagnostics.extend(
pyupgrade::rules::extraneous_parentheses(tokens, locator, settings, autofix)
.into_iter(),
);
}

View File

@@ -6,7 +6,7 @@ use colored::Colorize;
use super::black::Black;
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;
@@ -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);
}
@@ -392,7 +401,7 @@ mod tests {
use super::super::plugin::Plugin;
use super::convert;
use crate::registry::RuleCodePrefix;
use crate::registry::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
use crate::settings::options::Options;
@@ -428,11 +437,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 +448,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,
@@ -495,11 +501,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 +512,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,
@@ -562,11 +565,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 +576,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,
@@ -629,11 +629,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 +640,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,
@@ -696,11 +693,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 +704,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 {
@@ -772,10 +766,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 +781,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,
@@ -842,10 +837,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 +852,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

@@ -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 prefix(&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(codes: &BTreeSet<RuleSelector>) -> Vec<Plugin> {
[
Plugin::Flake8Annotations,
Plugin::Flake8Bandit,
@@ -287,10 +287,10 @@ 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]);
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::prefix));
select
}

View File

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Result};
use path_absolutize::{path_dedot, Absolutize};
use rustc_hash::FxHashSet;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings::hashable::{HashableGlobMatcher, HashableHashSet};
/// Extract the absolute path and basename (as strings) from a Path.
@@ -29,9 +29,9 @@ pub(crate) fn ignores_from_path<'a>(
pattern_code_pairs: &'a [(
HashableGlobMatcher,
HashableGlobMatcher,
HashableHashSet<RuleCode>,
HashableHashSet<Rule>,
)],
) -> Result<FxHashSet<&'a RuleCode>> {
) -> Result<FxHashSet<&'a Rule>> {
let (file_path, file_basename) = extract_path_names(path)?;
Ok(pattern_code_pairs
.iter()

View File

@@ -47,6 +47,7 @@ mod violations;
mod visibility;
use cfg_if::cfg_if;
pub use violation::{AutofixKind, Availability as AutofixAvailability};
pub use violations::IOError;
cfg_if! {

View File

@@ -7,11 +7,12 @@ use wasm_bindgen::prelude::*;
use crate::directives;
use crate::linter::check_path;
use crate::registry::RuleCode;
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;
@@ -51,13 +52,30 @@ export interface Diagnostic {
#[derive(Serialize)]
struct ExpandedMessage {
code: RuleCode,
code: SerializeRuleAsCode,
message: String,
location: Location,
end_location: Location,
fix: Option<ExpandedFix>,
}
struct SerializeRuleAsCode(Rule);
impl Serialize for SerializeRuleAsCode {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.code())
}
}
impl From<Rule> for SerializeRuleAsCode {
fn from(rule: Rule) -> Self {
Self(rule)
}
}
#[derive(Serialize)]
struct ExpandedFix {
content: String,
@@ -119,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()),
@@ -182,7 +201,7 @@ pub fn check(contents: &str, options: JsValue) -> Result<JsValue, JsValue> {
let messages: Vec<ExpandedMessage> = diagnostics
.into_iter()
.map(|diagnostic| ExpandedMessage {
code: diagnostic.kind.code().clone(),
code: diagnostic.kind.rule().clone().into(),
message: diagnostic.kind.body(),
location: diagnostic.location,
end_location: diagnostic.end_location,
@@ -224,7 +243,7 @@ mod test {
"if (1, 2): pass",
r#"{}"#,
[ExpandedMessage {
code: RuleCode::F634,
code: Rule::IfTuple.into(),
message: "If test is a tuple, which is always `True`".to_string(),
location: Location::new(1, 0),
end_location: Location::new(1, 15),

View File

@@ -16,7 +16,7 @@ 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;
use crate::registry::{Diagnostic, LintSource, RuleCode};
use crate::registry::{Diagnostic, LintSource, Rule};
use crate::settings::{flags, Settings};
use crate::source_code::{Indexer, Locator, Stylist};
use crate::{directives, fs, rustpython_helpers, violations};
@@ -48,7 +48,7 @@ pub fn check_path(
// Collect doc lines. This requires a rare mix of tokens (for comments) and AST
// (for docstrings), which demands special-casing at this level.
let use_doc_lines = settings.rules.enabled(&RuleCode::W505);
let use_doc_lines = settings.rules.enabled(&Rule::DocLineTooLong);
let mut doc_lines = vec![];
if use_doc_lines {
doc_lines.extend(doc_lines_from_tokens(&tokens));
@@ -116,7 +116,7 @@ pub fn check_path(
}
}
Err(parse_error) => {
if settings.rules.enabled(&RuleCode::E999) {
if settings.rules.enabled(&Rule::SyntaxError) {
diagnostics.push(Diagnostic::new(
violations::SyntaxError(parse_error.error.to_string()),
Range::new(parse_error.location, parse_error.location),
@@ -170,7 +170,7 @@ pub fn check_path(
if !ignores.is_empty() {
return Ok(diagnostics
.into_iter()
.filter(|diagnostic| !ignores.contains(&diagnostic.kind.code()))
.filter(|diagnostic| !ignores.contains(&diagnostic.kind.rule()))
.collect());
}
}

View File

@@ -44,13 +44,14 @@ macro_rules! notify_user {
}
}
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
#[derive(Debug, Default, PartialOrd, Ord, PartialEq, Eq)]
pub enum LogLevel {
// No output (+ `log::LevelFilter::Off`).
Silent,
// Only show lint violations, with no decorative output (+ `log::LevelFilter::Off`).
Quiet,
// All user-facing output (+ `log::LevelFilter::Info`).
#[default]
Default,
// All user-facing output (+ `log::LevelFilter::Debug`).
Verbose,
@@ -67,12 +68,6 @@ impl LogLevel {
}
}
impl Default for LogLevel {
fn default() -> Self {
LogLevel::Default
}
}
pub fn set_up_logging(level: &LogLevel) -> Result<()> {
fern::Dispatch::new()
.format(|out, message, record| {

View File

@@ -8,7 +8,7 @@ use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use crate::registry::{Diagnostic, RuleCode, CODE_REDIRECTS};
use crate::registry::{Diagnostic, Rule, CODE_REDIRECTS};
use crate::settings::hashable::HashableHashSet;
use crate::source_code::LineEnding;
@@ -69,11 +69,11 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
/// Returns `true` if the string list of `codes` includes `code` (or an alias
/// thereof).
pub fn includes(needle: &RuleCode, haystack: &[&str]) -> bool {
let needle: &str = needle.as_ref();
pub fn includes(needle: &Rule, haystack: &[&str]) -> bool {
let needle: &str = needle.code();
haystack.iter().any(|candidate| {
if let Some(candidate) = CODE_REDIRECTS.get(candidate) {
needle == candidate.as_ref()
needle == candidate.code()
} else {
&needle == candidate
}
@@ -101,21 +101,21 @@ fn add_noqa_inner(
external: &HashableHashSet<String>,
line_ending: &LineEnding,
) -> (usize, String) {
let mut matches_by_line: FxHashMap<usize, FxHashSet<&RuleCode>> = FxHashMap::default();
let mut matches_by_line: FxHashMap<usize, FxHashSet<&Rule>> = FxHashMap::default();
for (lineno, line) in contents.lines().enumerate() {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(line) {
return (0, contents.to_string());
}
let mut codes: FxHashSet<&RuleCode> = FxHashSet::default();
let mut codes: FxHashSet<&Rule> = FxHashSet::default();
for diagnostic in diagnostics {
// TODO(charlie): Consider respecting parent `noqa` directives. For now, we'll
// add a `noqa` for every diagnostic, on its own line. This could lead to
// duplication, whereby some parent `noqa` directives become
// redundant.
if diagnostic.location.row() == lineno + 1 {
codes.insert(diagnostic.kind.code());
codes.insert(diagnostic.kind.rule());
}
}
@@ -138,7 +138,7 @@ fn add_noqa_inner(
output.push_str(line);
output.push_str(line_ending);
}
Some(codes) => {
Some(rules) => {
match extract_noqa_directive(line) {
Directive::None => {
// Add existing content.
@@ -148,7 +148,7 @@ fn add_noqa_inner(
output.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> = codes.iter().map(AsRef::as_ref).collect();
let codes: Vec<&str> = rules.iter().map(|r| r.code()).collect();
let suffix = codes.join(", ");
output.push_str(&suffix);
output.push_str(line_ending);
@@ -163,7 +163,7 @@ fn add_noqa_inner(
// Add codes.
let codes: Vec<&str> =
codes.iter().map(AsRef::as_ref).sorted_unstable().collect();
rules.iter().map(|r| r.code()).sorted_unstable().collect();
let suffix = codes.join(", ");
output.push_str(&suffix);
output.push_str(line_ending);
@@ -181,9 +181,9 @@ fn add_noqa_inner(
formatted.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> = codes
let codes: Vec<&str> = rules
.iter()
.map(AsRef::as_ref)
.map(|r| r.code())
.chain(existing.into_iter().filter(|code| external.contains(*code)))
.sorted_unstable()
.collect();

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,12 +1,12 @@
//! Registry of [`RuleCode`] to [`DiagnosticKind`] mappings.
//! Registry of [`Rule`] to [`DiagnosticKind`] mappings.
use itertools::Itertools;
use once_cell::sync::Lazy;
use ruff_macros::RuleCodePrefix;
use ruff_macros::ParseCode;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::Location;
use serde::{Deserialize, Serialize};
use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use strum_macros::{AsRefStr, EnumIter};
use crate::ast::types::Range;
use crate::fix::Fix;
@@ -15,6 +15,7 @@ use crate::{rules, violations};
ruff_macros::define_rule_mapping!(
// pycodestyle errors
E101 => violations::MixedSpacesAndTabs,
E401 => violations::MultipleImportsOnOneLine,
E402 => violations::ModuleImportNotAtTopOfFile,
E501 => violations::LineTooLong,
@@ -251,8 +252,10 @@ ruff_macros::define_rule_mapping!(
UP028 => violations::RewriteYieldFrom,
UP029 => violations::UnnecessaryBuiltinImport,
UP030 => violations::FormatLiterals,
UP031 => violations::PrintfStringFormatting,
UP032 => violations::FString,
UP033 => violations::FunctoolsCache,
UP033 => violations::FunctoolsCache,
UP034 => violations::ExtraneousParentheses,
// pydocstyle
D100 => violations::PublicModule,
D101 => violations::PublicClass,
@@ -281,6 +284,7 @@ ruff_macros::define_rule_mapping!(
D300 => violations::UsesTripleQuotes,
D301 => violations::UsesRPrefixForBackslashedContent,
D400 => violations::EndsInPeriod,
D401 => crate::rules::pydocstyle::rules::non_imperative_mood::NonImperativeMood,
D402 => violations::NoSignature,
D403 => violations::FirstLineCapitalized,
D404 => violations::NoThisPrefix,
@@ -417,56 +421,111 @@ ruff_macros::define_rule_mapping!(
COM819 => violations::TrailingCommaProhibited,
// flake8-no-pep420
INP001 => violations::ImplicitNamespacePackage,
// 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
TRY300 => rules::tryceratops::rules::TryConsiderElse,
// Ruff
RUF001 => violations::AmbiguousUnicodeCharacterString,
RUF002 => violations::AmbiguousUnicodeCharacterDocstring,
RUF003 => violations::AmbiguousUnicodeCharacterComment,
RUF004 => violations::KeywordArgumentBeforeStarArgument,
RUF005 => violations::UnpackInsteadOfConcatenatingToCollectionLiteral,
RUF100 => violations::UnusedNOQA,
);
#[derive(EnumIter, Debug, PartialEq, Eq)]
pub enum RuleOrigin {
#[derive(EnumIter, Debug, PartialEq, Eq, ParseCode)]
pub enum Linter {
#[prefix = "F"]
Pyflakes,
#[prefix = "E"]
#[prefix = "W"]
Pycodestyle,
#[prefix = "C9"]
McCabe,
#[prefix = "I"]
Isort,
#[prefix = "D"]
Pydocstyle,
#[prefix = "UP"]
Pyupgrade,
#[prefix = "N"]
PEP8Naming,
#[prefix = "YTT"]
Flake82020,
#[prefix = "ANN"]
Flake8Annotations,
#[prefix = "S"]
Flake8Bandit,
#[prefix = "BLE"]
Flake8BlindExcept,
#[prefix = "FBT"]
Flake8BooleanTrap,
#[prefix = "B"]
Flake8Bugbear,
#[prefix = "A"]
Flake8Builtins,
#[prefix = "C4"]
Flake8Comprehensions,
#[prefix = "T10"]
Flake8Debugger,
#[prefix = "EM"]
Flake8ErrMsg,
#[prefix = "ISC"]
Flake8ImplicitStrConcat,
#[prefix = "ICN"]
Flake8ImportConventions,
#[prefix = "T20"]
Flake8Print,
#[prefix = "PT"]
Flake8PytestStyle,
#[prefix = "Q"]
Flake8Quotes,
#[prefix = "RET"]
Flake8Return,
#[prefix = "SIM"]
Flake8Simplify,
#[prefix = "TID"]
Flake8TidyImports,
#[prefix = "ARG"]
Flake8UnusedArguments,
#[prefix = "DTZ"]
Flake8Datetimez,
#[prefix = "ERA"]
Eradicate,
#[prefix = "PD"]
PandasVet,
#[prefix = "PGH"]
PygrepHooks,
#[prefix = "PL"]
Pylint,
#[prefix = "PIE"]
Flake8Pie,
#[prefix = "COM"]
Flake8Commas,
#[prefix = "INP"]
Flake8NoPep420,
#[prefix = "EXE"]
Flake8Executable,
#[prefix = "TYP"]
Flake8TypeChecking,
#[prefix = "TRY"]
Tryceratops,
#[prefix = "RUF"]
Ruff,
}
pub trait ParseCode: Sized {
fn parse_code(code: &str) -> Option<(Self, &str)>;
}
pub enum Prefixes {
Single(RuleCodePrefix),
Multiple(Vec<(RuleCodePrefix, &'static str)>),
Single(RuleSelector),
Multiple(Vec<(RuleSelector, &'static str)>),
}
impl Prefixes {
@@ -481,54 +540,57 @@ impl Prefixes {
}
}
include!(concat!(env!("OUT_DIR"), "/origin.rs"));
include!(concat!(env!("OUT_DIR"), "/linter.rs"));
impl RuleOrigin {
impl Linter {
pub fn prefixes(&self) -> Prefixes {
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::Eradicate => Prefixes::Single(RuleSelector::ERA),
Linter::Flake82020 => Prefixes::Single(RuleSelector::YTT),
Linter::Flake8Annotations => Prefixes::Single(RuleSelector::ANN),
Linter::Flake8Bandit => Prefixes::Single(RuleSelector::S),
Linter::Flake8BlindExcept => Prefixes::Single(RuleSelector::BLE),
Linter::Flake8BooleanTrap => Prefixes::Single(RuleSelector::FBT),
Linter::Flake8Bugbear => Prefixes::Single(RuleSelector::B),
Linter::Flake8Builtins => Prefixes::Single(RuleSelector::A),
Linter::Flake8Comprehensions => Prefixes::Single(RuleSelector::C4),
Linter::Flake8Datetimez => Prefixes::Single(RuleSelector::DTZ),
Linter::Flake8Debugger => Prefixes::Single(RuleSelector::T10),
Linter::Flake8ErrMsg => Prefixes::Single(RuleSelector::EM),
Linter::Flake8ImplicitStrConcat => Prefixes::Single(RuleSelector::ISC),
Linter::Flake8ImportConventions => Prefixes::Single(RuleSelector::ICN),
Linter::Flake8Print => Prefixes::Single(RuleSelector::T20),
Linter::Flake8PytestStyle => Prefixes::Single(RuleSelector::PT),
Linter::Flake8Quotes => Prefixes::Single(RuleSelector::Q),
Linter::Flake8Return => Prefixes::Single(RuleSelector::RET),
Linter::Flake8Simplify => Prefixes::Single(RuleSelector::SIM),
Linter::Flake8TidyImports => Prefixes::Single(RuleSelector::TID),
Linter::Flake8UnusedArguments => Prefixes::Single(RuleSelector::ARG),
Linter::Isort => Prefixes::Single(RuleSelector::I),
Linter::McCabe => Prefixes::Single(RuleSelector::C90),
Linter::PEP8Naming => Prefixes::Single(RuleSelector::N),
Linter::PandasVet => Prefixes::Single(RuleSelector::PD),
Linter::Pycodestyle => Prefixes::Multiple(vec![
(RuleSelector::E, "Error"),
(RuleSelector::W, "Warning"),
]),
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::Pydocstyle => Prefixes::Single(RuleSelector::D),
Linter::Pyflakes => Prefixes::Single(RuleSelector::F),
Linter::PygrepHooks => Prefixes::Single(RuleSelector::PGH),
Linter::Pylint => Prefixes::Multiple(vec![
(RuleSelector::PLC, "Convention"),
(RuleSelector::PLE, "Error"),
(RuleSelector::PLR, "Refactor"),
(RuleSelector::PLW, "Warning"),
]),
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),
Linter::Pyupgrade => Prefixes::Single(RuleSelector::UP),
Linter::Flake8Pie => Prefixes::Single(RuleSelector::PIE),
Linter::Flake8Commas => Prefixes::Single(RuleSelector::COM),
Linter::Flake8NoPep420 => Prefixes::Single(RuleSelector::INP),
Linter::Flake8Executable => Prefixes::Single(RuleSelector::EXE),
Linter::Flake8TypeChecking => Prefixes::Single(RuleSelector::TYP),
Linter::Tryceratops => Prefixes::Single(RuleSelector::TRY),
Linter::Ruff => Prefixes::Single(RuleSelector::RUF),
}
}
}
@@ -543,81 +605,45 @@ pub enum LintSource {
Filesystem,
}
impl RuleCode {
impl Rule {
/// The source for the diagnostic (either the AST, the filesystem, or the
/// physical lines).
pub fn lint_source(&self) -> &'static LintSource {
match self {
RuleCode::RUF100 => &LintSource::NoQa,
RuleCode::E501
| RuleCode::W292
| RuleCode::W505
| RuleCode::UP009
| RuleCode::PGH003
| RuleCode::PGH004 => &LintSource::Lines,
RuleCode::ERA001
| RuleCode::ISC001
| RuleCode::ISC002
| RuleCode::Q000
| RuleCode::Q001
| RuleCode::Q002
| RuleCode::Q003
| RuleCode::W605
| RuleCode::COM812
| RuleCode::COM818
| RuleCode::COM819
| RuleCode::RUF001
| RuleCode::RUF002
| RuleCode::RUF003 => &LintSource::Tokens,
RuleCode::E902 => &LintSource::Io,
RuleCode::I001 | RuleCode::I002 => &LintSource::Imports,
RuleCode::INP001 => &LintSource::Filesystem,
Rule::UnusedNOQA => &LintSource::NoQa,
Rule::BlanketNOQA
| Rule::BlanketTypeIgnore
| Rule::DocLineTooLong
| Rule::LineTooLong
| Rule::MixedSpacesAndTabs
| Rule::NoNewLineAtEndOfFile
| Rule::PEP3120UnnecessaryCodingComment
| Rule::ShebangNewline
| Rule::ShebangPython
| Rule::ShebangWhitespace => &LintSource::Lines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
| Rule::AvoidQuoteEscape
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
| Rule::CommentedOutCode
| Rule::ExtraneousParentheses
| Rule::InvalidEscapeSequence
| Rule::MultiLineImplicitStringConcatenation
| Rule::SingleLineImplicitStringConcatenation
| Rule::TrailingCommaMissing
| Rule::TrailingCommaOnBareTupleProhibited
| Rule::TrailingCommaProhibited => &LintSource::Tokens,
Rule::IOError => &LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
Rule::ImplicitNamespacePackage => &LintSource::Filesystem,
_ => &LintSource::Ast,
}
}
}
impl DiagnosticKind {
/// The summary text for the diagnostic. Typically a truncated form of the
/// body text.
pub fn summary(&self) -> String {
match self {
DiagnosticKind::UnaryPrefixIncrement(..) => {
"Python does not support the unary prefix increment".to_string()
}
DiagnosticKind::UnusedLoopControlVariable(violations::UnusedLoopControlVariable(
name,
)) => {
format!("Loop control variable `{name}` not used within the loop body")
}
DiagnosticKind::NoAssertRaisesException(..) => {
"`assertRaises(Exception)` should be considered evil".to_string()
}
DiagnosticKind::StarArgUnpackingAfterKeywordArg(..) => {
"Star-arg unpacking after a keyword argument is strongly discouraged".to_string()
}
// flake8-datetimez
DiagnosticKind::CallDatetimeToday(..) => {
"The use of `datetime.datetime.today()` is not allowed".to_string()
}
DiagnosticKind::CallDatetimeUtcnow(..) => {
"The use of `datetime.datetime.utcnow()` is not allowed".to_string()
}
DiagnosticKind::CallDatetimeUtcfromtimestamp(..) => {
"The use of `datetime.datetime.utcfromtimestamp()` is not allowed".to_string()
}
DiagnosticKind::CallDateToday(..) => {
"The use of `datetime.date.today()` is not allowed.".to_string()
}
DiagnosticKind::CallDateFromtimestamp(..) => {
"The use of `datetime.date.fromtimestamp()` is not allowed".to_string()
}
_ => self.body(),
}
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Diagnostic {
pub kind: DiagnosticKind,
@@ -650,95 +676,88 @@ impl Diagnostic {
}
/// Pairs of checks that shouldn't be enabled together.
pub const INCOMPATIBLE_CODES: &[(RuleCode, RuleCode, &str)] = &[(
RuleCode::D203,
RuleCode::D211,
pub const INCOMPATIBLE_CODES: &[(Rule, Rule, &str)] = &[(
Rule::OneBlankLineBeforeClass,
Rule::NoBlankLineBeforeClass,
"`D203` (OneBlankLineBeforeClass) and `D211` (NoBlankLinesBeforeClass) are incompatible. \
Consider adding `D203` to `ignore`.",
)];
/// A hash map from deprecated to latest `RuleCode`.
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, RuleCode>> = Lazy::new(|| {
/// A hash map from deprecated to latest `Rule`.
pub static CODE_REDIRECTS: Lazy<FxHashMap<&'static str, Rule>> = Lazy::new(|| {
FxHashMap::from_iter([
// TODO(charlie): Remove by 2023-01-01.
("U001", RuleCode::UP001),
("U003", RuleCode::UP003),
("U004", RuleCode::UP004),
("U005", RuleCode::UP005),
("U006", RuleCode::UP006),
("U007", RuleCode::UP007),
("U008", RuleCode::UP008),
("U009", RuleCode::UP009),
("U010", RuleCode::UP010),
("U011", RuleCode::UP011),
("U012", RuleCode::UP012),
("U013", RuleCode::UP013),
("U014", RuleCode::UP014),
("U015", RuleCode::UP015),
("U016", RuleCode::UP016),
("U017", RuleCode::UP017),
("U019", RuleCode::UP019),
("U001", Rule::UselessMetaclassType),
("U003", Rule::TypeOfPrimitive),
("U004", Rule::UselessObjectInheritance),
("U005", Rule::DeprecatedUnittestAlias),
("U006", Rule::UsePEP585Annotation),
("U007", Rule::UsePEP604Annotation),
("U008", Rule::SuperCallWithParameters),
("U009", Rule::PEP3120UnnecessaryCodingComment),
("U010", Rule::UnnecessaryFutureImport),
("U011", Rule::LRUCacheWithoutParameters),
("U012", Rule::UnnecessaryEncodeUTF8),
("U013", Rule::ConvertTypedDictFunctionalToClass),
("U014", Rule::ConvertNamedTupleFunctionalToClass),
("U015", Rule::RedundantOpenModes),
("U016", Rule::RemoveSixCompat),
("U017", Rule::DatetimeTimezoneUTC),
("U019", Rule::TypingTextStrAlias),
// TODO(charlie): Remove by 2023-02-01.
("I252", RuleCode::TID252),
("M001", RuleCode::RUF100),
("I252", Rule::RelativeImports),
("M001", Rule::UnusedNOQA),
// TODO(charlie): Remove by 2023-02-01.
("PDV002", RuleCode::PD002),
("PDV003", RuleCode::PD003),
("PDV004", RuleCode::PD004),
("PDV007", RuleCode::PD007),
("PDV008", RuleCode::PD008),
("PDV009", RuleCode::PD009),
("PDV010", RuleCode::PD010),
("PDV011", RuleCode::PD011),
("PDV012", RuleCode::PD012),
("PDV013", RuleCode::PD013),
("PDV015", RuleCode::PD015),
("PDV901", RuleCode::PD901),
("PDV002", Rule::UseOfInplaceArgument),
("PDV003", Rule::UseOfDotIsNull),
("PDV004", Rule::UseOfDotNotNull),
("PDV007", Rule::UseOfDotIx),
("PDV008", Rule::UseOfDotAt),
("PDV009", Rule::UseOfDotIat),
("PDV010", Rule::UseOfDotPivotOrUnstack),
("PDV011", Rule::UseOfDotValues),
("PDV012", Rule::UseOfDotReadTable),
("PDV013", Rule::UseOfDotStack),
("PDV015", Rule::UseOfPdMerge),
("PDV901", Rule::DfIsABadVariableName),
// TODO(charlie): Remove by 2023-02-01.
("R501", RuleCode::RET501),
("R502", RuleCode::RET502),
("R503", RuleCode::RET503),
("R504", RuleCode::RET504),
("R505", RuleCode::RET505),
("R506", RuleCode::RET506),
("R507", RuleCode::RET507),
("R508", RuleCode::RET508),
("R501", Rule::UnnecessaryReturnNone),
("R502", Rule::ImplicitReturnValue),
("R503", Rule::ImplicitReturn),
("R504", Rule::UnnecessaryAssign),
("R505", Rule::SuperfluousElseReturn),
("R506", Rule::SuperfluousElseRaise),
("R507", Rule::SuperfluousElseContinue),
("R508", Rule::SuperfluousElseBreak),
// TODO(charlie): Remove by 2023-02-01.
("IC001", RuleCode::ICN001),
("IC002", RuleCode::ICN001),
("IC003", RuleCode::ICN001),
("IC004", RuleCode::ICN001),
("IC001", Rule::ImportAliasIsNotConventional),
("IC002", Rule::ImportAliasIsNotConventional),
("IC003", Rule::ImportAliasIsNotConventional),
("IC004", Rule::ImportAliasIsNotConventional),
])
});
#[cfg(test)]
mod tests {
use std::str::FromStr;
use strum::IntoEnumIterator;
use crate::registry::RuleCode;
use super::{Linter, ParseCode, Rule};
#[test]
fn check_code_serialization() {
for check_code in RuleCode::iter() {
for rule in Rule::iter() {
assert!(
RuleCode::from_str(check_code.as_ref()).is_ok(),
"{check_code:?} could not be round-trip serialized."
Rule::from_code(rule.code()).is_ok(),
"{rule:?} could not be round-trip serialized."
);
}
}
#[test]
fn fixable_codes() {
for check_code in RuleCode::iter() {
let kind = check_code.kind();
if kind.fixable() {
assert!(
kind.commit().is_some(),
"{check_code:?} is fixable but has no commit message."
);
}
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

@@ -4,19 +4,18 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings;
#[test_case(RuleCode::ERA001, Path::new("ERA001.py"); "ERA001")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
#[test_case(Rule::CommentedOutCode, Path::new("ERA001.py"); "ERA001")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/eradicate")
.join(path)

View File

@@ -3,7 +3,7 @@ use rustpython_ast::Location;
use super::detection::comment_contains_code;
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::settings::{flags, Settings};
use crate::source_code::Locator;
use crate::violations;
@@ -35,7 +35,7 @@ pub fn commented_out_code(
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(&RuleCode::ERA001)
&& settings.rules.should_fix(&Rule::CommentedOutCode)
{
diagnostic.amend(Fix::deletion(location, end_location));
}

View File

@@ -3,28 +3,27 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings;
#[test_case(RuleCode::YTT101, Path::new("YTT101.py"); "YTT101")]
#[test_case(RuleCode::YTT102, Path::new("YTT102.py"); "YTT102")]
#[test_case(RuleCode::YTT103, Path::new("YTT103.py"); "YTT103")]
#[test_case(RuleCode::YTT201, Path::new("YTT201.py"); "YTT201")]
#[test_case(RuleCode::YTT202, Path::new("YTT202.py"); "YTT202")]
#[test_case(RuleCode::YTT203, Path::new("YTT203.py"); "YTT203")]
#[test_case(RuleCode::YTT204, Path::new("YTT204.py"); "YTT204")]
#[test_case(RuleCode::YTT301, Path::new("YTT301.py"); "YTT301")]
#[test_case(RuleCode::YTT302, Path::new("YTT302.py"); "YTT302")]
#[test_case(RuleCode::YTT303, Path::new("YTT303.py"); "YTT303")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
#[test_case(Rule::SysVersionSlice3Referenced, Path::new("YTT101.py"); "YTT101")]
#[test_case(Rule::SysVersion2Referenced, Path::new("YTT102.py"); "YTT102")]
#[test_case(Rule::SysVersionCmpStr3, Path::new("YTT103.py"); "YTT103")]
#[test_case(Rule::SysVersionInfo0Eq3Referenced, Path::new("YTT201.py"); "YTT201")]
#[test_case(Rule::SixPY3Referenced, Path::new("YTT202.py"); "YTT202")]
#[test_case(Rule::SysVersionInfo1CmpInt, Path::new("YTT203.py"); "YTT203")]
#[test_case(Rule::SysVersionInfoMinorCmpInt, Path::new("YTT204.py"); "YTT204")]
#[test_case(Rule::SysVersion0Referenced, Path::new("YTT301.py"); "YTT301")]
#[test_case(Rule::SysVersionCmpStr10, Path::new("YTT302.py"); "YTT302")]
#[test_case(Rule::SysVersionSlice1Referenced, Path::new("YTT303.py"); "YTT303")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_2020")
.join(path)

View File

@@ -3,7 +3,7 @@ use rustpython_ast::{Cmpop, Constant, Expr, ExprKind, Located};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::violations;
fn is_sys(checker: &Checker, expr: &Expr, target: &str) -> bool {
@@ -27,13 +27,21 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
..
} = &upper.node
{
if *i == BigInt::from(1) && checker.settings.rules.enabled(&RuleCode::YTT303) {
if *i == BigInt::from(1)
&& checker
.settings
.rules
.enabled(&Rule::SysVersionSlice1Referenced)
{
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionSlice1Referenced,
Range::from_located(value),
));
} else if *i == BigInt::from(3)
&& checker.settings.rules.enabled(&RuleCode::YTT101)
&& checker
.settings
.rules
.enabled(&Rule::SysVersionSlice3Referenced)
{
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionSlice3Referenced,
@@ -47,12 +55,15 @@ pub fn subscript(checker: &mut Checker, value: &Expr, slice: &Expr) {
value: Constant::Int(i),
..
} => {
if *i == BigInt::from(2) && checker.settings.rules.enabled(&RuleCode::YTT102) {
if *i == BigInt::from(2)
&& checker.settings.rules.enabled(&Rule::SysVersion2Referenced)
{
checker.diagnostics.push(Diagnostic::new(
violations::SysVersion2Referenced,
Range::from_located(value),
));
} else if *i == BigInt::from(0) && checker.settings.rules.enabled(&RuleCode::YTT301)
} else if *i == BigInt::from(0)
&& checker.settings.rules.enabled(&Rule::SysVersion0Referenced)
{
checker.diagnostics.push(Diagnostic::new(
violations::SysVersion0Referenced,
@@ -89,7 +100,10 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
) = (ops, comparators)
{
if *n == BigInt::from(3)
&& checker.settings.rules.enabled(&RuleCode::YTT201)
&& checker
.settings
.rules
.enabled(&Rule::SysVersionInfo0Eq3Referenced)
{
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionInfo0Eq3Referenced,
@@ -110,7 +124,7 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
}],
) = (ops, comparators)
{
if checker.settings.rules.enabled(&RuleCode::YTT203) {
if checker.settings.rules.enabled(&Rule::SysVersionInfo1CmpInt) {
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionInfo1CmpInt,
Range::from_located(left),
@@ -136,7 +150,11 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
}],
) = (ops, comparators)
{
if checker.settings.rules.enabled(&RuleCode::YTT204) {
if checker
.settings
.rules
.enabled(&Rule::SysVersionInfoMinorCmpInt)
{
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionInfoMinorCmpInt,
Range::from_located(left),
@@ -162,13 +180,13 @@ pub fn compare(checker: &mut Checker, left: &Expr, ops: &[Cmpop], comparators: &
) = (ops, comparators)
{
if s.len() == 1 {
if checker.settings.rules.enabled(&RuleCode::YTT302) {
if checker.settings.rules.enabled(&Rule::SysVersionCmpStr10) {
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionCmpStr10,
Range::from_located(left),
));
}
} else if checker.settings.rules.enabled(&RuleCode::YTT103) {
} else if checker.settings.rules.enabled(&Rule::SysVersionCmpStr3) {
checker.diagnostics.push(Diagnostic::new(
violations::SysVersionCmpStr3,
Range::from_located(left),

View File

@@ -11,7 +11,7 @@ mod tests {
use anyhow::Result;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings::Settings;
#[test]
@@ -20,17 +20,17 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_annotations/annotation_presence.py"),
&Settings {
..Settings::for_rules(vec![
RuleCode::ANN001,
RuleCode::ANN002,
RuleCode::ANN003,
RuleCode::ANN101,
RuleCode::ANN102,
RuleCode::ANN201,
RuleCode::ANN202,
RuleCode::ANN204,
RuleCode::ANN205,
RuleCode::ANN206,
RuleCode::ANN401,
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
])
},
)?;
@@ -50,11 +50,11 @@ mod tests {
allow_star_arg_any: false,
},
..Settings::for_rules(vec![
RuleCode::ANN001,
RuleCode::ANN002,
RuleCode::ANN003,
RuleCode::ANN101,
RuleCode::ANN102,
Rule::MissingTypeFunctionArgument,
Rule::MissingTypeArgs,
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
Rule::MissingTypeCls,
])
},
)?;
@@ -74,11 +74,11 @@ mod tests {
allow_star_arg_any: false,
},
..Settings::for_rules(vec![
RuleCode::ANN201,
RuleCode::ANN202,
RuleCode::ANN204,
RuleCode::ANN205,
RuleCode::ANN206,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
])
},
)?;
@@ -98,11 +98,11 @@ mod tests {
allow_star_arg_any: false,
},
..Settings::for_rules(vec![
RuleCode::ANN201,
RuleCode::ANN202,
RuleCode::ANN204,
RuleCode::ANN205,
RuleCode::ANN206,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
])
},
)?;
@@ -121,7 +121,7 @@ mod tests {
suppress_none_returning: false,
allow_star_arg_any: true,
},
..Settings::for_rules(vec![RuleCode::ANN401])
..Settings::for_rules(vec![Rule::DynamicallyTypedExpression])
},
)?;
insta::assert_yaml_snapshot!(diagnostics);
@@ -134,11 +134,11 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_annotations/allow_overload.py"),
&Settings {
..Settings::for_rules(vec![
RuleCode::ANN201,
RuleCode::ANN202,
RuleCode::ANN204,
RuleCode::ANN205,
RuleCode::ANN206,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
])
},
)?;
@@ -152,11 +152,11 @@ mod tests {
Path::new("./resources/test/fixtures/flake8_annotations/allow_nested_overload.py"),
&Settings {
..Settings::for_rules(vec![
RuleCode::ANN201,
RuleCode::ANN202,
RuleCode::ANN204,
RuleCode::ANN205,
RuleCode::ANN206,
Rule::MissingReturnTypePublicFunction,
Rule::MissingReturnTypePrivateFunction,
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
])
},
)?;

View File

@@ -8,7 +8,7 @@ use crate::ast::visitor::Visitor;
use crate::ast::{cast, helpers, visitor};
use crate::checkers::ast::Checker;
use crate::docstrings::definition::{Definition, DefinitionKind};
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::visibility::Visibility;
use crate::{violations, visibility};
@@ -85,14 +85,22 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
.chain(args.kwonlyargs.iter())
{
if let Some(expr) = &arg.node.annotation {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
check_dynamically_typed(checker, expr, || arg.node.arg.to_string());
};
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&RuleCode::ANN001) {
if checker
.settings
.rules
.enabled(&Rule::MissingTypeFunctionArgument)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeFunctionArgument(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -106,8 +114,12 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if let Some(arg) = &args.vararg {
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
let name = arg.node.arg.to_string();
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
@@ -115,7 +127,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&RuleCode::ANN002) {
if checker.settings.rules.enabled(&Rule::MissingTypeArgs) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeArgs(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -129,8 +141,12 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if let Some(arg) = &args.kwarg {
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
let name = arg.node.arg.to_string();
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
@@ -138,7 +154,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&RuleCode::ANN003) {
if checker.settings.rules.enabled(&Rule::MissingTypeKwargs) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeKwargs(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -150,7 +166,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
check_dynamically_typed(checker, expr, || name.to_string());
};
} else {
@@ -164,7 +184,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
match visibility {
Visibility::Public => {
if checker.settings.rules.enabled(&RuleCode::ANN201) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePublicFunction)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypePublicFunction(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
@@ -172,7 +196,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
Visibility::Private => {
if checker.settings.rules.enabled(&RuleCode::ANN202) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePrivateFunction)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypePrivateFunction(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
@@ -203,14 +231,22 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker.settings.rules.enabled(&RuleCode::ANN401) {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
check_dynamically_typed(checker, annotation, || arg.node.arg.to_string());
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&RuleCode::ANN001) {
if checker
.settings
.rules
.enabled(&Rule::MissingTypeFunctionArgument)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeFunctionArgument(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -225,8 +261,12 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
has_any_typed_arg = true;
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
let name = arg.node.arg.to_string();
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("*{name}"));
}
}
@@ -234,7 +274,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&RuleCode::ANN002) {
if checker.settings.rules.enabled(&Rule::MissingTypeArgs) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeArgs(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -249,8 +289,12 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
has_any_typed_arg = true;
if let Some(expr) = &arg.node.annotation {
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
let name = arg.node.arg.to_string();
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
let name = &arg.node.arg;
check_dynamically_typed(checker, expr, || format!("**{name}"));
}
}
@@ -258,7 +302,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.node.arg))
{
if checker.settings.rules.enabled(&RuleCode::ANN003) {
if checker.settings.rules.enabled(&Rule::MissingTypeKwargs) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeKwargs(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -273,14 +317,14 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
if let Some(arg) = args.args.first() {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(&RuleCode::ANN102) {
if checker.settings.rules.enabled(&Rule::MissingTypeCls) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeCls(arg.node.arg.to_string()),
Range::from_located(arg),
));
}
} else {
if checker.settings.rules.enabled(&RuleCode::ANN101) {
if checker.settings.rules.enabled(&Rule::MissingTypeSelf) {
checker.diagnostics.push(Diagnostic::new(
violations::MissingTypeSelf(arg.node.arg.to_string()),
Range::from_located(arg),
@@ -293,7 +337,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
// ANN201, ANN202
if let Some(expr) = &returns {
if checker.settings.rules.enabled(&RuleCode::ANN401) {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
check_dynamically_typed(checker, expr, || name.to_string());
}
} else {
@@ -306,14 +354,22 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(&RuleCode::ANN206) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeClassMethod)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypeClassMethod(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
));
}
} else if visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(&RuleCode::ANN205) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeStaticMethod)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypeStaticMethod(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
@@ -322,7 +378,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
} else if visibility::is_init(cast::name(stmt)) {
// Allow omission of return annotation in `__init__` functions, as long as at
// least one argument is typed.
if checker.settings.rules.enabled(&RuleCode::ANN204) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeSpecialMethod)
{
if !(checker.settings.flake8_annotations.mypy_init_return
&& has_any_typed_arg)
{
@@ -330,7 +390,7 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
violations::MissingReturnTypeSpecialMethod(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
);
if checker.patch(diagnostic.kind.code()) {
if checker.patch(diagnostic.kind.rule()) {
match fixes::add_return_none_annotation(checker.locator, stmt) {
Ok(fix) => {
diagnostic.amend(fix);
@@ -342,7 +402,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
} else if visibility::is_magic(cast::name(stmt)) {
if checker.settings.rules.enabled(&RuleCode::ANN204) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypeSpecialMethod)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypeSpecialMethod(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
@@ -351,7 +415,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
} else {
match visibility {
Visibility::Public => {
if checker.settings.rules.enabled(&RuleCode::ANN201) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePublicFunction)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypePublicFunction(name.to_string()),
helpers::identifier_range(stmt, checker.locator),
@@ -359,7 +427,11 @@ pub fn definition(checker: &mut Checker, definition: &Definition, visibility: &V
}
}
Visibility::Private => {
if checker.settings.rules.enabled(&RuleCode::ANN202) {
if checker
.settings
.rules
.enabled(&Rule::MissingReturnTypePrivateFunction)
{
checker.diagnostics.push(Diagnostic::new(
violations::MissingReturnTypePrivateFunction(name.to_string()),
helpers::identifier_range(stmt, checker.locator),

View File

@@ -11,26 +11,26 @@ mod tests {
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings::Settings;
#[test_case(RuleCode::S101, Path::new("S101.py"); "S101")]
#[test_case(RuleCode::S102, Path::new("S102.py"); "S102")]
#[test_case(RuleCode::S103, Path::new("S103.py"); "S103")]
#[test_case(RuleCode::S104, Path::new("S104.py"); "S104")]
#[test_case(RuleCode::S105, Path::new("S105.py"); "S105")]
#[test_case(RuleCode::S106, Path::new("S106.py"); "S106")]
#[test_case(RuleCode::S107, Path::new("S107.py"); "S107")]
#[test_case(RuleCode::S108, Path::new("S108.py"); "S108")]
#[test_case(RuleCode::S113, Path::new("S113.py"); "S113")]
#[test_case(RuleCode::S324, Path::new("S324.py"); "S324")]
#[test_case(RuleCode::S501, Path::new("S501.py"); "S501")]
#[test_case(RuleCode::S506, Path::new("S506.py"); "S506")]
#[test_case(RuleCode::S508, Path::new("S508.py"); "S508")]
#[test_case(RuleCode::S509, Path::new("S509.py"); "S509")]
#[test_case(RuleCode::S701, Path::new("S701.py"); "S701")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
#[test_case(Rule::AssertUsed, Path::new("S101.py"); "S101")]
#[test_case(Rule::ExecUsed, Path::new("S102.py"); "S102")]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"); "S103")]
#[test_case(Rule::HardcodedBindAllInterfaces, Path::new("S104.py"); "S104")]
#[test_case(Rule::HardcodedPasswordString, Path::new("S105.py"); "S105")]
#[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"); "S106")]
#[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"); "S107")]
#[test_case(Rule::HardcodedTempFile, Path::new("S108.py"); "S108")]
#[test_case(Rule::RequestWithoutTimeout, Path::new("S113.py"); "S113")]
#[test_case(Rule::HashlibInsecureHashFunction, Path::new("S324.py"); "S324")]
#[test_case(Rule::RequestWithNoCertValidation, Path::new("S501.py"); "S501")]
#[test_case(Rule::UnsafeYAMLLoad, Path::new("S506.py"); "S506")]
#[test_case(Rule::SnmpInsecureVersion, Path::new("S508.py"); "S508")]
#[test_case(Rule::SnmpWeakCryptography, Path::new("S509.py"); "S509")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bandit")
.join(path)
@@ -54,7 +54,7 @@ mod tests {
"/foo".to_string(),
],
},
..Settings::for_rule(RuleCode::S108)
..Settings::for_rule(Rule::HardcodedTempFile)
},
)?;
insta::assert_yaml_snapshot!("S108_extend", diagnostics);

View File

@@ -6,5 +6,8 @@ use crate::violations;
/// S101
pub fn assert_used(stmt: &Located<StmtKind>) -> Diagnostic {
Diagnostic::new(violations::AssertUsed, Range::from_located(stmt))
Diagnostic::new(
violations::AssertUsed,
Range::new(stmt.location, stmt.location.with_col_offset("assert".len())),
)
}

View File

@@ -9,7 +9,7 @@ expression: diagnostics
column: 0
end_location:
row: 2
column: 11
column: 6
fix: ~
parent: ~
- kind:
@@ -19,7 +19,7 @@ expression: diagnostics
column: 4
end_location:
row: 8
column: 17
column: 10
fix: ~
parent: ~
- kind:
@@ -29,7 +29,7 @@ expression: diagnostics
column: 4
end_location:
row: 11
column: 17
column: 10
fix: ~
parent: ~

View File

@@ -3,19 +3,18 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings;
#[test_case(RuleCode::BLE001, Path::new("BLE.py"); "BLE001")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
#[test_case(Rule::BlindExcept, Path::new("BLE.py"); "BLE001")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_blind_except")
.join(path)

View File

@@ -3,21 +3,20 @@ pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::convert::AsRef;
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings;
#[test_case(RuleCode::FBT001, Path::new("FBT.py"); "FBT001")]
#[test_case(RuleCode::FBT002, Path::new("FBT.py"); "FBT002")]
#[test_case(RuleCode::FBT003, Path::new("FBT.py"); "FBT003")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
#[test_case(Rule::BooleanPositionalArgInFunctionDefinition, Path::new("FBT.py"); "FBT001")]
#[test_case(Rule::BooleanDefaultValueInFunctionDefinition, Path::new("FBT.py"); "FBT002")]
#[test_case(Rule::BooleanPositionalValueInFunctionCall, Path::new("FBT.py"); "FBT003")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_boolean_trap")
.join(path)

View File

@@ -10,39 +10,39 @@ mod tests {
use test_case::test_case;
use crate::linter::test_path;
use crate::registry::RuleCode;
use crate::registry::Rule;
use crate::settings::Settings;
#[test_case(RuleCode::B002, Path::new("B002.py"); "B002")]
#[test_case(RuleCode::B003, Path::new("B003.py"); "B003")]
#[test_case(RuleCode::B004, Path::new("B004.py"); "B004")]
#[test_case(RuleCode::B005, Path::new("B005.py"); "B005")]
#[test_case(RuleCode::B006, Path::new("B006_B008.py"); "B006")]
#[test_case(RuleCode::B007, Path::new("B007.py"); "B007")]
#[test_case(RuleCode::B008, Path::new("B006_B008.py"); "B008")]
#[test_case(RuleCode::B009, Path::new("B009_B010.py"); "B009")]
#[test_case(RuleCode::B010, Path::new("B009_B010.py"); "B010")]
#[test_case(RuleCode::B011, Path::new("B011.py"); "B011")]
#[test_case(RuleCode::B012, Path::new("B012.py"); "B012")]
#[test_case(RuleCode::B013, Path::new("B013.py"); "B013")]
#[test_case(RuleCode::B014, Path::new("B014.py"); "B014")]
#[test_case(RuleCode::B015, Path::new("B015.py"); "B015")]
#[test_case(RuleCode::B016, Path::new("B016.py"); "B016")]
#[test_case(RuleCode::B017, Path::new("B017.py"); "B017")]
#[test_case(RuleCode::B018, Path::new("B018.py"); "B018")]
#[test_case(RuleCode::B019, Path::new("B019.py"); "B019")]
#[test_case(RuleCode::B020, Path::new("B020.py"); "B020")]
#[test_case(RuleCode::B021, Path::new("B021.py"); "B021")]
#[test_case(RuleCode::B022, Path::new("B022.py"); "B022")]
#[test_case(RuleCode::B023, Path::new("B023.py"); "B023")]
#[test_case(RuleCode::B024, Path::new("B024.py"); "B024")]
#[test_case(RuleCode::B025, Path::new("B025.py"); "B025")]
#[test_case(RuleCode::B026, Path::new("B026.py"); "B026")]
#[test_case(RuleCode::B027, Path::new("B027.py"); "B027")]
#[test_case(RuleCode::B904, Path::new("B904.py"); "B904")]
#[test_case(RuleCode::B905, Path::new("B905.py"); "B905")]
fn rules(rule_code: RuleCode, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
#[test_case(Rule::UnaryPrefixIncrement, Path::new("B002.py"); "B002")]
#[test_case(Rule::AssignmentToOsEnviron, Path::new("B003.py"); "B003")]
#[test_case(Rule::UnreliableCallableCheck, Path::new("B004.py"); "B004")]
#[test_case(Rule::StripWithMultiCharacters, Path::new("B005.py"); "B005")]
#[test_case(Rule::MutableArgumentDefault, Path::new("B006_B008.py"); "B006")]
#[test_case(Rule::UnusedLoopControlVariable, Path::new("B007.py"); "B007")]
#[test_case(Rule::FunctionCallArgumentDefault, Path::new("B006_B008.py"); "B008")]
#[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"); "B009")]
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"); "B010")]
#[test_case(Rule::DoNotAssertFalse, Path::new("B011.py"); "B011")]
#[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"); "B012")]
#[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"); "B013")]
#[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"); "B014")]
#[test_case(Rule::UselessComparison, Path::new("B015.py"); "B015")]
#[test_case(Rule::CannotRaiseLiteral, Path::new("B016.py"); "B016")]
#[test_case(Rule::NoAssertRaisesException, Path::new("B017.py"); "B017")]
#[test_case(Rule::UselessExpression, Path::new("B018.py"); "B018")]
#[test_case(Rule::CachedInstanceMethod, Path::new("B019.py"); "B019")]
#[test_case(Rule::LoopVariableOverridesIterator, Path::new("B020.py"); "B020")]
#[test_case(Rule::FStringDocstring, Path::new("B021.py"); "B021")]
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"); "B022")]
#[test_case(Rule::FunctionUsesLoopVariable, Path::new("B023.py"); "B023")]
#[test_case(Rule::AbstractBaseClassWithoutAbstractMethod, Path::new("B024.py"); "B024")]
#[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"); "B025")]
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"); "B026")]
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"); "B027")]
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"); "B904")]
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"); "B905")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("./resources/test/fixtures/flake8_bugbear")
.join(path)
@@ -65,7 +65,7 @@ mod tests {
"fastapi.Query".to_string(),
],
},
..Settings::for_rules(vec![RuleCode::B008])
..Settings::for_rules(vec![Rule::FunctionCallArgumentDefault])
},
)?;
insta::assert_yaml_snapshot!(snapshot, diagnostics);

View File

@@ -2,7 +2,7 @@ use rustpython_ast::{Constant, Expr, ExprKind, Keyword, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::violations;
use crate::visibility::{is_abstract, is_overload};
@@ -78,7 +78,11 @@ pub fn abstract_base_class(
let has_abstract_decorator = is_abstract(checker, decorator_list);
has_abstract_method |= has_abstract_decorator;
if !checker.settings.rules.enabled(&RuleCode::B027) {
if !checker
.settings
.rules
.enabled(&Rule::EmptyMethodWithoutAbstractDecorator)
{
continue;
}
@@ -89,7 +93,11 @@ pub fn abstract_base_class(
));
}
}
if checker.settings.rules.enabled(&RuleCode::B024) {
if checker
.settings
.rules
.enabled(&Rule::AbstractBaseClassWithoutAbstractMethod)
{
if !has_abstract_method {
checker.diagnostics.push(Diagnostic::new(
violations::AbstractBaseClassWithoutAbstractMethod(name.to_string()),

View File

@@ -47,7 +47,7 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option
};
let mut diagnostic = Diagnostic::new(violations::DoNotAssertFalse, Range::from_located(test));
if checker.patch(diagnostic.kind.code()) {
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_stmt(&assertion_error(msg));
diagnostic.amend(Fix::replacement(

View File

@@ -1,3 +1,13 @@
//! Checks for `self.assertRaises(Exception)`.
//!
//! ## Why is this bad?
//!
//! `assertRaises(Exception)` should be considered evil. It can lead to your
//! test passing even if the code being tested is never executed due to a
//! typo. Either assert for a more specific exception (builtin or
//! custom), use `assertRaisesRegex`, or use the context manager form of
//! `assertRaises`.
use rustpython_ast::{ExprKind, Stmt, Withitem};
use crate::ast::types::Range;

View File

@@ -6,7 +6,7 @@ use crate::ast::helpers;
use crate::ast::types::{CallPath, Range};
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::{Diagnostic, RuleCode};
use crate::registry::{Diagnostic, Rule};
use crate::source_code::Generator;
use crate::violations;
@@ -41,7 +41,11 @@ fn duplicate_handler_exceptions<'a>(
}
}
if checker.settings.rules.enabled(&RuleCode::B014) {
if checker
.settings
.rules
.enabled(&Rule::DuplicateHandlerException)
{
// TODO(charlie): Handle "BaseException" and redundant exception aliases.
if !duplicates.is_empty() {
let mut diagnostic = Diagnostic::new(
@@ -54,7 +58,7 @@ fn duplicate_handler_exceptions<'a>(
),
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.code()) {
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
if unique_elts.len() == 1 {
generator.unparse_expr(unique_elts[0], 0);
@@ -105,7 +109,11 @@ pub fn duplicate_exceptions(checker: &mut Checker, handlers: &[Excepthandler]) {
}
}
if checker.settings.rules.enabled(&RuleCode::B025) {
if checker
.settings
.rules
.enabled(&Rule::DuplicateTryBlockException)
{
for (name, exprs) in duplicates {
for expr in exprs {
checker.diagnostics.push(Diagnostic::new(

View File

@@ -3,7 +3,7 @@ use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::identifiers::is_identifier;
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::source_code::Generator;
@@ -38,7 +38,7 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
} = &arg.node else {
return;
};
if !IDENTIFIER_REGEX.is_match(value) {
if !is_identifier(value) {
return;
}
if KWLIST.contains(&value.as_str()) {
@@ -47,7 +47,7 @@ pub fn getattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
let mut diagnostic =
Diagnostic::new(violations::GetAttrWithConstant, Range::from_located(expr));
if checker.patch(diagnostic.kind.code()) {
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(&attribute(obj, value), 0);
diagnostic.amend(Fix::replacement(

View File

@@ -23,7 +23,7 @@ pub fn redundant_tuple_in_exception_handler(checker: &mut Checker, handlers: &[E
violations::RedundantTupleInExceptionHandler(elt.to_string()),
Range::from_located(type_),
);
if checker.patch(diagnostic.kind.code()) {
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(elt, 0);
diagnostic.amend(Fix::replacement(

View File

@@ -3,7 +3,7 @@ use rustpython_ast::{Constant, Expr, ExprContext, ExprKind, Location, Stmt, Stmt
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::python::identifiers::IDENTIFIER_REGEX;
use crate::python::identifiers::is_identifier;
use crate::python::keyword::KWLIST;
use crate::registry::Diagnostic;
use crate::source_code::{Generator, Stylist};
@@ -49,7 +49,7 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
} = &name.node else {
return;
};
if !IDENTIFIER_REGEX.is_match(name) {
if !is_identifier(name) {
return;
}
if KWLIST.contains(&name.as_str()) {
@@ -62,7 +62,7 @@ pub fn setattr_with_constant(checker: &mut Checker, expr: &Expr, func: &Expr, ar
if expr == child.as_ref() {
let mut diagnostic =
Diagnostic::new(violations::SetAttrWithConstant, Range::from_located(expr));
if checker.patch(diagnostic.kind.code()) {
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
assignment(obj, name, value, checker.stylist),
expr.location,

View File

@@ -1,3 +1,12 @@
//! Checks for `f(x=0, *(1, 2))`.
//!
//! ## Why is this bad?
//!
//! Star-arg unpacking after a keyword argument is strongly discouraged. It only
//! works when the keyword parameter is declared after all parameters supplied
//! by the unpacked sequence, and this change of ordering can surprise and
//! mislead readers.
use rustpython_ast::{Expr, ExprKind, Keyword};
use crate::ast::types::Range;

View File

@@ -1,3 +1,22 @@
//! Checks for `++n`.
//!
//! ## Why is this bad?
//!
//! Python does not support the unary prefix increment. Writing `++n` is
//! equivalent to `+(+(n))`, which equals `n`.
//!
//! ## Example
//!
//! ```python
//! ++n;
//! ```
//!
//! Use instead:
//!
//! ```python
//! n += 1
//! ```
use rustpython_ast::{Expr, ExprKind, Unaryop};
use crate::ast::types::Range;

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