Compare commits

...

204 Commits

Author SHA1 Message Date
Charlie Marsh
f8d46d09ef Implement asyncio-dangling-task to track asyncio.create_task calls (#2935)
This rule guards against `asyncio.create_task` usages of the form:

```py
asyncio.create_task(coordinator.ws_connect())  # Error
```

...which can lead to unexpected bugs due to the lack of a strong reference to the created task. See Will McGugan's blog post for reference: https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/.

Note that we can't detect issues like:

```py
def f():
    # Stored as `task`, but never used...
    task = asyncio.create_task(coordinator.ws_connect())
```

So that would be a false negative. But this catches the common case of failing to assign the task in any way.

Closes #2809.
2023-02-15 15:19:03 -05:00
Martin Fischer
294cd95c54 Update clap to fix ruff check --help description
My two clap bug fixes[1][2] have been merged and released
(see the change in README.md).

[1]: https://github.com/clap-rs/clap/pull/4710
[2]: https://github.com/clap-rs/clap/pull/4712
2023-02-15 13:30:06 -05:00
Charlie Marsh
d8e709648d Add Transformers to README (#2933) 2023-02-15 17:09:50 +00:00
Charlie Marsh
52cc4d6537 Deduplicate files provided on the command-line (#2931) 2023-02-15 12:08:34 -05:00
messense
08e9d12137 Upload ruff binaries to GitHub release (#2930) 2023-02-15 12:07:47 -05:00
Charlie Marsh
39fdc71b49 Bump version to 0.0.247 (#2932) 2023-02-15 12:06:58 -05:00
Charlie Marsh
6b0736cf4b Allow private accesses on current class (#2929) 2023-02-15 16:52:05 +00:00
Charlie Marsh
58269a918a Apply nullable-model-string-field to all classes (#2928) 2023-02-15 15:54:14 +00:00
Sawbez
9168a12679 [docs] flake8-self Private member access docs (#2912) 2023-02-15 15:42:38 +00:00
Charlie Marsh
cb971d3a48 Respect self as positional-only argument in annotation rules (#2927) 2023-02-15 15:25:17 +00:00
Charlie Marsh
57a5071b4e Rename some methods on Locator (#2926) 2023-02-15 10:21:49 -05:00
Charlie Marsh
976fe364d4 Remove setup.py (#2922) 2023-02-15 14:35:11 +00:00
messense
028c7855b2 Simplify release workflows (#2913)
* No need to build for PyPy since it only contains a binary so the platform tag is the same as CPython
* Update `maturin-action` location
2023-02-15 08:15:19 -05:00
Nick Pope
e5179f67fd Remove autogenerated docs/rules/*.md files (#2917) 2023-02-15 08:11:11 -05:00
Simon Brugman
c9c199dbca Remove testing resource introduced in #2891 (#2916) 2023-02-15 08:10:58 -05:00
Martin Fischer
70e378b736 Implement shell autocompletion for rule codes
For example:

    $ ruff check --select=EM<Tab>
    EM          -- flake8-errmsg
    EM10   EM1  --
    EM101       -- raw-string-in-exception
    EM102       -- f-string-in-exception
    EM103       -- dot-format-in-exception

(You will need to enable autocompletion as described
 in the Autocompletion section in the README.)

Fixes #2808.

(The --help help change in the README is due to a clap bug,
 for which I already submitted a fix:
 https://github.com/clap-rs/clap/pull/4710.)
2023-02-15 08:09:34 -05:00
Charlie Marsh
ca49b00e55 Add initial formatter implementation (#2883)
# Summary

This PR contains the code for the autoformatter proof-of-concept.

## Crate structure

The primary formatting hook is the `fmt` function in `crates/ruff_python_formatter/src/lib.rs`.

The current formatter approach is outlined in `crates/ruff_python_formatter/src/lib.rs`, and is structured as follows:

- Tokenize the code using the RustPython lexer.
- In `crates/ruff_python_formatter/src/trivia.rs`, extract a variety of trivia tokens from the token stream. These include comments, trailing commas, and empty lines.
- Generate the AST via the RustPython parser.
- In `crates/ruff_python_formatter/src/cst.rs`, convert the AST to a CST structure. As of now, the CST is nearly identical to the AST, except that every node gets a `trivia` vector. But we might want to modify it further.
- In `crates/ruff_python_formatter/src/attachment.rs`, attach each trivia token to the corresponding CST node. The logic for this is mostly in `decorate_trivia` and is ported almost directly from Prettier (given each token, find its preceding, following, and enclosing nodes, then attach the token to the appropriate node in a second pass).
- In `crates/ruff_python_formatter/src/newlines.rs`, normalize newlines to match Black’s preferences. This involves traversing the CST and inserting or removing `TriviaToken` values as we go.
- Call `format!` on the CST, which delegates to type-specific formatter implementations (e.g., `crates/ruff_python_formatter/src/format/stmt.rs` for `Stmt` nodes, and similar for `Expr` nodes; the others are trivial). Those type-specific implementations delegate to kind-specific functions (e.g., `format_func_def`).

## Testing and iteration

The formatter is being developed against the Black test suite, which was copied over in-full to `crates/ruff_python_formatter/resources/test/fixtures/black`.

The Black fixtures had to be modified to create `[insta](https://github.com/mitsuhiko/insta)`-compatible snapshots, which now exist in the repo.

My approach thus far has been to try and improve coverage by tackling fixtures one-by-one.

## What works, and what doesn’t

- *Most* nodes are supported at a basic level (though there are a few stragglers at time of writing, like `StmtKind::Try`).
- Newlines are properly preserved in most cases.
- Magic trailing commas are properly preserved in some (but not all) cases.
- Trivial leading and trailing standalone comments mostly work (although maybe not at the end of a file).
- Inline comments, and comments within expressions, often don’t work -- they work in a few cases, but it’s one-off right now. (We’re probably associating them with the “right” nodes more often than we are actually rendering them in the right place.)
- We don’t properly normalize string quotes. (At present, we just repeat any constants verbatim.)
- We’re mishandling a bunch of wrapping cases (if we treat Black as the reference implementation). Here are a few examples (demonstrating Black's stable behavior):

```py
# In some cases, if the end expression is "self-closing" (functions,
# lists, dictionaries, sets, subscript accesses, and any length-two
# boolean operations that end in these elments), Black
# will wrap like this...
if some_expression and f(
    b,
    c,
    d,
):
    pass

# ...whereas we do this:
if (
    some_expression
    and f(
        b,
        c,
        d,
    )
):
    pass

# If function arguments can fit on a single line, then Black will
# format them like this, rather than exploding them vertically.
if f(
    a, b, c, d, e, f, g, ...
):
    pass
```

- We don’t properly preserve parentheses in all cases. Black preserves parentheses in some but not all cases.
2023-02-15 04:06:35 +00:00
Charlie Marsh
f661c90bd7 Remove dependency on ruff_rowan (#2875)
This PR removes the dependency on `ruff_rowan` (i.e., Rome's fork of rust-analyzer's `rowan`), and in turn, trims out a lot of code in `ruff_formatter` that isn't necessary (or isn't _yet_ necessary) to power the autoformatter.

We may end up pulling some of this back in -- TBD. For example, the autoformatter has its own comment representation right now, but we may eventually want to use the `comments.rs` data structures defined in `rome_formatter`.
2023-02-15 03:54:08 +00:00
Charlie Marsh
5a84df293f Allow printing of consecutive empty lines (#2874) 2023-02-14 22:35:02 -05:00
Charlie Marsh
23d9309111 Remove JetBrains webinar badge (#2910) 2023-02-15 03:28:12 +00:00
Charlie Marsh
98ea94fdb7 Add StaticTextSlice kind to FormatElement enum (#2873)
Given our current parser abstractions, we need the ability to tell `ruff_formatter` to print a pre-defined slice from a fixed string of source code, which we've introduced here as `FormatElement::StaticTextSlice`.
2023-02-14 22:27:52 -05:00
Charlie Marsh
746e1d3436 Add contributors to acknowledgements (#2909) 2023-02-15 03:15:38 +00:00
Charlie Marsh
016ff01a04 Add an FAQ question around Python version support (#2908) 2023-02-15 03:11:44 +00:00
Charlie Marsh
298498e934 Add an Acknowledgements section to the README (#2907) 2023-02-15 00:25:07 +00:00
Charlie Marsh
3ef1c2e303 Add rome_formatter fork as ruff_formatter (#2872)
The Ruff autoformatter is going to be based on an intermediate representation (IR) formatted via [Wadler's algorithm](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf). This is architecturally similar to [Rome](https://github.com/rome/tools), Prettier, [Skip](https://github.com/skiplang/skip/blob/master/src/tools/printer/printer.sk), and others.

This PR adds a fork of the `rome_formatter` crate from [Rome](https://github.com/rome/tools), renamed here to `ruff_formatter`, which provides generic definitions for a formatter IR as well as a generic IR printer. (We've also pulled in `rome_rowan`, `rome_text_size`, and `rome_text_edit`, though some of these will be removed in future PRs.)

Why fork? `rome_formatter` contains code that's specific to Rome's AST representation (e.g., it relies on a fork of rust-analyzer's `rowan`), and we'll likely want to support different abstractions and formatting capabilities (there are already a few changes coming in future PRs). Once we've dropped `ruff_rowan` and trimmed down `ruff_formatter` to the code we currently need, it's also not a huge surface area to maintain and update.
2023-02-14 19:22:55 -05:00
Simon Brugman
ac028cd9f8 [numpy] deprecated type aliases (#2810)
Closes https://github.com/charliermarsh/ruff/issues/2455

Used `NPY` as prefix code as agreed in the issue.
2023-02-14 23:45:12 +00:00
Edgar R. M
c0eb5c28d1 [docs] Add docs for flake8-errmsg (#2888) 2023-02-14 23:21:34 +00:00
Martin Fischer
a77b4566e4 Fix option links in mkdocs rule pages
In 28c9263722 I introduced automatic
linkification of option references in rule documentation,
which automatically converted the following:

    ## Options

    * `namespace-packages`

to:

    ## Options

    * [`namespace-packages`]

    [`namespace-packages`]: ../../settings#namespace-packages

While the above is a correct CommonMark[1] link definition,
what I was missing was that we used mkdocs for our documentation
generation, which as it turns out uses a non-CommonMark-compliant
Markdown parser, namely Python-Markdown, which contrary to CommonMark
doesn't support link definitions containing code tags.

This commit fixes the broken links via a regex hack.

[1]: https://commonmark.org/
2023-02-14 17:56:21 -05:00
Martin Fischer
860993187e Fix link relativization in generate_mkdocs.py 2023-02-14 17:56:21 -05:00
Charlie Marsh
58d4e00604 Add publish = false to unpublished crates (#2905) 2023-02-14 22:41:14 +00:00
Simon Legner
2d95912699 docs: fix ruff generate-shell-completion (#2904) 2023-02-14 22:38:29 +00:00
Simon Brugman
4f927fbacc [flake8-tidy-imports] autofix relative imports (#2891)
Previous fix was bugged. This one is only fixing when the `module_path` is present, making it far more robust.

Closes #2764 and closes #2869
2023-02-14 22:24:59 +00:00
Anders Kaseorg
2e41301520 Switch some quotes to backticks in errors (#2889)
Improves consistency with the style decision in #723, I think.
2023-02-14 22:24:41 +00:00
Martin Fischer
3179fc110d Disable many-to-one mapping for now 2023-02-14 16:16:12 -05:00
Martin Fischer
03ae0118b7 many-to-one 9/9: Update table generation 2023-02-14 16:16:12 -05:00
Martin Fischer
05176890ee many-to-one 8/9: Drop codes from registry
This commit was generated by running:

    fastmod --accept-all '[A-Z]+[0-9]+ => ' '' crates/ruff/src/registry.rs
2023-02-14 16:16:12 -05:00
Martin Fischer
849b947b3e many-to-one 7/9: Update JSON schema 2023-02-14 16:16:12 -05:00
Martin Fischer
c314e10e54 many-to-one 6/9: Implement ruff_macros::map_codes 2023-02-14 16:16:12 -05:00
Martin Fischer
9eda286dcd many-to-one 5/9: Generate codes.rs from registry.rs
# This commit was generated by running the following Python code:
# (followed by `sed -Ei 's/(mod registry;)/\1mod codes;/' crates/ruff/src/lib.rs`
# and `cargo fmt`).

import json
import re
import subprocess

def parse_registry():
    file = open('crates/ruff/src/registry.rs')

    rules = []

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

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

    while (line := next(file)) != 'pub enum Linter {\n':
        continue

    prefixes = []
    prefix2linter = []

    while (line := next(file).strip()) != '}':
        if line.startswith('//'):
            continue
        if line.startswith('#[prefix = '):
            prefixes.append(line.split()[-1].strip('"]'))
        else:
            for prefix in prefixes:
                prefix2linter.append((prefix, line.rstrip(',')))
            prefixes.clear()

    prefix2linter.sort(key = lambda t: len(t[0]), reverse=True)

    return rules, prefix2linter

rules, prefix2linter = parse_registry()

def parse_code(code):
    prefix = re.match('[A-Z]+', code).group()
    if prefix in ('E', 'W'):
        return 'Pycodestyle', code

    for prefix, linter in prefix2linter:
        if code.startswith(prefix):
            return linter, code[len(prefix) :]

    assert False

text = '''
use crate::registry::{Linter, Rule};

pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
    #[allow(clippy::enum_glob_use)]
    use Linter::*;

    Some(match (linter, code) {
'''

for entry in rules:
    if isinstance(entry, str):
        if entry.startswith('//'):
            text += '\n' + entry
        else:
            text += entry
    else:
        namespace, code = parse_code(entry[0])
        text += f'({namespace}, "{code}") => Rule::{entry[1]},'
    text += '\n'

text += '''
       _ => return  None,
    })
}
'''

with open('crates/ruff/src/codes.rs', 'w') as f:
    f.write(text)
2023-02-14 16:16:12 -05:00
Martin Fischer
65a3461519 many-to-one 4/9: Rename define_rule_mapping! to register_rules!
Currently the define_rule_mapping! macro generates both the Rule enum as
well as the RuleCodePrefix enum and the mapping between the two.  After
this commit series the macro will only generate the Rule enum and the
RuleCodePrefix enum and the mapping will be generated by a new map_codes
proc macro, so we rename the macro now to fit its new purpose.
2023-02-14 16:16:12 -05:00
Martin Fischer
1b8d2df3bf many-to-one 3/9: Update RuleSelector::short_code
Same reasoning as for the previous commit ... one &'static str
becomes two &'static str because we split the RuleCodePrefix enum.
Note that the .unwrap() we have to add now, will actually
be removed in the 6th commit.
2023-02-14 16:16:12 -05:00
Martin Fischer
179ead0157 many-to-one 2/9: Newtype Rule::noqa_code return type
Rule::noqa_code previously return a single &'static str,
which was possible because we had one enum listing all
rule code prefixes. This commit series will however split up
the RuleCodePrefix enum into several enums ... so we'll end up
with two &'static str ... this commit wraps the return type
of Rule::noqa_code into a newtype so that we can easily change
it to return two &'static str in the 6th commit of this series.
2023-02-14 16:16:12 -05:00
Martin Fischer
d451c7a506 many-to-one 1/9: Rename Rule::code to Rule::noqa_code
Post this commit series several codes can be mapped to a single rule,
this commit therefore renames Rule::code to Rule::noqa_code,
which is the code that --add-noqa will add to ignore a rule.
2023-02-14 16:16:12 -05:00
Martin Fischer
502ce80c91 many-to-one 0/9: Introduce RuleSelector::Linter variant
We want to remove the variants denoting whole Linters
from the RuleCodePrefix enum, so we have to introduce
a new RuleSelector::Linter variant.
2023-02-14 16:16:12 -05:00
Charlie Marsh
49d22d8fe2 Ignore non-imperative-mood in Google docstring convention (#2900) 2023-02-14 20:42:20 +00:00
Chris May
08e0b76587 Add headers to configutation options (#2896)
This completes the word from the discussion in #2643, adding headers to clarify the `pyproject.toml` and `ruff.toml` sections.
2023-02-14 13:45:59 -05:00
Charlie Marsh
f7515739ac Improve consistency of some rule docs (#2887) 2023-02-14 04:36:37 +00:00
Sawbez
53e810ed3e [docs] Add docs for the entirety of flake8-builtins (#2840) 2023-02-14 04:30:30 +00:00
Charlie Marsh
66a195f805 Extend B904 to else branches (#2886) 2023-02-14 03:58:15 +00:00
Jeremiah England
b8483975a4 docs(SIM114): fix typo in example Python code (#2884) 2023-02-14 03:23:19 +00:00
Charlie Marsh
4dd2032687 Unversion unpublished crates (#2882) 2023-02-14 03:03:49 +00:00
Charlie Marsh
c6c15d5cf9 Avoid unnecessary-else violations in elif branches (#2881)
Long-time source of confusion -- two reports over 1800 issues apart.

Closes #1035.

Closes #2879.
2023-02-14 02:51:12 +00:00
Charlie Marsh
2bf7b35268 Re-enable custom allocators (#2876) 2023-02-14 02:37:22 +00:00
Charlie Marsh
6d1adc85fc Remove autofix for prefer-type-error (#2880) 2023-02-14 02:26:22 +00:00
Martin Fischer
02285c18d1 Remove autogenerated docs/rules/*.md files 2023-02-13 19:34:06 -05:00
Martin Fischer
8120d7c974 Change rule page links in README from GitHub to beta.ruff.rs 2023-02-13 19:34:06 -05:00
Martin Fischer
c858804ed4 refactor: Move docs/ gitignores to docs/.gitignore 2023-02-13 19:34:06 -05:00
Anders Kaseorg
b9d075c252 Alphabetize flake8-raise and flake8-self in documentation (#2871) 2023-02-13 18:03:09 -05:00
Charlie Marsh
7627e840c9 Avoid noqa removal upon unhandled syntax errors (#2864) 2023-02-13 10:37:55 -05:00
Charlie Marsh
3c03e2cb2e Rename flake8-django rules to match convention (#2861) 2023-02-13 15:30:04 +00:00
Charlie Marsh
aeae63b7ea Avoid false-positives for runtime-types in type checking blocks (#2863) 2023-02-13 10:26:34 -05:00
Charlie Marsh
7be17c5f1e Avoid false-positives with multi-byte characters in B005 (#2862) 2023-02-13 15:07:55 +00:00
Charlie Marsh
6128346b08 Re-show --target-version on CLI interface (#2859) 2023-02-13 15:04:11 +00:00
Charlie Marsh
1705574e75 Handle multiple receiver decorators in receiver-decorator (#2858) 2023-02-13 14:57:11 +00:00
Charlie Marsh
15f65fa8d6 Run cargo dev generate-all (#2860) 2023-02-13 14:55:44 +00:00
Ville Skyttä
d1cf0ee52b Remove "blanket" from RUF100 README message (#2844) 2023-02-13 14:43:35 +00:00
Florian Best
32520ff07f ci(gitignore): ignore VIM files (#2856) 2023-02-13 09:29:53 -05:00
Ville Skyttä
3659236580 Remove no longer needed setup.py INP001 ignore (#2846) 2023-02-13 09:00:35 -05:00
Charlie Marsh
dde69d50b5 Move more dependencies into workspace dependencies (#2842) 2023-02-13 04:19:26 +00:00
Charlie Marsh
67198ce7b3 Revert "Run release on tag creation"
This reverts commit c21a5912b9.
2023-02-12 23:11:31 -05:00
Charlie Marsh
c21a5912b9 Run release on tag creation 2023-02-12 22:33:01 -05:00
Charlie Marsh
48a5cd1dd9 Revert "perf: Use custom allocator (#2768)" (#2841)
This is causing wheel creation to fail on some of our more exotic build targets: https://github.com/charliermarsh/ruff/actions/runs/4159524132.

Let's figure out how to gate appropriately, but for now, reverting to get the release out.
2023-02-12 22:31:34 -05:00
Charlie Marsh
63f3d5e610 Update pre-commit instructions (#2838) 2023-02-13 00:06:40 +00:00
Charlie Marsh
7dab4807d0 Allow compound statements of single ellipsis (#2837)
This allows `class C: ...`-style compound statements in stub files.

Closes #2835.
2023-02-12 18:56:43 -05:00
Charlie Marsh
83f6e52c92 Bump version to 0.0.246 (#2834) 2023-02-12 23:39:51 +00:00
Charlie Marsh
5ce7ce5bc3 Check-in updated snapshot for SIM111 (#2836) 2023-02-12 23:37:52 +00:00
Florian Best
749d197119 docs(SIM114): fix typo in python code (#2833) 2023-02-12 18:35:29 -05:00
Charlie Marsh
46c184600f Include package inference during --add-noqa command (#2832) 2023-02-12 22:45:39 +00:00
Charlie Marsh
e2051ef72f Use smarter inversion for comparison checks (#2831) 2023-02-12 22:39:29 +00:00
Charlie Marsh
1abaece9ed Fix unused multi-assignments in a single pass (#2829) 2023-02-12 22:28:03 +00:00
Charlie Marsh
8b35b052b8 Avoid duplicates in if-with-same-arms (#2827) 2023-02-12 22:22:19 +00:00
Charlie Marsh
5a34504149 Implement ComparableStmt (#2826) 2023-02-12 22:00:01 +00:00
trag1c
0e53ddc2b3 Added Tables of Contents for CONTRIBUTING.md and CODE_OF_CONDUCT.md (#2824) 2023-02-12 16:38:18 -05:00
Colin Delahunty
1f07ad6e61 [flake8-simplify]: combine-if-conditions (#2823) 2023-02-12 21:00:32 +00:00
Charlie Marsh
1666e8ba1e Add a --show-fixes flag to include applied fixes in output (#2707) 2023-02-12 20:48:01 +00:00
Charlie Marsh
c399b3e6c1 Run cargo dev generate-all (#2822) 2023-02-12 19:11:49 +00:00
Charlie Marsh
9089ef74bc Upgrade RustPython (#2821) 2023-02-12 18:45:59 +00:00
Martin Fischer
28c9263722 Automatically linkify option references in rule documentation
Previously the rule documentation referenced configuration options
via full https:// URLs, which was bad for several reasons:

* changing the website would mean you'd have to change all URLs
* the links didn't work when building mkdocs locally
* the URLs showed up in the `ruff rule` output
* broken references weren't detected by our CI

This commit solves all of these problems by post-processing the
Markdown, recognizing sections such as:

    ## Options

    * `flake8-tidy-imports.ban-relative-imports`

`cargo dev generate-all` will automatically linkify such references
and panic if the referenced option doesn't exist.
Note that the option can also be linked in the other Markdown sections
via e.g. [`flake8-tidy-imports.ban-relative-imports`] since
the post-processing code generates a CommonMark link definition.

Resolves #2766.
2023-02-12 13:19:11 -05:00
Martin Fischer
fc4c927788 refactor: Introduce ConfigurationOptions::get method 2023-02-12 13:19:11 -05:00
Zeddicus414
26f39cac2f Add PD002 use-of-inplace-argument documentation (#2799) 2023-02-12 18:10:34 +00:00
Simon Brugman
02897a141b [flake8-tidy-imports] add documentation for banned-api (#2819) 2023-02-12 18:09:39 +00:00
Nyakku Shigure
fc465cc2af [flake8-pyi]: add rules for unrecognized platform check (PYI007, PYI008) (#2805)
Add two [flake8-pyi](https://github.com/PyCQA/flake8-pyi) rules (Y007, Y008). ref: #848

The specifications are described in [PEP 484 - Version and platform checking](https://peps.python.org/pep-0484/#version-and-platform-checking)

The original implementation in flake8-pyi is shown below.

- Implemention: 66f28a4407/pyi.py (L1429-L1443)
- Tests: 66f28a4407/tests/sysplatform.pyi
2023-02-12 18:02:38 +00:00
Charlie Marsh
ca8a122889 Add flake8-django to LICENSE (#2820) 2023-02-12 17:51:40 +00:00
Karol Onyśko
6769a5bce7 Implement flake8-django plugin rules (#2586) 2023-02-12 17:47:59 +00:00
Zeddicus414
fda93c6245 Add E722 bare-except documentation (#2796) 2023-02-12 16:51:32 +00:00
Charlie Marsh
099d5414f2 Allow non-verbose raise when cause is present (#2816)
The motivating issue here is of the following form:

```py
try:
    raise Exception("We want to hide this error message")
except Exception:
    try:
        raise Exception("We want to show this")
    except Exception as exc:
        raise exc from None
```

However, I think we should avoid this if _any_ cause is present, since causes require a named exception.

Closes #2814.
2023-02-12 16:48:13 +00:00
Charlie Marsh
9ddd5e4cfe Allow private accesses on super calls (#2815) 2023-02-12 16:11:25 +00:00
trag1c
b8835c2e35 Added MkDocs section to CONTRIBUTING.md (#2803) 2023-02-12 16:07:24 +00:00
Simon Brugman
1d4422f004 [flake8-comprehensions] improve autofix for C401, C402 and C417 (#2806) 2023-02-12 16:03:37 +00:00
Simon Brugman
2dccb7611a [flake8-comprehensions] bugfix for C413 autofix (#2804) 2023-02-12 15:56:07 +00:00
Simon Brugman
f8ac6d7bf0 fix: script add_plugin.py test import (#2807) 2023-02-12 09:58:23 -05:00
Simon Brugman
0123425be1 [flake8-comprehensions] autofix C414 and C417 + bugfix (#2693)
Closes https://github.com/charliermarsh/ruff/issues/2262 and closes https://github.com/charliermarsh/ruff/issues/2423

Fixes bug where some cases generated duplicated violations (see https://github.com/charliermarsh/ruff/pull/2732#issuecomment-1426397842)
2023-02-12 05:20:42 +00:00
Charlie Marsh
c53f91d943 Remove public re-export of commands (#2801) 2023-02-12 04:59:35 +00:00
Charlie Marsh
4a12ebb9b1 Improve f-string-missing-placeholders documentation (#2800) 2023-02-12 04:58:24 +00:00
Martin Fischer
0e4d5eeea7 Implement config subcommand
The synopsis is as follows.

List all top-level config keys:

    $ ruff config
    allowed-confusables
    builtins
    cache-dir
    ... etc.

List all config keys in a specific section:

    $ ruff config mccabe
    max-complexity

Describe a specific config option:

    $ ruff config mccabe.max-complexity
    The maximum McCabe complexity to allow before triggering `C901` errors.

    Default value: 10
    Type: int
    Example usage:
    ```toml
    # Flag errors (`C901`) whenever the complexity level exceeds 5.
    max-complexity = 5
    ```
2023-02-11 23:43:09 -05:00
Martin Fischer
bbe44360e8 refactor: Move name out of OptionField & OptionGroup 2023-02-11 23:43:09 -05:00
Martin Fischer
37e80d98ab refactor: Reorder members in ruff::settings::options_base 2023-02-11 23:43:09 -05:00
Charlie Marsh
306393063d Refactor generator to use Astor-derived precedence levels (#2798) 2023-02-12 04:30:16 +00:00
Martin Fischer
f5a3c90288 Rename new ruff rule output format to "pretty"
The new `ruff rule` output format introduced in
551b810aeb doesn't print Markdown but
rather some rich text with escape sequences for colors and links,
it's actually the "text" format that prints Markdown, so naming the new
format "markdown" is very confusing. This commit therefore renames it to
"pretty".

This isn't a breaking change since there hasn't been a release yet.
2023-02-11 23:23:37 -05:00
Charlie Marsh
8289ede00f Use output-stdout pattern for linter command (#2794) 2023-02-12 03:09:03 +00:00
Charlie Marsh
77e65c9ff5 Split commands.rs into separate files (#2792) 2023-02-12 02:58:13 +00:00
Charlie Marsh
d827a9156e Add documentation on enabling autocompletion (#2791) 2023-02-12 02:51:50 +00:00
Charlie Marsh
418808895e Add docs for f-string-missing-placeholders and unused-variable (#2790) 2023-02-12 02:48:36 +00:00
Charlie Marsh
ac4e212ed2 Move Wasm clippy to its own job (#2789) 2023-02-12 02:41:28 +00:00
Nick Pope
551b810aeb Add rendering of rule markdown for terminal output (#2747)
Add rendering of rule markdown for terminal output
    
This is achieved by making use of the `mdcat` crate.
    
See the following links for details:
    
- https://crates.io/crates/mdcat
- https://github.com/swsnr/mdcat
- https://docs.rs/mdcat/latest/mdcat/
2023-02-12 02:32:45 +00:00
Charlie Marsh
1b61d4e18b Support unused variable removal in multi-assignment statements (#2786) 2023-02-12 00:53:11 +00:00
Charlie Marsh
752c0150e1 Improve unused-variable autofixes for with statements (#2785) 2023-02-12 00:38:14 +00:00
Charlie Marsh
81651a8479 Respect continuations in noqa enforcement (#2783) 2023-02-11 23:29:37 +00:00
Charlie Marsh
86d0749ed7 Use consistent formatting for lint-failure messages (#2782) 2023-02-11 22:52:18 +00:00
Charlie Marsh
19fc410683 Remove raw string from hardcoded-sql-expression (#2780) 2023-02-11 20:05:57 +00:00
Charlie Marsh
5a70a573cd Avoid treating deferred string annotations as required-at-runtime (#2779) 2023-02-11 15:00:08 -05:00
Charlie Marsh
74731a3456 Fix reference to ban-relative-imports setting (#2776) 2023-02-11 18:34:25 +00:00
Micha Reiser
863e39fe5f perf: Use custom allocator (#2768)
This PR replaces the system allocator with a custom allocator to improve performance:

* Windows: mimalloc
* Unix: tikv-jemallocator

## Performance:

* Linux
  * `cpython --no-cache`: 208.8ms -> 190.5ms
  * `cpython`: 32.8ms -> 31ms
* Mac: 
  * `cpython --no-cache`: 436.3ms -> 380ms
  * `cpython`: 40.9ms -> 39.6ms
* Windows: 
  * `cpython --no-cache`: 367ms -> 268ms
  * `cpython`: 92.5ms -> 92.3ms
  
## Size

* Linux: +5MB from 13MB -> 18MB (I need to double check this)
* Mac: +0.7MB from 8.3MB-> 9MB
* Windows: -0.16MB from 8.29MB -> 8.13MB (that's unexpected)
2023-02-11 13:26:07 -05:00
Charlie Marsh
d0f9ee33ec Remove erroneous print statements 2023-02-11 12:45:40 -05:00
Charlie Marsh
1cf3d880a7 Don't treat all future import accesses as non-runtime (#2774)
This was just an oversight and misunderstanding on my part. We had some helpful tests, but I misunderstood the "right" behavior so thought they were passing.

Closes #2761.
2023-02-11 12:44:15 -05:00
Charlie Marsh
97dcb738fa Run cargo dev generate-all 2023-02-11 12:43:48 -05:00
Charlie Marsh
ffb4e89a98 Remove multiple-statements-on-one-line-def (E704) (#2773) 2023-02-11 12:34:21 -05:00
Charlie Marsh
43b7ee215c Ignore colon-after-lambda in compound statement rules (#2771) 2023-02-11 12:22:53 -05:00
Michał Mrówka
77099dcd4d implemented option lines-between-types for isort (#2762)
Fixes #2585

Add support for the isort option [lines_between_types](https://pycqa.github.io/isort/docs/configuration/options.html#lines-between-types)
2023-02-11 12:17:37 -05:00
Martin Fischer
70ff65154d Rename function-is-too-complex to complex-structure 2023-02-11 12:05:17 -05:00
Martin Fischer
7db6a2d6d4 Rename rules containing PEP reference in name 2023-02-11 12:05:17 -05:00
Martin Fischer
42924c0d9a Rename a bunch of pydocstyle rules 2023-02-11 12:05:17 -05:00
Martin Fischer
31d00936ee Drop no- from no-unnecessary-* rule names 2023-02-11 12:05:17 -05:00
Martin Fischer
c3c5d9a852 Rename nested-if-statements to collapsible-if 2023-02-11 12:05:17 -05:00
Martin Fischer
7e5c19385c Rename return-bool-condition-directly to needless-bool 2023-02-11 12:05:17 -05:00
Simon Brugman
5b54325c81 enable navigation in footer in docs (#2760) 2023-02-11 05:08:33 -05:00
trag1c
e6538a7969 Added logo and favicon for mkdocs (#2757) 2023-02-10 23:34:47 -05:00
Charlie Marsh
24faabf1f4 Bump version to 0.0.245 2023-02-10 22:15:27 -05:00
Charlie Marsh
9b0a160239 Only update docs on release (#2755) 2023-02-10 22:14:50 -05:00
Charlie Marsh
9fd29e2c54 Mention default in relative-imports doc 2023-02-10 22:12:22 -05:00
Simon Brugman
e83ed0ecba Implement autofix for relative imports (TID252) (#2739) 2023-02-10 22:05:47 -05:00
Charlie Marsh
dadbfea497 Flag private member accesses on calls et al (#2753) 2023-02-10 19:23:22 -05:00
Nick Pope
9f84c497f9 Adjust heading level in rule documentation (#2749) 2023-02-10 19:10:42 -05:00
Martin Fischer
0ec25d1514 Rename dynamically-typed-expression to any-type (#2751) 2023-02-10 19:02:31 -05:00
Charlie Marsh
6a87c99004 Use explicit fields for implicit-namespace-package 2023-02-10 18:09:30 -05:00
Charlie Marsh
c8f60c9588 Improve implicit-namespace-package documentation 2023-02-10 18:06:48 -05:00
Charlie Marsh
113610a8d4 Improve hardcoded-sql-expression documentation 2023-02-10 18:03:01 -05:00
Charlie Marsh
6376e5915e Improve dynamically-typed-expression documentation 2023-02-10 17:55:26 -05:00
Charlie Marsh
3d8fb5be20 Rewrite documentation for yield-in-init (#2748) 2023-02-10 17:49:55 -05:00
Charlie Marsh
0040991778 Respect NO_COLOR flags in --show-source (#2750) 2023-02-10 17:27:40 -05:00
Charlie Marsh
acb70520f8 Add colored environment variables to README (#2746) 2023-02-10 17:06:02 -05:00
Charlie Marsh
6eb9268675 Allow named unicodes in bidirectional escape check (#2710) 2023-02-10 16:59:28 -05:00
Charlie Marsh
e5f5142e3e Improve yield-in-init documentation 2023-02-10 16:47:44 -05:00
Charlie Marsh
98d5ffb817 Fix __init__.py-to-__init__ in documentation 2023-02-10 16:30:36 -05:00
Charlie Marsh
3f20f73413 Use function_type::classify for yield-in-init (#2742) 2023-02-10 16:19:45 -05:00
tomecki
a5e42d2f7c pylint: E0100 yield-in-init (#2716) 2023-02-10 16:15:15 -05:00
Charlie Marsh
0bc1f68111 Only trigger compound statements after select keywords (#2737) 2023-02-10 15:21:06 -05:00
Charlie Marsh
d2b09d77c5 Only validate __all__ bindings for global scope (#2738) 2023-02-10 15:16:21 -05:00
Charlie Marsh
0377834f9f Mark __all__ members as used at end-of-scope (#2733) 2023-02-10 14:32:05 -05:00
Charlie Marsh
3d650f9dd6 Relax conditions in bad-string-format-type (#2731) 2023-02-10 14:25:59 -05:00
Charlie Marsh
a72590ecde Expand S110 and S112 ranges to include entire exception handler (#2729) 2023-02-10 13:27:18 -05:00
Charlie Marsh
812b227334 Avoid flagging typed exceptions in tuples (#2728) 2023-02-10 13:24:45 -05:00
Martin Fischer
6f58717ba4 refactor: Stop including Rule::code() in pycodestyle .snap filenames 2023-02-10 13:15:47 -05:00
Florian Best
8aab96fb9e feat(isort): Implement known-local-folder (#2657) 2023-02-10 13:15:34 -05:00
Nick Pope
9e6f7153a9 Handle more functions that never return in RET503 (#2719) 2023-02-10 12:09:05 -05:00
Peter Pentchev
cda2ff0b18 Handle functions that never return in RET503 (#2701) 2023-02-10 09:28:34 -05:00
Martin Fischer
ec63658250 Disallow rule names starting with avoid-* 2023-02-10 09:25:29 -05:00
Martin Fischer
1a97de0b01 Disallow rule names starting with uses-* 2023-02-10 09:25:29 -05:00
Martin Fischer
1cbe48522e Disallow rule names ending in *-used 2023-02-10 09:25:29 -05:00
Martin Fischer
bfbde537af Disallow rule names starting with do-not-* 2023-02-10 09:25:29 -05:00
Martin Fischer
cba91b758b Add test for rule names 2023-02-10 09:25:29 -05:00
Martin Fischer
0bab642f5a Describe rule naming convention in CONTRIBUTING.md 2023-02-10 09:25:29 -05:00
Martin Fischer
bd09a1819f Drop unused once_cell dependency from ruff_macros 2023-02-10 09:25:29 -05:00
Martin Fischer
682d206992 refactor: Reduce code duplication 2023-02-10 08:24:22 -05:00
Martin Fischer
c32441e4ab refactor: Use format! keyword arguments 2023-02-10 08:24:22 -05:00
Martin Fischer
6f16f1c39b refactor: Reduce code duplication 2023-02-10 08:24:22 -05:00
Martin Fischer
9011456aa1 refactor: Simplify attribute handling in rule_code_prefix
if_all_same(codes.values().cloned()).unwrap_or_default()

was quite unreadable because it wasn't obvious that codes.values() are
the prefixes. It's better to introduce another Map rather than having
Maps within Maps.
2023-02-10 08:24:22 -05:00
Martin Fischer
fa191cceeb refactor: Avoid implicit precondition 2023-02-10 08:24:22 -05:00
Charlie Marsh
ac6c3affdd Remove public Rust API (#2709) 2023-02-09 23:16:49 -05:00
Charlie Marsh
9a018c1650 Import AutofixKind from violation 2023-02-09 23:06:02 -05:00
Charlie Marsh
0aef5c67a3 Remove src/registry.rs 2023-02-09 23:04:28 -05:00
Charlie Marsh
a048594416 Gate Path.readlink() behind Python 3.9+ guard (#2708) 2023-02-09 22:57:31 -05:00
Charlie Marsh
5437f1299b Remove lifetimes from Printer (#2704) 2023-02-09 21:44:15 -05:00
Charlie Marsh
41c0608a69 Add test module a test-only module (#2703) 2023-02-09 21:28:10 -05:00
messense
eb0d42187f Manage LibCST and RustPython with cargo workspace dependencies (#2700) 2023-02-09 20:49:50 -05:00
Colin Delahunty
48daa0f0ca [pylint]: bad-string-format-type (#2572) 2023-02-09 20:08:56 -05:00
Charlie Marsh
417fe4355f Add colors to statistics output (#2699) 2023-02-09 19:40:29 -05:00
Florian Best
a129181407 feat(cli): let --statistics show fixable codes (#2659) 2023-02-09 19:36:31 -05:00
Matt Oberle
fc628de667 Implement bandit's 'hardcoded-sql-expressions' S608 (#2698)
This is an attempt to implement `bandit` rule `B608` (renamed here `S608`).
- https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html

The rule inspects strings constructed via `+`, `%`, `.format`, and `f""`.

- `+` and `%` via `BinOp`
- `.format` via `Call`
- `f""` via `JoinedString`

Any SQL-ish strings that use Python string formatting are flagged.

The expressions and targeted expression types for the rule come from here:
- 7104b336d3/bandit/plugins/injection_sql.py

> Related Issue: https://github.com/charliermarsh/ruff/issues/1646
2023-02-09 19:28:17 -05:00
Charlie Marsh
9e2418097c Run cargo dev generate-all 2023-02-09 19:14:02 -05:00
Charlie Marsh
d4e5639aaf Add flake8-pyi to CONTRIBUTING.md 2023-02-09 19:04:55 -05:00
Steve Dignam
67e58a024a Add flake8-pyi with one rule (#2682)
Add basic scaffold for [flake8-pyi](https://github.com/PyCQA/flake8-pyi) and the first rule, Y001

rel: https://github.com/charliermarsh/ruff/issues/848
2023-02-09 19:03:11 -05:00
Charlie Marsh
233be0e074 Suppress parse errors with explicit # noqa: E999 directives (#2697) 2023-02-09 18:24:19 -05:00
Nick Pope
7750087f56 Remove duplicate documentation for TRY002 (#2692) 2023-02-09 12:08:00 -05:00
Charlie Marsh
7d5fb0de8a Add documentation for mccabe, isort, and flake8-annotations (#2691) 2023-02-09 11:56:18 -05:00
Charlie Marsh
8a98cfc4b8 Treat re-exported annotations as used-at-runtime (#2689) 2023-02-09 11:22:15 -05:00
Charlie Marsh
54d1719424 Hide rule configuration settings on CLI (#2687) 2023-02-09 11:13:04 -05:00
Charlie Marsh
0f622f0126 Upgrade RustPython to pull in newline-handling optimizations (#2688) 2023-02-09 11:12:43 -05:00
Charlie Marsh
739a92e99d Implement compound-statements (E701, E702, E703, E704) (#2680) 2023-02-08 22:57:39 -05:00
Charlie Marsh
5a07c9f57c Only include rule links once in README (#2678) 2023-02-08 21:48:05 -05:00
Colin Delahunty
31027497c6 [flake8-bandit]: try-except-continue (#2674) 2023-02-08 21:44:01 -05:00
Charlie Marsh
dabfdf718e Mark flake8-simplify rules as unfixable in non-fixable cases (#2676) 2023-02-08 21:28:28 -05:00
Charlie Marsh
5829bae976 Support callable decorators in classmethod_decorators et al (#2675) 2023-02-08 21:11:36 -05:00
Charlie Marsh
ff3665a24b Mark RUF005 as fixable 2023-02-08 18:02:33 -05:00
637 changed files with 36820 additions and 4468 deletions

View File

@@ -40,9 +40,20 @@ jobs:
run: rustup component add rustfmt
- run: cargo fmt --all --check
cargo_clippy:
cargo-clippy:
name: "cargo clippy"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
run: |
rustup component add clippy
- uses: Swatinem/rust-cache@v1
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo-clippy-wasm:
name: "cargo clippy (wasm)"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Install Rust toolchain"
@@ -50,7 +61,6 @@ jobs:
rustup component add clippy
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v1
- run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- run: cargo clippy -p ruff --target wasm32-unknown-unknown --all-features -- -D warnings
cargo-test:

View File

@@ -1,12 +1,8 @@
name: mkdocs
on:
push:
paths:
- README.md
- mkdocs.template.yml
- .github/workflows/docs.yaml
branches: [main]
release:
types: [published]
workflow_dispatch:
jobs:
@@ -17,9 +13,12 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
- name: "Install Rust toolchain"
run: rustup show
- uses: Swatinem/rust-cache@v1
- name: "Install dependencies"
run: |
pip install "mkdocs~=1.4.2" "mkdocs-material~=9.0.6"
pip install -r docs/requirements.txt
- name: "Copy README File"
run: |
python scripts/transform_readme.py --target mkdocs

View File

@@ -27,7 +27,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels - x86_64"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --out dist --sdist -m ./${{ env.CRATE_NAME }}/Cargo.toml
@@ -50,7 +50,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels - universal2"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
args: --release --universal2 --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel - universal2"
@@ -76,7 +76,7 @@ jobs:
- name: "Install Rust toolchain"
run: rustup show
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist -m ./${{ env.CRATE_NAME }}/Cargo.toml
@@ -102,7 +102,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -128,7 +128,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
@@ -166,7 +166,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -201,7 +201,7 @@ jobs:
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
@@ -222,40 +222,6 @@ jobs:
name: wheels
path: dist
pypy:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
target: [x86_64, aarch64]
python-version:
- "3.7"
- "3.8"
- "3.9"
exclude:
- os: macos-latest
target: aarch64
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: pypy${{ matrix.python-version }}
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -i pypy${{ matrix.python-version }} -m ./${{ env.CRATE_NAME }}/Cargo.toml
- name: "Install built wheel"
if: matrix.target == 'x86_64'
run: |
pip install dist/${{ env.CRATE_NAME }}-*.whl --force-reinstall
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
name: Release
runs-on: ubuntu-latest
@@ -267,7 +233,6 @@ jobs:
- linux-cross
- musllinux
- musllinux-cross
- pypy
steps:
- uses: actions/download-artifact@v3
with:

View File

@@ -29,7 +29,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - x86_64"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --out dist --sdist
@@ -41,6 +41,19 @@ jobs:
with:
name: wheels
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-x86_64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/x86_64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: binaries
path: |
*.tar.gz
*.sha256
macos-universal:
runs-on: macos-latest
steps:
@@ -52,7 +65,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels - universal2"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
args: --release --universal2 --out dist
- name: "Install built wheel - universal2"
@@ -63,26 +76,45 @@ jobs:
with:
name: wheels
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-aarch64-apple-darwin.tar.gz
tar czvf $ARCHIVE_FILE -C target/aarch64-apple-darwin/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: binaries
path: |
*.tar.gz
*.sha256
windows:
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
platform:
- target: x86_64-pc-windows-msvc
arch: x64
- target: i686-pc-windows-msvc
arch: x86
- target: aarch64-pc-windows-msvc
arch: x64
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.target }}
architecture: ${{ matrix.platform.arch }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
target: ${{ matrix.platform.target }}
args: --release --out dist
- name: "Install built wheel"
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
run: |
python -m pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
@@ -91,12 +123,27 @@ jobs:
with:
name: wheels
path: dist
- name: "Archive binary"
shell: bash
run: |
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.zip
7z a $ARCHIVE_FILE ./target/${{ matrix.platform.target }}/release/ruff.exe
sha256sum $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: binaries
path: |
*.zip
*.sha256
linux:
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, i686]
target:
- x86_64-unknown-linux-gnu
- i686-unknown-linux-gnu
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -106,13 +153,13 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
- name: "Install built wheel"
if: matrix.target == 'x86_64'
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: "Upload wheels"
@@ -120,12 +167,34 @@ jobs:
with:
name: wheels
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: binaries
path: |
*.tar.gz
*.sha256
linux-cross:
runs-on: ubuntu-latest
strategy:
matrix:
target: [aarch64, armv7, s390x, ppc64le, ppc64]
platform:
- target: aarch64-unknown-linux-gnu
arch: aarch64
- target: armv7-unknown-linux-gnueabihf
arch: armv7
- target: s390x-unknown-linux-gnu
arch: s390x
- target: powerpc64le-unknown-linux-gnu
arch: ppc64le
- target: powerpc64-unknown-linux-gnu
arch: ppc64
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -134,16 +203,16 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
target: ${{ matrix.platform.target }}
manylinux: auto
args: --release --out dist
- uses: uraimo/run-on-arch-action@v2.5.0
if: matrix.target != 'ppc64'
if: matrix.platform.arch != 'ppc64'
name: Install built wheel
with:
arch: ${{ matrix.target }}
arch: ${{ matrix.platform.arch }}
distro: ubuntu20.04
githubToken: ${{ github.token }}
install: |
@@ -157,6 +226,18 @@ jobs:
with:
name: wheels
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: binaries
path: |
*.tar.gz
*.sha256
musllinux:
runs-on: ubuntu-latest
@@ -174,7 +255,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
@@ -193,6 +274,18 @@ jobs:
with:
name: wheels
path: dist
- name: "Archive binary"
run: |
ARCHIVE_FILE=ruff-${{ matrix.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: binaries
path: |
*.tar.gz
*.sha256
musllinux-cross:
runs-on: ubuntu-latest
@@ -211,7 +304,7 @@ jobs:
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
@@ -231,42 +324,18 @@ jobs:
with:
name: wheels
path: dist
pypy:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
target: [x86_64, aarch64]
python-version:
- "3.7"
- "3.8"
- "3.9"
exclude:
- os: macos-latest
target: aarch64
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: pypy${{ matrix.python-version }}
- name: "Prep README.md"
run: python scripts/transform_readme.py --target pypi
- name: "Build wheels"
uses: messense/maturin-action@v1
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist -i pypy${{ matrix.python-version }}
- name: "Install built wheel"
if: matrix.target == 'x86_64'
- name: "Archive binary"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
- name: "Upload wheels"
ARCHIVE_FILE=ruff-${{ matrix.platform.target }}.tar.gz
tar czvf $ARCHIVE_FILE -C target/${{ matrix.platform.target }}/release ruff
shasum -a 256 $ARCHIVE_FILE > $ARCHIVE_FILE.sha256
- name: "Upload binary"
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
name: binaries
path: |
*.tar.gz
*.sha256
release:
name: Release
@@ -279,7 +348,6 @@ jobs:
- linux-cross
- musllinux
- musllinux-cross
- pypy
if: "startsWith(github.ref, 'refs/tags/')"
steps:
- uses: actions/download-artifact@v3
@@ -296,3 +364,11 @@ jobs:
- name: "Update pre-commit mirror"
run: |
curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${{ secrets.RUFF_PRE_COMMIT_PAT }}" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/charliermarsh/ruff-pre-commit/dispatches --data '{"event_type": "pypi_release"}'
- uses: actions/download-artifact@v3
with:
name: binaries
path: binaries
- name: Release
uses: softprops/action-gh-release@v1
with:
files: binaries/*

6
.gitignore vendored
View File

@@ -1,8 +1,6 @@
# Local cache
.ruff_cache
crates/ruff/resources/test/cpython
docs/*
!docs/rules
mkdocs.yml
.overrides
@@ -189,3 +187,7 @@ cython_debug/
# Visual Studio Code
.vscode/
# VIM
.*.sw?
.sw?

View File

@@ -1,5 +1,20 @@
# Breaking Changes
## 0.0.246
### `multiple-statements-on-one-line-def` (`E704`) was removed ([#2773](https://github.com/charliermarsh/ruff/pull/2773))
This rule was introduced in v0.0.245. However, it turns out that pycodestyle and Flake8 ignore this
rule by default, as it is not part of PEP 8. As such, we've removed it from Ruff.
## 0.0.245
### Ruff's public `check` method was removed ([#2709](https://github.com/charliermarsh/ruff/pull/2709))
Previously, Ruff exposed a `check` method as a public Rust API. This method was used by few,
if any clients, and was not well documented or supported. As such, it has been removed, with
the intention of adding a stable public API in the future.
## 0.0.238
### `select`, `extend-select`, `ignore`, and `extend-ignore` have new semantics ([#2312](https://github.com/charliermarsh/ruff/pull/2312))

View File

@@ -1,5 +1,17 @@
# Contributor Covenant Code of Conduct
- [Our Pledge](#our-pledge)
- [Our Standards](#our-standards)
- [Enforcement Responsibilities](#enforcement-responsibilities)
- [Scope](#scope)
- [Enforcement](#enforcement)
- [Enforcement Guidelines](#enforcement-guidelines)
- [1. Correction](#1-correction)
- [2. Warning](#2-warning)
- [3. Temporary Ban](#3-temporary-ban)
- [4. Permanent Ban](#4-permanent-ban)
- [Attribution](#attribution)
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our

View File

@@ -2,7 +2,18 @@
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
## The basics
- [The Basics](#the-basics)
- [Prerequisites](#prerequisites)
- [Development](#development)
- [Project Structure](#project-structure)
- [Example: Adding a new lint rule](#example-adding-a-new-lint-rule)
- [Rule naming convention](#rule-naming-convention)
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
- [MkDocs](#mkdocs)
- [Release Process](#release-process)
- [Benchmarks](#benchmarks)
## The Basics
Ruff welcomes contributions in the form of Pull Requests.
@@ -22,6 +33,9 @@ As a concrete example: consider taking on one of the rules from the [`tryceratop
plugin, and looking to the originating [Python source](https://github.com/guilatrova/tryceratops)
for guidance.
Alternatively, we've started work on the [`flake8-pyi`](https://github.com/charliermarsh/ruff/issues/848)
plugin (see the [Python source](https://github.com/PyCQA/flake8-pyi)) -- another good place to start.
### Prerequisites
Ruff is written in Rust. You'll need to install the
@@ -70,7 +84,7 @@ pre-commit run --all-files
Your Pull Request will be reviewed by a maintainer, which may involve a few rounds of iteration
prior to merging.
### Project structure
### Project Structure
Ruff is structured as a monorepo with a [flat crate structure](https://matklad.github.io/2021/08/22/large-rust-workspaces.html),
such that all crates are contained in a flat `crates` directory.
@@ -91,15 +105,16 @@ At time of writing, the repository includes the following crates:
At a high level, the steps involved in adding a new lint rule are as follows:
1. Create a file for your rule (e.g., `crates/ruff/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 `crates/ruff/src/registry.rs` (e.g., `E402`).
4. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast.rs` (for AST-based
1. Determine a name for the new rule as per our [rule naming convention](#rule-naming-convention).
2. Create a file for your rule (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`).
3. In that file, define a violation struct. You can grep for `define_violation!` to see examples.
4. Map the violation struct to a rule code in `crates/ruff/src/registry.rs` (e.g., `E402`).
5. Define the logic for triggering the violation in `crates/ruff/src/checkers/ast.rs` (for AST-based
checks), `crates/ruff/src/checkers/tokens.rs` (for token-based checks), `crates/ruff/src/checkers/lines.rs`
(for text-based checks), or `crates/ruff/src/checkers/filesystem.rs` (for filesystem-based
checks).
5. Add a test fixture.
6. Update the generated files (documentation and generated code).
6. Add a test fixture.
7. Update the generated files (documentation and generated code).
To define the violation, start by creating a dedicated file for your rule under the appropriate
rule linter (e.g., `crates/ruff/src/rules/flake8_bugbear/rules/abstract_base_class.rs`). That file should
@@ -129,6 +144,17 @@ generated snapshot, then commit the snapshot file alongside the rest of your cha
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
#### Rule naming convention
The rule name should make sense when read as "allow *rule-name*" or "allow *rule-name* items".
This implies that rule names:
* should state the bad thing being checked for
* should not contain instructions on what you what you should use instead
(these belong in the rule documentation and the `autofix_title` for rules that have autofix)
### Example: Adding a new configuration option
Ruff's user-facing settings live in a few different places.
@@ -155,7 +181,27 @@ lives in `crates/ruff/src/flake8_to_ruff/converter.rs`.
Finally, regenerate the documentation and generated code with `cargo dev generate-all`.
## Release process
## MkDocs
To preview any changes to the documentation locally:
1. Install MkDocs and Material for MkDocs with:
```shell
pip install -r docs/requirements.txt
```
2. Generate the MkDocs site with:
```shell
python scripts/generate_mkdocs.py
```
3. Run the development server with:
```shell
mkdocs serve
```
The documentation should then be available locally at
[http://127.0.0.1:8000/docs/](http://127.0.0.1:8000/docs/).
## Release Process
As of now, Ruff has an ad hoc release process: releases are cut with high frequency via GitHub
Actions, which automatically generates the appropriate wheels across architectures and publishes

1021
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,23 @@
members = ["crates/*"]
default-members = ["crates/ruff", "crates/ruff_cli"]
[workspace.dependencies]
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
once_cell = { version = "1.16.0" }
regex = { version = "1.6.0" }
rustc-hash = { version = "1.1.0" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "61b48f108982d865524f86624a9d5bc2ae3bccef" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "61b48f108982d865524f86624a9d5bc2ae3bccef" }
schemars = { version = "0.8.11" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
toml = { version = "0.6.0" }
[profile.release]
panic = "abort"
lto = "thin"

79
LICENSE
View File

@@ -245,6 +245,31 @@ are:
SOFTWARE.
"""
- flake8-pyi, licensed as follows:
"""
The MIT License (MIT)
Copyright (c) 2016 Łukasz Langa
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
- flake8-print, licensed as follows:
"""
MIT License
@@ -1035,3 +1060,57 @@ are:
"""
Freely Distributable
"""
- flake8-django, licensed under the GPL license.
- rust-analyzer/text-size, licensed under the MIT license:
"""
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
"""
- rome/tools, licensed under the MIT license:
"""
MIT License
Copyright (c) Rome Tools, Inc. and its affiliates.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""

430
README.md
View File

@@ -7,7 +7,6 @@
[![image](https://img.shields.io/pypi/l/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![image](https://img.shields.io/pypi/pyversions/ruff.svg)](https://pypi.python.org/pypi/ruff)
[![Actions status](https://github.com/charliermarsh/ruff/workflows/CI/badge.svg)](https://github.com/charliermarsh/ruff/actions)
[![image](https://img.shields.io/date/1676394000?label=Jetbrains%20Ruff%20Webinar&logo=jetbrains)](https://info.jetbrains.com/PyCharm-Webinar-February14-2023.html)
[**Discord**](https://discord.gg/c9MhzV8aU5) | [**Docs**](https://beta.ruff.rs/docs/) | [**Playground**](https://play.ruff.rs/)
@@ -27,7 +26,6 @@ An extremely fast Python linter, written in Rust.
* ⚡️ 10-100x faster than existing linters
* 🐍 Installable via `pip`
* 🤝 Python 3.11 compatibility
* 🛠️ `pyproject.toml` support
* 📦 Built-in caching, to avoid re-analyzing unchanged files
* 🔧 Autofix support, for automatic error correction (e.g., automatically remove unused imports)
@@ -49,10 +47,11 @@ Ruff is extremely actively developed and used in major open-source projects like
* [pandas](https://github.com/pandas-dev/pandas)
* [FastAPI](https://github.com/tiangolo/fastapi)
* [Transformers (Hugging Face)](https://github.com/huggingface/transformers)
* [Apache Airflow](https://github.com/apache/airflow)
* [SciPy](https://github.com/scipy/scipy)
* [Bokeh](https://github.com/bokeh/bokeh)
* [Zulip](https://github.com/zulip/zulip)
* [Bokeh](https://github.com/bokeh/bokeh)
* [Pydantic](https://github.com/pydantic/pydantic)
* [Dagster](https://github.com/dagster-io/dagster)
* [Dagger](https://github.com/dagger/dagger)
@@ -137,6 +136,7 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/).
1. [flake8-comprehensions (C4)](#flake8-comprehensions-c4)
1. [flake8-datetimez (DTZ)](#flake8-datetimez-dtz)
1. [flake8-debugger (T10)](#flake8-debugger-t10)
1. [flake8-django (DJ)](#flake8-django-dj)
1. [flake8-errmsg (EM)](#flake8-errmsg-em)
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-implicit-str-concat (ISC)](#flake8-implicit-str-concat-isc)
@@ -145,9 +145,12 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/).
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-pyi (PYI)](#flake8-pyi-pyi)
1. [flake8-pytest-style (PT)](#flake8-pytest-style-pt)
1. [flake8-quotes (Q)](#flake8-quotes-q)
1. [flake8-raise (RSE)](#flake8-raise-rse)
1. [flake8-return (RET)](#flake8-return-ret)
1. [flake8-self (SLF)](#flake8-self-slf)
1. [flake8-simplify (SIM)](#flake8-simplify-sim)
1. [flake8-tidy-imports (TID)](#flake8-tidy-imports-tid)
1. [flake8-type-checking (TCH)](#flake8-type-checking-tch)
@@ -158,14 +161,14 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/).
1. [pygrep-hooks (PGH)](#pygrep-hooks-pgh)
1. [Pylint (PL)](#pylint-pl)
1. [tryceratops (TRY)](#tryceratops-try)
1. [flake8-raise (RSE)](#flake8-raise-rse)
1. [flake8-self (SLF)](#flake8-self-slf)
1. [NumPy-specific rules (NPY)](#numpy-specific-rules-npy)
1. [Ruff-specific rules (RUF)](#ruff-specific-rules-ruf)<!-- End auto-generated table of contents. -->
1. [Editor Integrations](#editor-integrations)
1. [FAQ](#faq)
1. [Contributing](#contributing)
1. [Support](#support)
1. [Reference](#reference)
1. [Acknowledgements](#acknowledgements)
1. [License](#license)
## Installation and Usage
@@ -230,20 +233,33 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.244'
rev: 'v0.0.247'
hooks:
- id: ruff
```
Or, to enable autofix:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.247'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
```
<!-- End section: Installation and Usage -->
## Configuration
<!-- Begin section: Configuration -->
Ruff is configurable both via `pyproject.toml` and the command line. For a full list of configurable
Ruff is configurable both via `pyproject.toml`, `ruff.toml`, and the command line. For a full list of configurable
options, see the [API reference](#reference).
### Configure via `pyproject.toml`
If left unspecified, the default configuration is equivalent to:
```toml
@@ -349,6 +365,8 @@ If you're wondering how to configure Ruff, here are some **recommended guideline
* By default, Ruff's autofix is aggressive. If you find that it's too aggressive for your liking,
consider turning off autofix for specific rules or categories (see: [FAQ](#ruff-tried-to-fix-something-but-it-broke-my-code-what-should-i-do)).
### Configure via `ruff.toml`
As an alternative to `pyproject.toml`, Ruff will also respect a `ruff.toml` file, which implements
an equivalent schema (though the `[tool.ruff]` hierarchy can be omitted). For example, the
`pyproject.toml` described above would be represented via the following `ruff.toml`:
@@ -392,6 +410,7 @@ Usage: ruff [OPTIONS] <COMMAND>
Commands:
check Run Ruff on the given files or directories (default)
rule Explain a rule
config List or describe the available configuration options
linter List all supported upstream linters
clean Clear any caches in the current directory and any subdirectories
help Print this message or the help of the given subcommand(s)
@@ -423,18 +442,34 @@ Arguments:
[FILES]... List of files or directories to check
Options:
--fix Attempt to automatically fix lint violations
--show-source Show violations with source code
--diff Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-w, --watch Run in watch mode by re-running whenever files change
--fix-only Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
--format <FORMAT> Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint]
--config <CONFIG> Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--statistics Show counts for every rule with at least one violation
--add-noqa Enable automatic additions of `noqa` directives to failing lines
--show-files See the files Ruff will be run against with the current settings
--show-settings See the settings Ruff will use to lint a given Python file
-h, --help Print help
--fix
Attempt to automatically fix lint violations
--show-source
Show violations with source code
--show-fixes
Show an enumeration of all autofixed lint violations
--diff
Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-w, --watch
Run in watch mode by re-running whenever files change
--fix-only
Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
--format <FORMAT>
Output serialization format for violations [env: RUFF_FORMAT=] [possible values: text, json, junit, grouped, github, gitlab, pylint]
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--config <CONFIG>
Path to the `pyproject.toml` or `ruff.toml` file to use for configuration
--statistics
Show counts for every rule with at least one violation
--add-noqa
Enable automatic additions of `noqa` directives to failing lines
--show-files
See the files Ruff will be run against with the current settings
--show-settings
See the settings Ruff will use to lint a given Python file
-h, --help
Print help
Rule selection:
--select <RULE_CODE>
@@ -456,14 +491,6 @@ File selection:
--respect-gitignore Respect file exclusions via `.gitignore` and other standard ignore files
--force-exclude Enforce exclusions, even for paths passed to Ruff directly on the command-line
Rule configuration:
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated rules and automatic formatting
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
Miscellaneous:
-n, --no-cache
Disable cache reads
@@ -627,25 +654,44 @@ configuration.
See the [`isort` documentation](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
for more.
#### Exit codes
### Exit codes
By default, Ruff exits with the following status codes:
- `0` if no violations were found, or if all present violations were fixed automatically.
- `1` if violations were found.
- `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an internal error.
* `0` if no violations were found, or if all present violations were fixed automatically.
* `1` if violations were found.
* `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an internal error.
This convention mirrors that of tools like ESLint, Prettier, and RuboCop.
Ruff supports two command-line flags that alter its exit code behavior:
- `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
* `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
Note that Ruff will still exit with a status code of `2` if it terminates abnormally.
- `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
* `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
found, _even if_ all such violations were fixed automatically. Note that the use of
`--exit-non-zero-on-fix` can result in a non-zero exit code even if no violations remain after
autofixing.
### Autocompletion
Ruff supports autocompletion for most shells. A shell-specific completion script can be generated
by `ruff generate-shell-completion <SHELL>`, where `<SHELL>` is one of `bash`, `elvish`, `fig`, `fish`,
`powershell`, or `zsh`.
The exact steps required to enable autocompletion will vary by shell. For example instructions,
see the [Poetry](https://python-poetry.org/docs/#enable-tab-completion-for-bash-fish-or-zsh) or
[ripgrep](https://github.com/BurntSushi/ripgrep/blob/master/FAQ.md#complete) documentation.
As an example: to enable autocompletion for Zsh, run
`ruff generate-shell-completion zsh > ~/.zfunc/_ruff`. Then add the following line to your
`~/.zshrc` file, if they're not already present:
```zsh
fpath+=~/.zfunc
autoload -Uz compinit && compinit
```
<!-- End section: Configuration -->
## Supported Rules
@@ -671,7 +717,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| ---- | ---- | ------- | --- |
| F401 | unused-import | `{name}` imported but unused; consider adding to `__all__` or using a redundant alias | 🛠 |
| F402 | import-shadowed-by-loop-var | Import `{name}` from line {line} shadowed by loop variable | |
| F403 | import-star-used | `from {name} import *` used; unable to detect undefined names | |
| F403 | import-star | `from {name} import *` used; unable to detect undefined names | |
| F404 | late-future-import | `from __future__` imports must occur at the beginning of the file | |
| F405 | import-star-usage | `{name}` may be undefined, or defined from star imports: {sources} | |
| F406 | import-star-not-permitted | `from {name} import *` only allowed at module level | |
@@ -684,13 +730,13 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| F506 | percent-format-mixed-positional-and-named | `%`-format string has mixed positional and named placeholders | |
| F507 | percent-format-positional-count-mismatch | `%`-format string has {wanted} placeholder(s) but {got} substitution(s) | |
| F508 | percent-format-star-requires-sequence | `%`-format string `*` specifier requires sequence | |
| F509 | percent-format-unsupported-format-character | `%`-format string has unsupported format character '{char}' | |
| F509 | percent-format-unsupported-format-character | `%`-format string has unsupported format character `{char}` | |
| F521 | string-dot-format-invalid-format | `.format` call has invalid format string: {message} | |
| F522 | string-dot-format-extra-named-arguments | `.format` call has unused named argument(s): {message} | 🛠 |
| F523 | string-dot-format-extra-positional-arguments | `.format` call has unused arguments at position(s): {message} | |
| F524 | string-dot-format-missing-arguments | `.format` call is missing argument(s) for placeholder(s): {message} | |
| F525 | string-dot-format-mixing-automatic | `.format` string mixes automatic and manual numbering | |
| F541 | f-string-missing-placeholders | f-string without any placeholders | 🛠 |
| F541 | [f-string-missing-placeholders](https://beta.ruff.rs/docs/rules/f-string-missing-placeholders/) | f-string without any placeholders | 🛠 |
| F601 | multi-value-repeated-key-literal | Dictionary key literal `{name}` repeated | 🛠 |
| F602 | multi-value-repeated-key-variable | Dictionary key `{name}` repeated | 🛠 |
| F621 | expressions-in-star-assignment | Too many expressions in star-unpacking assignment | |
@@ -709,7 +755,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| F821 | undefined-name | Undefined name `{name}` | |
| F822 | undefined-export | Undefined name `{name}` in `__all__` | |
| F823 | undefined-local | Local variable `{name}` referenced before assignment | |
| F841 | unused-variable | Local variable `{name}` is assigned to but never used | 🛠 |
| F841 | [unused-variable](https://beta.ruff.rs/docs/rules/unused-variable/) | Local variable `{name}` is assigned to but never used | 🛠 |
| F842 | unused-annotation | Local variable `{name}` is annotated but never used | |
| F901 | raise-not-implemented | `raise NotImplemented` should be `raise NotImplementedError` | 🛠 |
@@ -725,13 +771,16 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/) on PyPI.
| E401 | multiple-imports-on-one-line | Multiple imports on one line | |
| E402 | module-import-not-at-top-of-file | Module level import not at top of file | |
| E501 | line-too-long | Line too long ({length} > {limit} characters) | |
| E701 | multiple-statements-on-one-line-colon | Multiple statements on one line (colon) | |
| E702 | multiple-statements-on-one-line-semicolon | Multiple statements on one line (semicolon) | |
| E703 | useless-semicolon | Statement ends with an unnecessary semicolon | |
| E711 | none-comparison | Comparison to `None` should be `cond is None` | 🛠 |
| E712 | true-false-comparison | Comparison to `True` should be `cond is True` | 🛠 |
| E713 | not-in-test | Test for membership should be `not in` | 🛠 |
| E714 | not-is-test | Test for object identity should be `is not` | 🛠 |
| E721 | type-comparison | Do not compare types, use `isinstance()` | |
| E722 | do-not-use-bare-except | Do not use bare `except` | |
| E731 | do-not-assign-lambda | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E722 | [bare-except](https://beta.ruff.rs/docs/rules/bare-except/) | Do not use bare `except` | |
| E731 | lambda-assignment | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E741 | ambiguous-variable-name | Ambiguous variable name: `{name}` | |
| E742 | ambiguous-class-name | Ambiguous class name: `{name}` | |
| E743 | ambiguous-function-name | Ambiguous function name: `{name}` | |
@@ -744,7 +793,7 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/) on PyPI.
| ---- | ---- | ------- | --- |
| W292 | no-new-line-at-end-of-file | No newline at end of file | 🛠 |
| W505 | doc-line-too-long | Doc line too long ({length} > {limit} characters) | |
| W605 | invalid-escape-sequence | Invalid escape sequence: '\{char}' | 🛠 |
| W605 | invalid-escape-sequence | Invalid escape sequence: `\{char}` | 🛠 |
### mccabe (C90)
@@ -752,7 +801,7 @@ For more, see [mccabe](https://pypi.org/project/mccabe/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C901 | function-is-too-complex | `{name}` is too complex ({complexity}) | |
| C901 | [complex-structure](https://beta.ruff.rs/docs/rules/complex-structure/) | `{name}` is too complex ({complexity}) | |
### isort (I)
@@ -760,8 +809,8 @@ For more, see [isort](https://pypi.org/project/isort/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I001 | unsorted-imports | Import block is un-sorted or un-formatted | 🛠 |
| I002 | missing-required-import | Missing required import: `{name}` | 🛠 |
| I001 | [unsorted-imports](https://beta.ruff.rs/docs/rules/unsorted-imports/) | Import block is un-sorted or un-formatted | 🛠 |
| I002 | [missing-required-import](https://beta.ruff.rs/docs/rules/missing-required-import/) | Missing required import: `{name}` | 🛠 |
### pep8-naming (N)
@@ -815,13 +864,13 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| D213 | multi-line-summary-second-line | Multi-line docstring summary should start at the second line | 🛠 |
| D214 | section-not-over-indented | Section is over-indented ("{name}") | 🛠 |
| D215 | section-underline-not-over-indented | Section underline is over-indented ("{name}") | 🛠 |
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
| D301 | uses-r-prefix-for-backslashed-content | Use r""" if any backslashes in a docstring | |
| D300 | triple-single-quotes | Use triple double quotes `"""` | |
| D301 | escape-sequence-in-docstring | Use `r"""` if any backslashes in a docstring | |
| D400 | ends-in-period | First line should end with a period | 🛠 |
| D401 | non-imperative-mood | First line of docstring should be in imperative mood: "{first_line}" | |
| D402 | no-signature | First line should not be the function's signature | |
| D403 | first-line-capitalized | First word of the first line should be properly capitalized | |
| D404 | no-this-prefix | First word of the docstring should not be "This" | |
| D404 | docstring-starts-with-this | First word of the docstring should not be "This" | |
| D405 | capitalize-section-name | Section name should be properly capitalized ("{name}") | 🛠 |
| D406 | new-line-after-section-name | Section name should end with a newline ("{name}") | 🛠 |
| D407 | dashed-underline-after-section | Missing dashed underline after section ("{name}") | 🛠 |
@@ -831,12 +880,12 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| D411 | blank-line-before-section | Missing blank line before section ("{name}") | 🛠 |
| D412 | no-blank-lines-between-header-and-content | No blank lines allowed between a section header and its content ("{name}") | 🛠 |
| D413 | blank-line-after-last-section | Missing blank line after last section ("{name}") | 🛠 |
| D414 | non-empty-section | Section has no content ("{name}") | |
| D414 | empty-docstring-section | Section has no content ("{name}") | |
| D415 | ends-in-punctuation | First line should end with a period, question mark, or exclamation point | 🛠 |
| D416 | section-name-ends-in-colon | Section name should end with a colon ("{name}") | 🛠 |
| D417 | document-all-arguments | Missing argument description in the docstring: `{name}` | |
| D418 | skip-docstring | Function decorated with `@overload` shouldn't contain a docstring | |
| D419 | non-empty | Docstring is empty | |
| D417 | undocumented-param | Missing argument description in the docstring: `{name}` | |
| D418 | overload-with-docstring | Function decorated with `@overload` shouldn't contain a docstring | |
| D419 | empty-docstring | Docstring is empty | |
### pyupgrade (UP)
@@ -848,10 +897,10 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI.
| UP003 | type-of-primitive | Use `{}` instead of `type(...)` | 🛠 |
| UP004 | useless-object-inheritance | Class `{name}` inherits from `object` | 🛠 |
| UP005 | deprecated-unittest-alias | `{alias}` is deprecated, use `{target}` | 🛠 |
| UP006 | use-pep585-annotation | Use `{}` instead of `{}` for type annotations | 🛠 |
| UP007 | use-pep604-annotation | Use `X \| Y` for type annotations | 🛠 |
| UP006 | deprecated-collection-type | Use `{}` instead of `{}` for type annotations | 🛠 |
| UP007 | typing-union | Use `X \| Y` for type annotations | 🛠 |
| UP008 | super-call-with-parameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
| UP009 | pep3120-unnecessary-coding-comment | UTF-8 encoding declaration is unnecessary | 🛠 |
| UP009 | utf8-encoding-declaration | UTF-8 encoding declaration is unnecessary | 🛠 |
| UP010 | unnecessary-future-import | Unnecessary `__future__` import `{import}` for target Python version | 🛠 |
| UP011 | lru-cache-without-parameters | Unnecessary parameters to `functools.lru_cache` | 🛠 |
| UP012 | unnecessary-encode-utf8 | Unnecessary call to `encode` as UTF-8 | 🛠 |
@@ -903,17 +952,17 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ANN001 | missing-type-function-argument | Missing type annotation for function argument `{name}` | |
| ANN002 | missing-type-args | Missing type annotation for `*{name}` | |
| ANN003 | missing-type-kwargs | Missing type annotation for `**{name}` | |
| ANN101 | missing-type-self | Missing type annotation for `{name}` in method | |
| ANN102 | missing-type-cls | Missing type annotation for `{name}` in classmethod | |
| ANN201 | missing-return-type-public-function | Missing return type annotation for public function `{name}` | |
| ANN202 | missing-return-type-private-function | Missing return type annotation for private function `{name}` | |
| ANN204 | missing-return-type-special-method | Missing return type annotation for special method `{name}` | 🛠 |
| ANN205 | missing-return-type-static-method | Missing return type annotation for staticmethod `{name}` | |
| ANN206 | missing-return-type-class-method | Missing return type annotation for classmethod `{name}` | |
| ANN401 | dynamically-typed-expression | Dynamically typed expressions (typing.Any) are disallowed in `{name}` | |
| ANN001 | [missing-type-function-argument](https://beta.ruff.rs/docs/rules/missing-type-function-argument/) | Missing type annotation for function argument `{name}` | |
| ANN002 | [missing-type-args](https://beta.ruff.rs/docs/rules/missing-type-args/) | Missing type annotation for `*{name}` | |
| ANN003 | [missing-type-kwargs](https://beta.ruff.rs/docs/rules/missing-type-kwargs/) | Missing type annotation for `**{name}` | |
| ANN101 | [missing-type-self](https://beta.ruff.rs/docs/rules/missing-type-self/) | Missing type annotation for `{name}` in method | |
| ANN102 | [missing-type-cls](https://beta.ruff.rs/docs/rules/missing-type-cls/) | Missing type annotation for `{name}` in classmethod | |
| ANN201 | [missing-return-type-public-function](https://beta.ruff.rs/docs/rules/missing-return-type-public-function/) | Missing return type annotation for public function `{name}` | |
| ANN202 | [missing-return-type-private-function](https://beta.ruff.rs/docs/rules/missing-return-type-private-function/) | Missing return type annotation for private function `{name}` | |
| ANN204 | [missing-return-type-special-method](https://beta.ruff.rs/docs/rules/missing-return-type-special-method/) | Missing return type annotation for special method `{name}` | 🛠 |
| ANN205 | [missing-return-type-static-method](https://beta.ruff.rs/docs/rules/missing-return-type-static-method/) | Missing return type annotation for staticmethod `{name}` | |
| ANN206 | [missing-return-type-class-method](https://beta.ruff.rs/docs/rules/missing-return-type-class-method/) | Missing return type annotation for classmethod `{name}` | |
| ANN401 | [any-type](https://beta.ruff.rs/docs/rules/any-type/) | Dynamically typed expressions (typing.Any) are disallowed in `{name}` | |
### flake8-bandit (S)
@@ -921,8 +970,8 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| S101 | assert-used | Use of `assert` detected | |
| S102 | exec-used | Use of `exec` detected | |
| S101 | assert | Use of `assert` detected | |
| S102 | exec-builtin | Use of `exec` detected | |
| S103 | bad-file-permissions | `os.chmod` setting a permissive mask `{mask:#o}` on file or directory | |
| S104 | hardcoded-bind-all-interfaces | Possible binding to all interfaces | |
| S105 | hardcoded-password-string | Possible hardcoded password: "{}" | |
@@ -930,12 +979,14 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| S107 | hardcoded-password-default | Possible hardcoded password: "{}" | |
| S108 | hardcoded-temp-file | Probable insecure usage of temporary file or directory: "{}" | |
| S110 | try-except-pass | `try`-`except`-`pass` detected, consider logging the exception | |
| S112 | try-except-continue | `try`-`except`-`continue` detected, consider logging the exception | |
| S113 | request-without-timeout | Probable use of requests call with timeout set to `{value}` | |
| S324 | hashlib-insecure-hash-function | Probable use of insecure hash functions in `hashlib`: "{}" | |
| S324 | hashlib-insecure-hash-function | Probable use of insecure hash functions in `hashlib`: `{string}` | |
| S501 | request-with-no-cert-validation | Probable use of `{string}` call with `verify=False` disabling SSL certificate checks | |
| S506 | unsafe-yaml-load | Probable use of unsafe loader `{name}` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
| S508 | snmp-insecure-version | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
| S509 | snmp-weak-cryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
| S608 | [hardcoded-sql-expression](https://beta.ruff.rs/docs/rules/hardcoded-sql-expression/) | Possible SQL injection vector through string-based query construction | |
| S612 | logging-config-insecure-listen | Use of insecure `logging.config.listen` detected | |
| S701 | jinja2-autoescape-false | Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function. | |
@@ -972,13 +1023,13 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) on PyPI
| B008 | function-call-argument-default | Do not perform function call `{name}` in argument defaults | |
| B009 | get-attr-with-constant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | set-attr-with-constant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B011 | do-not-assert-false | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B011 | assert-false | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B012 | jump-statement-in-finally | `{name}` inside `finally` blocks cause exceptions to be silenced | |
| B013 | redundant-tuple-in-exception-handler | A length-one tuple literal is redundant. Write `except {name}` instead of `except ({name},)`. | 🛠 |
| B014 | duplicate-handler-exception | Exception handler with duplicate exception: `{name}` | 🛠 |
| B015 | useless-comparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | |
| B016 | cannot-raise-literal | Cannot raise a literal. Did you intend to return it or raise an Exception? | |
| [B017](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | [assert-raises-exception](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | `assertRaises(Exception)` should be considered evil | |
| B017 | [assert-raises-exception](https://beta.ruff.rs/docs/rules/assert-raises-exception/) | `assertRaises(Exception)` should be considered evil | |
| B018 | useless-expression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | cached-instance-method | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B020 | loop-variable-overrides-iterator | Loop control variable `{name}` overrides iterable it iterates | |
@@ -998,9 +1049,9 @@ For more, see [flake8-builtins](https://pypi.org/project/flake8-builtins/) on Py
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| A001 | builtin-variable-shadowing | Variable `{name}` is shadowing a python builtin | |
| A002 | builtin-argument-shadowing | Argument `{name}` is shadowing a python builtin | |
| A003 | builtin-attribute-shadowing | Class attribute `{name}` is shadowing a python builtin | |
| A001 | [builtin-variable-shadowing](https://beta.ruff.rs/docs/rules/builtin-variable-shadowing/) | Variable `{name}` is shadowing a python builtin | |
| A002 | [builtin-argument-shadowing](https://beta.ruff.rs/docs/rules/builtin-argument-shadowing/) | Argument `{name}` is shadowing a python builtin | |
| A003 | [builtin-attribute-shadowing](https://beta.ruff.rs/docs/rules/builtin-attribute-shadowing/) | Class attribute `{name}` is shadowing a python builtin | |
### flake8-commas (COM)
@@ -1018,9 +1069,9 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C400 | unnecessary-generator-list | Unnecessary generator (rewrite as a `list` comprehension) | 🛠 |
| C401 | unnecessary-generator-set | Unnecessary generator (rewrite as a `set` comprehension) | 🛠 |
| C402 | unnecessary-generator-dict | Unnecessary generator (rewrite as a `dict` comprehension) | 🛠 |
| C400 | [unnecessary-generator-list](https://beta.ruff.rs/docs/rules/unnecessary-generator-list/) | Unnecessary generator (rewrite as a `list` comprehension) | 🛠 |
| C401 | [unnecessary-generator-set](https://beta.ruff.rs/docs/rules/unnecessary-generator-set/) | Unnecessary generator (rewrite as a `set` comprehension) | 🛠 |
| C402 | [unnecessary-generator-dict](https://beta.ruff.rs/docs/rules/unnecessary-generator-dict/) | Unnecessary generator (rewrite as a `dict` comprehension) | 🛠 |
| C403 | unnecessary-list-comprehension-set | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | 🛠 |
| C404 | unnecessary-list-comprehension-dict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | 🛠 |
| C405 | unnecessary-literal-set | Unnecessary `{obj_type}` literal (rewrite as a `set` literal) | 🛠 |
@@ -1029,11 +1080,11 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| C409 | unnecessary-literal-within-tuple-call | Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` literal) | 🛠 |
| C410 | unnecessary-literal-within-list-call | Unnecessary `{literal}` literal passed to `list()` (remove the outer call to `list()`) | 🛠 |
| C411 | unnecessary-list-call | Unnecessary `list` call (remove the outer call to `list()`) | 🛠 |
| C413 | unnecessary-call-around-sorted | Unnecessary `{func}` call around `sorted()` | 🛠 |
| C414 | unnecessary-double-cast-or-process | Unnecessary `{inner}` call within `{outer}()` | |
| C413 | [unnecessary-call-around-sorted](https://beta.ruff.rs/docs/rules/unnecessary-call-around-sorted/) | Unnecessary `{func}` call around `sorted()` | 🛠 |
| C414 | [unnecessary-double-cast-or-process](https://beta.ruff.rs/docs/rules/unnecessary-double-cast-or-process/) | Unnecessary `{inner}` call within `{outer}()` | 🛠 |
| C415 | unnecessary-subscript-reversal | Unnecessary subscript reversal of iterable within `{func}()` | |
| C416 | unnecessary-comprehension | Unnecessary `{obj_type}` comprehension (rewrite using `{obj_type}()`) | 🛠 |
| C417 | unnecessary-map | Unnecessary `map` usage (rewrite using a generator expression) | |
| C417 | [unnecessary-map](https://beta.ruff.rs/docs/rules/unnecessary-map/) | Unnecessary `map` usage (rewrite using a generator expression) | 🛠 |
### flake8-datetimez (DTZ)
@@ -1059,15 +1110,25 @@ For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/) on Py
| ---- | ---- | ------- | --- |
| T100 | debugger | Trace found: `{name}` used | |
### flake8-django (DJ)
For more, see [flake8-django](https://pypi.org/project/flake8-django/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| DJ001 | [nullable-model-string-field](https://beta.ruff.rs/docs/rules/nullable-model-string-field/) | Avoid using `null=True` on string-based fields such as {field_name} | |
| DJ008 | [model-without-dunder-str](https://beta.ruff.rs/docs/rules/model-without-dunder-str/) | Model does not define `__str__` method | |
| DJ013 | [non-leading-receiver-decorator](https://beta.ruff.rs/docs/rules/non-leading-receiver-decorator/) | `@receiver` decorator must be on top of all the other decorators | |
### flake8-errmsg (EM)
For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| EM101 | raw-string-in-exception | Exception must not use a string literal, assign to variable first | |
| EM102 | f-string-in-exception | Exception must not use an f-string literal, assign to variable first | |
| EM103 | dot-format-in-exception | Exception must not use a `.format()` string directly, assign to variable first | |
| EM101 | [raw-string-in-exception](https://beta.ruff.rs/docs/rules/raw-string-in-exception/) | Exception must not use a string literal, assign to variable first | |
| EM102 | [f-string-in-exception](https://beta.ruff.rs/docs/rules/f-string-in-exception/) | Exception must not use an f-string literal, assign to variable first | |
| EM103 | [dot-format-in-exception](https://beta.ruff.rs/docs/rules/dot-format-in-exception/) | Exception must not use a `.format()` string directly, assign to variable first | |
### flake8-executable (EXE)
@@ -1077,7 +1138,7 @@ For more, see [flake8-executable](https://pypi.org/project/flake8-executable/) o
| ---- | ---- | ------- | --- |
| EXE001 | shebang-not-executable | Shebang is present but file is not executable | |
| EXE002 | shebang-missing-executable-file | The file is executable but no shebang is present | |
| EXE003 | shebang-python | Shebang should contain "python" | |
| EXE003 | shebang-python | Shebang should contain `python` | |
| EXE004 | shebang-whitespace | Avoid whitespace before shebang | 🛠 |
| EXE005 | shebang-newline | Shebang should be at the beginning of the file | |
@@ -1097,7 +1158,7 @@ For more, see [flake8-import-conventions](https://github.com/joaopalmeiro/flake8
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [ICN001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | [unconventional-import-alias](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | `{name}` should be imported as `{asname}` | |
| ICN001 | [unconventional-import-alias](https://beta.ruff.rs/docs/rules/unconventional-import-alias/) | `{name}` should be imported as `{asname}` | |
### flake8-logging-format (G)
@@ -1120,7 +1181,7 @@ For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [INP001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | [implicit-namespace-package](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
| INP001 | [implicit-namespace-package](https://beta.ruff.rs/docs/rules/implicit-namespace-package/) | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
### flake8-pie (PIE)
@@ -1128,11 +1189,11 @@ For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PIE790 | no-unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE790 | unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE794 | dupe-class-field-definitions | Class field `{name}` is defined multiple times | 🛠 |
| PIE796 | prefer-unique-enums | Enum contains duplicate value: `{value}` | |
| PIE800 | no-unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | no-unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE800 | unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE807 | prefer-list-builtin | Prefer `list` over useless lambda | 🛠 |
| PIE810 | single-starts-ends-with | Call `{attr}` once with a `tuple` | |
@@ -1145,6 +1206,16 @@ For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI.
| T201 | print-found | `print` found | |
| T203 | p-print-found | `pprint` found | |
### flake8-pyi (PYI)
For more, see [flake8-pyi](https://pypi.org/project/flake8-pyi/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PYI001 | [prefix-type-params](https://beta.ruff.rs/docs/rules/prefix-type-params/) | Name of private `{kind}` must start with `_` | |
| PYI007 | [unrecognized-platform-check](https://beta.ruff.rs/docs/rules/unrecognized-platform-check/) | Unrecognized `sys.platform` check | |
| PYI008 | [unrecognized-platform-name](https://beta.ruff.rs/docs/rules/unrecognized-platform-name/) | Unrecognized platform `{platform}` | |
### flake8-pytest-style (PT)
For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) on PyPI.
@@ -1183,10 +1254,18 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [Q000](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | [bad-quotes-inline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | Double quotes found but single quotes preferred | 🛠 |
| [Q001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | [bad-quotes-multiline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | Double quote multiline found but single quotes preferred | 🛠 |
| [Q002](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | [bad-quotes-docstring](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | Double quote docstring found but single quotes preferred | 🛠 |
| [Q003](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoid-quote-escape.md) | [avoid-quote-escape](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoid-quote-escape.md) | Change outer quotes to avoid escaping inner quotes | 🛠 |
| Q000 | [bad-quotes-inline-string](https://beta.ruff.rs/docs/rules/bad-quotes-inline-string/) | Double quotes found but single quotes preferred | 🛠 |
| Q001 | [bad-quotes-multiline-string](https://beta.ruff.rs/docs/rules/bad-quotes-multiline-string/) | Double quote multiline found but single quotes preferred | 🛠 |
| Q002 | [bad-quotes-docstring](https://beta.ruff.rs/docs/rules/bad-quotes-docstring/) | Double quote docstring found but single quotes preferred | 🛠 |
| Q003 | [avoidable-escaped-quote](https://beta.ruff.rs/docs/rules/avoidable-escaped-quote/) | Change outer quotes to avoid escaping inner quotes | 🛠 |
### flake8-raise (RSE)
For more, see [flake8-raise](https://pypi.org/project/flake8-raise/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| RSE102 | unnecessary-paren-on-raise-exception | Unnecessary parentheses on raised exception | 🛠 |
### flake8-return (RET)
@@ -1203,6 +1282,14 @@ For more, see [flake8-return](https://pypi.org/project/flake8-return/) on PyPI.
| RET507 | superfluous-else-continue | Unnecessary `{branch}` after `continue` statement | |
| RET508 | superfluous-else-break | Unnecessary `{branch}` after `break` statement | |
### flake8-self (SLF)
For more, see [flake8-self](https://pypi.org/project/flake8-self/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SLF001 | [private-member-access](https://beta.ruff.rs/docs/rules/private-member-access/) | Private member accessed: `{access}` | |
### flake8-simplify (SIM)
For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on PyPI.
@@ -1210,8 +1297,8 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on Py
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM101 | duplicate-isinstance-call | Multiple `isinstance` calls for `{name}`, merge into a single call | 🛠 |
| SIM102 | nested-if-statements | Use a single `if` statement instead of nested `if` statements | 🛠 |
| SIM103 | return-bool-condition-directly | Return the condition `{cond}` directly | 🛠 |
| SIM102 | collapsible-if | Use a single `if` statement instead of nested `if` statements | 🛠 |
| SIM103 | needless-bool | Return the condition `{condition}` directly | 🛠 |
| SIM105 | use-contextlib-suppress | Use `contextlib.suppress({exception})` instead of try-except-pass | |
| SIM107 | return-in-try-except-finally | Don't use `return` in `try`/`except` and `finally` | |
| SIM108 | use-ternary-operator | Use ternary operator `{contents}` instead of if-else-block | 🛠 |
@@ -1219,6 +1306,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on Py
| SIM110 | convert-loop-to-any | Use `{any}` instead of `for` loop | 🛠 |
| SIM111 | convert-loop-to-all | Use `{all}` instead of `for` loop | 🛠 |
| SIM112 | use-capital-environment-variables | Use capitalized environment variable `{expected}` instead of `{original}` | 🛠 |
| SIM114 | [if-with-same-arms](https://beta.ruff.rs/docs/rules/if-with-same-arms/) | Combine `if` branches using logical `or` operator | |
| SIM115 | open-file-with-context-handler | Use context handler for opening files | |
| SIM117 | multiple-with-statements | Use a single `with` statement with multiple contexts instead of nested `with` statements | 🛠 |
| SIM118 | key-in-dict | Use `{key} in {dict}` instead of `{key} in {dict}.keys()` | 🛠 |
@@ -1241,8 +1329,8 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TID251 | banned-api | `{name}` is banned: {message} | |
| TID252 | relative-imports | Relative imports from parent modules are banned | |
| TID251 | [banned-api](https://beta.ruff.rs/docs/rules/banned-api/) | `{name}` is banned: {message} | |
| TID252 | [relative-imports](https://beta.ruff.rs/docs/rules/relative-imports/) | Relative imports from parent modules are banned | 🛠 |
### flake8-type-checking (TCH)
@@ -1306,7 +1394,7 @@ For more, see [eradicate](https://pypi.org/project/eradicate/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [ERA001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | [commented-out-code](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | Found commented-out code | 🛠 |
| ERA001 | [commented-out-code](https://beta.ruff.rs/docs/rules/commented-out-code/) | Found commented-out code | 🛠 |
### pandas-vet (PD)
@@ -1314,7 +1402,7 @@ For more, see [pandas-vet](https://pypi.org/project/pandas-vet/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PD002 | use-of-inplace-argument | `inplace=True` should be avoided; it has inconsistent behavior | 🛠 |
| PD002 | [use-of-inplace-argument](https://beta.ruff.rs/docs/rules/use-of-inplace-argument/) | `inplace=True` should be avoided; it has inconsistent behavior | 🛠 |
| PD003 | use-of-dot-is-null | `.isna` is preferred to `.isnull`; functionality is equivalent | |
| PD004 | use-of-dot-not-null | `.notna` is preferred to `.notnull`; functionality is equivalent | |
| PD007 | use-of-dot-ix | `.ix` is deprecated; use more explicit `.loc` or `.iloc` | |
@@ -1353,13 +1441,15 @@ For more, see [Pylint](https://pypi.org/project/pylint/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLE0100 | [yield-in-init](https://beta.ruff.rs/docs/rules/yield-in-init/) | `__init__` method is a generator | |
| PLE0117 | nonlocal-without-binding | Nonlocal name `{name}` found without binding | |
| PLE0118 | used-prior-global-declaration | Name `{name}` is used prior to global declaration on line {line} | |
| PLE0604 | invalid-all-object | Invalid object in `__all__`, must contain only strings | |
| PLE0605 | invalid-all-format | Invalid format for `__all__`, must be `tuple` or `list` | |
| PLE1142 | await-outside-async | `await` should be used within an async function | |
| PLE1307 | [bad-string-format-type](https://beta.ruff.rs/docs/rules/bad-string-format-type/) | Format type does not match argument type | |
| PLE1310 | bad-str-strip-call | String `{strip}` call contains duplicate characters (did you mean `{removal}`?) | |
| PLE2502 | bidirectional-unicode | Avoid using bidirectional unicode | |
| PLE2502 | bidirectional-unicode | Contains control characters that can permit obfuscated code | |
#### Refactor (PLR)
@@ -1380,7 +1470,7 @@ For more, see [Pylint](https://pypi.org/project/pylint/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLW0120 | useless-else-on-loop | Else clause on loop without a break statement, remove the else and de-indent all the code inside it | |
| PLW0120 | useless-else-on-loop | `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it | |
| PLW0602 | global-variable-not-assigned | Using global for `{name}` but no assignment is done | |
### tryceratops (TRY)
@@ -1389,41 +1479,32 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [TRY002](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | [raise-vanilla-class](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | Create your own exception | |
| TRY002 | [raise-vanilla-class](https://beta.ruff.rs/docs/rules/raise-vanilla-class/) | Create your own exception | |
| TRY003 | raise-vanilla-args | Avoid specifying long messages outside the exception class | |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | 🛠 |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | |
| TRY200 | reraise-no-cause | Use `raise from` to specify exception cause | |
| TRY201 | verbose-raise | Use `raise` without specifying exception name | |
| TRY300 | try-consider-else | Consider moving this statement to an `else` block | |
| TRY301 | raise-within-try | Abstract `raise` to an inner function | |
| TRY400 | error-instead-of-exception | Use `logging.exception` instead of `logging.error` | |
### flake8-raise (RSE)
For more, see [flake8-raise](https://pypi.org/project/flake8-raise/) on PyPI.
### NumPy-specific rules (NPY)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| RSE102 | unnecessary-paren-on-raise-exception | Unnecessary parentheses on raised exception | 🛠 |
### flake8-self (SLF)
For more, see [flake8-self](https://pypi.org/project/flake8-self/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SLF001 | private-member-access | Private member accessed: `{access}` | |
| NPY001 | [numpy-deprecated-type-alias](https://beta.ruff.rs/docs/rules/numpy-deprecated-type-alias/) | Type alias `np.{type_name}` is deprecated, replace with builtin type | 🛠 |
### Ruff-specific rules (RUF)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| RUF001 | ambiguous-unicode-character-string | String contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF002 | ambiguous-unicode-character-docstring | Docstring contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF003 | ambiguous-unicode-character-comment | Comment contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF001 | ambiguous-unicode-character-string | String contains ambiguous unicode character `{confusable}` (did you mean `{representant}`?) | 🛠 |
| RUF002 | ambiguous-unicode-character-docstring | Docstring contains ambiguous unicode character `{confusable}` (did you mean `{representant}`?) | 🛠 |
| RUF003 | ambiguous-unicode-character-comment | Comment contains ambiguous unicode character `{confusable}` (did you mean `{representant}`?) | 🛠 |
| RUF004 | keyword-argument-before-star-argument | Keyword argument `{name}` must come after starred arguments | |
| RUF005 | unpack-instead-of-concatenating-to-collection-literal | Consider `{expr}` instead of concatenation | |
| RUF100 | unused-noqa | Unused blanket `noqa` directive | 🛠 |
| RUF005 | unpack-instead-of-concatenating-to-collection-literal | Consider `{expr}` instead of concatenation | 🛠 |
| RUF006 | [asyncio-dangling-task](https://beta.ruff.rs/docs/rules/asyncio-dangling-task/) | Store a reference to the return value of `asyncio.create_task` | |
| RUF100 | unused-noqa | Unused `noqa` directive | 🛠 |
<!-- End auto-generated sections. -->
@@ -1694,6 +1775,7 @@ natively, including:
* [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
* [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
* [flake8-debugger](https://pypi.org/project/flake8-debugger/)
* [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
* [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
* [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
* [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
@@ -1704,6 +1786,7 @@ natively, including:
* [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420)
* [flake8-pie](https://pypi.org/project/flake8-pie/)
* [flake8-print](https://pypi.org/project/flake8-print/)
* [flake8-pyi](https://pypi.org/project/flake8-pyi/)
* [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
* [flake8-quotes](https://pypi.org/project/flake8-quotes/)
* [flake8-raise](https://pypi.org/project/flake8-raise/)
@@ -1791,6 +1874,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
* [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
* [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
* [flake8-debugger](https://pypi.org/project/flake8-debugger/)
* [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
* [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
* [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
* [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
@@ -1823,6 +1907,22 @@ implemented in [pyupgrade](https://pypi.org/project/pyupgrade/) ([#827](https://
If you're looking to use Ruff, but rely on an unsupported Flake8 plugin, feel free to file an Issue.
### What versions of Python does Ruff support?
Ruff can lint code for any Python version from 3.7 onwards. However, Ruff lacks support for a few
language features that were introduced in Python 3.10 and later. Specifically, Ruff does not
support:
- "Structural Pattern Matching" ([PEP 622](https://peps.python.org/pep-0622/)), introduced in Python 3.10.
- "Exception Groups and except* ([PEP 654](https://www.python.org/dev/peps/pep-0654/)), introduced in Python 3.11.
Support for these features is planned.
Ruff does not support Python 2. Ruff _may_ run on pre-Python 3.7 code, although such versions
are not officially supported (e.g., Ruff does _not_ respect type comments).
Ruff is installable under any Python version from 3.7 onwards.
### Do I need to install Rust to use Ruff?
Nope! Ruff is available as [`ruff`](https://pypi.org/project/ruff/) on PyPI:
@@ -1975,6 +2075,15 @@ unfixable = ["B", "SIM", "TRY", "RUF"]
If you find a case where Ruff's autofix breaks your code, please file an Issue!
### How can I disable Ruff's color output?
Ruff's color output is powered by the [`colored`](https://crates.io/crates/colored) crate, which
attempts to automatically detect whether the output stream supports color. However, you can force
colors off by setting the `NO_COLOR` environment variable to any value (e.g., `NO_COLOR=1`).
[`colored`](https://crates.io/crates/colored) also supports the the `CLICOLOR` and `CLICOLOR_FORCE`
environment variables (see the [spec](https://bixense.com/clicolors/)).
<!-- End section: FAQ -->
## Contributing
@@ -2502,6 +2611,25 @@ select = ["E", "F", "B", "Q"]
---
#### [`show-fixes`](#show-fixes)
Whether to show an enumeration of all autofixed lint violations
(overridden by the `--show-fixes` command-line flag).
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
# By default, always enumerate fixed violations.
show-fixes = true
```
---
#### [`show-source`](#show-source)
Whether to show source code snippets when reporting lint violations
@@ -3396,7 +3524,7 @@ alias (e.g., `import A as B`) to wrap such that every line contains
exactly one member. For example, this formatting would be retained,
rather than condensing to a single line:
```py
```python
from .utils import (
test_directory as test_directory,
test_id as test_id
@@ -3458,6 +3586,24 @@ known-first-party = ["src"]
---
#### [`known-local-folder`](#known-local-folder)
A list of modules to consider being a local folder.
Generally, this is reserved for relative imports (`from . import module`).
**Default value**: `[]`
**Type**: `list[str]`
**Example usage**:
```toml
[tool.ruff.isort]
known-local-folder = ["src"]
```
---
#### [`known-third-party`](#known-third-party)
A list of modules to consider third-party, regardless of whether they
@@ -3479,7 +3625,7 @@ known-third-party = ["src"]
#### [`lines-after-imports`](#lines-after-imports)
The number of blank lines to place after imports.
-1 for automatic determination.
Use `-1` for automatic determination.
**Default value**: `-1`
@@ -3495,6 +3641,24 @@ lines-after-imports = 1
---
#### [`lines-between-types`](#lines-between-types)
The number of lines to place between "direct" and `import from` imports.
**Default value**: `0`
**Type**: `int`
**Example usage**:
```toml
[tool.ruff.isort]
# Use a single line between direct and from import
lines-between-types = 1
```
---
#### [`no-lines-before`](#no-lines-before)
A list of sections that should _not_ be delineated from the previous
@@ -3889,6 +4053,32 @@ keep-runtime-typing = true
<!-- End section: Settings -->
<!-- Begin section: Acknowledgements -->
## Acknowledgements
Ruff's linter draws on both the APIs and implementation details of many other
tools in the Python ecosystem, especially [Flake8](https://github.com/PyCQA/flake8), [Pyflakes](https://github.com/PyCQA/pyflakes),
[pycodestyle](https://github.com/PyCQA/pycodestyle), [pydocstyle](https://github.com/PyCQA/pydocstyle),
[pyupgrade](https://github.com/asottile/pyupgrade), and [isort](https://github.com/PyCQA/isort).
In some cases, Ruff includes a "direct" Rust port of the corresponding tool.
We're grateful to the maintainers of these tools for their work, and for all
the value they've provided to the Python community.
Ruff's autoformatter is built on a fork of Rome's [`rome_formatter`](https://github.com/rome/tools/tree/main/crates/rome_formatter),
and again draws on both the APIs and implementation details of [Rome](https://github.com/rome/tools),
[Prettier](https://github.com/prettier/prettier), and [Black](https://github.com/psf/black).
Ruff is also influenced by a number of tools outside the Python ecosystem, like
[Clippy](https://github.com/rust-lang/rust-clippy) and [ESLint](https://github.com/eslint/eslint).
Ruff is the beneficiary of a large number of [contributors](https://github.com/charliermarsh/ruff/graphs/contributors).
Ruff is released under the MIT license.
<!-- End section: Acknowledgements -->
## License
MIT

View File

@@ -1,2 +1,6 @@
[files]
extend-exclude = ["snapshots"]
extend-exclude = ["snapshots", "black"]
[default.extend-words]
trivias = "trivias"
hel = "hel"

7
clippy.toml Normal file
View File

@@ -0,0 +1,7 @@
doc-valid-idents = [
"StackOverflow",
"CodeQL",
"IPython",
"NumPy",
"..",
]

View File

@@ -1,19 +1,19 @@
[package]
name = "flake8-to-ruff"
version = "0.0.244"
version = "0.0.247"
edition = "2021"
[dependencies]
anyhow = { version = "1.0.66" }
clap = { version = "4.0.1", features = ["derive"] }
anyhow = { workspace = true }
clap = { workspace = true }
colored = { version = "2.0.0" }
configparser = { version = "3.0.2" }
once_cell = { version = "1.16.0" }
regex = { version = "1.6.0" }
once_cell = { workspace = true }
regex = { workspace = true }
ruff = { path = "../ruff", default-features = false }
rustc-hash = { version = "1.1.0" }
serde = { version = "1.0.147", features = ["derive"] }
serde_json = { version = "1.0.87" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
toml = { version = "0.6.0" }
rustc-hash = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }
strum_macros = { workspace = true }
toml = { workspace = true }

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.244"
version = "0.0.247"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -16,12 +16,12 @@ crate-type = ["cdylib", "rlib"]
doctest = false
[dependencies]
anyhow = { version = "1.0.66" }
anyhow = { workspace = true }
bisection = { version = "0.1.0" }
bitflags = { version = "1.3.2" }
cfg-if = { version = "1.0.0" }
chrono = { version = "0.4.21", default-features = false, features = ["clock"] }
clap = { version = "4.0.1", features = ["derive", "env"] }
clap = { workspace = true, features = ["derive", "env", "string"] }
colored = { version = "2.0.0" }
dirs = { version = "4.0.0" }
fern = { version = "0.6.1" }
@@ -29,32 +29,32 @@ 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" }
itertools = { workspace = true }
libcst = { workspace = true }
log = { version = "0.4.17" }
natord = { version = "1.0.9" }
nohash-hasher = { version = "0.2.0" }
num-bigint = { version = "0.4.3" }
num-traits = "0.2.15"
once_cell = { version = "1.16.0" }
once_cell = { workspace = true }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ruff_macros = { version = "0.0.244", path = "../ruff_macros" }
ruff_python = { version = "0.0.244", path = "../ruff_python" }
rustc-hash = { version = "1.1.0" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
schemars = { version = "0.8.11" }
regex = { workspace = true }
ruff_macros = { path = "../ruff_macros" }
ruff_python = { path = "../ruff_python" }
rustc-hash = { workspace = true }
rustpython-common = { workspace = true }
rustpython-parser = { workspace = true }
schemars = { workspace = true }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }
serde = { workspace = true }
shellexpand = { version = "3.0.0" }
smallvec = { version = "1.10.0" }
strum = { version = "0.24.1", features = ["strum_macros"] }
strum_macros = { version = "0.24.3" }
strum = { workspace = true }
strum_macros = { workspace = true }
textwrap = { version = "0.16.0" }
thiserror = { version = "1.0" }
titlecase = { version = "2.2.1" }
toml = { version = "0.6.0" }
toml = { workspace = true }
# https://docs.rs/getrandom/0.2.7/getrandom/#webassembly-support
# For (future) wasm-pack support

View File

@@ -0,0 +1,4 @@
avoid-*
do-not-*
uses-*
*-used

View File

@@ -103,3 +103,7 @@ class Foo:
@classmethod
def foo(cls, a: int, b: int) -> int:
pass
# ANN101
def foo(self, /, a: int, b: int) -> int:
pass

View File

@@ -0,0 +1,29 @@
try:
pass
except Exception:
continue
try:
pass
except:
continue
try:
pass
except (Exception,):
continue
try:
pass
except (Exception, ValueError):
continue
try:
pass
except ValueError:
continue
try:
pass
except (ValueError,):
continue

View File

@@ -0,0 +1,95 @@
# single-line failures
query1 = "SELECT %s FROM table" % (var,) # bad
query2 = "SELECT var FROM " + table
query3 = "SELECT " + val + " FROM " + table
query4 = "SELECT {} FROM table;".format(var)
query5 = f"SELECT * FROM table WHERE var = {var}"
query6 = "DELETE FROM table WHERE var = %s" % (var,)
query7 = "DELETE FROM table WHERE VAR = " + var
query8 = "DELETE FROM " + table + "WHERE var = " + var
query9 = "DELETE FROM table WHERE var = {}".format(var)
query10 = f"DELETE FROM table WHERE var = {var}"
query11 = "INSERT INTO table VALUES (%s)" % (var,)
query12 = "INSERT INTO TABLE VALUES (" + var + ")"
query13 = "INSERT INTO {} VALUES ({})".format(table, var)
query14 = f"INSERT INTO {table} VALUES var = {var}"
query15 = "UPDATE %s SET var = %s" % (table, var)
query16 = "UPDATE " + table + " SET var = " + var
query17 = "UPDATE {} SET var = {}".format(table, var)
query18 = f"UPDATE {table} SET var = {var}"
query19 = "select %s from table" % (var,)
query20 = "select var from " + table
query21 = "select " + val + " from " + table
query22 = "select {} from table;".format(var)
query23 = f"select * from table where var = {var}"
query24 = "delete from table where var = %s" % (var,)
query25 = "delete from table where var = " + var
query26 = "delete from " + table + "where var = " + var
query27 = "delete from table where var = {}".format(var)
query28 = f"delete from table where var = {var}"
query29 = "insert into table values (%s)" % (var,)
query30 = "insert into table values (" + var + ")"
query31 = "insert into {} values ({})".format(table, var)
query32 = f"insert into {table} values var = {var}"
query33 = "update %s set var = %s" % (table, var)
query34 = "update " + table + " set var = " + var
query35 = "update {} set var = {}".format(table, var)
query36 = f"update {table} set var = {var}"
# multi-line failures
def query37():
return """
SELECT *
FROM table
WHERE var = %s
""" % var
def query38():
return """
SELECT *
FROM TABLE
WHERE var =
""" + var
def query39():
return """
SELECT *
FROM table
WHERE var = {}
""".format(var)
def query40():
return f"""
SELECT *
FROM table
WHERE var = {var}
"""
def query41():
return (
"SELECT *"
"FROM table"
f"WHERE var = {var}"
)
# # cursor-wrapped failures
query42 = cursor.execute("SELECT * FROM table WHERE var = %s" % var)
query43 = cursor.execute(f"SELECT * FROM table WHERE var = {var}")
query44 = cursor.execute("SELECT * FROM table WHERE var = {}".format(var))
query45 = cursor.executemany("SELECT * FROM table WHERE var = %s" % var, [])
# # pass
query = "SELECT * FROM table WHERE id = 1"
query = "DELETE FROM table WHERE id = 1"
query = "INSERT INTO table VALUES (1)"
query = "UPDATE table SET id = 1"
cursor.execute('SELECT * FROM table WHERE id = %s', var)
cursor.execute('SELECT * FROM table WHERE id = 1')
cursor.executemany('SELECT * FROM table WHERE id = %s', [var, var2])

View File

@@ -17,6 +17,9 @@ s.rstrip(".facebook.com") # warning
s.rstrip("e") # no warning
s.rstrip("\n\t ") # no warning
s.rstrip(r"\n\t ") # warning
s.strip("a") # no warning
s.strip("") # no warning
s.strip("ああ") # warning
from somewhere import other_type, strip

View File

@@ -1,6 +1,6 @@
"""
Should emit:
B904 - on lines 10, 11 and 16
B904 - on lines 10, 11, 16, 62, and 64
"""
try:
@@ -53,3 +53,12 @@ except ImportError:
raise ValueError
except ValueError:
raise
try:
...
except Exception as e:
if ...:
raise RuntimeError("boom!")
else:
raise RuntimeError("bang!")

View File

@@ -2,7 +2,9 @@ x = set(x for x in range(3))
x = set(
x for x in range(3)
)
y = f'{set(a if a < 6 else 0 for a in range(3))}'
_ = '{}'.format(set(a if a < 6 else 0 for a in range(3)))
print(f'Hello {set(a for a in range(3))} World')
def set(*args, **kwargs):
return None

View File

@@ -3,3 +3,5 @@ dict(
(x, x) for x in range(3)
)
dict(((x, x) for x in range(3)), z=3)
y = f'{dict((x, x) for x in range(3))}'
print(f'Hello {dict((x, x) for x in range(3))} World')

View File

@@ -4,7 +4,9 @@ list(sorted(x))
reversed(sorted(x))
reversed(sorted(x, key=lambda e: e))
reversed(sorted(x, reverse=True))
reversed(sorted(x, key=lambda e: e, reverse=True))
reversed(sorted(x, reverse=True, key=lambda e: e))
reversed(sorted(x, reverse=False))
def reversed(*args, **kwargs):
return None

View File

@@ -12,3 +12,9 @@ sorted(list(x))
sorted(tuple(x))
sorted(sorted(x))
sorted(reversed(x))
tuple(
list(
[x, 3, "hell"\
"o"]
)
)

View File

@@ -1,6 +1,34 @@
# Errors.
nums = [1, 2, 3]
map(lambda x: x + 1, nums)
map(lambda x: str(x), nums)
list(map(lambda x: x * 2, nums))
set(map(lambda x: x % 2 == 0, nums))
dict(map(lambda v: (v, v**2), nums))
map(lambda: "const", nums)
map(lambda _: 3.0, nums)
_ = "".join(map(lambda x: x in nums and "1" or "0", range(123)))
all(map(lambda v: isinstance(v, dict), nums))
filter(func, map(lambda v: v, nums))
# When inside f-string, then the fix should be surrounded by whitespace
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
# Error, but unfixable.
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
map(lambda x=1: x, nums)
# False negatives.
map(lambda x=2, y=1: x + y, nums, nums)
set(map(lambda x, y: x, nums, nums))
def myfunc(arg1: int, arg2: int = 4):
return 2 * arg1 + arg2
list(map(myfunc, nums))
[x for x in nums]

View File

@@ -0,0 +1,48 @@
from django.db.models import Model as DjangoModel
from django.db import models
from django.db.models import CharField as SmthCharField
class IncorrectModel(models.Model):
charfield = models.CharField(max_length=255, null=True)
textfield = models.TextField(max_length=255, null=True)
slugfield = models.SlugField(max_length=255, null=True)
emailfield = models.EmailField(max_length=255, null=True)
filepathfield = models.FilePathField(max_length=255, null=True)
urlfield = models.URLField(max_length=255, null=True)
class IncorrectModelWithAlias(DjangoModel):
charfield = DjangoModel.CharField(max_length=255, null=True)
textfield = SmthCharField(max_length=255, null=True)
slugfield = models.SlugField(max_length=255, null=True)
emailfield = models.EmailField(max_length=255, null=True)
filepathfield = models.FilePathField(max_length=255, null=True)
urlfield = models.URLField(max_length=255, null=True)
class IncorrectModelWithoutSuperclass:
charfield = DjangoModel.CharField(max_length=255, null=True)
textfield = SmthCharField(max_length=255, null=True)
slugfield = models.SlugField(max_length=255, null=True)
emailfield = models.EmailField(max_length=255, null=True)
filepathfield = models.FilePathField(max_length=255, null=True)
urlfield = models.URLField(max_length=255, null=True)
class CorrectModel(models.Model):
charfield = models.CharField(max_length=255, null=False, blank=True)
textfield = models.TextField(max_length=255, null=False, blank=True)
slugfield = models.SlugField(max_length=255, null=False, blank=True)
emailfield = models.EmailField(max_length=255, null=False, blank=True)
filepathfield = models.FilePathField(max_length=255, null=False, blank=True)
urlfield = models.URLField(max_length=255, null=False, blank=True)
charfieldu = models.CharField(max_length=255, null=True, blank=True, unique=True)
textfieldu = models.TextField(max_length=255, null=True, blank=True, unique=True)
slugfieldu = models.SlugField(max_length=255, null=True, blank=True, unique=True)
emailfieldu = models.EmailField(max_length=255, null=True, blank=True, unique=True)
filepathfieldu = models.FilePathField(
max_length=255, null=True, blank=True, unique=True
)
urlfieldu = models.URLField(max_length=255, null=True, blank=True, unique=True)

View File

@@ -0,0 +1,167 @@
from django.db import models
from django.db.models import Model
# Models without __str__
class TestModel1(models.Model):
new_field = models.CharField(max_length=10)
class Meta:
verbose_name = "test model"
verbose_name_plural = "test models"
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
class TestModel2(Model):
new_field = models.CharField(max_length=10)
class Meta:
verbose_name = "test model"
verbose_name_plural = "test models"
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
class TestModel3(Model):
new_field = models.CharField(max_length=10)
class Meta:
abstract = False
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
# Models with __str__
class TestModel4(Model):
new_field = models.CharField(max_length=10)
class Meta:
verbose_name = "test model"
verbose_name_plural = "test models"
def __str__(self):
return self.new_field
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
class TestModel5(models.Model):
new_field = models.CharField(max_length=10)
class Meta:
verbose_name = "test model"
verbose_name_plural = "test models"
def __str__(self):
return self.new_field
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
# Abstract models without str
class AbstractTestModel1(models.Model):
new_field = models.CharField(max_length=10)
class Meta:
abstract = True
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
class AbstractTestModel2(Model):
new_field = models.CharField(max_length=10)
class Meta:
abstract = True
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
# Abstract models with __str__
class AbstractTestModel3(Model):
new_field = models.CharField(max_length=10)
class Meta:
abstract = True
def __str__(self):
return self.new_field
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
class AbstractTestModel4(models.Model):
new_field = models.CharField(max_length=10)
class Meta:
abstract = True
def __str__(self):
return self.new_field
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2
class AbstractTestModel5(models.Model):
new_field = models.CharField(max_length=10)
class Meta:
abstract = False
def __str__(self):
return self.new_field
@property
def my_brand_new_property(self):
return 1
def my_beautiful_method(self):
return 2

View File

@@ -0,0 +1,37 @@
from django.db.models.signals import pre_save
from django.dispatch import receiver
from myapp.models import MyModel
test_decorator = lambda func: lambda *args, **kwargs: func(*args, **kwargs)
@receiver(pre_save, sender=MyModel)
@test_decorator
def correct_pre_save_handler():
pass
@test_decorator
@receiver(pre_save, sender=MyModel)
def incorrect_pre_save_handler():
pass
@receiver(pre_save, sender=MyModel)
@receiver(pre_save, sender=MyModel)
@test_decorator
def correct_multiple():
pass
@receiver(pre_save, sender=MyModel)
@receiver(pre_save, sender=MyModel)
def correct_multiple():
pass
@receiver(pre_save, sender=MyModel)
@test_decorator
@receiver(pre_save, sender=MyModel)
def incorrect_multiple():
pass

View File

@@ -0,0 +1,13 @@
from typing import ParamSpec, TypeVar, TypeVarTuple
T = TypeVar("T") # OK
TTuple = TypeVarTuple("TTuple") # OK
P = ParamSpec("P") # OK
_T = TypeVar("_T") # OK
_TTuple = TypeVarTuple("_TTuple") # OK
_P = ParamSpec("_P") # OK

View File

@@ -0,0 +1,13 @@
from typing import ParamSpec, TypeVar, TypeVarTuple
T = TypeVar("T") # Error: TypeVars in stubs must start with _
TTuple = TypeVarTuple("TTuple") # Error: TypeVarTuples must also start with _
P = ParamSpec("P") # Error: ParamSpecs must start with _
_T = TypeVar("_T") # OK
_TTuple = TypeVarTuple("_TTuple") # OK
_P = ParamSpec("_P") # OK

View File

@@ -0,0 +1,11 @@
import sys
if sys.platform == "platform_name_1": ... # OK
if sys.platform != "platform_name_2": ... # OK
if sys.platform in ["linux"]: ... # OK
if sys.platform > 3: ... # OK
if sys.platform == 10.12: ... # OK

View File

@@ -0,0 +1,11 @@
import sys
if sys.platform == "platform_name_1": ... # OK
if sys.platform != "platform_name_2": ... # OK
if sys.platform in ["linux"]: ... # Error: PYI007 Unrecognized sys.platform check
if sys.platform > 3: ... # Error: PYI007 Unrecognized sys.platform check
if sys.platform == 10.12: ... # Error: PYI007 Unrecognized sys.platform check

View File

@@ -0,0 +1,11 @@
import sys
if sys.platform == "linus": ... # OK
if sys.platform != "linux": ... # OK
if sys.platform == "win32": ... # OK
if sys.platform != "darwin": ... # OK
if sys.platform == "cygwin": ... # OK

View File

@@ -0,0 +1,11 @@
import sys
if sys.platform == "linus": ... # Error: PYI008 Unrecognized platform `linus`
if sys.platform != "linux": ... # OK
if sys.platform == "win32": ... # OK
if sys.platform != "darwin": ... # OK
if sys.platform == "cygwin": ... # OK

View File

@@ -1,3 +1,14 @@
import builtins
import os
import posix
from posix import abort
import sys as std_sys
import _thread
import _winapi
import pytest
from pytest import xfail as py_xfail
###
# Errors
###
@@ -39,6 +50,20 @@ def x(y):
print() # error
# A nonexistent function
def func_unknown(x):
if x > 0:
return False
no_such_function() # error
# A function that does return the control
def func_no_noreturn(x):
if x > 0:
return False
print("", end="") # error
###
# Non-errors
###
@@ -123,3 +148,106 @@ def prompts(self, foo):
for x in foo:
yield x
yield x + 1
# Functions that never return
def noreturn_exit(x):
if x > 0:
return 1
exit()
def noreturn_quit(x):
if x > 0:
return 1
quit()
def noreturn_builtins_exit(x):
if x > 0:
return 1
builtins.exit()
def noreturn_builtins_quit(x):
if x > 0:
return 1
builtins.quit()
def noreturn_os__exit(x):
if x > 0:
return 1
os._exit(0)
def noreturn_os_abort(x):
if x > 0:
return 1
os.abort()
def noreturn_posix__exit():
if x > 0:
return 1
posix._exit()
def noreturn_posix_abort():
if x > 0:
return 1
posix.abort()
def noreturn_posix_abort_2():
if x > 0:
return 1
abort()
def noreturn_sys_exit():
if x > 0:
return 1
std_sys.exit(0)
def noreturn__thread_exit():
if x > 0:
return 1
_thread.exit(0)
def noreturn__winapi_exitprocess():
if x > 0:
return 1
_winapi.ExitProcess(0)
def noreturn_pytest_exit():
if x > 0:
return 1
pytest.exit("oof")
def noreturn_pytest_fail():
if x > 0:
return 1
pytest.fail("oof")
def noreturn_pytest_skip():
if x > 0:
return 1
pytest.skip("oof")
def noreturn_pytest_xfail():
if x > 0:
return 1
pytest.xfail("oof")
def noreturn_pytest_xfail_2():
if x > 0:
return 1
py_xfail("oof")

View File

@@ -136,3 +136,24 @@ def nested2(x, y, z):
break
else:
a = z
def elif1(x, y, w, z):
for i in x:
if i > y:
a = z
elif i > w:
break
else:
a = z
def elif2(x, y, w, z):
for i in x:
if i > y:
a = z
else:
if i > w:
break
else:
a = z

View File

@@ -33,16 +33,25 @@ class Foo(metaclass=BazMeta):
def get_bar():
if self.bar._private: # SLF001
return None
if self.bar()._private: # SLF001
return None
if Bar._private_thing: # SLF001
return None
if Foo._private_thing:
return None
Foo = Bar()
if Foo._private_thing: # SLF001
return None
return self.bar
def public_func(self):
pass
super().public_func()
def _private_func(self):
pass
super()._private_func()
def __really_private_func(self, arg):
pass
super().__really_private_func(arg)
foo = Foo()
@@ -51,9 +60,12 @@ print(foo.public_thing)
print(foo.public_func())
print(foo.__dict__)
print(foo.__str__())
print(foo().__class__)
print(foo._private_thing) # SLF001
print(foo.__really_private_thing) # SLF001
print(foo._private_func()) # SLF001
print(foo.__really_private_func(1)) # SLF001
print(foo.bar._private) # SLF001
print(foo()._private_thing) # SLF001
print(foo()._private_thing__) # SLF001

View File

@@ -155,3 +155,19 @@ def f():
if check(x):
return False
return True
def f():
# SIM111
for x in iterable:
if x not in y:
return False
return True
def f():
# SIM111
for x in iterable:
if x > y:
return False
return True

View File

@@ -0,0 +1,96 @@
# Errors
if a:
b
elif c:
b
if x == 1:
for _ in range(20):
print("hello")
elif x == 2:
for _ in range(20):
print("hello")
if x == 1:
if True:
for _ in range(20):
print("hello")
elif x == 2:
if True:
for _ in range(20):
print("hello")
if x == 1:
if True:
for _ in range(20):
print("hello")
elif False:
for _ in range(20):
print("hello")
elif x == 2:
if True:
for _ in range(20):
print("hello")
elif False:
for _ in range(20):
print("hello")
if (
x == 1
and y == 2
and z == 3
and a == 4
and b == 5
and c == 6
and d == 7
and e == 8
and f == 9
and g == 10
and h == 11
and i == 12
and j == 13
and k == 14
):
pass
elif 1 == 2:
pass
if result.eofs == "O":
pass
elif result.eofs == "S":
skipped = 1
elif result.eofs == "F":
errors = 1
elif result.eofs == "E":
errors = 1
# OK
def complicated_calc(*arg, **kwargs):
return 42
def foo(p):
if p == 2:
return complicated_calc(microsecond=0)
elif p == 3:
return complicated_calc(microsecond=0, second=0)
return None
a = False
b = True
c = True
if a:
z = 1
elif b:
z = 2
elif c:
z = 1
# False negative (or arguably a different rule)
if result.eofs == "F":
errors = 1
else:
errors = 1

View File

@@ -1,12 +1,28 @@
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
# OK
import other
import other.example
from other import example
# TID252
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
from .parent import hello
from .\
parent import \
hello_world
from \
..parent\
import \
world_hello
from ..... import ultragrantparent
from ...... import ultragrantparent
from ....... import ultragrantparent
from ......... import ultragrantparent
from ........................... import ultragrantparent
from .....parent import ultragrantparent
from .........parent import ultragrantparent
from ...........................parent import ultragrantparent

View File

@@ -0,0 +1,7 @@
from typing import TYPE_CHECKING, Any, ClassVar
import attrs
from ..protocol import commands, definitions, responses
from ..server import example
from . import logger, models

View File

@@ -44,9 +44,9 @@ def f():
def f():
import pandas as pd
import pandas as pd # TCH002
x = dict["pd.DataFrame", "pd.DataFrame"] # TCH002
x = dict["pd.DataFrame", "pd.DataFrame"]
def f():

View File

@@ -0,0 +1,6 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import List
__all__ = ("List",)

View File

@@ -0,0 +1,8 @@
from __future__ import annotations
from typing import Any, TYPE_CHECKING, TypeAlias
if TYPE_CHECKING:
from collections.abc import Callable
AnyCallable: TypeAlias = Callable[..., Any]

View File

@@ -0,0 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from typing import Final, Literal, TypeAlias
RatingKey: TypeAlias = Literal["good", "fair", "poor"]
RATING_KEYS: Final[tuple[RatingKey, ...]] = ("good", "fair", "poor")

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
import datetime
import json
from binascii import hexlify
import requests
from sanic import Sanic
from loguru import Logger
from . import config
from .data import Data

View File

@@ -3,3 +3,5 @@ line-length = 88
[tool.ruff.isort]
lines-after-imports = 3
lines-between-types = 2
known-local-folder = ["ruff"]

View File

@@ -1,4 +1,5 @@
import sys
import ruff
import leading_prefix
import os
from . import leading_prefix

View File

@@ -0,0 +1,20 @@
import numpy as npy
import numpy as np
import numpy
# Error
npy.bool
npy.int
if dtype == np.object:
...
result = result.select_dtypes([np.byte, np.ubyte, np.short, np.ushort, np.int, np.long])
pdf = pd.DataFrame(
data=[[1, 2, 3]],
columns=["a", "b", "c"],
dtype=numpy.object,
)
_ = arr.astype(np.int)

View File

@@ -0,0 +1,56 @@
#: E701:1:5
if a: a = False
#: E701:1:40
if not header or header[:6] != 'bytes=': return
#: E702:1:10
a = False; b = True
#: E702:1:17
import bdist_egg; bdist_egg.write_safety_flag(cmd.egg_info, safe)
#: E703:1:13
import shlex;
#: E702:1:9 E703:1:23
del a[:]; a.append(42);
#: E704:1:1
def f(x): return 2
#: E704:1:1
async def f(x): return 2
#: E704:1:1 E271:1:6
async def f(x): return 2
#: E704:1:1 E226:1:19
def f(x): return 2*x
#: E704:2:5 E226:2:23
while all is round:
def f(x): return 2*x
#: E704:1:8 E702:1:11 E703:1:14
if True: x; y;
#: E701:1:8
if True: lambda a: b
#: E701:1:10
if a := 1: pass
# E701:1:4 E701:2:18 E701:3:8
try: lambda foo: bar
except ValueError: pass
finally: pass
# E701:1:7
class C: pass
# E701:1:7
with C(): pass
# E701:1:14
async with C(): pass
#:
lambda a: b
#:
a: List[str] = []
#:
if a := 1:
pass
#:
func = lambda x: x** 2 if cond else lambda x:x
#:
class C: ...
#:
def f(): ...
#: E701:1:8 E702:1:13
class C: ...; x = 1
#: E701:1:8 E702:1:13
class C: ...; ...

View File

@@ -5,17 +5,16 @@ f = lambda x: 2 * x
#: E731
while False:
this = lambda y, z: 2 * x
#: E731
f = lambda: (yield 1)
#: E731
f = lambda: (yield from g())
f = object()
f.method = lambda: "Method"
f = {}
f["a"] = lambda x: x ** 2
f["a"] = lambda x: x**2
f = []
f.append(lambda x: x ** 2)
f = g = lambda x: x ** 2
f.append(lambda x: x**2)
f = g = lambda x: x**2
lambda: "no-op"

View File

@@ -0,0 +1,4 @@
"""Test: late-binding of `__all__`."""
__all__ = ("bar",)
from foo import bar, baz

View File

@@ -0,0 +1,7 @@
__all__ = ["foo"]
foo = 1
def bar():
pass

View File

@@ -66,3 +66,40 @@ def f(a, b):
y = \
a if a is not None else b
def f():
with Nested(m) as (cm):
pass
def f():
with (Nested(m) as (cm),):
pass
def f():
with Nested(m) as (x, y):
pass
def f():
toplevel = tt = lexer.get_token()
if not tt:
break
def f():
toplevel = tt = lexer.get_token()
def f():
toplevel = (a, b) = lexer.get_token()
def f():
(a, b) = toplevel = lexer.get_token()
def f():
toplevel = tt = 1

View File

@@ -0,0 +1,57 @@
# Errors
print("foo %(foo)d bar %(bar)d" % {"foo": "1", "bar": "2"})
"foo %e bar %s" % ("1", 2)
"%d" % "1"
"%o" % "1"
"%(key)d" % {"key": "1"}
"%x" % 1.1
"%(key)x" % {"key": 1.1}
"%d" % []
"%d" % ([],)
"%(key)d" % {"key": []}
print("%d" % ("%s" % ("nested",),))
"%d" % ((1, 2, 3),)
# False negatives
WORD = "abc"
"%d" % WORD
"%d %s" % (WORD, WORD)
VALUES_TO_FORMAT = (1, "2", 3.0)
"%d %d %f" % VALUES_TO_FORMAT
# OK
"%d %s %f" % VALUES_TO_FORMAT
"%s" % "1"
"%s %s %s" % ("1", 2, 3.5)
print("%d %d"
%
(1, 1.1))
"%s" % 1
"%d" % 1
"%f" % 1
"%s" % 1
"%(key)s" % {"key": 1}
"%d" % 1
"%(key)d" % {"key": 1}
"%f" % 1
"%(key)f" % {"key": 1}
"%d" % 1.1
"%(key)d" % {"key": 1.1}
"%s" % []
"%(key)s" % {"key": []}
"%s" % None
"%(key)s" % {"key": None}
print("%s" % ("%s" % ("nested",),))
print("%s" % ("%d" % (5,),))
"%d %d" % "1"
"%d" "%d" % "1"
"-%f" % time.time()
"%r" % (object['dn'],)
r'\%03o' % (ord(c),)
('%02X' % int(_) for _ in o)
"%s;range=%d-*" % (attr, upper + 1)
"%d" % (len(foo),)
'(%r, %r, %r, %r)' % (hostname, address, username, '$PASSWORD')
'%r' % ({'server_school_roles': server_school_roles, 'is_school_multiserver_domain': is_school_multiserver_domain}, )

View File

@@ -1,6 +1,3 @@
# E2502
print("\u202B\u202E\u05e9\u05DC\u05D5\u05DD\u202C")
# E2502
print("שלום‬")
@@ -20,5 +17,12 @@ def subtract_funds(account: str, amount: int):
return
# OK
print("\u202B\u202E\u05e9\u05DC\u05D5\u05DD\u202C")
# OK
print("\N{RIGHT-TO-LEFT MARK}")
# OK
print("Hello World")

View File

@@ -0,0 +1,17 @@
def a():
yield
def __init__():
yield
class A:
def __init__(self):
yield
class B:
def __init__(self):
yield from self.gen()
def gen(self):
yield 5

View File

@@ -0,0 +1,52 @@
import asyncio
# Error
def f():
asyncio.create_task(coordinator.ws_connect()) # Error
# OK
def f():
background_tasks = set()
for i in range(10):
task = asyncio.create_task(some_coro(param=i))
# Add task to the set. This creates a strong reference.
background_tasks.add(task)
# To prevent keeping references to finished tasks forever,
# make each task remove its own reference from the set after
# completion:
task.add_done_callback(background_tasks.discard)
# OK
def f():
ctx.task = asyncio.create_task(make_request())
# OK
def f():
tasks.append(asyncio.create_task(self._populate_collection(coll, coll_info)))
# OK
def f():
asyncio.wait([asyncio.create_task(client.close()) for client in clients.values()])
# OK
def f():
tasks = [asyncio.create_task(task) for task in tasks]
# Ok (false negative)
def f():
task = asyncio.create_task(coordinator.ws_connect())
# Ok (potential false negative)
def f():
do_nothing_with_the_task(asyncio.create_task(coordinator.ws_connect()))

View File

@@ -27,6 +27,7 @@ def good():
logger.exception("process failed")
raise
def still_good():
try:
process()
@@ -35,6 +36,14 @@ def still_good():
raise
def still_good_too():
try:
process()
except MyException as e:
print(e)
raise e from None
def still_actually_good():
try:
process()
@@ -60,5 +69,6 @@ def bad_that_needs_recursion_2():
except MyException as e:
logger.exception("process failed")
if True:
def foo():
raise e

View File

@@ -3,8 +3,9 @@
use num_bigint::BigInt;
use rustpython_parser::ast::{
Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Keyword,
Operator, Unaryop,
Alias, Arg, Arguments, Boolop, Cmpop, Comprehension, Constant, Excepthandler,
ExcepthandlerKind, Expr, ExprContext, ExprKind, Keyword, Operator, Stmt, StmtKind, Unaryop,
Withitem,
};
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -126,6 +127,36 @@ impl From<&Cmpop> for ComparableCmpop {
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableAlias<'a> {
pub name: &'a str,
pub asname: Option<&'a str>,
}
impl<'a> From<&'a Alias> for ComparableAlias<'a> {
fn from(alias: &'a Alias) -> Self {
Self {
name: &alias.node.name,
asname: alias.node.asname.as_deref(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableWithitem<'a> {
pub context_expr: ComparableExpr<'a>,
pub optional_vars: Option<ComparableExpr<'a>>,
}
impl<'a> From<&'a Withitem> for ComparableWithitem<'a> {
fn from(withitem: &'a Withitem) -> Self {
Self {
context_expr: (&withitem.context_expr).into(),
optional_vars: withitem.optional_vars.as_ref().map(Into::into),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableConstant<'a> {
None,
@@ -147,9 +178,7 @@ impl<'a> From<&'a Constant> for ComparableConstant<'a> {
Constant::Str(value) => Self::Str(value),
Constant::Bytes(value) => Self::Bytes(value),
Constant::Int(value) => Self::Int(value),
Constant::Tuple(value) => {
Self::Tuple(value.iter().map(std::convert::Into::into).collect())
}
Constant::Tuple(value) => Self::Tuple(value.iter().map(Into::into).collect()),
Constant::Float(value) => Self::Float(value.to_bits()),
Constant::Complex { real, imag } => Self::Complex {
real: real.to_bits(),
@@ -174,37 +203,23 @@ pub struct ComparableArguments<'a> {
impl<'a> From<&'a Arguments> for ComparableArguments<'a> {
fn from(arguments: &'a Arguments) -> Self {
Self {
posonlyargs: arguments
.posonlyargs
.iter()
.map(std::convert::Into::into)
.collect(),
args: arguments
.args
.iter()
.map(std::convert::Into::into)
.collect(),
vararg: arguments.vararg.as_ref().map(std::convert::Into::into),
kwonlyargs: arguments
.kwonlyargs
.iter()
.map(std::convert::Into::into)
.collect(),
kw_defaults: arguments
.kw_defaults
.iter()
.map(std::convert::Into::into)
.collect(),
kwarg: arguments.vararg.as_ref().map(std::convert::Into::into),
defaults: arguments
.defaults
.iter()
.map(std::convert::Into::into)
.collect(),
posonlyargs: arguments.posonlyargs.iter().map(Into::into).collect(),
args: arguments.args.iter().map(Into::into).collect(),
vararg: arguments.vararg.as_ref().map(Into::into),
kwonlyargs: arguments.kwonlyargs.iter().map(Into::into).collect(),
kw_defaults: arguments.kw_defaults.iter().map(Into::into).collect(),
kwarg: arguments.vararg.as_ref().map(Into::into),
defaults: arguments.defaults.iter().map(Into::into).collect(),
}
}
}
impl<'a> From<&'a Box<Arguments>> for ComparableArguments<'a> {
fn from(arguments: &'a Box<Arguments>) -> Self {
(&**arguments).into()
}
}
impl<'a> From<&'a Box<Arg>> for ComparableArg<'a> {
fn from(arg: &'a Box<Arg>) -> Self {
(&**arg).into()
@@ -222,7 +237,7 @@ impl<'a> From<&'a Arg> for ComparableArg<'a> {
fn from(arg: &'a Arg) -> Self {
Self {
arg: &arg.node.arg,
annotation: arg.node.annotation.as_ref().map(std::convert::Into::into),
annotation: arg.node.annotation.as_ref().map(Into::into),
type_comment: arg.node.type_comment.as_deref(),
}
}
@@ -256,16 +271,32 @@ impl<'a> From<&'a Comprehension> for ComparableComprehension<'a> {
Self {
target: (&comprehension.target).into(),
iter: (&comprehension.iter).into(),
ifs: comprehension
.ifs
.iter()
.map(std::convert::Into::into)
.collect(),
ifs: comprehension.ifs.iter().map(Into::into).collect(),
is_async: &comprehension.is_async,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableExcepthandler<'a> {
ExceptHandler {
type_: Option<ComparableExpr<'a>>,
name: Option<&'a str>,
body: Vec<ComparableStmt<'a>>,
},
}
impl<'a> From<&'a Excepthandler> for ComparableExcepthandler<'a> {
fn from(excepthandler: &'a Excepthandler) -> Self {
let ExcepthandlerKind::ExceptHandler { type_, name, body } = &excepthandler.node;
Self::ExceptHandler {
type_: type_.as_ref().map(Into::into),
name: name.as_deref(),
body: body.iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableExpr<'a> {
BoolOp {
@@ -399,7 +430,7 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
match &expr.node {
ExprKind::BoolOp { op, values } => Self::BoolOp {
op: op.into(),
values: values.iter().map(std::convert::Into::into).collect(),
values: values.iter().map(Into::into).collect(),
},
ExprKind::NamedExpr { target, value } => Self::NamedExpr {
target: target.into(),
@@ -426,20 +457,20 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
ExprKind::Dict { keys, values } => Self::Dict {
keys: keys
.iter()
.map(|expr| expr.as_ref().map(std::convert::Into::into))
.map(|expr| expr.as_ref().map(Into::into))
.collect(),
values: values.iter().map(std::convert::Into::into).collect(),
values: values.iter().map(Into::into).collect(),
},
ExprKind::Set { elts } => Self::Set {
elts: elts.iter().map(std::convert::Into::into).collect(),
elts: elts.iter().map(Into::into).collect(),
},
ExprKind::ListComp { elt, generators } => Self::ListComp {
elt: elt.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
generators: generators.iter().map(Into::into).collect(),
},
ExprKind::SetComp { elt, generators } => Self::SetComp {
elt: elt.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
generators: generators.iter().map(Into::into).collect(),
},
ExprKind::DictComp {
key,
@@ -448,17 +479,17 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
} => Self::DictComp {
key: key.into(),
value: value.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
generators: generators.iter().map(Into::into).collect(),
},
ExprKind::GeneratorExp { elt, generators } => Self::GeneratorExp {
elt: elt.into(),
generators: generators.iter().map(std::convert::Into::into).collect(),
generators: generators.iter().map(Into::into).collect(),
},
ExprKind::Await { value } => Self::Await {
value: value.into(),
},
ExprKind::Yield { value } => Self::Yield {
value: value.as_ref().map(std::convert::Into::into),
value: value.as_ref().map(Into::into),
},
ExprKind::YieldFrom { value } => Self::YieldFrom {
value: value.into(),
@@ -469,8 +500,8 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
comparators,
} => Self::Compare {
left: left.into(),
ops: ops.iter().map(std::convert::Into::into).collect(),
comparators: comparators.iter().map(std::convert::Into::into).collect(),
ops: ops.iter().map(Into::into).collect(),
comparators: comparators.iter().map(Into::into).collect(),
},
ExprKind::Call {
func,
@@ -478,8 +509,8 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
keywords,
} => Self::Call {
func: func.into(),
args: args.iter().map(std::convert::Into::into).collect(),
keywords: keywords.iter().map(std::convert::Into::into).collect(),
args: args.iter().map(Into::into).collect(),
keywords: keywords.iter().map(Into::into).collect(),
},
ExprKind::FormattedValue {
value,
@@ -488,10 +519,10 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
} => Self::FormattedValue {
value: value.into(),
conversion,
format_spec: format_spec.as_ref().map(std::convert::Into::into),
format_spec: format_spec.as_ref().map(Into::into),
},
ExprKind::JoinedStr { values } => Self::JoinedStr {
values: values.iter().map(std::convert::Into::into).collect(),
values: values.iter().map(Into::into).collect(),
},
ExprKind::Constant { value, kind } => Self::Constant {
value: value.into(),
@@ -516,18 +547,314 @@ impl<'a> From<&'a Expr> for ComparableExpr<'a> {
ctx: ctx.into(),
},
ExprKind::List { elts, ctx } => Self::List {
elts: elts.iter().map(std::convert::Into::into).collect(),
elts: elts.iter().map(Into::into).collect(),
ctx: ctx.into(),
},
ExprKind::Tuple { elts, ctx } => Self::Tuple {
elts: elts.iter().map(std::convert::Into::into).collect(),
elts: elts.iter().map(Into::into).collect(),
ctx: ctx.into(),
},
ExprKind::Slice { lower, upper, step } => Self::Slice {
lower: lower.as_ref().map(std::convert::Into::into),
upper: upper.as_ref().map(std::convert::Into::into),
step: step.as_ref().map(std::convert::Into::into),
lower: lower.as_ref().map(Into::into),
upper: upper.as_ref().map(Into::into),
step: step.as_ref().map(Into::into),
},
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableStmt<'a> {
FunctionDef {
name: &'a str,
args: ComparableArguments<'a>,
body: Vec<ComparableStmt<'a>>,
decorator_list: Vec<ComparableExpr<'a>>,
returns: Option<ComparableExpr<'a>>,
type_comment: Option<&'a str>,
},
AsyncFunctionDef {
name: &'a str,
args: ComparableArguments<'a>,
body: Vec<ComparableStmt<'a>>,
decorator_list: Vec<ComparableExpr<'a>>,
returns: Option<ComparableExpr<'a>>,
type_comment: Option<&'a str>,
},
ClassDef {
name: &'a str,
bases: Vec<ComparableExpr<'a>>,
keywords: Vec<ComparableKeyword<'a>>,
body: Vec<ComparableStmt<'a>>,
decorator_list: Vec<ComparableExpr<'a>>,
},
Return {
value: Option<ComparableExpr<'a>>,
},
Delete {
targets: Vec<ComparableExpr<'a>>,
},
Assign {
targets: Vec<ComparableExpr<'a>>,
value: ComparableExpr<'a>,
type_comment: Option<&'a str>,
},
AugAssign {
target: ComparableExpr<'a>,
op: ComparableOperator,
value: ComparableExpr<'a>,
},
AnnAssign {
target: ComparableExpr<'a>,
annotation: ComparableExpr<'a>,
value: Option<ComparableExpr<'a>>,
simple: usize,
},
For {
target: ComparableExpr<'a>,
iter: ComparableExpr<'a>,
body: Vec<ComparableStmt<'a>>,
orelse: Vec<ComparableStmt<'a>>,
type_comment: Option<&'a str>,
},
AsyncFor {
target: ComparableExpr<'a>,
iter: ComparableExpr<'a>,
body: Vec<ComparableStmt<'a>>,
orelse: Vec<ComparableStmt<'a>>,
type_comment: Option<&'a str>,
},
While {
test: ComparableExpr<'a>,
body: Vec<ComparableStmt<'a>>,
orelse: Vec<ComparableStmt<'a>>,
},
If {
test: ComparableExpr<'a>,
body: Vec<ComparableStmt<'a>>,
orelse: Vec<ComparableStmt<'a>>,
},
With {
items: Vec<ComparableWithitem<'a>>,
body: Vec<ComparableStmt<'a>>,
type_comment: Option<&'a str>,
},
AsyncWith {
items: Vec<ComparableWithitem<'a>>,
body: Vec<ComparableStmt<'a>>,
type_comment: Option<&'a str>,
},
Raise {
exc: Option<ComparableExpr<'a>>,
cause: Option<ComparableExpr<'a>>,
},
Try {
body: Vec<ComparableStmt<'a>>,
handlers: Vec<ComparableExcepthandler<'a>>,
orelse: Vec<ComparableStmt<'a>>,
finalbody: Vec<ComparableStmt<'a>>,
},
Assert {
test: ComparableExpr<'a>,
msg: Option<ComparableExpr<'a>>,
},
Import {
names: Vec<ComparableAlias<'a>>,
},
ImportFrom {
module: Option<&'a str>,
names: Vec<ComparableAlias<'a>>,
level: Option<usize>,
},
Global {
names: Vec<&'a str>,
},
Nonlocal {
names: Vec<&'a str>,
},
Expr {
value: ComparableExpr<'a>,
},
Pass,
Break,
Continue,
}
impl<'a> From<&'a Stmt> for ComparableStmt<'a> {
fn from(stmt: &'a Stmt) -> Self {
match &stmt.node {
StmtKind::FunctionDef {
name,
args,
body,
decorator_list,
returns,
type_comment,
} => Self::FunctionDef {
name,
args: args.into(),
body: body.iter().map(Into::into).collect(),
decorator_list: decorator_list.iter().map(Into::into).collect(),
returns: returns.as_ref().map(Into::into),
type_comment: type_comment.as_ref().map(std::string::String::as_str),
},
StmtKind::AsyncFunctionDef {
name,
args,
body,
decorator_list,
returns,
type_comment,
} => Self::AsyncFunctionDef {
name,
args: args.into(),
body: body.iter().map(Into::into).collect(),
decorator_list: decorator_list.iter().map(Into::into).collect(),
returns: returns.as_ref().map(Into::into),
type_comment: type_comment.as_ref().map(std::string::String::as_str),
},
StmtKind::ClassDef {
name,
bases,
keywords,
body,
decorator_list,
} => Self::ClassDef {
name,
bases: bases.iter().map(Into::into).collect(),
keywords: keywords.iter().map(Into::into).collect(),
body: body.iter().map(Into::into).collect(),
decorator_list: decorator_list.iter().map(Into::into).collect(),
},
StmtKind::Return { value } => Self::Return {
value: value.as_ref().map(Into::into),
},
StmtKind::Delete { targets } => Self::Delete {
targets: targets.iter().map(Into::into).collect(),
},
StmtKind::Assign {
targets,
value,
type_comment,
} => Self::Assign {
targets: targets.iter().map(Into::into).collect(),
value: value.into(),
type_comment: type_comment.as_ref().map(std::string::String::as_str),
},
StmtKind::AugAssign { target, op, value } => Self::AugAssign {
target: target.into(),
op: op.into(),
value: value.into(),
},
StmtKind::AnnAssign {
target,
annotation,
value,
simple,
} => Self::AnnAssign {
target: target.into(),
annotation: annotation.into(),
value: value.as_ref().map(Into::into),
simple: *simple,
},
StmtKind::For {
target,
iter,
body,
orelse,
type_comment,
} => Self::For {
target: target.into(),
iter: iter.into(),
body: body.iter().map(Into::into).collect(),
orelse: orelse.iter().map(Into::into).collect(),
type_comment: type_comment.as_ref().map(String::as_str),
},
StmtKind::AsyncFor {
target,
iter,
body,
orelse,
type_comment,
} => Self::AsyncFor {
target: target.into(),
iter: iter.into(),
body: body.iter().map(Into::into).collect(),
orelse: orelse.iter().map(Into::into).collect(),
type_comment: type_comment.as_ref().map(String::as_str),
},
StmtKind::While { test, body, orelse } => Self::While {
test: test.into(),
body: body.iter().map(Into::into).collect(),
orelse: orelse.iter().map(Into::into).collect(),
},
StmtKind::If { test, body, orelse } => Self::If {
test: test.into(),
body: body.iter().map(Into::into).collect(),
orelse: orelse.iter().map(Into::into).collect(),
},
StmtKind::With {
items,
body,
type_comment,
} => Self::With {
items: items.iter().map(Into::into).collect(),
body: body.iter().map(Into::into).collect(),
type_comment: type_comment.as_ref().map(String::as_str),
},
StmtKind::AsyncWith {
items,
body,
type_comment,
} => Self::AsyncWith {
items: items.iter().map(Into::into).collect(),
body: body.iter().map(Into::into).collect(),
type_comment: type_comment.as_ref().map(String::as_str),
},
StmtKind::Match { .. } => unreachable!("StmtKind::Match is not supported"),
StmtKind::Raise { exc, cause } => Self::Raise {
exc: exc.as_ref().map(Into::into),
cause: cause.as_ref().map(Into::into),
},
StmtKind::Try {
body,
handlers,
orelse,
finalbody,
} => Self::Try {
body: body.iter().map(Into::into).collect(),
handlers: handlers.iter().map(Into::into).collect(),
orelse: orelse.iter().map(Into::into).collect(),
finalbody: finalbody.iter().map(Into::into).collect(),
},
StmtKind::Assert { test, msg } => Self::Assert {
test: test.into(),
msg: msg.as_ref().map(Into::into),
},
StmtKind::Import { names } => Self::Import {
names: names.iter().map(Into::into).collect(),
},
StmtKind::ImportFrom {
module,
names,
level,
} => Self::ImportFrom {
module: module.as_ref().map(String::as_str),
names: names.iter().map(Into::into).collect(),
level: *level,
},
StmtKind::Global { names } => Self::Global {
names: names.iter().map(String::as_str).collect(),
},
StmtKind::Nonlocal { names } => Self::Nonlocal {
names: names.iter().map(String::as_str).collect(),
},
StmtKind::Expr { value } => Self::Expr {
value: value.into(),
},
StmtKind::Pass => Self::Pass,
StmtKind::Break => Self::Break,
StmtKind::Continue => Self::Continue,
}
}
}

View File

@@ -1,6 +1,6 @@
use rustpython_parser::ast::Expr;
use crate::ast::helpers::to_call_path;
use crate::ast::helpers::{map_callable, to_call_path};
use crate::ast::types::{Scope, ScopeKind};
use crate::checkers::ast::Checker;
@@ -29,18 +29,20 @@ pub fn classify(
if decorator_list.iter().any(|expr| {
// The method is decorated with a static method decorator (like
// `@staticmethod`).
checker.resolve_call_path(expr).map_or(false, |call_path| {
staticmethod_decorators
.iter()
.any(|decorator| call_path == to_call_path(decorator))
})
checker
.resolve_call_path(map_callable(expr))
.map_or(false, |call_path| {
staticmethod_decorators
.iter()
.any(|decorator| call_path == to_call_path(decorator))
})
}) {
FunctionType::StaticMethod
} else if CLASS_METHODS.contains(&name)
// Special-case class method, like `__new__`.
|| scope.bases.iter().any(|expr| {
// The class itself extends a known metaclass, so all methods are class methods.
checker.resolve_call_path(expr).map_or(false, |call_path| {
checker.resolve_call_path(map_callable(expr)).map_or(false, |call_path| {
METACLASS_BASES
.iter()
.any(|(module, member)| call_path.as_slice() == [*module, *member])
@@ -48,7 +50,7 @@ pub fn classify(
})
|| decorator_list.iter().any(|expr| {
// The method is decorated with a class method decorator (like `@classmethod`).
checker.resolve_call_path(expr).map_or(false, |call_path| {
checker.resolve_call_path(map_callable(expr)).map_or(false, |call_path| {
classmethod_decorators
.iter()
.any(|decorator| call_path == to_call_path(decorator))

View File

@@ -568,6 +568,17 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
arg_names
}
/// Given an [`Expr`] that can be callable or not (like a decorator, which could
/// be used with or without explicit call syntax), return the underlying
/// callable.
pub fn map_callable(decorator: &Expr) -> &Expr {
if let ExprKind::Call { func, .. } = &decorator.node {
func
} else {
decorator
}
}
/// Returns `true` if a statement or expression includes at least one comment.
pub fn has_comments<T>(located: &Located<T>, locator: &Locator) -> bool {
let start = if match_leading_content(located, locator) {
@@ -585,7 +596,7 @@ pub fn has_comments<T>(located: &Located<T>, locator: &Locator) -> bool {
/// Returns `true` if a [`Range`] includes at least one comment.
pub fn has_comments_in(range: Range, locator: &Locator) -> bool {
for tok in lexer::make_tokenizer(locator.slice_source_code_range(&range)) {
for tok in lexer::make_tokenizer(locator.slice(&range)) {
match tok {
Ok((_, tok, _)) => {
if matches!(tok, Tok::Comment(..)) {
@@ -712,7 +723,7 @@ where
StmtKind::FunctionDef { .. } | StmtKind::AsyncFunctionDef { .. } => {
// Don't recurse.
}
StmtKind::Return { value } => self.returns.push(value.as_ref().map(|expr| &**expr)),
StmtKind::Return { value } => self.returns.push(value.as_deref()),
_ => visitor::walk_stmt(self, stmt),
}
}
@@ -745,7 +756,7 @@ pub fn to_relative(absolute: Location, base: Location) -> Location {
/// Return `true` if a [`Located`] has leading content.
pub fn match_leading_content<T>(located: &Located<T>, locator: &Locator) -> bool {
let range = Range::new(Location::new(located.location.row(), 0), located.location);
let prefix = locator.slice_source_code_range(&range);
let prefix = locator.slice(&range);
prefix.chars().any(|char| !char.is_whitespace())
}
@@ -755,7 +766,7 @@ pub fn match_trailing_content<T>(located: &Located<T>, locator: &Locator) -> boo
located.end_location.unwrap(),
Location::new(located.end_location.unwrap().row() + 1, 0),
);
let suffix = locator.slice_source_code_range(&range);
let suffix = locator.slice(&range);
for char in suffix.chars() {
if char == '#' {
return false;
@@ -773,7 +784,7 @@ pub fn match_trailing_comment<T>(located: &Located<T>, locator: &Locator) -> Opt
located.end_location.unwrap(),
Location::new(located.end_location.unwrap().row() + 1, 0),
);
let suffix = locator.slice_source_code_range(&range);
let suffix = locator.slice(&range);
for (i, char) in suffix.chars().enumerate() {
if char == '#' {
return Some(i);
@@ -787,8 +798,7 @@ pub fn match_trailing_comment<T>(located: &Located<T>, locator: &Locator) -> Opt
/// Return the number of trailing empty lines following a statement.
pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize {
let suffix =
locator.slice_source_code_at(Location::new(stmt.end_location.unwrap().row() + 1, 0));
let suffix = locator.skip(Location::new(stmt.end_location.unwrap().row() + 1, 0));
suffix
.lines()
.take_while(|line| line.trim().is_empty())
@@ -797,7 +807,7 @@ pub fn count_trailing_lines(stmt: &Stmt, locator: &Locator) -> usize {
/// Return the range of the first parenthesis pair after a given [`Location`].
pub fn match_parens(start: Location, locator: &Locator) -> Option<Range> {
let contents = locator.slice_source_code_at(start);
let contents = locator.skip(start);
let mut fix_start = None;
let mut fix_end = None;
let mut count: usize = 0;
@@ -832,7 +842,7 @@ pub fn identifier_range(stmt: &Stmt, locator: &Locator) -> Range {
| StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
) {
let contents = locator.slice_source_code_range(&Range::from_located(stmt));
let contents = locator.slice(&Range::from_located(stmt));
for (start, tok, end) in lexer::make_tokenizer_located(contents, stmt.location).flatten() {
if matches!(tok, Tok::Name { .. }) {
return Range::new(start, end);
@@ -860,7 +870,7 @@ pub fn binding_range(binding: &Binding, locator: &Locator) -> Range {
// Return the ranges of `Name` tokens within a specified node.
pub fn find_names<T>(located: &Located<T>, locator: &Locator) -> Vec<Range> {
let contents = locator.slice_source_code_range(&Range::from_located(located));
let contents = locator.slice(&Range::from_located(located));
lexer::make_tokenizer_located(contents, located.location)
.flatten()
.filter(|(_, tok, _)| matches!(tok, Tok::Name { .. }))
@@ -879,8 +889,7 @@ pub fn excepthandler_name_range(handler: &Excepthandler, locator: &Locator) -> O
match (name, type_) {
(Some(_), Some(type_)) => {
let type_end_location = type_.end_location.unwrap();
let contents =
locator.slice_source_code_range(&Range::new(type_end_location, body[0].location));
let contents = locator.slice(&Range::new(type_end_location, body[0].location));
let range = lexer::make_tokenizer_located(contents, type_end_location)
.flatten()
.tuple_windows()
@@ -904,7 +913,7 @@ pub fn except_range(handler: &Excepthandler, locator: &Locator) -> Range {
.expect("Expected body to be non-empty")
.location
};
let contents = locator.slice_source_code_range(&Range {
let contents = locator.slice(&Range {
location: handler.location,
end_location: end,
});
@@ -921,7 +930,7 @@ pub fn except_range(handler: &Excepthandler, locator: &Locator) -> Range {
/// Find f-strings that don't contain any formatted values in a `JoinedStr`.
pub fn find_useless_f_strings(expr: &Expr, locator: &Locator) -> Vec<(Range, Range)> {
let contents = locator.slice_source_code_range(&Range::from_located(expr));
let contents = locator.slice(&Range::from_located(expr));
lexer::make_tokenizer_located(contents, expr.location)
.flatten()
.filter_map(|(location, tok, end_location)| match tok {
@@ -929,7 +938,7 @@ pub fn find_useless_f_strings(expr: &Expr, locator: &Locator) -> Vec<(Range, Ran
kind: StringKind::FString | StringKind::RawFString,
..
} => {
let first_char = locator.slice_source_code_range(&Range {
let first_char = locator.slice(&Range {
location,
end_location: Location::new(location.row(), location.column() + 1),
});
@@ -969,7 +978,7 @@ pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
.expect("Expected body to be non-empty")
.end_location
.unwrap();
let contents = locator.slice_source_code_range(&Range {
let contents = locator.slice(&Range {
location: body_end,
end_location: orelse
.first()
@@ -991,7 +1000,7 @@ pub fn else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
/// Return the `Range` of the first `Tok::Colon` token in a `Range`.
pub fn first_colon_range(range: Range, locator: &Locator) -> Option<Range> {
let contents = locator.slice_source_code_range(&range);
let contents = locator.slice(&range);
let range = lexer::make_tokenizer_located(contents, range.location)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Colon))
@@ -1021,7 +1030,7 @@ pub fn elif_else_range(stmt: &Stmt, locator: &Locator) -> Option<Range> {
[stmt, ..] => stmt.location,
_ => return None,
};
let contents = locator.slice_source_code_range(&Range::new(start, end));
let contents = locator.slice(&Range::new(start, end));
let range = lexer::make_tokenizer_located(contents, start)
.flatten()
.find(|(_, kind, _)| matches!(kind, Tok::Elif | Tok::Else))

View File

@@ -8,7 +8,7 @@ use crate::source_code::Locator;
/// Extract the leading indentation from a line.
pub fn indentation<'a, T>(locator: &'a Locator, located: &'a Located<T>) -> Option<&'a str> {
let range = Range::from_located(located);
let indentation = locator.slice_source_code_range(&Range::new(
let indentation = locator.slice(&Range::new(
Location::new(range.location.row(), 0),
Location::new(range.location.row(), range.location.column()),
));

View File

@@ -81,7 +81,7 @@ fn is_lone_child(child: &Stmt, parent: &Stmt, deleted: &[&Stmt]) -> Result<bool>
/// Return the location of a trailing semicolon following a `Stmt`, if it's part
/// of a multi-statement line.
fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
let contents = locator.slice_source_code_at(stmt.end_location.unwrap());
let contents = locator.skip(stmt.end_location.unwrap());
for (row, line) in LinesWithTrailingNewline::from(contents).enumerate() {
let trimmed = line.trim();
if trimmed.starts_with(';') {
@@ -104,7 +104,7 @@ fn trailing_semicolon(stmt: &Stmt, locator: &Locator) -> Option<Location> {
/// Find the next valid break for a `Stmt` after a semicolon.
fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
let start_location = Location::new(semicolon.row(), semicolon.column() + 1);
let contents = locator.slice_source_code_at(start_location);
let contents = locator.skip(start_location);
for (row, line) in LinesWithTrailingNewline::from(contents).enumerate() {
let trimmed = line.trim();
// Skip past any continuations.
@@ -136,7 +136,7 @@ fn next_stmt_break(semicolon: Location, locator: &Locator) -> Location {
/// Return `true` if a `Stmt` occurs at the end of a file.
fn is_end_of_file(stmt: &Stmt, locator: &Locator) -> bool {
let contents = locator.slice_source_code_at(stmt.end_location.unwrap());
let contents = locator.skip(stmt.end_location.unwrap());
contents.is_empty()
}
@@ -209,7 +209,7 @@ pub fn remove_unused_imports<'a>(
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(stmt));
let module_text = locator.slice(&Range::from_located(stmt));
let mut tree = match_module(module_text)?;
let Some(Statement::Simple(body)) = tree.body.first_mut() else {
@@ -339,7 +339,7 @@ pub fn remove_argument(
remove_parentheses: bool,
) -> Result<Fix> {
// TODO(sbrugman): Preserve trailing comments.
let contents = locator.slice_source_code_at(stmt_at);
let contents = locator.skip(stmt_at);
let mut fix_start = None;
let mut fix_end = None;

View File

@@ -1,42 +1,49 @@
use std::collections::BTreeSet;
use itertools::Itertools;
use rustc_hash::FxHashMap;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::fix::Fix;
use crate::linter::FixTable;
use crate::registry::Diagnostic;
use crate::source_code::Locator;
pub mod helpers;
/// Auto-fix errors in a file, and write the fixed source code to disk.
pub fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<(String, usize)> {
pub fn fix_file(diagnostics: &[Diagnostic], locator: &Locator) -> Option<(String, FixTable)> {
if diagnostics.iter().all(|check| check.fix.is_none()) {
return None;
None
} else {
Some(apply_fixes(diagnostics.iter(), locator))
}
Some(apply_fixes(
diagnostics.iter().filter_map(|check| check.fix.as_ref()),
locator,
))
}
/// Apply a series of fixes.
fn apply_fixes<'a>(
fixes: impl Iterator<Item = &'a Fix>,
diagnostics: impl Iterator<Item = &'a Diagnostic>,
locator: &'a Locator<'a>,
) -> (String, usize) {
) -> (String, FixTable) {
let mut output = String::with_capacity(locator.len());
let mut last_pos: Location = Location::new(1, 0);
let mut applied: BTreeSet<&Fix> = BTreeSet::default();
let mut num_fixed: usize = 0;
let mut fixed = FxHashMap::default();
for fix in fixes.sorted_by_key(|fix| fix.location) {
for (rule, fix) in diagnostics
.filter_map(|diagnostic| {
diagnostic
.fix
.as_ref()
.map(|fix| (diagnostic.kind.rule(), fix))
})
.sorted_by_key(|(.., fix)| fix.location)
{
// If we already applied an identical fix as part of another correction, skip
// any re-application.
if applied.contains(&fix) {
num_fixed += 1;
*fixed.entry(rule).or_default() += 1;
continue;
}
@@ -47,7 +54,7 @@ fn apply_fixes<'a>(
}
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice_source_code_range(&Range::new(last_pos, fix.location));
let slice = locator.slice(&Range::new(last_pos, fix.location));
output.push_str(slice);
// Add the patch itself.
@@ -56,14 +63,14 @@ fn apply_fixes<'a>(
// Track that the fix was applied.
last_pos = fix.end_location;
applied.insert(fix);
num_fixed += 1;
*fixed.entry(rule).or_default() += 1;
}
// Add the remaining content.
let slice = locator.slice_source_code_at(last_pos);
let slice = locator.skip(last_pos);
output.push_str(slice);
(output, num_fixed)
(output, fixed)
}
/// Apply a single fix.
@@ -71,14 +78,14 @@ pub(crate) fn apply_fix(fix: &Fix, locator: &Locator) -> String {
let mut output = String::with_capacity(locator.len());
// Add all contents from `last_pos` to `fix.location`.
let slice = locator.slice_source_code_range(&Range::new(Location::new(1, 0), fix.location));
let slice = locator.slice(&Range::new(Location::new(1, 0), fix.location));
output.push_str(slice);
// Add the patch itself.
output.push_str(&fix.content);
// Add the remaining content.
let slice = locator.slice_source_code_at(fix.end_location);
let slice = locator.skip(fix.end_location);
output.push_str(slice);
output
@@ -90,24 +97,41 @@ mod tests {
use crate::autofix::{apply_fix, apply_fixes};
use crate::fix::Fix;
use crate::registry::Diagnostic;
use crate::rules::pycodestyle::rules::NoNewLineAtEndOfFile;
use crate::source_code::Locator;
#[test]
fn empty_file() {
let fixes = vec![];
let fixes: Vec<Diagnostic> = vec![];
let locator = Locator::new(r#""#);
let (contents, fixed) = apply_fixes(fixes.iter(), &locator);
assert_eq!(contents, "");
assert_eq!(fixed, 0);
assert_eq!(fixed.values().sum::<usize>(), 0);
}
impl From<Fix> for Diagnostic {
fn from(fix: Fix) -> Self {
Diagnostic {
// The choice of rule here is arbitrary.
kind: NoNewLineAtEndOfFile.into(),
location: fix.location,
end_location: fix.end_location,
fix: Some(fix),
parent: None,
}
}
}
#[test]
fn apply_one_replacement() {
let fixes = vec![Fix {
let fixes: Vec<Diagnostic> = vec![Fix {
content: "Bar".to_string(),
location: Location::new(1, 8),
end_location: Location::new(1, 14),
}];
}
.into()];
let locator = Locator::new(
r#"
class A(object):
@@ -124,16 +148,17 @@ class A(Bar):
"#
.trim(),
);
assert_eq!(fixed, 1);
assert_eq!(fixed.values().sum::<usize>(), 1);
}
#[test]
fn apply_one_removal() {
let fixes = vec![Fix {
let fixes: Vec<Diagnostic> = vec![Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
}];
}
.into()];
let locator = Locator::new(
r#"
class A(object):
@@ -150,22 +175,24 @@ class A:
"#
.trim()
);
assert_eq!(fixed, 1);
assert_eq!(fixed.values().sum::<usize>(), 1);
}
#[test]
fn apply_two_removals() {
let fixes = vec![
let fixes: Vec<Diagnostic> = vec![
Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 16),
},
}
.into(),
Fix {
content: String::new(),
location: Location::new(1, 16),
end_location: Location::new(1, 23),
},
}
.into(),
];
let locator = Locator::new(
r#"
@@ -184,22 +211,24 @@ class A:
"#
.trim()
);
assert_eq!(fixed, 2);
assert_eq!(fixed.values().sum::<usize>(), 2);
}
#[test]
fn ignore_overlapping_fixes() {
let fixes = vec![
let fixes: Vec<Diagnostic> = vec![
Fix {
content: String::new(),
location: Location::new(1, 7),
end_location: Location::new(1, 15),
},
}
.into(),
Fix {
content: "ignored".to_string(),
location: Location::new(1, 9),
end_location: Location::new(1, 11),
},
}
.into(),
];
let locator = Locator::new(
r#"
@@ -217,7 +246,7 @@ class A:
"#
.trim(),
);
assert_eq!(fixed, 1);
assert_eq!(fixed.values().sum::<usize>(), 1);
}
#[test]

View File

@@ -6,8 +6,6 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use nohash_hasher::IntMap;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_common::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{
@@ -17,6 +15,9 @@ use rustpython_parser::ast::{
use rustpython_parser::parser;
use smallvec::smallvec;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use crate::ast::helpers::{
binding_range, collect_call_path, extract_handler_names, from_relative_import, to_module_path,
};
@@ -30,16 +31,15 @@ use crate::ast::typing::{match_annotated_subscript, Callable, SubscriptKind};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, typing, visitor};
use crate::docstrings::definition::{Definition, DefinitionKind, Docstring, Documentable};
use crate::noqa::Directive;
use crate::registry::{Diagnostic, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_django, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise,
flake8_return, flake8_self, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, flake8_use_pathlib, mccabe, numpy, pandas_vet, pep8_naming,
pycodestyle, pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -50,6 +50,7 @@ use crate::{autofix, docstrings, noqa, visibility};
const GLOBAL_SCOPE_INDEX: usize = 0;
type DeferralContext<'a> = (Vec<usize>, Vec<RefEquality<'a, Stmt>>);
type AnnotationContext = (bool, bool);
#[allow(clippy::struct_excessive_bools)]
pub struct Checker<'a> {
@@ -84,8 +85,8 @@ pub struct Checker<'a> {
pub(crate) scopes: Vec<Scope<'a>>,
pub(crate) scope_stack: Vec<usize>,
pub(crate) dead_scopes: Vec<(usize, Vec<usize>)>,
deferred_string_type_definitions: Vec<(Range, &'a str, bool, DeferralContext<'a>)>,
deferred_type_definitions: Vec<(&'a Expr, bool, DeferralContext<'a>)>,
deferred_string_type_definitions: Vec<(Range, &'a str, AnnotationContext, DeferralContext<'a>)>,
deferred_type_definitions: Vec<(&'a Expr, AnnotationContext, DeferralContext<'a>)>,
deferred_functions: Vec<(&'a Stmt, DeferralContext<'a>, VisibleScope)>,
deferred_lambdas: Vec<(&'a Expr, DeferralContext<'a>)>,
deferred_for_loops: Vec<(&'a Stmt, DeferralContext<'a>)>,
@@ -282,7 +283,7 @@ impl<'a> Checker<'a> {
}
/// Return `true` if a `Rule` is disabled by a `noqa` directive.
pub fn is_ignored(&self, code: &Rule, lineno: usize) -> bool {
pub fn rule_is_ignored(&self, code: &Rule, lineno: usize) -> bool {
// TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`.
// However, in rare cases, we need to check them here. For example, when
// removing unused imports, we create a single fix that's applied to all
@@ -293,16 +294,7 @@ impl<'a> Checker<'a> {
if matches!(self.noqa, flags::Noqa::Disabled) {
return false;
}
let noqa_lineno = self.noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = self.locator.slice_source_code_range(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match noqa::extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => noqa::includes(code, &codes),
}
noqa::rule_is_ignored(code, lineno, self.noqa_line_for, self.locator)
}
}
@@ -473,6 +465,17 @@ where
body,
..
} => {
if self
.settings
.rules
.enabled(&Rule::NonLeadingReceiverDecorator)
{
self.diagnostics
.extend(flake8_django::rules::non_leading_receiver_decorator(
decorator_list,
|expr| self.resolve_call_path(expr),
));
}
if self.settings.rules.enabled(&Rule::AmbiguousFunctionName) {
if let Some(diagnostic) =
pycodestyle::rules::ambiguous_function_name(name, || {
@@ -575,7 +578,7 @@ where
flake8_return::rules::function(self, body);
}
if self.settings.rules.enabled(&Rule::FunctionIsTooComplex) {
if self.settings.rules.enabled(&Rule::ComplexStructure) {
if let Some(diagnostic) = mccabe::rules::function_is_too_complex(
stmt,
name,
@@ -780,6 +783,19 @@ where
decorator_list,
body,
} => {
if self.settings.rules.enabled(&Rule::NullableModelStringField) {
self.diagnostics
.extend(flake8_django::rules::nullable_model_string_field(
self, body,
));
}
if self.settings.rules.enabled(&Rule::ModelWithoutDunderStr) {
if let Some(diagnostic) =
flake8_django::rules::model_without_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
}
if self.settings.rules.enabled(&Rule::UselessObjectInheritance) {
pyupgrade::rules::useless_object_inheritance(self, stmt, name, bases, keywords);
}
@@ -1235,9 +1251,9 @@ where
}
}
if self.settings.rules.enabled(&Rule::ImportStarUsed) {
if self.settings.rules.enabled(&Rule::ImportStar) {
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsed {
pyflakes::rules::ImportStar {
name: helpers::format_import_from(
level.as_ref(),
module.as_deref(),
@@ -1300,8 +1316,11 @@ where
if self.settings.rules.enabled(&Rule::RelativeImports) {
if let Some(diagnostic) =
flake8_tidy_imports::relative_imports::banned_relative_import(
self,
stmt,
level.as_ref(),
module.as_deref(),
self.module_path.as_ref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
)
{
@@ -1486,7 +1505,7 @@ where
if self.settings.rules.enabled(&Rule::IfTuple) {
pyflakes::rules::if_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::NestedIfStatements) {
if self.settings.rules.enabled(&Rule::CollapsibleIf) {
flake8_simplify::rules::nested_if_statements(
self,
stmt,
@@ -1496,11 +1515,14 @@ where
self.current_stmt_parent().map(Into::into),
);
}
if self
.settings
.rules
.enabled(&Rule::ReturnBoolConditionDirectly)
{
if self.settings.rules.enabled(&Rule::IfWithSameArms) {
flake8_simplify::rules::if_with_same_arms(
self,
stmt,
self.current_stmt_parent().map(std::convert::Into::into),
);
}
if self.settings.rules.enabled(&Rule::NeedlessBool) {
flake8_simplify::rules::return_bool_condition_directly(self, stmt);
}
if self.settings.rules.enabled(&Rule::UseTernaryOperator) {
@@ -1537,15 +1559,10 @@ where
if self.settings.rules.enabled(&Rule::AssertTuple) {
pyflakes::rules::assert_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::DoNotAssertFalse) {
flake8_bugbear::rules::assert_false(
self,
stmt,
test,
msg.as_ref().map(|expr| &**expr),
);
if self.settings.rules.enabled(&Rule::AssertFalse) {
flake8_bugbear::rules::assert_false(self, stmt, test, msg.as_deref());
}
if self.settings.rules.enabled(&Rule::AssertUsed) {
if self.settings.rules.enabled(&Rule::Assert) {
self.diagnostics
.push(flake8_bandit::rules::assert_used(stmt));
}
@@ -1704,9 +1721,9 @@ where
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
if self.settings.rules.enabled(&Rule::LambdaAssignment) {
if let [target] = &targets[..] {
pycodestyle::rules::do_not_assign_lambda(self, target, value, stmt);
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
}
}
@@ -1722,6 +1739,12 @@ where
}
}
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
if self.path.extension().map_or(false, |ext| ext == "pyi") {
flake8_pyi::rules::prefix_type_params(self, value, targets);
}
}
if self.settings.rules.enabled(&Rule::UselessMetaclassType) {
pyupgrade::rules::useless_metaclass_type(self, stmt, value, targets);
}
@@ -1754,9 +1777,9 @@ where
}
}
StmtKind::AnnAssign { target, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
if self.settings.rules.enabled(&Rule::LambdaAssignment) {
if let Some(value) = value {
pycodestyle::rules::do_not_assign_lambda(self, target, value, stmt);
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
}
}
}
@@ -1772,6 +1795,13 @@ where
{
flake8_simplify::rules::use_capital_environment_variables(self, value);
}
if self.settings.rules.enabled(&Rule::AsyncioDanglingTask) {
if let Some(diagnostic) = ruff::rules::asyncio_dangling_task(value, |expr| {
self.resolve_call_path(expr)
}) {
self.diagnostics.push(diagnostic);
}
}
}
_ => {}
}
@@ -2042,13 +2072,13 @@ where
self.deferred_string_type_definitions.push((
Range::from_located(expr),
value,
self.in_annotation,
(self.in_annotation, self.in_type_checking_block),
(self.scope_stack.clone(), self.parents.clone()),
));
} else {
self.deferred_type_definitions.push((
expr,
self.in_annotation,
(self.in_annotation, self.in_type_checking_block),
(self.scope_stack.clone(), self.parents.clone()),
));
}
@@ -2066,7 +2096,7 @@ where
// Ex) Optional[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP604Annotation)
&& self.settings.rules.enabled(&Rule::TypingUnion)
&& (self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& self.annotations_future_enabled
@@ -2117,11 +2147,14 @@ where
if self.settings.rules.enabled(&Rule::TypingTextStrAlias) {
pyupgrade::rules::typing_text_str_alias(self, expr);
}
if self.settings.rules.enabled(&Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
// Ex) List[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP585Annotation)
&& self.settings.rules.enabled(&Rule::DeprecatedCollectionType)
&& (self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.annotations_future_enabled
@@ -2166,7 +2199,7 @@ where
// Ex) typing.List[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP585Annotation)
&& self.settings.rules.enabled(&Rule::DeprecatedCollectionType)
&& (self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.annotations_future_enabled
@@ -2183,6 +2216,9 @@ where
if self.settings.rules.enabled(&Rule::TypingTextStrAlias) {
pyupgrade::rules::typing_text_str_alias(self, expr);
}
if self.settings.rules.enabled(&Rule::NumpyDeprecatedTypeAlias) {
numpy::rules::deprecated_type_alias(self, expr);
}
if self.settings.rules.enabled(&Rule::RewriteMockImport) {
pyupgrade::rules::rewrite_mock_attribute(self, expr);
}
@@ -2367,12 +2403,12 @@ where
}
// flake8-pie
if self.settings.rules.enabled(&Rule::NoUnnecessaryDictKwargs) {
if self.settings.rules.enabled(&Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::no_unnecessary_dict_kwargs(self, expr, keywords);
}
// flake8-bandit
if self.settings.rules.enabled(&Rule::ExecUsed) {
if self.settings.rules.enabled(&Rule::ExecBuiltin) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
self.diagnostics.push(diagnostic);
}
@@ -2405,6 +2441,9 @@ where
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
if self
.settings
.rules
@@ -2435,12 +2474,22 @@ where
}
if self.settings.rules.enabled(&Rule::UnnecessaryGeneratorSet) {
flake8_comprehensions::rules::unnecessary_generator_set(
self, expr, func, args, keywords,
self,
expr,
self.current_expr_parent().map(Into::into),
func,
args,
keywords,
);
}
if self.settings.rules.enabled(&Rule::UnnecessaryGeneratorDict) {
flake8_comprehensions::rules::unnecessary_generator_dict(
self, expr, func, args, keywords,
self,
expr,
self.current_expr_parent().map(Into::into),
func,
args,
keywords,
);
}
if self
@@ -2529,7 +2578,13 @@ where
);
}
if self.settings.rules.enabled(&Rule::UnnecessaryMap) {
flake8_comprehensions::rules::unnecessary_map(self, expr, func, args);
flake8_comprehensions::rules::unnecessary_map(
self,
expr,
self.current_expr_parent().map(Into::into),
func,
args,
);
}
// flake8-boolean-trap
@@ -2782,7 +2837,7 @@ where
pyflakes::rules::repeated_keys(self, keys, values);
}
if self.settings.rules.enabled(&Rule::NoUnnecessarySpread) {
if self.settings.rules.enabled(&Rule::UnnecessarySpread) {
flake8_pie::rules::no_unnecessary_spread(self, keys, values);
}
}
@@ -2790,11 +2845,17 @@ where
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
}
if self.settings.rules.enabled(&Rule::YieldInInit) {
pylint::rules::yield_in_init(self, expr);
}
}
ExprKind::YieldFrom { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
}
if self.settings.rules.enabled(&Rule::YieldInInit) {
pylint::rules::yield_in_init(self, expr);
}
}
ExprKind::Await { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
@@ -2812,6 +2873,9 @@ where
{
pyflakes::rules::f_string_missing_placeholders(expr, values, self);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
ExprKind::BinOp {
left,
@@ -2973,6 +3037,12 @@ where
if self.settings.rules.enabled(&Rule::PrintfStringFormatting) {
pyupgrade::rules::printf_string_formatting(self, expr, left, right);
}
if self.settings.rules.enabled(&Rule::BadStringFormatType) {
pylint::rules::bad_string_format_type(self, expr, right);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
}
ExprKind::BinOp {
@@ -2994,6 +3064,9 @@ where
{
ruff::rules::unpack_instead_of_concatenating_to_collection_literal(self, expr);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
ExprKind::UnaryOp { op, operand } => {
let check_not_in = self.settings.rules.enabled(&Rule::NotInTest);
@@ -3100,6 +3173,23 @@ where
if self.settings.rules.enabled(&Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators);
}
if self
.settings
.rules
.enabled(&Rule::UnrecognizedPlatformCheck)
|| self.settings.rules.enabled(&Rule::UnrecognizedPlatformName)
{
if self.path.extension().map_or(false, |ext| ext == "pyi") {
flake8_pyi::rules::unrecognized_platform(
self,
expr,
left,
ops,
comparators,
);
}
}
}
ExprKind::Constant {
value: Constant::Str(value),
@@ -3109,7 +3199,7 @@ where
self.deferred_string_type_definitions.push((
Range::from_located(expr),
value,
self.in_annotation,
(self.in_annotation, self.in_type_checking_block),
(self.scope_stack.clone(), self.parents.clone()),
));
}
@@ -3137,9 +3227,6 @@ where
if self.settings.rules.enabled(&Rule::RewriteUnicodeLiteral) {
pyupgrade::rules::rewrite_unicode_literal(self, expr, kind.as_deref());
}
if self.settings.rules.enabled(&Rule::BidirectionalUnicode) {
pylint::rules::bidirectional_unicode(self, expr, value);
}
}
ExprKind::Lambda { args, body, .. } => {
if self.settings.rules.enabled(&Rule::PreferListBuiltin) {
@@ -3521,8 +3608,8 @@ where
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.rules.enabled(&Rule::DoNotUseBareExcept) {
if let Some(diagnostic) = pycodestyle::rules::do_not_use_bare_except(
if self.settings.rules.enabled(&Rule::BareExcept) {
if let Some(diagnostic) = pycodestyle::rules::bare_except(
type_.as_deref(),
body,
excepthandler,
@@ -3549,6 +3636,17 @@ where
if self.settings.rules.enabled(&Rule::TryExceptPass) {
flake8_bandit::rules::try_except_pass(
self,
excepthandler,
type_.as_deref(),
name.as_deref(),
body,
self.settings.flake8_bandit.check_typed_exception,
);
}
if self.settings.rules.enabled(&Rule::TryExceptContinue) {
flake8_bandit::rules::try_except_continue(
self,
excepthandler,
type_.as_deref(),
name.as_deref(),
body,
@@ -3742,7 +3840,7 @@ where
}
fn visit_body(&mut self, body: &'b [Stmt]) {
if self.settings.rules.enabled(&Rule::NoUnnecessaryPass) {
if self.settings.rules.enabled(&Rule::UnnecessaryPass) {
flake8_pie::rules::no_unnecessary_pass(self, body);
}
@@ -3855,6 +3953,14 @@ impl<'a> Checker<'a> {
&self.scopes[*(self.scope_stack.last().expect("No current scope found"))]
}
pub fn current_scope_parent(&self) -> Option<&Scope> {
self.scope_stack
.iter()
.rev()
.nth(1)
.map(|index| &self.scopes[*index])
}
pub fn current_scopes(&self) -> impl Iterator<Item = &Scope> {
self.scope_stack
.iter()
@@ -3870,7 +3976,6 @@ impl<'a> Checker<'a> {
if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
ExecutionContext::Typing
} else {
@@ -4304,16 +4409,18 @@ impl<'a> Checker<'a> {
} {
let (all_names, all_names_flags) = extract_all_names(self, parent, current);
if self.settings.rules.enabled(&Rule::InvalidAllFormat)
&& matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT)
{
pylint::rules::invalid_all_format(self, expr);
if self.settings.rules.enabled(&Rule::InvalidAllFormat) {
if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) {
self.diagnostics
.push(pylint::rules::invalid_all_format(expr));
}
}
if self.settings.rules.enabled(&Rule::InvalidAllObject)
&& matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT)
{
pylint::rules::invalid_all_object(self, expr);
if self.settings.rules.enabled(&Rule::InvalidAllObject) {
if matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT) {
self.diagnostics
.push(pylint::rules::invalid_all_object(expr));
}
}
self.add_binding(
@@ -4397,12 +4504,13 @@ impl<'a> Checker<'a> {
fn check_deferred_type_definitions(&mut self) {
self.deferred_type_definitions.reverse();
while let Some((expr, in_annotation, (scopes, parents))) =
while let Some((expr, (in_annotation, in_type_checking_block), (scopes, parents))) =
self.deferred_type_definitions.pop()
{
self.scope_stack = scopes;
self.parents = parents;
self.in_annotation = in_annotation;
self.in_type_checking_block = in_type_checking_block;
self.in_type_definition = true;
self.in_deferred_type_definition = true;
self.visit_expr(expr);
@@ -4417,7 +4525,7 @@ impl<'a> Checker<'a> {
{
let mut stacks = vec![];
self.deferred_string_type_definitions.reverse();
while let Some((range, expression, in_annotation, context)) =
while let Some((range, expression, (in_annotation, in_type_checking_block), deferral)) =
self.deferred_string_type_definitions.pop()
{
if let Ok(mut expr) = parser::parse_expression(expression, "<filename>") {
@@ -4428,7 +4536,7 @@ impl<'a> Checker<'a> {
}
relocate_expr(&mut expr, range);
allocator.push(expr);
stacks.push((in_annotation, context));
stacks.push(((in_annotation, in_type_checking_block), deferral));
} else {
if self
.settings
@@ -4444,10 +4552,13 @@ impl<'a> Checker<'a> {
}
}
}
for (expr, (in_annotation, (scopes, parents))) in allocator.iter().zip(stacks) {
for (expr, ((in_annotation, in_type_checking_block), (scopes, parents))) in
allocator.iter().zip(stacks)
{
self.scope_stack = scopes;
self.parents = parents;
self.in_annotation = in_annotation;
self.in_type_checking_block = in_type_checking_block;
self.in_type_definition = true;
self.in_deferred_string_type_definition = true;
self.visit_expr(expr);
@@ -4578,11 +4689,54 @@ impl<'a> Checker<'a> {
return;
}
// Mark anything referenced in `__all__` as used.
let global_scope = &self.scopes[GLOBAL_SCOPE_INDEX];
let all_names: Option<(&Vec<String>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some((names, binding.range)),
_ => None,
});
let all_bindings: Option<(Vec<usize>, Range)> = all_names.map(|(names, range)| {
(
names
.iter()
.filter_map(|name| global_scope.bindings.get(name.as_str()).copied())
.collect(),
range,
)
});
if let Some((bindings, range)) = all_bindings {
for index in bindings {
self.bindings[index].mark_used(
GLOBAL_SCOPE_INDEX,
range,
ExecutionContext::Runtime,
);
}
}
// Extract `__all__` names from the global scope.
let all_names: Option<(Vec<&str>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => {
Some((names.iter().map(String::as_str).collect(), binding.range))
}
_ => None,
});
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let runtime_imports: Vec<Vec<&Binding>> = if !self.settings.flake8_type_checking.strict
&& (self
let runtime_imports: Vec<Vec<&Binding>> = if self.settings.flake8_type_checking.strict {
vec![]
} else {
if self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
@@ -4597,29 +4751,41 @@ impl<'a> Checker<'a> {
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport))
{
self.scopes
.iter()
.map(|scope| {
scope
.bindings
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
.enabled(&Rule::TypingOnlyStandardLibraryImport)
{
self.scopes
.iter()
.map(|scope| {
scope
.bindings
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
}
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for (index, stack) in self.dead_scopes.iter().rev() {
let scope = &self.scopes[*index];
// F822
if *index == GLOBAL_SCOPE_INDEX {
if self.settings.rules.enabled(&Rule::UndefinedExport) {
if let Some((names, range)) = &all_names {
diagnostics.extend(pyflakes::rules::undefined_export(
names, range, self.path, scope,
));
}
}
}
// PLW0602
if self
.settings
@@ -4648,35 +4814,6 @@ impl<'a> Checker<'a> {
continue;
}
let all_binding: Option<&Binding> = scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index]);
let all_names: Option<Vec<&str>> =
all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names.iter().map(String::as_str).collect()),
_ => None,
});
if self.settings.rules.enabled(&Rule::UndefinedExport) {
if !scope.import_starred && !self.path.ends_with("__init__.py") {
if let Some(all_binding) = all_binding {
if let Some(names) = &all_names {
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedExport {
name: name.to_string(),
},
all_binding.range,
));
}
}
}
}
}
}
// Look for any bindings that were redefined in another scope, and remain
// unused. Note that we only store references in `redefinitions` if
// the bindings are in different scopes.
@@ -4692,13 +4829,7 @@ impl<'a> Checker<'a> {
| BindingKind::StarImportation(..)
| BindingKind::FutureImportation
) {
// Skip used exports from `__all__`
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default()
{
if binding.used() {
continue;
}
@@ -4728,31 +4859,27 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
if scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = &all_names {
let mut from_list = vec![];
for binding in
scope.bindings.values().map(|index| &self.bindings[*index])
{
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
if let Some((names, range)) = &all_names {
let mut from_list = vec![];
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
from_list.sort();
}
from_list.sort();
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
all_binding.range,
));
}
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
*range,
));
}
}
}
@@ -4820,7 +4947,7 @@ impl<'a> Checker<'a> {
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
FxHashMap::default();
for (name, index) in &scope.bindings {
for index in scope.bindings.values() {
let binding = &self.bindings[*index];
let full_name = match &binding.kind {
@@ -4830,13 +4957,7 @@ impl<'a> Checker<'a> {
_ => continue,
};
// Skip used exports from `__all__`
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default()
{
if binding.used() {
continue;
}
@@ -4853,9 +4974,9 @@ impl<'a> Checker<'a> {
None
};
if self.is_ignored(&Rule::UnusedImport, diagnostic_lineno)
if self.rule_is_ignored(&Rule::UnusedImport, diagnostic_lineno)
|| parent_lineno.map_or(false, |parent_lineno| {
self.is_ignored(&Rule::UnusedImport, parent_lineno)
self.rule_is_ignored(&Rule::UnusedImport, parent_lineno)
})
{
ignored
@@ -4987,10 +5108,7 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::MissingReturnTypeClassMethod)
|| self
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression);
|| self.settings.rules.enabled(&Rule::AnyType);
let enforce_docstrings = self.settings.rules.enabled(&Rule::PublicModule)
|| self.settings.rules.enabled(&Rule::PublicClass)
|| self.settings.rules.enabled(&Rule::PublicMethod)
@@ -5030,16 +5148,16 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::SectionUnderlineNotOverIndented)
|| self.settings.rules.enabled(&Rule::UsesTripleQuotes)
|| self.settings.rules.enabled(&Rule::TripleSingleQuotes)
|| self
.settings
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
.enabled(&Rule::EscapeSequenceInDocstring)
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|| self.settings.rules.enabled(&Rule::NonImperativeMood)
|| self.settings.rules.enabled(&Rule::NoSignature)
|| self.settings.rules.enabled(&Rule::FirstLineCapitalized)
|| self.settings.rules.enabled(&Rule::NoThisPrefix)
|| self.settings.rules.enabled(&Rule::DocstringStartsWithThis)
|| self.settings.rules.enabled(&Rule::CapitalizeSectionName)
|| self.settings.rules.enabled(&Rule::NewLineAfterSectionName)
|| self
@@ -5064,12 +5182,12 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::BlankLineAfterLastSection)
|| self.settings.rules.enabled(&Rule::NonEmptySection)
|| self.settings.rules.enabled(&Rule::EmptyDocstringSection)
|| self.settings.rules.enabled(&Rule::EndsInPunctuation)
|| self.settings.rules.enabled(&Rule::SectionNameEndsInColon)
|| self.settings.rules.enabled(&Rule::DocumentAllArguments)
|| self.settings.rules.enabled(&Rule::SkipDocstring)
|| self.settings.rules.enabled(&Rule::NonEmpty);
|| self.settings.rules.enabled(&Rule::UndocumentedParam)
|| self.settings.rules.enabled(&Rule::OverloadWithDocstring)
|| self.settings.rules.enabled(&Rule::EmptyDocstring);
let mut overloaded_name: Option<String> = None;
self.definitions.reverse();
@@ -5110,10 +5228,8 @@ impl<'a> Checker<'a> {
// Extract a `Docstring` from a `Definition`.
let expr = definition.docstring.unwrap();
let contents = self
.locator
.slice_source_code_range(&Range::from_located(expr));
let indentation = self.locator.slice_source_code_range(&Range::new(
let contents = self.locator.slice(&Range::from_located(expr));
let indentation = self.locator.slice(&Range::new(
Location::new(expr.location.row(), 0),
Location::new(expr.location.row(), expr.location.column()),
));
@@ -5178,13 +5294,13 @@ impl<'a> Checker<'a> {
{
pydocstyle::rules::multi_line_summary_start(self, &docstring);
}
if self.settings.rules.enabled(&Rule::UsesTripleQuotes) {
if self.settings.rules.enabled(&Rule::TripleSingleQuotes) {
pydocstyle::rules::triple_quotes(self, &docstring);
}
if self
.settings
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
.enabled(&Rule::EscapeSequenceInDocstring)
{
pydocstyle::rules::backslashes(self, &docstring);
}
@@ -5200,13 +5316,13 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::FirstLineCapitalized) {
pydocstyle::rules::capitalized(self, &docstring);
}
if self.settings.rules.enabled(&Rule::NoThisPrefix) {
if self.settings.rules.enabled(&Rule::DocstringStartsWithThis) {
pydocstyle::rules::starts_with_this(self, &docstring);
}
if self.settings.rules.enabled(&Rule::EndsInPunctuation) {
pydocstyle::rules::ends_with_punctuation(self, &docstring);
}
if self.settings.rules.enabled(&Rule::SkipDocstring) {
if self.settings.rules.enabled(&Rule::OverloadWithDocstring) {
pydocstyle::rules::if_needed(self, &docstring);
}
if self
@@ -5242,9 +5358,9 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::BlankLineAfterLastSection)
|| self.settings.rules.enabled(&Rule::NonEmptySection)
|| self.settings.rules.enabled(&Rule::EmptyDocstringSection)
|| self.settings.rules.enabled(&Rule::SectionNameEndsInColon)
|| self.settings.rules.enabled(&Rule::DocumentAllArguments)
|| self.settings.rules.enabled(&Rule::UndocumentedParam)
{
pydocstyle::rules::sections(
self,

View File

@@ -52,8 +52,7 @@ pub fn check_logical_lines(
// Extract the indentation level.
let start_loc = line.mapping[0].1;
let start_line = locator
.slice_source_code_range(&Range::new(Location::new(start_loc.row(), 0), start_loc));
let start_line = locator.slice(&Range::new(Location::new(start_loc.row(), 0), start_loc));
let indent_level = expand_indent(start_line);
let indent_size = 4;

View File

@@ -4,6 +4,7 @@ use nohash_hasher::IntMap;
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::codes::NoqaCode;
use crate::fix::Fix;
use crate::noqa;
use crate::noqa::{is_file_exempt, Directive};
@@ -20,7 +21,7 @@ pub fn check_noqa(
settings: &Settings,
autofix: flags::Autofix,
) {
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
let mut ignored = vec![];
let enforce_noqa = settings.rules.enabled(&Rule::UnusedNOQA);
@@ -55,13 +56,13 @@ pub fn check_noqa(
});
match noqa {
(Directive::All(..), matches) => {
matches.push(diagnostic.kind.rule().code());
matches.push(diagnostic.kind.rule().noqa_code());
ignored.push(index);
continue;
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().code());
matches.push(diagnostic.kind.rule().noqa_code());
ignored.push(index);
continue;
}
@@ -82,12 +83,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.rule().code());
matches.push(diagnostic.kind.rule().noqa_code());
ignored.push(index);
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().code());
matches.push(diagnostic.kind.rule().noqa_code());
ignored.push(index);
}
}
@@ -128,12 +129,12 @@ pub fn check_noqa(
let mut self_ignore = false;
for code in codes {
let code = get_redirect_target(code).unwrap_or(code);
if code == Rule::UnusedNOQA.code() {
if Rule::UnusedNOQA.noqa_code() == code {
self_ignore = true;
break;
}
if matches.contains(&code) || settings.external.contains(code) {
if matches.iter().any(|m| *m == code) || settings.external.contains(code) {
valid_codes.push(code);
} else {
if let Ok(rule) = Rule::from_code(code) {

View File

@@ -11,6 +11,7 @@ use crate::rules::pycodestyle::rules::{
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::pylint;
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
use crate::source_code::Stylist;
@@ -37,15 +38,12 @@ pub fn check_physical_lines(
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_unnecessary_coding_comment = settings.rules.enabled(&Rule::UTF8EncodingDeclaration);
let enforce_mixed_spaces_and_tabs = settings.rules.enabled(&Rule::MixedSpacesAndTabs);
let enforce_bidirectional_unicode = settings.rules.enabled(&Rule::BidirectionalUnicode);
let fix_unnecessary_coding_comment = matches!(autofix, flags::Autofix::Enabled)
&& settings
.rules
.should_fix(&Rule::PEP3120UnnecessaryCodingComment);
&& settings.rules.should_fix(&Rule::UTF8EncodingDeclaration);
let fix_shebang_whitespace = matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::ShebangWhitespace);
@@ -137,6 +135,10 @@ pub fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
if enforce_bidirectional_unicode {
diagnostics.extend(pylint::rules::bidirectional_unicode(index, line));
}
}
if enforce_no_newline_at_end_of_file {

View File

@@ -32,8 +32,15 @@ pub fn check_tokens(
let enforce_quotes = settings.rules.enabled(&Rule::BadQuotesInlineString)
|| settings.rules.enabled(&Rule::BadQuotesMultilineString)
|| settings.rules.enabled(&Rule::BadQuotesDocstring)
|| settings.rules.enabled(&Rule::AvoidQuoteEscape);
|| settings.rules.enabled(&Rule::AvoidableEscapedQuote);
let enforce_commented_out_code = settings.rules.enabled(&Rule::CommentedOutCode);
let enforce_compound_statements = settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineColon)
|| settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineSemicolon)
|| settings.rules.enabled(&Rule::UselessSemicolon);
let enforce_invalid_escape_sequence = settings.rules.enabled(&Rule::InvalidEscapeSequence);
let enforce_implicit_string_concatenation = settings
.rules
@@ -48,10 +55,8 @@ pub fn check_tokens(
|| settings.rules.enabled(&Rule::TrailingCommaProhibited);
let enforce_extraneous_parenthesis = settings.rules.enabled(&Rule::ExtraneousParentheses);
if enforce_ambiguous_unicode_character
|| enforce_commented_out_code
|| enforce_invalid_escape_sequence
{
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
let mut state_machine = StateMachine::default();
for &(start, ref tok, end) in tokens.iter().flatten() {
let is_docstring = if enforce_ambiguous_unicode_character {
@@ -60,54 +65,64 @@ pub fn check_tokens(
false
};
// RUF001, RUF002, RUF003
if enforce_ambiguous_unicode_character {
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
} else {
Context::String
}
if matches!(tok, Tok::String { .. } | Tok::Comment(_)) {
diagnostics.extend(ruff::rules::ambiguous_unicode_character(
locator,
start,
end,
if matches!(tok, Tok::String { .. }) {
if is_docstring {
Context::Docstring
} else {
Context::Comment
},
settings,
autofix,
));
}
Context::String
}
} else {
Context::Comment
},
settings,
autofix,
));
}
}
}
// eradicate
if enforce_commented_out_code {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, start, end, settings, autofix)
{
diagnostics.push(diagnostic);
}
}
}
// W605
if enforce_invalid_escape_sequence {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
start,
end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
// ERA001
if enforce_commented_out_code {
for (start, tok, end) in tokens.iter().flatten() {
if matches!(tok, Tok::Comment(_)) {
if let Some(diagnostic) =
eradicate::rules::commented_out_code(locator, *start, *end, settings, autofix)
{
diagnostics.push(diagnostic);
}
}
}
}
// W605
if enforce_invalid_escape_sequence {
for (start, tok, end) in tokens.iter().flatten() {
if matches!(tok, Tok::String { .. }) {
diagnostics.extend(pycodestyle::rules::invalid_escape_sequence(
locator,
*start,
*end,
matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::InvalidEscapeSequence),
));
}
}
}
// E701, E702, E703
if enforce_compound_statements {
diagnostics.extend(
pycodestyle::rules::compound_statements(tokens)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);
}
// Q001, Q002, Q003
if enforce_quotes {
diagnostics.extend(

609
crates/ruff/src/codes.rs Normal file
View File

@@ -0,0 +1,609 @@
use crate::registry::{Linter, Rule};
#[ruff_macros::map_codes]
pub fn code_to_rule(linter: Linter, code: &str) -> Option<Rule> {
#[allow(clippy::enum_glob_use)]
use Linter::*;
Some(match (linter, code) {
// pycodestyle errors
(Pycodestyle, "E101") => Rule::MixedSpacesAndTabs,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E111") => Rule::IndentationWithInvalidMultiple,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E112") => Rule::NoIndentedBlock,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E113") => Rule::UnexpectedIndentation,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E114") => Rule::IndentationWithInvalidMultipleComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E115") => Rule::NoIndentedBlockComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E116") => Rule::UnexpectedIndentationComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E117") => Rule::OverIndented,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E201") => Rule::WhitespaceAfterOpenBracket,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E202") => Rule::WhitespaceBeforeCloseBracket,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E203") => Rule::WhitespaceBeforePunctuation,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E221") => Rule::MultipleSpacesBeforeOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E222") => Rule::MultipleSpacesAfterOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E223") => Rule::TabBeforeOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E224") => Rule::TabAfterOperator,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E261") => Rule::TooFewSpacesBeforeInlineComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E262") => Rule::NoSpaceAfterInlineComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E265") => Rule::NoSpaceAfterBlockComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E266") => Rule::MultipleLeadingHashesForBlockComment,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E271") => Rule::MultipleSpacesAfterKeyword,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E272") => Rule::MultipleSpacesBeforeKeyword,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E273") => Rule::TabAfterKeyword,
#[cfg(feature = "logical_lines")]
(Pycodestyle, "E274") => Rule::TabBeforeKeyword,
(Pycodestyle, "E401") => Rule::MultipleImportsOnOneLine,
(Pycodestyle, "E402") => Rule::ModuleImportNotAtTopOfFile,
(Pycodestyle, "E501") => Rule::LineTooLong,
(Pycodestyle, "E701") => Rule::MultipleStatementsOnOneLineColon,
(Pycodestyle, "E702") => Rule::MultipleStatementsOnOneLineSemicolon,
(Pycodestyle, "E703") => Rule::UselessSemicolon,
(Pycodestyle, "E711") => Rule::NoneComparison,
(Pycodestyle, "E712") => Rule::TrueFalseComparison,
(Pycodestyle, "E713") => Rule::NotInTest,
(Pycodestyle, "E714") => Rule::NotIsTest,
(Pycodestyle, "E721") => Rule::TypeComparison,
(Pycodestyle, "E722") => Rule::BareExcept,
(Pycodestyle, "E731") => Rule::LambdaAssignment,
(Pycodestyle, "E741") => Rule::AmbiguousVariableName,
(Pycodestyle, "E742") => Rule::AmbiguousClassName,
(Pycodestyle, "E743") => Rule::AmbiguousFunctionName,
(Pycodestyle, "E902") => Rule::IOError,
(Pycodestyle, "E999") => Rule::SyntaxError,
// pycodestyle warnings
(Pycodestyle, "W292") => Rule::NoNewLineAtEndOfFile,
(Pycodestyle, "W505") => Rule::DocLineTooLong,
(Pycodestyle, "W605") => Rule::InvalidEscapeSequence,
// pyflakes
(Pyflakes, "401") => Rule::UnusedImport,
(Pyflakes, "402") => Rule::ImportShadowedByLoopVar,
(Pyflakes, "403") => Rule::ImportStar,
(Pyflakes, "404") => Rule::LateFutureImport,
(Pyflakes, "405") => Rule::ImportStarUsage,
(Pyflakes, "406") => Rule::ImportStarNotPermitted,
(Pyflakes, "407") => Rule::FutureFeatureNotDefined,
(Pyflakes, "501") => Rule::PercentFormatInvalidFormat,
(Pyflakes, "502") => Rule::PercentFormatExpectedMapping,
(Pyflakes, "503") => Rule::PercentFormatExpectedSequence,
(Pyflakes, "504") => Rule::PercentFormatExtraNamedArguments,
(Pyflakes, "505") => Rule::PercentFormatMissingArgument,
(Pyflakes, "506") => Rule::PercentFormatMixedPositionalAndNamed,
(Pyflakes, "507") => Rule::PercentFormatPositionalCountMismatch,
(Pyflakes, "508") => Rule::PercentFormatStarRequiresSequence,
(Pyflakes, "509") => Rule::PercentFormatUnsupportedFormatCharacter,
(Pyflakes, "521") => Rule::StringDotFormatInvalidFormat,
(Pyflakes, "522") => Rule::StringDotFormatExtraNamedArguments,
(Pyflakes, "523") => Rule::StringDotFormatExtraPositionalArguments,
(Pyflakes, "524") => Rule::StringDotFormatMissingArguments,
(Pyflakes, "525") => Rule::StringDotFormatMixingAutomatic,
(Pyflakes, "541") => Rule::FStringMissingPlaceholders,
(Pyflakes, "601") => Rule::MultiValueRepeatedKeyLiteral,
(Pyflakes, "602") => Rule::MultiValueRepeatedKeyVariable,
(Pyflakes, "621") => Rule::ExpressionsInStarAssignment,
(Pyflakes, "622") => Rule::TwoStarredExpressions,
(Pyflakes, "631") => Rule::AssertTuple,
(Pyflakes, "632") => Rule::IsLiteral,
(Pyflakes, "633") => Rule::InvalidPrintSyntax,
(Pyflakes, "634") => Rule::IfTuple,
(Pyflakes, "701") => Rule::BreakOutsideLoop,
(Pyflakes, "702") => Rule::ContinueOutsideLoop,
(Pyflakes, "704") => Rule::YieldOutsideFunction,
(Pyflakes, "706") => Rule::ReturnOutsideFunction,
(Pyflakes, "707") => Rule::DefaultExceptNotLast,
(Pyflakes, "722") => Rule::ForwardAnnotationSyntaxError,
(Pyflakes, "811") => Rule::RedefinedWhileUnused,
(Pyflakes, "821") => Rule::UndefinedName,
(Pyflakes, "822") => Rule::UndefinedExport,
(Pyflakes, "823") => Rule::UndefinedLocal,
(Pyflakes, "841") => Rule::UnusedVariable,
(Pyflakes, "842") => Rule::UnusedAnnotation,
(Pyflakes, "901") => Rule::RaiseNotImplemented,
// pylint
(Pylint, "E0100") => Rule::YieldInInit,
(Pylint, "E0604") => Rule::InvalidAllObject,
(Pylint, "E0605") => Rule::InvalidAllFormat,
(Pylint, "E1307") => Rule::BadStringFormatType,
(Pylint, "E2502") => Rule::BidirectionalUnicode,
(Pylint, "E1310") => Rule::BadStrStripCall,
(Pylint, "C0414") => Rule::UselessImportAlias,
(Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall,
(Pylint, "E0117") => Rule::NonlocalWithoutBinding,
(Pylint, "E0118") => Rule::UsedPriorGlobalDeclaration,
(Pylint, "E1142") => Rule::AwaitOutsideAsync,
(Pylint, "R0206") => Rule::PropertyWithParameters,
(Pylint, "R0402") => Rule::ConsiderUsingFromImport,
(Pylint, "R0133") => Rule::ComparisonOfConstant,
(Pylint, "R1701") => Rule::ConsiderMergingIsinstance,
(Pylint, "R1722") => Rule::ConsiderUsingSysExit,
(Pylint, "R2004") => Rule::MagicValueComparison,
(Pylint, "W0120") => Rule::UselessElseOnLoop,
(Pylint, "W0602") => Rule::GlobalVariableNotAssigned,
(Pylint, "R0911") => Rule::TooManyReturnStatements,
(Pylint, "R0913") => Rule::TooManyArguments,
(Pylint, "R0912") => Rule::TooManyBranches,
(Pylint, "R0915") => Rule::TooManyStatements,
// flake8-builtins
(Flake8Builtins, "001") => Rule::BuiltinVariableShadowing,
(Flake8Builtins, "002") => Rule::BuiltinArgumentShadowing,
(Flake8Builtins, "003") => Rule::BuiltinAttributeShadowing,
// flake8-bugbear
(Flake8Bugbear, "002") => Rule::UnaryPrefixIncrement,
(Flake8Bugbear, "003") => Rule::AssignmentToOsEnviron,
(Flake8Bugbear, "004") => Rule::UnreliableCallableCheck,
(Flake8Bugbear, "005") => Rule::StripWithMultiCharacters,
(Flake8Bugbear, "006") => Rule::MutableArgumentDefault,
(Flake8Bugbear, "007") => Rule::UnusedLoopControlVariable,
(Flake8Bugbear, "008") => Rule::FunctionCallArgumentDefault,
(Flake8Bugbear, "009") => Rule::GetAttrWithConstant,
(Flake8Bugbear, "010") => Rule::SetAttrWithConstant,
(Flake8Bugbear, "011") => Rule::AssertFalse,
(Flake8Bugbear, "012") => Rule::JumpStatementInFinally,
(Flake8Bugbear, "013") => Rule::RedundantTupleInExceptionHandler,
(Flake8Bugbear, "014") => Rule::DuplicateHandlerException,
(Flake8Bugbear, "015") => Rule::UselessComparison,
(Flake8Bugbear, "016") => Rule::CannotRaiseLiteral,
(Flake8Bugbear, "017") => Rule::AssertRaisesException,
(Flake8Bugbear, "018") => Rule::UselessExpression,
(Flake8Bugbear, "019") => Rule::CachedInstanceMethod,
(Flake8Bugbear, "020") => Rule::LoopVariableOverridesIterator,
(Flake8Bugbear, "021") => Rule::FStringDocstring,
(Flake8Bugbear, "022") => Rule::UselessContextlibSuppress,
(Flake8Bugbear, "023") => Rule::FunctionUsesLoopVariable,
(Flake8Bugbear, "024") => Rule::AbstractBaseClassWithoutAbstractMethod,
(Flake8Bugbear, "025") => Rule::DuplicateTryBlockException,
(Flake8Bugbear, "026") => Rule::StarArgUnpackingAfterKeywordArg,
(Flake8Bugbear, "027") => Rule::EmptyMethodWithoutAbstractDecorator,
(Flake8Bugbear, "904") => Rule::RaiseWithoutFromInsideExcept,
(Flake8Bugbear, "905") => Rule::ZipWithoutExplicitStrict,
// flake8-blind-except
(Flake8BlindExcept, "001") => Rule::BlindExcept,
// flake8-comprehensions
(Flake8Comprehensions, "00") => Rule::UnnecessaryGeneratorList,
(Flake8Comprehensions, "01") => Rule::UnnecessaryGeneratorSet,
(Flake8Comprehensions, "02") => Rule::UnnecessaryGeneratorDict,
(Flake8Comprehensions, "03") => Rule::UnnecessaryListComprehensionSet,
(Flake8Comprehensions, "04") => Rule::UnnecessaryListComprehensionDict,
(Flake8Comprehensions, "05") => Rule::UnnecessaryLiteralSet,
(Flake8Comprehensions, "06") => Rule::UnnecessaryLiteralDict,
(Flake8Comprehensions, "08") => Rule::UnnecessaryCollectionCall,
(Flake8Comprehensions, "09") => Rule::UnnecessaryLiteralWithinTupleCall,
(Flake8Comprehensions, "10") => Rule::UnnecessaryLiteralWithinListCall,
(Flake8Comprehensions, "11") => Rule::UnnecessaryListCall,
(Flake8Comprehensions, "13") => Rule::UnnecessaryCallAroundSorted,
(Flake8Comprehensions, "14") => Rule::UnnecessaryDoubleCastOrProcess,
(Flake8Comprehensions, "15") => Rule::UnnecessarySubscriptReversal,
(Flake8Comprehensions, "16") => Rule::UnnecessaryComprehension,
(Flake8Comprehensions, "17") => Rule::UnnecessaryMap,
// flake8-debugger
(Flake8Debugger, "0") => Rule::Debugger,
// mccabe
(McCabe, "1") => Rule::ComplexStructure,
// flake8-tidy-imports
(Flake8TidyImports, "251") => Rule::BannedApi,
(Flake8TidyImports, "252") => Rule::RelativeImports,
// flake8-return
(Flake8Return, "501") => Rule::UnnecessaryReturnNone,
(Flake8Return, "502") => Rule::ImplicitReturnValue,
(Flake8Return, "503") => Rule::ImplicitReturn,
(Flake8Return, "504") => Rule::UnnecessaryAssign,
(Flake8Return, "505") => Rule::SuperfluousElseReturn,
(Flake8Return, "506") => Rule::SuperfluousElseRaise,
(Flake8Return, "507") => Rule::SuperfluousElseContinue,
(Flake8Return, "508") => Rule::SuperfluousElseBreak,
// flake8-implicit-str-concat
(Flake8ImplicitStrConcat, "001") => Rule::SingleLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "002") => Rule::MultiLineImplicitStringConcatenation,
(Flake8ImplicitStrConcat, "003") => Rule::ExplicitStringConcatenation,
// flake8-print
(Flake8Print, "1") => Rule::PrintFound,
(Flake8Print, "3") => Rule::PPrintFound,
// flake8-quotes
(Flake8Quotes, "000") => Rule::BadQuotesInlineString,
(Flake8Quotes, "001") => Rule::BadQuotesMultilineString,
(Flake8Quotes, "002") => Rule::BadQuotesDocstring,
(Flake8Quotes, "003") => Rule::AvoidableEscapedQuote,
// flake8-annotations
(Flake8Annotations, "001") => Rule::MissingTypeFunctionArgument,
(Flake8Annotations, "002") => Rule::MissingTypeArgs,
(Flake8Annotations, "003") => Rule::MissingTypeKwargs,
(Flake8Annotations, "101") => Rule::MissingTypeSelf,
(Flake8Annotations, "102") => Rule::MissingTypeCls,
(Flake8Annotations, "201") => Rule::MissingReturnTypePublicFunction,
(Flake8Annotations, "202") => Rule::MissingReturnTypePrivateFunction,
(Flake8Annotations, "204") => Rule::MissingReturnTypeSpecialMethod,
(Flake8Annotations, "205") => Rule::MissingReturnTypeStaticMethod,
(Flake8Annotations, "206") => Rule::MissingReturnTypeClassMethod,
(Flake8Annotations, "401") => Rule::AnyType,
// flake8-2020
(Flake82020, "101") => Rule::SysVersionSlice3Referenced,
(Flake82020, "102") => Rule::SysVersion2Referenced,
(Flake82020, "103") => Rule::SysVersionCmpStr3,
(Flake82020, "201") => Rule::SysVersionInfo0Eq3Referenced,
(Flake82020, "202") => Rule::SixPY3Referenced,
(Flake82020, "203") => Rule::SysVersionInfo1CmpInt,
(Flake82020, "204") => Rule::SysVersionInfoMinorCmpInt,
(Flake82020, "301") => Rule::SysVersion0Referenced,
(Flake82020, "302") => Rule::SysVersionCmpStr10,
(Flake82020, "303") => Rule::SysVersionSlice1Referenced,
// flake8-simplify
(Flake8Simplify, "101") => Rule::DuplicateIsinstanceCall,
(Flake8Simplify, "102") => Rule::CollapsibleIf,
(Flake8Simplify, "103") => Rule::NeedlessBool,
(Flake8Simplify, "105") => Rule::UseContextlibSuppress,
(Flake8Simplify, "107") => Rule::ReturnInTryExceptFinally,
(Flake8Simplify, "108") => Rule::UseTernaryOperator,
(Flake8Simplify, "109") => Rule::CompareWithTuple,
(Flake8Simplify, "110") => Rule::ConvertLoopToAny,
(Flake8Simplify, "111") => Rule::ConvertLoopToAll,
(Flake8Simplify, "112") => Rule::UseCapitalEnvironmentVariables,
(Flake8Simplify, "114") => Rule::IfWithSameArms,
(Flake8Simplify, "115") => Rule::OpenFileWithContextHandler,
(Flake8Simplify, "117") => Rule::MultipleWithStatements,
(Flake8Simplify, "118") => Rule::KeyInDict,
(Flake8Simplify, "201") => Rule::NegateEqualOp,
(Flake8Simplify, "202") => Rule::NegateNotEqualOp,
(Flake8Simplify, "208") => Rule::DoubleNegation,
(Flake8Simplify, "210") => Rule::IfExprWithTrueFalse,
(Flake8Simplify, "211") => Rule::IfExprWithFalseTrue,
(Flake8Simplify, "212") => Rule::IfExprWithTwistedArms,
(Flake8Simplify, "220") => Rule::AAndNotA,
(Flake8Simplify, "221") => Rule::AOrNotA,
(Flake8Simplify, "222") => Rule::OrTrue,
(Flake8Simplify, "223") => Rule::AndFalse,
(Flake8Simplify, "300") => Rule::YodaConditions,
(Flake8Simplify, "401") => Rule::DictGetWithDefault,
// pyupgrade
(Pyupgrade, "001") => Rule::UselessMetaclassType,
(Pyupgrade, "003") => Rule::TypeOfPrimitive,
(Pyupgrade, "004") => Rule::UselessObjectInheritance,
(Pyupgrade, "005") => Rule::DeprecatedUnittestAlias,
(Pyupgrade, "006") => Rule::DeprecatedCollectionType,
(Pyupgrade, "007") => Rule::TypingUnion,
(Pyupgrade, "008") => Rule::SuperCallWithParameters,
(Pyupgrade, "009") => Rule::UTF8EncodingDeclaration,
(Pyupgrade, "010") => Rule::UnnecessaryFutureImport,
(Pyupgrade, "011") => Rule::LRUCacheWithoutParameters,
(Pyupgrade, "012") => Rule::UnnecessaryEncodeUTF8,
(Pyupgrade, "013") => Rule::ConvertTypedDictFunctionalToClass,
(Pyupgrade, "014") => Rule::ConvertNamedTupleFunctionalToClass,
(Pyupgrade, "015") => Rule::RedundantOpenModes,
(Pyupgrade, "017") => Rule::DatetimeTimezoneUTC,
(Pyupgrade, "018") => Rule::NativeLiterals,
(Pyupgrade, "019") => Rule::TypingTextStrAlias,
(Pyupgrade, "020") => Rule::OpenAlias,
(Pyupgrade, "021") => Rule::ReplaceUniversalNewlines,
(Pyupgrade, "022") => Rule::ReplaceStdoutStderr,
(Pyupgrade, "023") => Rule::RewriteCElementTree,
(Pyupgrade, "024") => Rule::OSErrorAlias,
(Pyupgrade, "025") => Rule::RewriteUnicodeLiteral,
(Pyupgrade, "026") => Rule::RewriteMockImport,
(Pyupgrade, "027") => Rule::RewriteListComprehension,
(Pyupgrade, "028") => Rule::RewriteYieldFrom,
(Pyupgrade, "029") => Rule::UnnecessaryBuiltinImport,
(Pyupgrade, "030") => Rule::FormatLiterals,
(Pyupgrade, "031") => Rule::PrintfStringFormatting,
(Pyupgrade, "032") => Rule::FString,
(Pyupgrade, "033") => Rule::FunctoolsCache,
(Pyupgrade, "034") => Rule::ExtraneousParentheses,
(Pyupgrade, "035") => Rule::ImportReplacements,
(Pyupgrade, "036") => Rule::OutdatedVersionBlock,
(Pyupgrade, "037") => Rule::QuotedAnnotation,
// pydocstyle
(Pydocstyle, "100") => Rule::PublicModule,
(Pydocstyle, "101") => Rule::PublicClass,
(Pydocstyle, "102") => Rule::PublicMethod,
(Pydocstyle, "103") => Rule::PublicFunction,
(Pydocstyle, "104") => Rule::PublicPackage,
(Pydocstyle, "105") => Rule::MagicMethod,
(Pydocstyle, "106") => Rule::PublicNestedClass,
(Pydocstyle, "107") => Rule::PublicInit,
(Pydocstyle, "200") => Rule::FitsOnOneLine,
(Pydocstyle, "201") => Rule::NoBlankLineBeforeFunction,
(Pydocstyle, "202") => Rule::NoBlankLineAfterFunction,
(Pydocstyle, "203") => Rule::OneBlankLineBeforeClass,
(Pydocstyle, "204") => Rule::OneBlankLineAfterClass,
(Pydocstyle, "205") => Rule::BlankLineAfterSummary,
(Pydocstyle, "206") => Rule::IndentWithSpaces,
(Pydocstyle, "207") => Rule::NoUnderIndentation,
(Pydocstyle, "208") => Rule::NoOverIndentation,
(Pydocstyle, "209") => Rule::NewLineAfterLastParagraph,
(Pydocstyle, "210") => Rule::NoSurroundingWhitespace,
(Pydocstyle, "211") => Rule::NoBlankLineBeforeClass,
(Pydocstyle, "212") => Rule::MultiLineSummaryFirstLine,
(Pydocstyle, "213") => Rule::MultiLineSummarySecondLine,
(Pydocstyle, "214") => Rule::SectionNotOverIndented,
(Pydocstyle, "215") => Rule::SectionUnderlineNotOverIndented,
(Pydocstyle, "300") => Rule::TripleSingleQuotes,
(Pydocstyle, "301") => Rule::EscapeSequenceInDocstring,
(Pydocstyle, "400") => Rule::EndsInPeriod,
(Pydocstyle, "401") => Rule::NonImperativeMood,
(Pydocstyle, "402") => Rule::NoSignature,
(Pydocstyle, "403") => Rule::FirstLineCapitalized,
(Pydocstyle, "404") => Rule::DocstringStartsWithThis,
(Pydocstyle, "405") => Rule::CapitalizeSectionName,
(Pydocstyle, "406") => Rule::NewLineAfterSectionName,
(Pydocstyle, "407") => Rule::DashedUnderlineAfterSection,
(Pydocstyle, "408") => Rule::SectionUnderlineAfterName,
(Pydocstyle, "409") => Rule::SectionUnderlineMatchesSectionLength,
(Pydocstyle, "410") => Rule::BlankLineAfterSection,
(Pydocstyle, "411") => Rule::BlankLineBeforeSection,
(Pydocstyle, "412") => Rule::NoBlankLinesBetweenHeaderAndContent,
(Pydocstyle, "413") => Rule::BlankLineAfterLastSection,
(Pydocstyle, "414") => Rule::EmptyDocstringSection,
(Pydocstyle, "415") => Rule::EndsInPunctuation,
(Pydocstyle, "416") => Rule::SectionNameEndsInColon,
(Pydocstyle, "417") => Rule::UndocumentedParam,
(Pydocstyle, "418") => Rule::OverloadWithDocstring,
(Pydocstyle, "419") => Rule::EmptyDocstring,
// pep8-naming
(PEP8Naming, "801") => Rule::InvalidClassName,
(PEP8Naming, "802") => Rule::InvalidFunctionName,
(PEP8Naming, "803") => Rule::InvalidArgumentName,
(PEP8Naming, "804") => Rule::InvalidFirstArgumentNameForClassMethod,
(PEP8Naming, "805") => Rule::InvalidFirstArgumentNameForMethod,
(PEP8Naming, "806") => Rule::NonLowercaseVariableInFunction,
(PEP8Naming, "807") => Rule::DunderFunctionName,
(PEP8Naming, "811") => Rule::ConstantImportedAsNonConstant,
(PEP8Naming, "812") => Rule::LowercaseImportedAsNonLowercase,
(PEP8Naming, "813") => Rule::CamelcaseImportedAsLowercase,
(PEP8Naming, "814") => Rule::CamelcaseImportedAsConstant,
(PEP8Naming, "815") => Rule::MixedCaseVariableInClassScope,
(PEP8Naming, "816") => Rule::MixedCaseVariableInGlobalScope,
(PEP8Naming, "817") => Rule::CamelcaseImportedAsAcronym,
(PEP8Naming, "818") => Rule::ErrorSuffixOnExceptionName,
// isort
(Isort, "001") => Rule::UnsortedImports,
(Isort, "002") => Rule::MissingRequiredImport,
// eradicate
(Eradicate, "001") => Rule::CommentedOutCode,
// flake8-bandit
(Flake8Bandit, "101") => Rule::Assert,
(Flake8Bandit, "102") => Rule::ExecBuiltin,
(Flake8Bandit, "103") => Rule::BadFilePermissions,
(Flake8Bandit, "104") => Rule::HardcodedBindAllInterfaces,
(Flake8Bandit, "105") => Rule::HardcodedPasswordString,
(Flake8Bandit, "106") => Rule::HardcodedPasswordFuncArg,
(Flake8Bandit, "107") => Rule::HardcodedPasswordDefault,
(Flake8Bandit, "608") => Rule::HardcodedSQLExpression,
(Flake8Bandit, "108") => Rule::HardcodedTempFile,
(Flake8Bandit, "110") => Rule::TryExceptPass,
(Flake8Bandit, "112") => Rule::TryExceptContinue,
(Flake8Bandit, "113") => Rule::RequestWithoutTimeout,
(Flake8Bandit, "324") => Rule::HashlibInsecureHashFunction,
(Flake8Bandit, "501") => Rule::RequestWithNoCertValidation,
(Flake8Bandit, "506") => Rule::UnsafeYAMLLoad,
(Flake8Bandit, "508") => Rule::SnmpInsecureVersion,
(Flake8Bandit, "509") => Rule::SnmpWeakCryptography,
(Flake8Bandit, "612") => Rule::LoggingConfigInsecureListen,
(Flake8Bandit, "701") => Rule::Jinja2AutoescapeFalse,
// flake8-boolean-trap
(Flake8BooleanTrap, "001") => Rule::BooleanPositionalArgInFunctionDefinition,
(Flake8BooleanTrap, "002") => Rule::BooleanDefaultValueInFunctionDefinition,
(Flake8BooleanTrap, "003") => Rule::BooleanPositionalValueInFunctionCall,
// flake8-unused-arguments
(Flake8UnusedArguments, "001") => Rule::UnusedFunctionArgument,
(Flake8UnusedArguments, "002") => Rule::UnusedMethodArgument,
(Flake8UnusedArguments, "003") => Rule::UnusedClassMethodArgument,
(Flake8UnusedArguments, "004") => Rule::UnusedStaticMethodArgument,
(Flake8UnusedArguments, "005") => Rule::UnusedLambdaArgument,
// flake8-import-conventions
(Flake8ImportConventions, "001") => Rule::UnconventionalImportAlias,
// flake8-datetimez
(Flake8Datetimez, "001") => Rule::CallDatetimeWithoutTzinfo,
(Flake8Datetimez, "002") => Rule::CallDatetimeToday,
(Flake8Datetimez, "003") => Rule::CallDatetimeUtcnow,
(Flake8Datetimez, "004") => Rule::CallDatetimeUtcfromtimestamp,
(Flake8Datetimez, "005") => Rule::CallDatetimeNowWithoutTzinfo,
(Flake8Datetimez, "006") => Rule::CallDatetimeFromtimestamp,
(Flake8Datetimez, "007") => Rule::CallDatetimeStrptimeWithoutZone,
(Flake8Datetimez, "011") => Rule::CallDateToday,
(Flake8Datetimez, "012") => Rule::CallDateFromtimestamp,
// pygrep-hooks
(PygrepHooks, "001") => Rule::NoEval,
(PygrepHooks, "002") => Rule::DeprecatedLogWarn,
(PygrepHooks, "003") => Rule::BlanketTypeIgnore,
(PygrepHooks, "004") => Rule::BlanketNOQA,
// pandas-vet
(PandasVet, "002") => Rule::UseOfInplaceArgument,
(PandasVet, "003") => Rule::UseOfDotIsNull,
(PandasVet, "004") => Rule::UseOfDotNotNull,
(PandasVet, "007") => Rule::UseOfDotIx,
(PandasVet, "008") => Rule::UseOfDotAt,
(PandasVet, "009") => Rule::UseOfDotIat,
(PandasVet, "010") => Rule::UseOfDotPivotOrUnstack,
(PandasVet, "011") => Rule::UseOfDotValues,
(PandasVet, "012") => Rule::UseOfDotReadTable,
(PandasVet, "013") => Rule::UseOfDotStack,
(PandasVet, "015") => Rule::UseOfPdMerge,
(PandasVet, "901") => Rule::DfIsABadVariableName,
// flake8-errmsg
(Flake8ErrMsg, "101") => Rule::RawStringInException,
(Flake8ErrMsg, "102") => Rule::FStringInException,
(Flake8ErrMsg, "103") => Rule::DotFormatInException,
// flake8-pyi
(Flake8Pyi, "001") => Rule::PrefixTypeParams,
(Flake8Pyi, "007") => Rule::UnrecognizedPlatformCheck,
(Flake8Pyi, "008") => Rule::UnrecognizedPlatformName,
// flake8-pytest-style
(Flake8PytestStyle, "001") => Rule::IncorrectFixtureParenthesesStyle,
(Flake8PytestStyle, "002") => Rule::FixturePositionalArgs,
(Flake8PytestStyle, "003") => Rule::ExtraneousScopeFunction,
(Flake8PytestStyle, "004") => Rule::MissingFixtureNameUnderscore,
(Flake8PytestStyle, "005") => Rule::IncorrectFixtureNameUnderscore,
(Flake8PytestStyle, "006") => Rule::ParametrizeNamesWrongType,
(Flake8PytestStyle, "007") => Rule::ParametrizeValuesWrongType,
(Flake8PytestStyle, "008") => Rule::PatchWithLambda,
(Flake8PytestStyle, "009") => Rule::UnittestAssertion,
(Flake8PytestStyle, "010") => Rule::RaisesWithoutException,
(Flake8PytestStyle, "011") => Rule::RaisesTooBroad,
(Flake8PytestStyle, "012") => Rule::RaisesWithMultipleStatements,
(Flake8PytestStyle, "013") => Rule::IncorrectPytestImport,
(Flake8PytestStyle, "015") => Rule::AssertAlwaysFalse,
(Flake8PytestStyle, "016") => Rule::FailWithoutMessage,
(Flake8PytestStyle, "017") => Rule::AssertInExcept,
(Flake8PytestStyle, "018") => Rule::CompositeAssertion,
(Flake8PytestStyle, "019") => Rule::FixtureParamWithoutValue,
(Flake8PytestStyle, "020") => Rule::DeprecatedYieldFixture,
(Flake8PytestStyle, "021") => Rule::FixtureFinalizerCallback,
(Flake8PytestStyle, "022") => Rule::UselessYieldFixture,
(Flake8PytestStyle, "023") => Rule::IncorrectMarkParenthesesStyle,
(Flake8PytestStyle, "024") => Rule::UnnecessaryAsyncioMarkOnFixture,
(Flake8PytestStyle, "025") => Rule::ErroneousUseFixturesOnFixture,
(Flake8PytestStyle, "026") => Rule::UseFixturesWithoutParameters,
// flake8-pie
(Flake8Pie, "790") => Rule::UnnecessaryPass,
(Flake8Pie, "794") => Rule::DupeClassFieldDefinitions,
(Flake8Pie, "796") => Rule::PreferUniqueEnums,
(Flake8Pie, "800") => Rule::UnnecessarySpread,
(Flake8Pie, "804") => Rule::UnnecessaryDictKwargs,
(Flake8Pie, "807") => Rule::PreferListBuiltin,
(Flake8Pie, "810") => Rule::SingleStartsEndsWith,
// flake8-commas
(Flake8Commas, "812") => Rule::TrailingCommaMissing,
(Flake8Commas, "818") => Rule::TrailingCommaOnBareTupleProhibited,
(Flake8Commas, "819") => Rule::TrailingCommaProhibited,
// flake8-no-pep420
(Flake8NoPep420, "001") => Rule::ImplicitNamespacePackage,
// flake8-executable
(Flake8Executable, "001") => Rule::ShebangNotExecutable,
(Flake8Executable, "002") => Rule::ShebangMissingExecutableFile,
(Flake8Executable, "003") => Rule::ShebangPython,
(Flake8Executable, "004") => Rule::ShebangWhitespace,
(Flake8Executable, "005") => Rule::ShebangNewline,
// flake8-type-checking
(Flake8TypeChecking, "001") => Rule::TypingOnlyFirstPartyImport,
(Flake8TypeChecking, "002") => Rule::TypingOnlyThirdPartyImport,
(Flake8TypeChecking, "003") => Rule::TypingOnlyStandardLibraryImport,
(Flake8TypeChecking, "004") => Rule::RuntimeImportInTypeCheckingBlock,
(Flake8TypeChecking, "005") => Rule::EmptyTypeCheckingBlock,
// tryceratops
(Tryceratops, "002") => Rule::RaiseVanillaClass,
(Tryceratops, "003") => Rule::RaiseVanillaArgs,
(Tryceratops, "004") => Rule::PreferTypeError,
(Tryceratops, "200") => Rule::ReraiseNoCause,
(Tryceratops, "201") => Rule::VerboseRaise,
(Tryceratops, "300") => Rule::TryConsiderElse,
(Tryceratops, "301") => Rule::RaiseWithinTry,
(Tryceratops, "400") => Rule::ErrorInsteadOfException,
// flake8-use-pathlib
(Flake8UsePathlib, "100") => Rule::PathlibAbspath,
(Flake8UsePathlib, "101") => Rule::PathlibChmod,
(Flake8UsePathlib, "102") => Rule::PathlibMkdir,
(Flake8UsePathlib, "103") => Rule::PathlibMakedirs,
(Flake8UsePathlib, "104") => Rule::PathlibRename,
(Flake8UsePathlib, "105") => Rule::PathlibReplace,
(Flake8UsePathlib, "106") => Rule::PathlibRmdir,
(Flake8UsePathlib, "107") => Rule::PathlibRemove,
(Flake8UsePathlib, "108") => Rule::PathlibUnlink,
(Flake8UsePathlib, "109") => Rule::PathlibGetcwd,
(Flake8UsePathlib, "110") => Rule::PathlibExists,
(Flake8UsePathlib, "111") => Rule::PathlibExpanduser,
(Flake8UsePathlib, "112") => Rule::PathlibIsDir,
(Flake8UsePathlib, "113") => Rule::PathlibIsFile,
(Flake8UsePathlib, "114") => Rule::PathlibIsLink,
(Flake8UsePathlib, "115") => Rule::PathlibReadlink,
(Flake8UsePathlib, "116") => Rule::PathlibStat,
(Flake8UsePathlib, "117") => Rule::PathlibIsAbs,
(Flake8UsePathlib, "118") => Rule::PathlibJoin,
(Flake8UsePathlib, "119") => Rule::PathlibBasename,
(Flake8UsePathlib, "120") => Rule::PathlibDirname,
(Flake8UsePathlib, "121") => Rule::PathlibSamefile,
(Flake8UsePathlib, "122") => Rule::PathlibSplitext,
(Flake8UsePathlib, "123") => Rule::PathlibOpen,
(Flake8UsePathlib, "124") => Rule::PathlibPyPath,
// flake8-logging-format
(Flake8LoggingFormat, "001") => Rule::LoggingStringFormat,
(Flake8LoggingFormat, "002") => Rule::LoggingPercentFormat,
(Flake8LoggingFormat, "003") => Rule::LoggingStringConcat,
(Flake8LoggingFormat, "004") => Rule::LoggingFString,
(Flake8LoggingFormat, "010") => Rule::LoggingWarn,
(Flake8LoggingFormat, "101") => Rule::LoggingExtraAttrClash,
(Flake8LoggingFormat, "201") => Rule::LoggingExcInfo,
(Flake8LoggingFormat, "202") => Rule::LoggingRedundantExcInfo,
// flake8-raise
(Flake8Raise, "102") => Rule::UnnecessaryParenOnRaiseException,
// flake8-self
(Flake8Self, "001") => Rule::PrivateMemberAccess,
// numpy
(Numpy, "001") => Rule::NumpyDeprecatedTypeAlias,
// ruff
(Ruff, "001") => Rule::AmbiguousUnicodeCharacterString,
(Ruff, "002") => Rule::AmbiguousUnicodeCharacterDocstring,
(Ruff, "003") => Rule::AmbiguousUnicodeCharacterComment,
(Ruff, "004") => Rule::KeywordArgumentBeforeStarArgument,
(Ruff, "005") => Rule::UnpackInsteadOfConcatenatingToCollectionLiteral,
(Ruff, "006") => Rule::AsyncioDanglingTask,
(Ruff, "100") => Rule::UnusedNOQA,
// flake8-django
(Flake8Django, "001") => Rule::NullableModelStringField,
(Flake8Django, "008") => Rule::ModelWithoutDunderStr,
(Flake8Django, "013") => Rule::NonLeadingReceiverDecorator,
_ => return None,
})
}

View File

@@ -59,6 +59,7 @@ pub fn extract_directives(lxr: &[LexResult], flags: Flags) -> Directives {
/// Extract a mapping from logical line to noqa line.
pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
let mut noqa_line_for: IntMap<usize, usize> = IntMap::default();
let mut prev_non_newline: Option<(&Location, &Tok, &Location)> = None;
for (start, tok, end) in lxr.iter().flatten() {
if matches!(tok, Tok::EndOfFile) {
break;
@@ -70,6 +71,21 @@ pub fn extract_noqa_line_for(lxr: &[LexResult]) -> IntMap<usize, usize> {
noqa_line_for.insert(i, end.row());
}
}
// For continuations, we expect `noqa` directives on the last line of the
// continuation.
if matches!(
tok,
Tok::Newline | Tok::NonLogicalNewline | Tok::Comment(..)
) {
if let Some((.., end)) = prev_non_newline {
for i in end.row()..start.row() {
noqa_line_for.insert(i, start.row());
}
}
prev_non_newline = None;
} else if prev_non_newline.is_none() {
prev_non_newline = Some((start, tok, end));
}
}
noqa_line_for
}
@@ -193,11 +209,11 @@ z = x + 1",
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = '''abc
def
ghi
'''
z = 2",
y = '''abc
def
ghi
'''
z = 2",
)
.collect();
assert_eq!(
@@ -207,16 +223,51 @@ z = x + 1",
let lxr: Vec<LexResult> = lexer::make_tokenizer(
"x = 1
y = '''abc
def
ghi
'''",
y = '''abc
def
ghi
'''",
)
.collect();
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(2, 5), (3, 5), (4, 5)])
);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
r#"x = \
1"#,
)
.collect();
assert_eq!(extract_noqa_line_for(&lxr), IntMap::from_iter([(1, 2)]));
let lxr: Vec<LexResult> = lexer::make_tokenizer(
r#"from foo import \
bar as baz, \
qux as quux"#,
)
.collect();
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(1, 3), (2, 3)])
);
let lxr: Vec<LexResult> = lexer::make_tokenizer(
r#"
# Foo
from foo import \
bar as baz, \
qux as quux # Baz
x = \
1
y = \
2"#,
)
.collect();
assert_eq!(
extract_noqa_line_for(&lxr),
IntMap::from_iter([(3, 5), (4, 5), (6, 7), (8, 9)])
);
}
#[test]

View File

@@ -6,8 +6,8 @@ use itertools::Itertools;
use super::external_config::ExternalConfig;
use super::plugin::Plugin;
use super::{parser, plugin};
use crate::registry::RuleCodePrefix;
use crate::rule_selector::{prefix_to_selector, RuleSelector};
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::flake8_pytest_style::types::{
ParametrizeNameType, ParametrizeValuesRowType, ParametrizeValuesType,
};
@@ -23,9 +23,8 @@ use crate::settings::pyproject::Pyproject;
use crate::warn_user;
const DEFAULT_SELECTORS: &[RuleSelector] = &[
prefix_to_selector(RuleCodePrefix::F),
prefix_to_selector(RuleCodePrefix::E),
prefix_to_selector(RuleCodePrefix::W),
RuleSelector::Linter(Linter::Pyflakes),
RuleSelector::Linter(Linter::Pycodestyle),
];
pub fn convert(
@@ -359,13 +358,13 @@ pub fn convert(
options.select = Some(
select
.into_iter()
.sorted_by_key(RuleSelector::short_code)
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
options.ignore = Some(
ignore
.into_iter()
.sorted_by_key(RuleSelector::short_code)
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
);
if flake8_annotations != flake8_annotations::settings::Options::default() {
@@ -433,7 +432,7 @@ pub fn convert(
/// plugins.
fn resolve_select(plugins: &[Plugin]) -> HashSet<RuleSelector> {
let mut select: HashSet<_> = DEFAULT_SELECTORS.iter().cloned().collect();
select.extend(plugins.iter().map(Plugin::selector));
select.extend(plugins.iter().map(|p| Linter::from(p).into()));
select
}
@@ -448,7 +447,7 @@ mod tests {
use super::convert;
use crate::flake8_to_ruff::converter::DEFAULT_SELECTORS;
use crate::flake8_to_ruff::ExternalConfig;
use crate::registry::RuleCodePrefix;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::rules::pydocstyle::settings::Convention;
use crate::rules::{flake8_quotes, pydocstyle};
@@ -463,7 +462,7 @@ mod tests {
.iter()
.cloned()
.chain(plugins)
.sorted_by_key(RuleSelector::short_code)
.sorted_by_key(RuleSelector::prefix_and_code)
.collect(),
),
..Options::default()
@@ -578,7 +577,7 @@ mod tests {
pydocstyle: Some(pydocstyle::settings::Options {
convention: Some(Convention::Numpy),
}),
..default_options([RuleCodePrefix::D.into()])
..default_options([Linter::Pydocstyle.into()])
});
assert_eq!(actual, expected);
@@ -602,7 +601,7 @@ mod tests {
docstring_quotes: None,
avoid_escape: None,
}),
..default_options([RuleCodePrefix::Q.into()])
..default_options([Linter::Flake8Quotes.into()])
});
assert_eq!(actual, expected);

View File

@@ -196,7 +196,8 @@ mod tests {
use anyhow::Result;
use super::{parse_files_to_codes_mapping, parse_prefix_codes, parse_strings};
use crate::registry::RuleCodePrefix;
use crate::codes;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
use crate::settings::types::PatternPrefixPair;
@@ -211,19 +212,25 @@ mod tests {
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401");
let expected = vec![RuleCodePrefix::F401.into()];
let expected = vec![codes::Pyflakes::_401.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,");
let expected = vec![RuleCodePrefix::F401.into()];
let expected = vec![codes::Pyflakes::_401.into()];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401,E501");
let expected = vec![RuleCodePrefix::F401.into(), RuleCodePrefix::E501.into()];
let expected = vec![
codes::Pyflakes::_401.into(),
codes::Pycodestyle::E501.into(),
];
assert_eq!(actual, expected);
let actual = parse_prefix_codes("F401, E501");
let expected = vec![RuleCodePrefix::F401.into(), RuleCodePrefix::E501.into()];
let expected = vec![
codes::Pyflakes::_401.into(),
codes::Pycodestyle::E501.into(),
];
assert_eq!(actual, expected);
}
@@ -276,11 +283,11 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "locust/test/*".to_string(),
prefix: RuleCodePrefix::F841.into(),
prefix: codes::Pyflakes::_841.into(),
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: RuleCodePrefix::F841.into(),
prefix: codes::Pyflakes::_841.into(),
},
];
assert_eq!(actual, expected);
@@ -296,23 +303,23 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "t/*".to_string(),
prefix: RuleCodePrefix::D.into(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "setup.py".to_string(),
prefix: RuleCodePrefix::D.into(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "examples/*".to_string(),
prefix: RuleCodePrefix::D.into(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "docs/*".to_string(),
prefix: RuleCodePrefix::D.into(),
prefix: Linter::Pydocstyle.into(),
},
PatternPrefixPair {
pattern: "extra/*".to_string(),
prefix: RuleCodePrefix::D.into(),
prefix: Linter::Pydocstyle.into(),
},
];
assert_eq!(actual, expected);
@@ -334,47 +341,47 @@ mod tests {
let expected: Vec<PatternPrefixPair> = vec![
PatternPrefixPair {
pattern: "scrapy/__init__.py".to_string(),
prefix: RuleCodePrefix::E402.into(),
prefix: codes::Pycodestyle::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/core/downloader/handlers/http.py".to_string(),
prefix: RuleCodePrefix::F401.into(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/http/__init__.py".to_string(),
prefix: RuleCodePrefix::F401.into(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: RuleCodePrefix::E402.into(),
prefix: codes::Pycodestyle::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/linkextractors/__init__.py".to_string(),
prefix: RuleCodePrefix::F401.into(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/selector/__init__.py".to_string(),
prefix: RuleCodePrefix::F401.into(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: RuleCodePrefix::E402.into(),
prefix: codes::Pycodestyle::E402.into(),
},
PatternPrefixPair {
pattern: "scrapy/spiders/__init__.py".to_string(),
prefix: RuleCodePrefix::F401.into(),
prefix: codes::Pyflakes::_401.into(),
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: RuleCodePrefix::F403.into(),
prefix: codes::Pyflakes::_403.into(),
},
PatternPrefixPair {
pattern: "scrapy/utils/url.py".to_string(),
prefix: RuleCodePrefix::F405.into(),
prefix: codes::Pyflakes::_405.into(),
},
PatternPrefixPair {
pattern: "tests/test_loader.py".to_string(),
prefix: RuleCodePrefix::E741.into(),
prefix: codes::Pycodestyle::E741.into(),
},
];
assert_eq!(actual, expected);

View File

@@ -4,7 +4,7 @@ use std::str::FromStr;
use anyhow::anyhow;
use crate::registry::RuleCodePrefix;
use crate::registry::Linter;
use crate::rule_selector::RuleSelector;
#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
@@ -131,43 +131,42 @@ impl fmt::Debug for Plugin {
}
}
// TODO(martin): Convert into `impl From<Plugin> for Linter`
impl Plugin {
pub fn selector(&self) -> RuleSelector {
match self {
Plugin::Flake82020 => RuleCodePrefix::YTT.into(),
Plugin::Flake8Annotations => RuleCodePrefix::ANN.into(),
Plugin::Flake8Bandit => RuleCodePrefix::S.into(),
Plugin::Flake8BlindExcept => RuleCodePrefix::BLE.into(),
Plugin::Flake8BooleanTrap => RuleCodePrefix::FBT.into(),
Plugin::Flake8Bugbear => RuleCodePrefix::B.into(),
Plugin::Flake8Builtins => RuleCodePrefix::A.into(),
Plugin::Flake8Commas => RuleCodePrefix::COM.into(),
Plugin::Flake8Comprehensions => RuleCodePrefix::C4.into(),
Plugin::Flake8Datetimez => RuleCodePrefix::DTZ.into(),
Plugin::Flake8Debugger => RuleCodePrefix::T1.into(),
Plugin::Flake8Docstrings => RuleCodePrefix::D.into(),
Plugin::Flake8Eradicate => RuleCodePrefix::ERA.into(),
Plugin::Flake8ErrMsg => RuleCodePrefix::EM.into(),
Plugin::Flake8Executable => RuleCodePrefix::EXE.into(),
Plugin::Flake8ImplicitStrConcat => RuleCodePrefix::ISC.into(),
Plugin::Flake8ImportConventions => RuleCodePrefix::ICN.into(),
Plugin::Flake8NoPep420 => RuleCodePrefix::INP.into(),
Plugin::Flake8Pie => RuleCodePrefix::PIE.into(),
Plugin::Flake8Print => RuleCodePrefix::T2.into(),
Plugin::Flake8PytestStyle => RuleCodePrefix::PT.into(),
Plugin::Flake8Quotes => RuleCodePrefix::Q.into(),
Plugin::Flake8Return => RuleCodePrefix::RET.into(),
Plugin::Flake8Simplify => RuleCodePrefix::SIM.into(),
Plugin::Flake8TidyImports => RuleCodePrefix::TID.into(),
Plugin::Flake8TypeChecking => RuleCodePrefix::TCH.into(),
Plugin::Flake8UnusedArguments => RuleCodePrefix::ARG.into(),
Plugin::Flake8UsePathlib => RuleCodePrefix::PTH.into(),
Plugin::McCabe => RuleCodePrefix::C9.into(),
Plugin::PEP8Naming => RuleCodePrefix::N.into(),
Plugin::PandasVet => RuleCodePrefix::PD.into(),
Plugin::Pyupgrade => RuleCodePrefix::UP.into(),
Plugin::Tryceratops => RuleCodePrefix::TRY.into(),
impl From<&Plugin> for Linter {
fn from(plugin: &Plugin) -> Self {
match plugin {
Plugin::Flake82020 => Linter::Flake82020,
Plugin::Flake8Annotations => Linter::Flake8Annotations,
Plugin::Flake8Bandit => Linter::Flake8Bandit,
Plugin::Flake8BlindExcept => Linter::Flake8BlindExcept,
Plugin::Flake8BooleanTrap => Linter::Flake8BooleanTrap,
Plugin::Flake8Bugbear => Linter::Flake8Bugbear,
Plugin::Flake8Builtins => Linter::Flake8Builtins,
Plugin::Flake8Commas => Linter::Flake8Commas,
Plugin::Flake8Comprehensions => Linter::Flake8Comprehensions,
Plugin::Flake8Datetimez => Linter::Flake8Datetimez,
Plugin::Flake8Debugger => Linter::Flake8Debugger,
Plugin::Flake8Docstrings => Linter::Pydocstyle,
Plugin::Flake8Eradicate => Linter::Eradicate,
Plugin::Flake8ErrMsg => Linter::Flake8ErrMsg,
Plugin::Flake8Executable => Linter::Flake8Executable,
Plugin::Flake8ImplicitStrConcat => Linter::Flake8ImplicitStrConcat,
Plugin::Flake8ImportConventions => Linter::Flake8ImportConventions,
Plugin::Flake8NoPep420 => Linter::Flake8NoPep420,
Plugin::Flake8Pie => Linter::Flake8Pie,
Plugin::Flake8Print => Linter::Flake8Print,
Plugin::Flake8PytestStyle => Linter::Flake8PytestStyle,
Plugin::Flake8Quotes => Linter::Flake8Quotes,
Plugin::Flake8Return => Linter::Flake8Return,
Plugin::Flake8Simplify => Linter::Flake8Simplify,
Plugin::Flake8TidyImports => Linter::Flake8TidyImports,
Plugin::Flake8TypeChecking => Linter::Flake8TypeChecking,
Plugin::Flake8UnusedArguments => Linter::Flake8UnusedArguments,
Plugin::Flake8UsePathlib => Linter::Flake8UsePathlib,
Plugin::McCabe => Linter::McCabe,
Plugin::PEP8Naming => Linter::PEP8Naming,
Plugin::PandasVet => Linter::PandasVet,
Plugin::Pyupgrade => Linter::Pyupgrade,
Plugin::Tryceratops => Linter::Tryceratops,
}
}
}
@@ -334,7 +333,7 @@ pub fn infer_plugins_from_codes(selectors: &HashSet<RuleSelector>) -> Vec<Plugin
for selector in selectors {
if selector
.into_iter()
.any(|rule| plugin.selector().into_iter().any(|r| r == rule))
.any(|rule| Linter::from(plugin).into_iter().any(|r| r == rule))
{
return true;
}

View File

@@ -6,9 +6,10 @@
use rustpython_parser::lexer::Tok;
#[derive(Debug)]
#[derive(Default)]
enum State {
// Start of the module: first string gets marked as a docstring.
#[default]
ExpectModuleDocstring,
// After seeing a class definition, we're waiting for the block colon (and do bracket
// counting).
@@ -23,25 +24,13 @@ enum State {
Other,
}
#[derive(Default)]
pub struct StateMachine {
state: State,
bracket_count: usize,
}
impl Default for StateMachine {
fn default() -> Self {
Self::new()
}
}
impl StateMachine {
pub const fn new() -> Self {
Self {
state: State::ExpectModuleDocstring,
bracket_count: 0,
}
}
pub fn consume(&mut self, tok: &Tok) -> bool {
if matches!(
tok,

View File

@@ -5,11 +5,17 @@
//!
//! [Ruff]: https://github.com/charliermarsh/ruff
use cfg_if::cfg_if;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub use violation::{AutofixKind, Availability as AutofixAvailability};
mod assert_yaml_snapshot;
mod ast;
mod autofix;
pub mod cache;
mod checkers;
mod codes;
mod cst;
mod directives;
mod doc_lines;
@@ -34,20 +40,12 @@ mod vendor;
mod violation;
mod visibility;
use cfg_if::cfg_if;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
pub use violation::{AutofixKind, Availability as AutofixAvailability};
cfg_if! {
if #[cfg(not(target_family = "wasm"))] {
pub mod packaging;
mod lib_native;
pub use lib_native::check;
} else {
if #[cfg(target_family = "wasm")] {
mod lib_wasm;
pub use lib_wasm::check;
} else {
pub mod packaging;
}
}

View File

@@ -1,67 +0,0 @@
use std::path::Path;
use anyhow::Result;
use path_absolutize::path_dedot;
use rustpython_parser::lexer::LexResult;
use crate::linter::check_path;
use crate::registry::Diagnostic;
use crate::resolver::Relativity;
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
use crate::settings::{flags, pyproject, Settings};
use crate::source_code::{Indexer, Locator, Stylist};
use crate::{directives, packaging, resolver};
/// Load the relevant `Settings` for a given `Path`.
fn resolve(path: &Path) -> Result<Settings> {
if let Some(pyproject) = pyproject::find_settings_toml(path)? {
// First priority: `pyproject.toml` in the current `Path`.
Ok(resolver::resolve_settings(&pyproject, &Relativity::Parent)?.lib)
} else if let Some(pyproject) = pyproject::find_user_settings_toml() {
// Second priority: user-specific `pyproject.toml`.
Ok(resolver::resolve_settings(&pyproject, &Relativity::Cwd)?.lib)
} else {
// Fallback: default settings.
Settings::from_configuration(Configuration::default(), &path_dedot::CWD)
}
}
/// Run Ruff over Python source code directly.
pub fn check(path: &Path, contents: &str, autofix: bool) -> Result<Vec<Diagnostic>> {
// Load the relevant `Settings` for the given `Path`.
let settings = resolve(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = tokenize(contents);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
// Detect the current code style (lazily).
let stylist = Stylist::from_contents(contents, &locator);
// Extra indices from the code.
let indexer: Indexer = tokens.as_slice().into();
// Extract the `# noqa` and `# isort: skip` directives from the source.
let directives =
directives::extract_directives(&tokens, directives::Flags::from_settings(&settings));
// Generate diagnostics.
let result = check_path(
path,
packaging::detect_package_root(path, &settings.namespace_packages),
contents,
tokens,
&locator,
&stylist,
&indexer,
&directives,
&settings,
autofix.into(),
flags::Noqa::Enabled,
);
Ok(result.data)
}

View File

@@ -66,7 +66,7 @@ impl Serialize for SerializeRuleAsCode {
where
S: serde::Serializer,
{
serializer.serialize_str(self.0.code())
serializer.serialize_str(&self.0.noqa_code().to_string())
}
}
@@ -127,6 +127,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
show_fixes: None,
show_source: None,
src: None,
task_tags: None,

View File

@@ -4,6 +4,7 @@ use std::path::Path;
use anyhow::{anyhow, Result};
use colored::Colorize;
use log::error;
use rustc_hash::FxHashMap;
use rustpython_parser::error::ParseError;
use rustpython_parser::lexer::LexResult;
@@ -18,7 +19,7 @@ use crate::checkers::tokens::check_tokens;
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::noqa::{add_noqa, rule_is_ignored};
use crate::registry::{Diagnostic, LintSource, Rule};
use crate::rules::pycodestyle;
use crate::settings::{flags, Settings};
@@ -46,6 +47,8 @@ impl<T> LinterResult<T> {
}
}
pub type FixTable = FxHashMap<&'static Rule, usize>;
/// Generate `Diagnostic`s from the source code contents at the
/// given `Path`.
#[allow(clippy::too_many_arguments)]
@@ -149,7 +152,17 @@ pub fn check_path(
if settings.rules.enabled(&Rule::SyntaxError) {
pycodestyle::rules::syntax_error(&mut diagnostics, &parse_error);
}
error = Some(parse_error);
// If the syntax error is ignored, suppress it (regardless of whether
// `Rule::SyntaxError` is enabled).
if !rule_is_ignored(
&Rule::SyntaxError,
parse_error.location.row(),
&directives.noqa_line_for,
locator,
) {
error = Some(parse_error);
}
}
}
}
@@ -198,7 +211,7 @@ pub fn check_path(
indexer.commented_lines(),
&directives.noqa_line_for,
settings,
autofix,
error.as_ref().map_or(autofix, |_| flags::Autofix::Disabled),
);
}
@@ -207,8 +220,8 @@ pub fn check_path(
const MAX_ITERATIONS: usize = 100;
/// Add any missing `#noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
/// Add any missing `# noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> {
// Read the file from disk.
let contents = fs::read_file(path)?;
@@ -234,7 +247,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
error,
} = check_path(
path,
None,
package,
&contents,
tokens,
&locator,
@@ -331,11 +344,11 @@ pub fn lint_fix<'a>(
path: &Path,
package: Option<&Path>,
settings: &Settings,
) -> Result<(LinterResult<Vec<Message>>, Cow<'a, str>, usize)> {
) -> Result<(LinterResult<Vec<Message>>, Cow<'a, str>, FixTable)> {
let mut transformed = Cow::Borrowed(contents);
// Track the number of fixed errors across iterations.
let mut fixed = 0;
let mut fixed = FxHashMap::default();
// As an escape hatch, bail after 100 iterations.
let mut iterations = 0;
@@ -409,7 +422,9 @@ This indicates a bug in `{}`. If you could open an issue at:
if let Some((fixed_contents, applied)) = fix_file(&result.data, &locator) {
if iterations < MAX_ITERATIONS {
// Count the number of fixed errors.
fixed += applied;
for (rule, count) in applied {
*fixed.entry(rule).or_default() += count;
}
// Store the fixed contents.
transformed = Cow::Owned(fixed_contents);

View File

@@ -71,9 +71,9 @@ impl Source {
} else {
Location::new(diagnostic.end_location.row() + 1, 0)
};
let source = locator.slice_source_code_range(&Range::new(location, end_location));
let source = locator.slice(&Range::new(location, end_location));
let num_chars_in_range = locator
.slice_source_code_range(&Range::new(diagnostic.location, diagnostic.end_location))
.slice(&Range::new(diagnostic.location, diagnostic.end_location))
.chars()
.count();
Source {

View File

@@ -1,3 +1,4 @@
use std::fmt::{Display, Write};
use std::fs;
use std::path::Path;
@@ -7,10 +8,12 @@ use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::registry::{Diagnostic, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::source_code::LineEnding;
use crate::source_code::{LineEnding, Locator};
static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -70,12 +73,31 @@ 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: &Rule, haystack: &[&str]) -> bool {
let needle: &str = needle.code();
let needle = needle.noqa_code();
haystack
.iter()
.any(|candidate| needle == get_redirect_target(candidate).unwrap_or(candidate))
}
/// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`.
pub fn rule_is_ignored(
code: &Rule,
lineno: usize,
noqa_line_for: &IntMap<usize, usize>,
locator: &Locator,
) -> bool {
let noqa_lineno = noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = locator.slice(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => includes(code, &codes),
}
}
pub fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],
@@ -184,9 +206,7 @@ fn add_noqa_inner(
output.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> = rules.iter().map(|r| r.code()).collect();
let suffix = codes.join(", ");
output.push_str(&suffix);
push_codes(&mut output, rules.iter().map(|r| r.noqa_code()));
output.push_str(line_ending);
count += 1;
}
@@ -207,14 +227,14 @@ fn add_noqa_inner(
formatted.push_str(" # noqa: ");
// Add codes.
let codes: Vec<&str> = rules
.iter()
.map(|r| r.code())
.chain(existing.into_iter())
.sorted_unstable()
.collect();
let suffix = codes.join(", ");
formatted.push_str(&suffix);
push_codes(
&mut formatted,
rules
.iter()
.map(|r| r.noqa_code().to_string())
.chain(existing.into_iter().map(ToString::to_string))
.sorted_unstable(),
);
output.push_str(&formatted);
output.push_str(line_ending);
@@ -232,6 +252,17 @@ fn add_noqa_inner(
(count, output)
}
fn push_codes<I: Display>(str: &mut String, codes: impl Iterator<Item = I>) {
let mut first = true;
for code in codes {
if !first {
str.push_str(", ");
}
let _ = write!(str, "{}", code);
first = false;
}
}
#[cfg(test)]
mod tests {
use nohash_hasher::IntMap;

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ use std::sync::RwLock;
use anyhow::{anyhow, bail, Result};
use ignore::{DirEntry, WalkBuilder, WalkState};
use itertools::Itertools;
use log::debug;
use path_absolutize::path_dedot;
use rustc_hash::FxHashSet;
@@ -217,7 +218,7 @@ pub fn python_files_in_path(
processor: impl ConfigProcessor,
) -> Result<(Vec<Result<DirEntry, ignore::Error>>, Resolver)> {
// Normalize every path (e.g., convert from relative to absolute).
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).collect();
let mut paths: Vec<PathBuf> = paths.iter().map(fs::normalize_path).unique().collect();
// Search for `pyproject.toml` files in all parent directories.
let mut resolver = Resolver::default();

View File

@@ -15,6 +15,11 @@ pub(crate) fn get_redirect(code: &str) -> Option<(&'static str, &'static str)> {
static REDIRECTS: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
HashMap::from_iter([
("C", "C4"),
("C9", "C90"),
("T", "T10"),
("T1", "T10"),
("T2", "T20"),
// TODO(charlie): Remove by 2023-02-01.
("R", "RET"),
("R5", "RET5"),

View File

@@ -1,5 +1,6 @@
use std::str::FromStr;
use itertools::Itertools;
use schemars::_serde_json::Value;
use schemars::schema::{InstanceType, Schema, SchemaObject};
use schemars::JsonSchema;
@@ -8,19 +9,27 @@ use serde::{Deserialize, Serialize};
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use crate::registry::{Rule, RuleCodePrefix, RuleIter};
use crate::codes::RuleCodePrefix;
use crate::registry::{Linter, Rule, RuleIter, RuleNamespace};
use crate::rule_redirects::get_redirect;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum RuleSelector {
/// All rules
All,
Linter(Linter),
Prefix {
prefix: RuleCodePrefix,
redirected_from: Option<&'static str>,
},
}
impl From<Linter> for RuleSelector {
fn from(linter: Linter) -> Self {
Self::Linter(linter)
}
}
impl FromStr for RuleSelector {
type Err = ParseError;
@@ -32,9 +41,17 @@ impl FromStr for RuleSelector {
Some((from, target)) => (target, Some(from)),
None => (s, None),
};
let (linter, code) =
Linter::parse_code(s).ok_or_else(|| ParseError::Unknown(s.to_string()))?;
if code.is_empty() {
return Ok(Self::Linter(linter));
}
Ok(Self::Prefix {
prefix: RuleCodePrefix::from_str(s)
.map_err(|_| ParseError::Unknown(s.to_string()))?,
prefix: RuleCodePrefix::parse(&linter, code)
.map_err(|_| ParseError::Unknown(code.to_string()))?,
redirected_from,
})
}
@@ -50,10 +67,13 @@ pub enum ParseError {
}
impl RuleSelector {
pub fn short_code(&self) -> &'static str {
pub fn prefix_and_code(&self) -> (&'static str, &'static str) {
match self {
RuleSelector::All => "ALL",
RuleSelector::Prefix { prefix, .. } => prefix.into(),
RuleSelector::All => ("", "ALL"),
RuleSelector::Prefix { prefix, .. } => {
(prefix.linter().common_prefix(), prefix.short_code())
}
RuleSelector::Linter(l) => (l.common_prefix(), ""),
}
}
}
@@ -63,7 +83,8 @@ impl Serialize for RuleSelector {
where
S: serde::Serializer,
{
serializer.serialize_str(self.short_code())
let (prefix, code) = self.prefix_and_code();
serializer.serialize_str(&format!("{prefix}{code}"))
}
}
@@ -117,14 +138,15 @@ impl IntoIterator for &RuleSelector {
fn into_iter(self) -> Self::IntoIter {
match self {
RuleSelector::All => RuleSelectorIter::All(Rule::iter()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Prefix(prefix.into_iter()),
RuleSelector::Linter(linter) => RuleSelectorIter::Vec(linter.into_iter()),
RuleSelector::Prefix { prefix, .. } => RuleSelectorIter::Vec(prefix.into_iter()),
}
}
}
pub enum RuleSelectorIter {
All(RuleIter),
Prefix(std::vec::IntoIter<Rule>),
Vec(std::vec::IntoIter<Rule>),
}
impl Iterator for RuleSelectorIter {
@@ -133,7 +155,7 @@ impl Iterator for RuleSelectorIter {
fn next(&mut self) -> Option<Self::Item> {
match self {
RuleSelectorIter::All(iter) => iter.next(),
RuleSelectorIter::Prefix(iter) => iter.next(),
RuleSelectorIter::Vec(iter) => iter.next(),
}
}
}
@@ -160,7 +182,19 @@ impl JsonSchema for RuleSelector {
instance_type: Some(InstanceType::String.into()),
enum_values: Some(
std::iter::once("ALL".to_string())
.chain(RuleCodePrefix::iter().map(|s| s.as_ref().to_string()))
.chain(
RuleCodePrefix::iter()
.map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
format!("{prefix}{code}")
})
.chain(Linter::iter().filter_map(|l| {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| prefix.to_string())
}))
.sorted(),
)
.map(Value::String)
.collect(),
),
@@ -173,7 +207,18 @@ impl RuleSelector {
pub(crate) fn specificity(&self) -> Specificity {
match self {
RuleSelector::All => Specificity::All,
RuleSelector::Prefix { prefix, .. } => prefix.specificity(),
RuleSelector::Linter(..) => Specificity::Linter,
RuleSelector::Prefix { prefix, .. } => {
let prefix: &'static str = prefix.short_code();
match prefix.len() {
1 => Specificity::Code1Char,
2 => Specificity::Code2Chars,
3 => Specificity::Code3Chars,
4 => Specificity::Code4Chars,
5 => Specificity::Code5Chars,
_ => panic!("RuleSelector::specificity doesn't yet support codes with so many characters"),
}
}
}
}
}
@@ -188,3 +233,76 @@ pub(crate) enum Specificity {
Code4Chars,
Code5Chars,
}
mod clap_completion {
use clap::builder::{PossibleValue, TypedValueParser, ValueParserFactory};
use strum::IntoEnumIterator;
use crate::{
codes::RuleCodePrefix,
registry::{Linter, RuleNamespace},
RuleSelector,
};
#[derive(Clone)]
pub struct RuleSelectorParser;
impl ValueParserFactory for RuleSelector {
type Parser = RuleSelectorParser;
fn value_parser() -> Self::Parser {
RuleSelectorParser
}
}
impl TypedValueParser for RuleSelectorParser {
type Value = RuleSelector;
fn parse_ref(
&self,
_cmd: &clap::Command,
_arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
let value = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
value
.parse()
.map_err(|e| clap::Error::raw(clap::error::ErrorKind::InvalidValue, e))
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
Some(Box::new(
std::iter::once(PossibleValue::new("ALL").help("all rules")).chain(
Linter::iter()
.filter_map(|l| {
let prefix = l.common_prefix();
(!prefix.is_empty()).then(|| PossibleValue::new(prefix).help(l.name()))
})
.chain(RuleCodePrefix::iter().map(|p| {
let prefix = p.linter().common_prefix();
let code = p.short_code();
let mut rules_iter = p.into_iter();
let rule1 = rules_iter.next();
let rule2 = rules_iter.next();
let value = PossibleValue::new(format!("{prefix}{code}"));
if rule2.is_none() {
let rule1 = rule1.unwrap();
let name: &'static str = rule1.into();
value.help(name)
} else {
value
}
})),
),
))
}
}
}

View File

@@ -15,7 +15,7 @@ mod tests {
#[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 snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("eradicate").join(path).as_path(),
&settings::Settings::for_rule(rule_code),

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