Compare commits

...

131 Commits

Author SHA1 Message Date
Charlie Marsh
41e77bb01d Add some additional users to "Who's Using Ruff?" (#3035) 2023-02-19 16:30:01 +00:00
Charlie Marsh
2ff3dd5fbe Bump version to 0.0.248 (#3034) 2023-02-19 16:21:30 +00:00
Charlie Marsh
0f0e7a521a Avoid false-positives for break in with (#3032) 2023-02-19 11:17:04 -05:00
Jonathan Plasse
b75663be6d Add missing rust-version in crates (#3009) 2023-02-19 15:07:17 +00:00
Tomer Chachamu
4d3d04ee61 [PLE0101] error when __init__ returns a value (#3007) 2023-02-19 14:54:43 +00:00
Manuel Jacob
87422ba362 Add configuration option for C408 to allow dict calls with keyword arguments. (#2977)
When creating a dict with string keys, some prefer to call dict instead of writing a dict literal.
For example: `dict(a=1, b=2, c=3)` instead of `{"a": 1, "b": 2, "c": 3}`.
2023-02-19 14:47:03 +00:00
Jeremy Goh
c1d2976fff [docs] Add docs for flake8-implicit-str-concat rules (#3028) 2023-02-19 14:38:59 +00:00
Jeremy Goh
13281cd9ca [docs] Add some docs for flake8-simplify (#3027) 2023-02-19 14:26:56 +00:00
Jonathan Plasse
e53652779d Avoid raising B027 violations in .pyi files (#3016) 2023-02-19 14:21:33 +00:00
Jonathan Plasse
db4c611c6f Fix broken links and markdown style (#3017) 2023-02-19 08:46:49 -05:00
Charlie Marsh
c25be31eb1 Fix documentation-link detection in generate_mkdocs.py (#3030) 2023-02-19 12:20:44 +00:00
Micha Reiser
a7c533634d chore: Remove default_members from Cargo.toml (#3006)
This PR removes the `default_members` from the workspace configuration. 

## Why

I'm not familiar with the motivation for why the `default_members` setting was added initially, and I do not object to keeping it. I'll explain my motivation for removing it below. 

My main reason for removing the `default_members` override is that new contributors may not know that `cargo test`, `cargo build`, and other commands only run on a subset of crates. They may then be surprised that their PRs are failing in CI, but everything works locally. 

My guess why `default_members` was added is to speed up the development workflow. That's fair, but I question the value because `ruff` is the heaviest crate to build.
2023-02-19 07:18:47 -05:00
Simon Brugman
cfa6883431 docs(readme): add Diffusers (#3029) 2023-02-19 07:10:02 -05:00
Nyakku Shigure
216aa929af Remove duplicate underline in B007 autofix message (#3021) 2023-02-18 19:38:20 -05:00
Simon Brugman
9e45424ed6 [pycodestyle] autofix useless semicolons (#3001) 2023-02-17 18:52:42 -05:00
Charlie Marsh
db7f16e276 Support positional messages in assertion rewrites (#3002) 2023-02-17 23:44:13 +00:00
Charlie Marsh
a10a500a26 Ignore namedtuple methods in flake8-self (#2998) 2023-02-17 17:16:25 -05:00
Charlie Marsh
b9fef7cef7 Unlink flake8-bugbear in summary (#2997) 2023-02-17 15:58:33 -05:00
Charlie Marsh
34294ccc00 Deduplicate user list (#2996) 2023-02-17 20:07:42 +00:00
Simon Brugman
a934d01bdb [flake8-tidy-imports] extend autofix of relative imports (#2990)
This extends the autofix for TID252 to work with for relative imports without `module` (i.e. `from .. import`). Tested with `matplotlib` and `bokeh`.
(Previously it would panic on unwrap of the module) 

Note that pandas has [replaced](6057d7a93e) `absolufy-imports` with `ruff` now!
2023-02-17 19:35:28 +00:00
Simon Brugman
0dd590f137 Fix for F541 unescape f-string (#2971) 2023-02-17 14:27:01 -05:00
Charlie Marsh
909a5c3253 Avoid zero-indexed column for IOError (#2995) 2023-02-17 14:14:28 -05:00
Charlie Marsh
5c987874c4 Enforce D403 on methods (#2992) 2023-02-17 18:05:48 +00:00
Nyakku Shigure
0cfe4f9c69 Remove a whitespace in B004 message (#2991) 2023-02-17 12:37:08 -05:00
Charlie Marsh
6a369e4a30 Remove via from sentence in README (#2987) 2023-02-17 13:49:09 +00:00
Charlie Marsh
6f97e2c457 Split list of users into top-level and dedicated section (#2986) 2023-02-17 13:36:32 +00:00
Charlie Marsh
bebd412469 Adjust header depth in docs (#2985) 2023-02-17 13:19:55 +00:00
Charlie Marsh
cd1f57b713 Move FAQ into MkDocs (#2984) 2023-02-17 13:15:53 +00:00
Charlie Marsh
a0912deb2b Move editor integrations into MkDocs (#2983) 2023-02-17 13:12:20 +00:00
Charlie Marsh
50ee14a418 Fix references to specific settings in README.md (#2982) 2023-02-17 13:07:37 +00:00
Martin Fischer
f5adbbebc5 Fix table of contents enumeration 2023-02-17 07:55:50 -05:00
Martin Fischer
c88e05dc1b Merge Reference README section into Configuration section 2023-02-17 07:55:50 -05:00
Martin Fischer
d658bfc024 Remove options from README 2023-02-17 07:55:50 -05:00
Martin Fischer
b0d72c47b4 refactor: Move Top-level heading into ruff_dev 2023-02-17 07:55:50 -05:00
Martin Fischer
8195873cdf Remove rule tables from README 2023-02-17 07:55:50 -05:00
Martin Fischer
bf8108469f Remove auto-generated table of contents 2023-02-17 07:55:50 -05:00
Martin Fischer
a2277cfeba refactor: Move fix symbol legend into ruff_dev 2023-02-17 07:55:50 -05:00
Charlie Marsh
180541a924 Unify comment terminology with that of rome_formatter (#2979) 2023-02-17 03:02:25 +00:00
Simon Brugman
34664a0ca0 [numpy] numpy-legacy-random (#2960)
The new `Generator` in NumPy uses bits provided by [PCG64](https://numpy.org/doc/stable/reference/random/bit_generators/pcg64.html#numpy.random.PCG64) which has better statistical properties than the legacy [MT19937](https://numpy.org/doc/stable/reference/random/bit_generators/mt19937.html#numpy.random.MT19937) used in [RandomState](https://numpy.org/doc/stable/reference/random/legacy.html#numpy.random.RandomState). Global random functions can also be problematic with parallel processing.

This rule is probably quite useful for data scientists (perhaps in combination with `nbqa`)

References:
- [Legacy Random Generation](https://numpy.org/doc/stable/reference/random/legacy.html#legacy)
- [Random Sampling](https://numpy.org/doc/stable/reference/random/index.html#random-quick-start)
- [Using PyTorch + NumPy? You're making a mistake.](https://tanelp.github.io/posts/a-bug-that-plagues-thousands-of-open-source-ml-projects/)
2023-02-17 02:06:30 +00:00
Charlie Marsh
e081455b06 Add support for file-scoped noqa directives (#2978)
# Summary

This allows users to do things like:

```py
# ruff: noqa: F401
```

...to ignore all `F401` directives in a file. It's equivalent to `per-file-ignores`, but allows users to specify the behavior inline.

Note that Flake8 does _not_ support this, so we _don't_ respect `# flake8: noqa: F401`. (Flake8 treats that as equivalent to `# flake8: noqa`, so ignores _all_ errors in the file. I think all of [these usages](https://cs.github.com/?scopeName=All+repos&scope=&q=%22%23+flake8%3A+noqa%3A+%22) are probably mistakes!)

A couple notes on the details:

- If a user has `# ruff: noqa: F401` in the file, but also `# noqa: F401` on a line that would legitimately trigger an `F401` violation, we _do_ mark that as "unused" for `RUF100` purposes. This may be the wrong choice. The `noqa` is legitimately unused, but it's also not "wrong". It's just redundant.
- If a user has `# ruff: noqa: F401`, and runs `--add-noqa`, we _won't_ add `# noqa: F401` to any lines (which seems like the obvious right choice to me).

Closes #1054 (which has some extra pieces that I'll carve out into a separate issue).

Closes #2446.
2023-02-17 01:59:01 +00:00
Artem Mukhin
4f18fa6733 Add test case for '\u' prefix in B005 (#2976)
Based on #2958.
2023-02-16 19:45:29 -05:00
Charlie Marsh
6088a36cd3 Use line_suffix for end-of-line comments (#2975) 2023-02-16 18:37:40 -05:00
Charlie Marsh
66a162fa40 Handle non-from __future__ imports (#2974)
These are uncommon, but currently panic.

Closes #2967.
2023-02-16 22:56:03 +00:00
Mike Taves
e6722f92ed Add Rust Trove classifier (#2973) 2023-02-16 17:38:36 -05:00
Charlie Marsh
750c28868f Enable jemalloc on FreeBSD and NetBSD (#2965) 2023-02-16 15:21:34 -05:00
Charlie Marsh
5157f584ab Improve pow operator spacing (#2970)
Ensure that we add spaces to expressions like `foo.bar() ** 2`.
2023-02-16 15:17:32 -05:00
Charlie Marsh
1c01ec21cb Regenerate expected Black snapshots (#2968) 2023-02-16 19:39:17 +00:00
Manuel Jacob
879512742f Skip .pytype directory by default. (#2966)
Pytype stores .pyi files in .pytype that ruff shouldn’t check or touch.
2023-02-16 14:38:08 -05:00
Florian Best
a919041dda feat(isort): Implement isort.force_to_top (#2877) 2023-02-16 19:01:59 +00:00
Charlie Marsh
059601d968 Avoid trying to fix implicit returns with control flow (#2962) 2023-02-16 13:42:46 -05:00
Charlie Marsh
2ec1701543 Remove link in asyncio.create_task (#2963) 2023-02-16 17:50:56 +00:00
Charlie Marsh
370c3a5daf Remove mdcat dependency (#2959) 2023-02-16 12:09:37 -05:00
Charlie Marsh
fdcb78fd8c Avoid jemallocator on BSD (#2957) 2023-02-16 11:48:51 -05:00
Simon Brugman
2a744d24e5 docs: flake8-self remove unnecessary backticks (#2951) 2023-02-16 08:25:34 -05:00
Simon Brugman
cc30738148 Implement flake8-module-naming (#2855)
- Implement N999 (following flake8-module-naming) in pep8_naming
- Refactor pep8_naming: split rules.rs into file per rule
- Documentation for majority of the violations

Closes https://github.com/charliermarsh/ruff/issues/2734
2023-02-16 04:20:33 +00:00
Edgar R. M
147c6ff1db Exclude crates/ruff_python_formatter/resources from pre-commit check (#2947) 2023-02-15 22:56:42 -05:00
Charlie Marsh
036380e6a8 Fix add-required-import with multi-line offsets (#2946) 2023-02-16 03:24:55 +00:00
Charlie Marsh
b6587e51ee Use an enum to represent composition kind (#2945) 2023-02-15 22:14:00 -05:00
Simon Brugman
1bc37110d4 [flake8-pytest-style] autofix for composite-assertion (PT018) (#2732) 2023-02-16 00:36:07 +00:00
Lunarmagpie
28acdb76cf Add support for ensure_future for RUF006 (#2943) 2023-02-15 23:18:11 +00:00
Martin Fischer
7b09972c97 Merge convert-loop-to-any & convert-loop-to-all to reimplemented-builtin 2023-02-15 16:24:31 -05:00
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
545 changed files with 33698 additions and 8955 deletions

View File

@@ -13,6 +13,9 @@ 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 -r docs/requirements.txt

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

@@ -2,9 +2,8 @@ name: "[ruff] Release"
on:
workflow_dispatch:
push:
tags:
- '**'
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -30,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
@@ -42,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:
@@ -53,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"
@@ -64,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
@@ -92,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
@@ -107,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"
@@ -121,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
@@ -135,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: |
@@ -158,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
@@ -175,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
@@ -194,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
@@ -212,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
@@ -232,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
@@ -280,7 +348,6 @@ jobs:
- linux-cross
- musllinux
- musllinux-cross
- pypy
if: "startsWith(github.ref, 'refs/tags/')"
steps:
- uses: actions/download-artifact@v3
@@ -297,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/*

8
.gitignore vendored
View File

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

View File

@@ -1,3 +1,4 @@
fail_fast: true
repos:
- repo: https://github.com/abravalheri/validate-pyproject
rev: v0.10.1
@@ -12,6 +13,7 @@ repos:
- --disable
- MD013 # line-length
- MD033 # no-inline-html
- MD041 # first-line-h1
- --
- repo: local
@@ -32,7 +34,11 @@ repos:
language: rust
types_or: [python, pyi]
require_serial: true
exclude: ^crates/ruff/resources
exclude: |
(?x)^(
crates/ruff/resources/.*|
crates/ruff_python_formatter/resources/.*
)$
- id: dev-generate-all
name: dev-generate-all
entry: cargo dev generate-all

View File

@@ -1,16 +1,16 @@
# 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](#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

View File

@@ -2,16 +2,16 @@
Welcome! We're happy to have you here. Thank you in advance for your contribution to Ruff.
- [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](#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
@@ -94,12 +94,12 @@ The vast majority of the code, including all lint rules, lives in the `ruff` cra
At time of writing, the repository includes the following crates:
- `crates/ruff`: library crate containing all lint rules and the core logic for running them.
- `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
- `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`).
- `crates/ruff_macros`: library crate containing macros used by Ruff.
- `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of standard library modules by versionb).
- `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration.
* `crates/ruff`: library crate containing all lint rules and the core logic for running them.
* `crates/ruff_cli`: binary crate containing Ruff's command-line interface.
* `crates/ruff_dev`: binary crate containing utilities used in the development of Ruff itself (e.g., `cargo dev generate-all`).
* `crates/ruff_macros`: library crate containing macros used by Ruff.
* `crates/ruff_python`: library crate implementing Python-specific functionality (e.g., lists of standard library modules by versionb).
* `crates/flake8_to_ruff`: binary crate for generating Ruff configuration from Flake8 configuration.
### Example: Adding a new lint rule
@@ -146,7 +146,7 @@ Finally, regenerate the documentation and generated code with `cargo dev generat
#### Rule naming convention
The rule name should make sense when read as "allow *rule-name*" or "allow *rule-name* items".
The rule name should make sense when read as "allow _rule-name_" or "allow _rule-name_ items".
This implies that rule names:
@@ -186,14 +186,19 @@ Finally, regenerate the documentation and generated code with `cargo dev generat
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
```

888
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,26 @@
[workspace]
members = ["crates/*"]
default-members = ["crates/ruff", "crates/ruff_cli"]
[workspace.package]
edition = "2021"
rust-version = "1.65.0"
[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"

52
LICENSE
View File

@@ -1062,3 +1062,55 @@ are:
"""
- 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.
"""

3581
README.md

File diff suppressed because it is too large Load Diff

View File

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

7
clippy.toml Normal file
View File

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

View File

@@ -1,19 +1,20 @@
[package]
name = "flake8-to-ruff"
version = "0.0.246"
edition = "2021"
version = "0.0.248"
edition = { workspace = true }
rust-version = { workspace = true }
[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,9 +1,9 @@
[package]
name = "ruff"
version = "0.0.246"
version = "0.0.248"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
edition = { workspace = true }
rust-version = { workspace = true }
documentation = "https://github.com/charliermarsh/ruff"
homepage = "https://github.com/charliermarsh/ruff"
repository = "https://github.com/charliermarsh/ruff"
@@ -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" }
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.246", path = "../ruff_macros" }
ruff_python = { version = "0.0.246", path = "../ruff_python" }
rustc-hash = { version = "1.1.0" }
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 = { version = "0.8.11" }
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

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

@@ -17,6 +17,11 @@ 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
s.strip("\ufeff") # no warning
s.strip("\u0074\u0065\u0073\u0074") # warning
from somewhere import other_type, strip

View File

@@ -0,0 +1,101 @@
"""
Should emit:
B027 - on lines 13, 16, 19, 23
"""
import abc
from abc import ABC
from abc import abstractmethod, abstractproperty
from abc import abstractmethod as notabstract
from abc import abstractproperty as notabstract_property
class AbstractClass(ABC):
def empty_1(self): # error
...
def empty_2(self): # error
pass
def empty_3(self): # error
"""docstring"""
...
def empty_4(self): # error
"""multiple ellipsis/pass"""
...
pass
...
pass
@notabstract
def abstract_0(self):
...
@abstractmethod
def abstract_1(self):
...
@abstractmethod
def abstract_2(self):
pass
@abc.abstractmethod
def abstract_3(self):
...
@abc.abstractproperty
def abstract_4(self):
...
@abstractproperty
def abstract_5(self):
...
@notabstract_property
def abstract_6(self):
...
def body_1(self):
print("foo")
...
def body_2(self):
self.body_1()
class NonAbstractClass:
def empty_1(self): # safe
...
def empty_2(self): # safe
pass
# ignore @overload, fixes issue #304
# ignore overload with other imports, fixes #308
import typing
import typing as t
import typing as anything
from typing import Union, overload
class AbstractClass(ABC):
@overload
def empty_1(self, foo: str):
...
@typing.overload
def empty_1(self, foo: int):
...
@t.overload
def empty_1(self, foo: list):
...
@anything.overload
def empty_1(self, foo: float):
...
@abstractmethod
def empty_1(self, foo: Union[str, int, list, float]):
...

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

@@ -12,7 +12,16 @@ class IncorrectModel(models.Model):
urlfield = models.URLField(max_length=255, null=True)
class IncorrectModelWithAliasedBase(DjangoModel):
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)

View File

@@ -15,3 +15,23 @@ def correct_pre_save_handler():
@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

@@ -6,13 +6,24 @@ def test_ok():
assert something or something_else
assert something or something_else and something_third
assert not (something and something_else)
assert something, "something message"
assert something or something_else and something_third, "another message"
def test_error():
assert something and something_else
assert something and something_else and something_third
assert something and not something_else
assert something and (something_else or something_third)
assert not something and something_else
assert not (something or something_else)
assert not (something or something_else or something_third)
# recursive case
assert not (a or not (b or c))
assert not (a or not (b and c)) # note that we only reduce once here
# detected, but no autofix for messages
assert something and something_else, "error message"
assert not (something or something_else and something_third), "with message"
# detected, but no autofix for mixed conditions (e.g. `a or b and c`)
assert not (something or something_else and something_third)

View File

@@ -251,3 +251,11 @@ def noreturn_pytest_xfail_2():
if x > 0:
return 1
py_xfail("oof")
def nested(values):
if not values:
return False
for value in values:
print(value)

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

@@ -35,6 +35,13 @@ class Foo(metaclass=BazMeta):
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):
@@ -49,15 +56,17 @@ class Foo(metaclass=BazMeta):
foo = Foo()
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
print(foo.public_thing)
print(foo.public_func())
print(foo.__dict__)
print(foo.__str__())
print(foo().__class__)
print(foo._asdict())

View File

@@ -18,8 +18,6 @@ from \
..parent\
import \
world_hello
# TID252 (without autofix; too many levels up)
from ..... import ultragrantparent
from ...... import ultragrantparent
from ....... import ultragrantparent

View File

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

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,23 @@
import lib6
import lib2
import lib5
import lib1
import lib3
import lib4
import foo
import z
from foo import bar
from lib1 import foo
from lib2 import foo
from lib1.lib2 import foo
from foo.lib1.bar import baz
from lib4 import lib1
from lib5 import lib2
from lib4 import lib2
from lib5 import lib1
import lib3.lib4
import lib3.lib4.lib5
from lib3.lib4 import foo
from lib3.lib4.lib5 import foo

View File

@@ -5,3 +5,4 @@ line-length = 88
lines-after-imports = 3
lines-between-types = 2
known-local-folder = ["ruff"]
force-to-top = ["lib1", "lib3", "lib5", "lib3.lib4", "z"]

View File

@@ -0,0 +1,4 @@
"""a
b"""
# b
import os

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,62 @@
# Do this (new version)
from numpy.random import default_rng
rng = default_rng()
vals = rng.standard_normal(10)
more_vals = rng.standard_normal(10)
numbers = rng.integers(high, size=5)
# instead of this (legacy version)
from numpy import random
vals = random.standard_normal(10)
more_vals = random.standard_normal(10)
numbers = random.integers(high, size=5)
import numpy
numpy.random.seed()
numpy.random.get_state()
numpy.random.set_state()
numpy.random.rand()
numpy.random.randn()
numpy.random.randint()
numpy.random.random_integers()
numpy.random.random_sample()
numpy.random.choice()
numpy.random.bytes()
numpy.random.shuffle()
numpy.random.permutation()
numpy.random.beta()
numpy.random.binomial()
numpy.random.chisquare()
numpy.random.dirichlet()
numpy.random.exponential()
numpy.random.f()
numpy.random.gamma()
numpy.random.geometric()
numpy.random.get_state()
numpy.random.gumbel()
numpy.random.hypergeometric()
numpy.random.laplace()
numpy.random.logistic()
numpy.random.lognormal()
numpy.random.logseries()
numpy.random.multinomial()
numpy.random.multivariate_normal()
numpy.random.negative_binomial()
numpy.random.noncentral_chisquare()
numpy.random.noncentral_f()
numpy.random.normal()
numpy.random.pareto()
numpy.random.poisson()
numpy.random.power()
numpy.random.rayleigh()
numpy.random.standard_cauchy()
numpy.random.standard_exponential()
numpy.random.standard_gamma()
numpy.random.standard_normal()
numpy.random.standard_t()
numpy.random.triangular()
numpy.random.uniform()
numpy.random.vonmises()
numpy.random.wald()
numpy.random.weibull()
numpy.random.zipf()

View File

@@ -4,3 +4,5 @@ from __future__ import absolute_import
from collections import namedtuple
from __future__ import print_function
import __future__

View File

@@ -33,3 +33,11 @@ f"{f'{v:0.2f}'}"
# Errors
f"{v:{f'0.2f'}}"
f"{f''}"
f"{{test}}"
f'{{ 40 }}'
f"{{a {{x}}"
f"{{{{x}}}}"
# To be fixed
# Error: f-string: single '}' is not allowed at line 41 column 8
# f"\{{x}}"

View File

@@ -0,0 +1,41 @@
def a():
return
def __init__():
return
class A:
def __init__(self):
return
class B:
def __init__(self):
return 3
def gen(self):
return 5
class MyClass:
def __init__(self):
return 1
class MyClass2:
"""dummy class"""
def __init__(self):
return
class MyClass3:
"""dummy class"""
def __init__(self):
return None
class MyClass5:
"""dummy class"""
def __init__(self):
self.callable = lambda: (yield None)

View File

@@ -113,3 +113,14 @@ def test_break_in_if_orelse():
else:
return True
return False
def test_break_in_with():
"""no false positive for break in with"""
for name in ["demo"]:
with open(__file__) as f:
if name in f.read():
break
else:
return True
return False

View File

@@ -0,0 +1,73 @@
import asyncio
# Error
def f():
asyncio.create_task(coordinator.ws_connect()) # Error
# Error
def f():
asyncio.ensure_future(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():
background_tasks = set()
for i in range(10):
task = asyncio.ensure_future(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

@@ -0,0 +1,8 @@
# ruff: noqa: F401
import os
import foo
def f():
x = 1

View File

@@ -596,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(..)) {
@@ -723,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),
}
}
@@ -756,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())
}
@@ -766,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;
@@ -784,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);
@@ -798,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())
@@ -808,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;
@@ -843,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);
@@ -871,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 { .. }))
@@ -890,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()
@@ -915,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,
});
@@ -932,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 {
@@ -940,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),
});
@@ -980,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()
@@ -1002,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))
@@ -1032,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

@@ -54,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.
@@ -67,7 +67,7 @@ fn apply_fixes<'a>(
}
// Add the remaining content.
let slice = locator.slice_source_code_at(last_pos);
let slice = locator.skip(last_pos);
output.push_str(slice);
(output, fixed)
@@ -78,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

View File

@@ -32,14 +32,15 @@ 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::registry::{Diagnostic, Rule};
use crate::resolver::is_interface_definition_path;
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_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, pandas_vet, pep8_naming, pycodestyle,
pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
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 +51,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> {
@@ -57,6 +59,7 @@ pub struct Checker<'a> {
pub(crate) path: &'a Path,
module_path: Option<Vec<String>>,
package: Option<&'a Path>,
is_interface_definition: bool,
autofix: flags::Autofix,
noqa: flags::Noqa,
pub(crate) settings: &'a Settings,
@@ -84,8 +87,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>)>,
@@ -125,6 +128,7 @@ impl<'a> Checker<'a> {
style: &'a Stylist,
indexer: &'a Indexer,
) -> Checker<'a> {
let is_interface_definition = is_interface_definition_path(path);
Checker {
settings,
noqa_line_for,
@@ -133,6 +137,7 @@ impl<'a> Checker<'a> {
path,
package,
module_path,
is_interface_definition,
locator,
stylist: style,
indexer,
@@ -172,7 +177,7 @@ impl<'a> Checker<'a> {
in_type_checking_block: false,
seen_import_boundary: false,
futures_allowed: true,
annotations_future_enabled: path.extension().map_or(false, |ext| ext == "pyi"),
annotations_future_enabled: is_interface_definition,
except_handlers: vec![],
// Check-specific state.
flake8_bugbear_seen: vec![],
@@ -464,14 +469,16 @@ where
body,
..
} => {
if self.settings.rules.enabled(&Rule::ReceiverDecoratorChecker) {
if let Some(diagnostic) =
flake8_django::rules::receiver_decorator_checker(decorator_list, |expr| {
self.resolve_call_path(expr)
})
{
self.diagnostics.push(diagnostic);
}
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) =
@@ -772,6 +779,9 @@ where
if self.settings.rules.enabled(&Rule::ReturnOutsideFunction) {
pyflakes::rules::return_outside_function(self, stmt);
}
if self.settings.rules.enabled(&Rule::ReturnInInit) {
pylint::rules::return_in_init(self, stmt);
}
}
StmtKind::ClassDef {
name,
@@ -780,15 +790,15 @@ where
decorator_list,
body,
} => {
if self.settings.rules.enabled(&Rule::ModelStringFieldNullable) {
if self.settings.rules.enabled(&Rule::NullableModelStringField) {
self.diagnostics
.extend(flake8_django::rules::model_string_field_nullable(
self, bases, body,
.extend(flake8_django::rules::nullable_model_string_field(
self, body,
));
}
if self.settings.rules.enabled(&Rule::ModelDunderStr) {
if self.settings.rules.enabled(&Rule::ModelWithoutDunderStr) {
if let Some(diagnostic) =
flake8_django::rules::model_dunder_str(self, bases, body, stmt)
flake8_django::rules::model_without_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
@@ -832,18 +842,20 @@ where
flake8_bugbear::rules::useless_expression(self, body);
}
if self
.settings
.rules
.enabled(&Rule::AbstractBaseClassWithoutAbstractMethod)
|| self
if !self.is_interface_definition {
if self
.settings
.rules
.enabled(&Rule::EmptyMethodWithoutAbstractDecorator)
{
flake8_bugbear::rules::abstract_base_class(
self, stmt, name, bases, keywords, body,
);
.enabled(&Rule::AbstractBaseClassWithoutAbstractMethod)
|| self
.settings
.rules
.enabled(&Rule::EmptyMethodWithoutAbstractDecorator)
{
flake8_bugbear::rules::abstract_base_class(
self, stmt, name, bases, keywords, body,
);
}
}
if self
@@ -897,7 +909,38 @@ where
}
for alias in names {
if alias.node.name.contains('.') && alias.node.asname.is_none() {
if alias.node.name == "__future__" {
let name = alias.node.asname.as_ref().unwrap_or(&alias.node.name);
self.add_binding(
name,
Binding {
kind: BindingKind::FutureImportation,
runtime_usage: None,
// Always mark `__future__` imports as used.
synthetic_usage: Some((
self.scopes[*(self
.scope_stack
.last()
.expect("No current scope found"))]
.id,
Range::from_located(alias),
)),
typing_usage: None,
range: Range::from_located(alias),
source: Some(self.current_stmt().clone()),
context: self.execution_context(),
},
);
if self.settings.rules.enabled(&Rule::LateFutureImport)
&& !self.futures_allowed
{
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::LateFutureImport,
Range::from_located(stmt),
));
}
} else if alias.node.name.contains('.') && alias.node.asname.is_none() {
// Given `import foo.bar`, `name` would be "foo", and `full_name` would be
// "foo.bar".
let name = alias.node.name.split('.').next().unwrap();
@@ -915,10 +958,6 @@ where
},
);
} else {
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, stmt, false);
}
// Given `import foo`, `name` and `full_name` would both be `foo`.
// Given `import foo as bar`, `name` would be `bar` and `full_name` would
// be `foo`.
@@ -954,6 +993,10 @@ where
context: self.execution_context(),
},
);
if let Some(asname) = &alias.node.asname {
self.check_builtin_shadowing(asname, stmt, false);
}
}
// flake8-debugger
@@ -1317,8 +1360,8 @@ where
stmt,
level.as_ref(),
module.as_deref(),
self.module_path.as_ref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
self.path,
)
{
self.diagnostics.push(diagnostic);
@@ -1557,12 +1600,7 @@ where
pyflakes::rules::assert_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::AssertFalse) {
flake8_bugbear::rules::assert_false(
self,
stmt,
test,
msg.as_ref().map(|expr| &**expr),
);
flake8_bugbear::rules::assert_false(self, stmt, test, msg.as_deref());
}
if self.settings.rules.enabled(&Rule::Assert) {
self.diagnostics
@@ -1574,11 +1612,12 @@ where
}
}
if self.settings.rules.enabled(&Rule::CompositeAssertion) {
if let Some(diagnostic) =
flake8_pytest_style::rules::composite_condition(stmt, test)
{
self.diagnostics.push(diagnostic);
}
flake8_pytest_style::rules::composite_condition(
self,
stmt,
test,
msg.as_deref(),
);
}
}
StmtKind::With { items, body, .. } => {
@@ -1645,9 +1684,7 @@ where
pylint::rules::useless_else_on_loop(self, stmt, body, orelse);
}
if matches!(stmt.node, StmtKind::For { .. }) {
if self.settings.rules.enabled(&Rule::ConvertLoopToAny)
|| self.settings.rules.enabled(&Rule::ConvertLoopToAll)
{
if self.settings.rules.enabled(&Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(
self,
stmt,
@@ -1741,8 +1778,8 @@ where
}
}
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
if self.path.extension().map_or(false, |ext| ext == "pyi") {
if self.is_interface_definition {
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
flake8_pyi::rules::prefix_type_params(self, value, targets);
}
}
@@ -1797,6 +1834,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);
}
}
}
_ => {}
}
@@ -2067,13 +2111,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()),
));
}
@@ -2142,6 +2186,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);
}
// Ex) List[...]
if !self.in_deferred_string_type_definition
@@ -2208,6 +2255,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);
}
@@ -2515,7 +2565,12 @@ where
.enabled(&Rule::UnnecessaryCollectionCall)
{
flake8_comprehensions::rules::unnecessary_collection_call(
self, expr, func, args, keywords,
self,
expr,
func,
args,
keywords,
&self.settings.flake8_comprehensions,
);
}
if self
@@ -2800,6 +2855,11 @@ where
flake8_use_pathlib::helpers::replaceable_by_pathlib(self, func);
}
// numpy
if self.settings.rules.enabled(&Rule::NumpyLegacyRandom) {
numpy::rules::numpy_legacy_random(self, func);
}
// flake8-logging-format
if self.settings.rules.enabled(&Rule::LoggingStringFormat)
|| self.settings.rules.enabled(&Rule::LoggingPercentFormat)
@@ -3163,13 +3223,13 @@ where
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") {
if self.is_interface_definition {
if self
.settings
.rules
.enabled(&Rule::UnrecognizedPlatformCheck)
|| self.settings.rules.enabled(&Rule::UnrecognizedPlatformName)
{
flake8_pyi::rules::unrecognized_platform(
self,
expr,
@@ -3188,7 +3248,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()),
));
}
@@ -4493,12 +4553,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);
@@ -4513,7 +4574,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>") {
@@ -4524,7 +4585,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
@@ -4540,10 +4601,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);
@@ -5213,10 +5277,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()),
));

View File

@@ -2,6 +2,7 @@ use std::path::Path;
use crate::registry::{Diagnostic, Rule};
use crate::rules::flake8_no_pep420::rules::implicit_namespace_package;
use crate::rules::pep8_naming::rules::invalid_module_name;
use crate::settings::Settings;
pub fn check_file_path(
@@ -20,5 +21,12 @@ pub fn check_file_path(
}
}
// pep8-naming
if settings.rules.enabled(&Rule::InvalidModuleName) {
if let Some(diagnostic) = invalid_module_name(path, package) {
diagnostics.push(diagnostic);
}
}
diagnostics
}

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

@@ -1,12 +1,14 @@
//! `NoQA` enforcement and validation.
use log::warn;
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};
use crate::noqa::{extract_file_exemption, Directive, Exemption};
use crate::registry::{Diagnostic, DiagnosticKind, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::rules::ruff::rules::{UnusedCodes, UnusedNOQA};
@@ -20,17 +22,38 @@ pub fn check_noqa(
settings: &Settings,
autofix: flags::Autofix,
) {
let mut noqa_directives: IntMap<usize, (Directive, Vec<&str>)> = IntMap::default();
let mut ignored = vec![];
let enforce_noqa = settings.rules.enabled(&Rule::UnusedNOQA);
// Whether the file is exempted from all checks.
let mut file_exempted = false;
// Codes that are globally exempted (within the current file).
let mut file_exemptions: Vec<NoqaCode> = vec![];
// Map from line number to `noqa` directive on that line, along with any codes
// that were matched by the directive.
let mut noqa_directives: IntMap<usize, (Directive, Vec<NoqaCode>)> = IntMap::default();
// Indices of diagnostics that were ignored by a `noqa` directive.
let mut ignored_diagnostics = vec![];
let lines: Vec<&str> = contents.lines().collect();
for lineno in commented_lines {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(lines[lineno - 1]) {
diagnostics.drain(..);
return;
match extract_file_exemption(lines[lineno - 1]) {
Exemption::All => {
file_exempted = true;
}
Exemption::Codes(codes) => {
file_exemptions.extend(codes.into_iter().filter_map(|code| {
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
Some(rule.noqa_code())
} else {
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
None
}
}));
}
Exemption::None => {}
}
if enforce_noqa {
@@ -46,6 +69,20 @@ pub fn check_noqa(
continue;
}
// If the file is exempted, ignore all diagnostics.
if file_exempted {
ignored_diagnostics.push(index);
continue;
}
// If the diagnostic is ignored by a global exemption, ignore it.
if !file_exemptions.is_empty() {
if file_exemptions.contains(&diagnostic.kind.rule().noqa_code()) {
ignored_diagnostics.push(index);
continue;
}
}
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
@@ -55,14 +92,14 @@ pub fn check_noqa(
});
match noqa {
(Directive::All(..), matches) => {
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
}
@@ -82,13 +119,15 @@ 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());
ignored.push(index);
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
(Directive::Codes(.., codes), matches) => {
if noqa::includes(diagnostic.kind.rule(), codes) {
matches.push(diagnostic.kind.rule().code());
ignored.push(index);
matches.push(diagnostic.kind.rule().noqa_code());
ignored_diagnostics.push(index);
continue;
}
}
(Directive::None, ..) => {}
@@ -128,12 +167,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) {
@@ -202,8 +241,8 @@ pub fn check_noqa(
}
}
ignored.sort_unstable();
for index in ignored.iter().rev() {
ignored_diagnostics.sort_unstable();
for index in ignored_diagnostics.iter().rev() {
diagnostics.swap_remove(*index);
}
}

View File

@@ -117,7 +117,7 @@ pub fn check_tokens(
// E701, E702, E703
if enforce_compound_statements {
diagnostics.extend(
pycodestyle::rules::compound_statements(tokens)
pycodestyle::rules::compound_statements(tokens, settings, autofix)
.into_iter()
.filter(|diagnostic| settings.rules.enabled(diagnostic.kind.rule())),
);

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

@@ -0,0 +1,612 @@
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, "E0101") => Rule::ReturnInInit,
(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::ReimplementedBuiltin,
// (Flake8Simplify, "111") => Rule::ReimplementedBuiltin,
(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,
(PEP8Naming, "999") => Rule::InvalidModuleName,
// 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,
(Numpy, "002") => Rule::NumpyLegacyRandom,
// 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

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

@@ -5,6 +5,7 @@
//!
//! [Ruff]: https://github.com/charliermarsh/ruff
pub use ast::types::Range;
use cfg_if::cfg_if;
pub use rule_selector::RuleSelector;
pub use rules::pycodestyle::rules::IOError;
@@ -15,6 +16,7 @@ mod ast;
mod autofix;
pub mod cache;
mod checkers;
mod codes;
mod cst;
mod directives;
mod doc_lines;

View File

@@ -9,10 +9,10 @@ use crate::directives;
use crate::linter::{check_path, LinterResult};
use crate::registry::Rule;
use crate::rules::{
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_errmsg,
flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style, flake8_quotes,
flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments, isort, mccabe, pep8_naming,
pycodestyle, pydocstyle, pylint, pyupgrade,
flake8_annotations, flake8_bandit, flake8_bugbear, flake8_builtins, flake8_comprehensions,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_pytest_style,
flake8_quotes, flake8_self, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
isort, mccabe, pep8_naming, pycodestyle, pydocstyle, pylint, pyupgrade,
};
use crate::rustpython_helpers::tokenize;
use crate::settings::configuration::Configuration;
@@ -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())
}
}
@@ -139,9 +139,11 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
flake8_bandit: Some(flake8_bandit::settings::Settings::default().into()),
flake8_bugbear: Some(flake8_bugbear::settings::Settings::default().into()),
flake8_builtins: Some(flake8_builtins::settings::Settings::default().into()),
flake8_comprehensions: Some(flake8_comprehensions::settings::Settings::default().into()),
flake8_errmsg: Some(flake8_errmsg::settings::Settings::default().into()),
flake8_pytest_style: Some(flake8_pytest_style::settings::Settings::default().into()),
flake8_quotes: Some(flake8_quotes::settings::Settings::default().into()),
flake8_self: Some(flake8_self::settings::Settings::default().into()),
flake8_implicit_str_concat: Some(
flake8_implicit_str_concat::settings::Settings::default().into(),
),

View File

@@ -211,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),
);
}

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,8 +1,10 @@
use std::fmt::{Display, Write};
use std::fs;
use std::path::Path;
use anyhow::Result;
use itertools::Itertools;
use log::warn;
use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
@@ -10,6 +12,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::codes::NoqaCode;
use crate::registry::{Diagnostic, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::source_code::{LineEnding, Locator};
@@ -22,16 +25,47 @@ static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
});
static SPLIT_COMMA_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[,\s]").unwrap());
#[derive(Debug)]
pub enum Exemption<'a> {
None,
All,
Codes(Vec<&'a str>),
}
/// Return `true` if a file is exempt from checking based on the contents of the
/// given line.
pub fn is_file_exempt(line: &str) -> bool {
pub fn extract_file_exemption(line: &str) -> Exemption {
let line = line.trim_start();
line.starts_with("# flake8: noqa")
if line.starts_with("# flake8: noqa")
|| line.starts_with("# flake8: NOQA")
|| line.starts_with("# flake8: NoQA")
|| line.starts_with("# ruff: noqa")
|| line.starts_with("# ruff: NOQA")
|| line.starts_with("# ruff: NoQA")
{
return Exemption::All;
}
if let Some(remainder) = line
.strip_prefix("# ruff: noqa")
.or_else(|| line.strip_prefix("# ruff: NOQA"))
.or_else(|| line.strip_prefix("# ruff: NoQA"))
{
if remainder.is_empty() {
return Exemption::All;
} else if let Some(codes) = remainder.strip_prefix(':') {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.trim())
.map(str::trim)
.filter(|code| !code.is_empty())
.collect();
if codes.is_empty() {
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}
return Exemption::Codes(codes);
}
warn!("Unexpected suffix on `noqa` directive: \"{line}\"");
}
Exemption::None
}
#[derive(Debug)]
@@ -47,16 +81,22 @@ pub fn extract_noqa_directive(line: &str) -> Directive {
Some(caps) => match caps.name("spaces") {
Some(spaces) => match caps.name("noqa") {
Some(noqa) => match caps.name("codes") {
Some(codes) => Directive::Codes(
spaces.as_str().chars().count(),
noqa.start(),
noqa.end(),
SPLIT_COMMA_REGEX
.split(codes.as_str())
Some(codes) => {
let codes: Vec<&str> = SPLIT_COMMA_REGEX
.split(codes.as_str().trim())
.map(str::trim)
.filter(|code| !code.is_empty())
.collect(),
),
.collect();
if codes.is_empty() {
warn!("Expected rule codes on `noqa` directive: \"{line}\"");
}
Directive::Codes(
spaces.as_str().chars().count(),
noqa.start(),
noqa.end(),
codes,
)
}
None => {
Directive::All(spaces.as_str().chars().count(), noqa.start(), noqa.end())
}
@@ -72,7 +112,7 @@ 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))
@@ -86,7 +126,7 @@ pub fn rule_is_ignored(
locator: &Locator,
) -> bool {
let noqa_lineno = noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = locator.slice_source_code_range(&Range::new(
let line = locator.slice(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
@@ -123,68 +163,93 @@ fn add_noqa_inner(
noqa_line_for: &IntMap<usize, usize>,
line_ending: &LineEnding,
) -> (usize, String) {
// Map of line number to set of (non-ignored) diagnostic codes that are triggered on that line.
let mut matches_by_line: FxHashMap<usize, FxHashSet<&Rule>> = FxHashMap::default();
// Whether the file is exempted from all checks.
let mut file_exempted = false;
// Codes that are globally exempted (within the current file).
let mut file_exemptions: Vec<NoqaCode> = vec![];
let lines: Vec<&str> = contents.lines().collect();
for (lineno, line) in lines.iter().enumerate() {
// If we hit an exemption for the entire file, bail.
if is_file_exempt(line) {
return (0, contents.to_string());
for lineno in commented_lines {
match extract_file_exemption(lines[lineno - 1]) {
Exemption::All => {
file_exempted = true;
}
Exemption::Codes(codes) => {
file_exemptions.extend(codes.into_iter().filter_map(|code| {
if let Ok(rule) = Rule::from_code(get_redirect_target(code).unwrap_or(code)) {
Some(rule.noqa_code())
} else {
warn!("Invalid code provided to `# ruff: noqa`: {}", code);
None
}
}));
}
Exemption::None => {}
}
}
// Mark any non-ignored diagnostics.
for diagnostic in diagnostics {
// If the file is exempted, don't add any noqa directives.
if file_exempted {
continue;
}
// Grab the noqa (logical) line number for the current (physical) line.
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
let mut codes: FxHashSet<&Rule> = FxHashSet::default();
for diagnostic in diagnostics {
if diagnostic.location.row() == lineno + 1 {
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
}
}
// Is the diagnostic ignored by a `noqa` directive on the same line?
let diagnostic_lineno = diagnostic.location.row();
let noqa_lineno = noqa_line_for
.get(&diagnostic_lineno)
.unwrap_or(&diagnostic_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
}
// The diagnostic is not ignored by any `noqa` directive; add it to the list.
codes.insert(diagnostic.kind.rule());
// If the diagnostic is ignored by a global exemption, don't add a noqa directive.
if !file_exemptions.is_empty() {
if file_exemptions.contains(&diagnostic.kind.rule().noqa_code()) {
continue;
}
}
if !codes.is_empty() {
matches_by_line
.entry(noqa_lineno)
.or_default()
.extend(codes);
// Is the violation ignored by a `noqa` directive on the parent line?
if let Some(parent_lineno) = diagnostic.parent.map(|location| location.row()) {
let noqa_lineno = noqa_line_for.get(&parent_lineno).unwrap_or(&parent_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
}
}
// Is the diagnostic ignored by a `noqa` directive on the same line?
let diagnostic_lineno = diagnostic.location.row();
let noqa_lineno = noqa_line_for
.get(&diagnostic_lineno)
.unwrap_or(&diagnostic_lineno);
if commented_lines.contains(noqa_lineno) {
match extract_noqa_directive(lines[noqa_lineno - 1]) {
Directive::All(..) => {
continue;
}
Directive::Codes(.., codes) => {
if includes(diagnostic.kind.rule(), &codes) {
continue;
}
}
Directive::None => {}
}
}
// The diagnostic is not ignored by any `noqa` directive; add it to the list.
let lineno = diagnostic.location.row() - 1;
let noqa_lineno = noqa_line_for.get(&(lineno + 1)).unwrap_or(&(lineno + 1)) - 1;
matches_by_line
.entry(noqa_lineno)
.or_default()
.insert(diagnostic.kind.rule());
}
let mut count: usize = 0;
@@ -205,9 +270,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;
}
@@ -228,14 +291,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);
@@ -253,6 +316,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;
@@ -202,6 +203,11 @@ fn is_python_path(path: &Path) -> bool {
.map_or(false, |ext| ext == "py" || ext == "pyi")
}
/// Return `true` if the `Path` appears to be that of a Python interface definition file (`.pyi`).
pub fn is_interface_definition_path(path: &Path) -> bool {
path.extension().map_or(false, |ext| ext == "pyi")
}
/// Return `true` if the `Entry` appears to be that of a Python file.
pub fn is_python_entry(entry: &DirEntry) -> bool {
is_python_path(entry.path())
@@ -217,7 +223,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,14 @@ 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([
// The following are here because we don't yet have the many-to-one mapping enabled.
("SIM111", "SIM110"),
// The following are deprecated.
("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),

View File

@@ -55,7 +55,7 @@ pub fn commented_out_code(
) -> Option<Diagnostic> {
let location = Location::new(start.row(), 0);
let end_location = Location::new(end.row() + 1, 0);
let line = locator.slice_source_code_range(&Range::new(location, end_location));
let line = locator.slice(&Range::new(location, end_location));
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {

View File

@@ -23,7 +23,7 @@ mod tests {
#[test_case(Rule::SysVersionCmpStr10, Path::new("YTT302.py"); "YTT302")]
#[test_case(Rule::SysVersionSlice1Referenced, Path::new("YTT303.py"); "YTT303")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_2020").join(path).as_path(),
&settings::Settings::for_rule(rule_code),

View File

@@ -10,7 +10,7 @@ use crate::source_code::Locator;
/// ANN204
pub fn add_return_none_annotation(locator: &Locator, stmt: &Stmt) -> Result<Fix> {
let range = Range::from_located(stmt);
let contents = locator.slice_source_code_range(&range);
let contents = locator.slice(&range);
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;

View File

@@ -475,9 +475,9 @@ pub fn definition(
// ANN001, ANN401
for arg in args
.args
.posonlyargs
.iter()
.chain(args.posonlyargs.iter())
.chain(args.args.iter())
.chain(args.kwonlyargs.iter())
.skip(
// If this is a non-static method, skip `cls` or `self`.
@@ -581,7 +581,7 @@ pub fn definition(
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(checker, cast::decorator_list(stmt)) {
if let Some(arg) = args.args.first() {
if let Some(arg) = args.posonlyargs.first().or_else(|| args.args.first()) {
if arg.node.annotation.is_none() {
if visibility::is_classmethod(checker, cast::decorator_list(stmt)) {
if checker.settings.rules.enabled(&Rule::MissingTypeCls) {

View File

@@ -244,4 +244,15 @@ expression: diagnostics
column: 15
fix: ~
parent: ~
- kind:
MissingTypeSelf:
name: self
location:
row: 108
column: 12
end_location:
row: 108
column: 16
fix: ~
parent: ~

View File

@@ -35,7 +35,7 @@ mod tests {
#[test_case(Rule::TryExceptPass, Path::new("S110.py"); "S110")]
#[test_case(Rule::TryExceptContinue, Path::new("S112.py"); "S112")]
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("flake8_bandit").join(path).as_path(),
&Settings::for_rule(rule_code),

View File

@@ -17,10 +17,7 @@ impl Violation for HashlibInsecureHashFunction {
#[derive_message_formats]
fn message(&self) -> String {
let HashlibInsecureHashFunction { string } = self;
format!(
"Probable use of insecure hash functions in `hashlib`: \"{}\"",
string.escape_debug()
)
format!("Probable use of insecure hash functions in `hashlib`: `{string}`")
}
}

View File

@@ -14,7 +14,7 @@ mod tests {
#[test_case(Rule::BlindExcept, Path::new("BLE.py"); "BLE001")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_blind_except").join(path).as_path(),
&settings::Settings::for_rule(rule_code),

View File

@@ -16,7 +16,7 @@ mod tests {
#[test_case(Rule::BooleanDefaultValueInFunctionDefinition, Path::new("FBT.py"); "FBT002")]
#[test_case(Rule::BooleanPositionalValueInFunctionCall, Path::new("FBT.py"); "FBT003")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_boolean_trap").join(path).as_path(),
&settings::Settings::for_rule(rule_code),

View File

@@ -40,10 +40,11 @@ mod tests {
#[test_case(Rule::DuplicateTryBlockException, Path::new("B025.py"); "B025")]
#[test_case(Rule::StarArgUnpackingAfterKeywordArg, Path::new("B026.py"); "B026")]
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.py"); "B027")]
#[test_case(Rule::EmptyMethodWithoutAbstractDecorator, Path::new("B027.pyi"); "B027_pyi")]
#[test_case(Rule::RaiseWithoutFromInsideExcept, Path::new("B904.py"); "B904")]
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"); "B905")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_bugbear").join(path).as_path(),
&Settings::for_rule(rule_code),

View File

@@ -3,6 +3,7 @@ use ruff_python::string::is_lower;
use rustpython_parser::ast::{ExprKind, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::ast::visitor;
use crate::ast::visitor::Visitor;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
@@ -44,15 +45,16 @@ impl<'a> Visitor<'a> for RaiseVisitor {
| StmtKind::FunctionDef { .. }
| StmtKind::AsyncFunctionDef { .. }
| StmtKind::Try { .. } => {}
StmtKind::If { body, .. }
| StmtKind::While { body, .. }
StmtKind::If { body, orelse, .. } => {
visitor::walk_body(self, body);
visitor::walk_body(self, orelse);
}
StmtKind::While { body, .. }
| StmtKind::With { body, .. }
| StmtKind::AsyncWith { body, .. }
| StmtKind::For { body, .. }
| StmtKind::AsyncFor { body, .. } => {
for stmt in body {
self.visit_stmt(stmt);
}
visitor::walk_body(self, body);
}
_ => {}
}
@@ -63,8 +65,6 @@ pub fn raise_without_from_inside_except(checker: &mut Checker, body: &[Stmt]) {
let mut visitor = RaiseVisitor {
diagnostics: vec![],
};
for stmt in body {
visitor.visit_stmt(stmt);
}
visitor::walk_body(&mut visitor, body);
checker.diagnostics.extend(visitor.diagnostics);
}

View File

@@ -36,7 +36,8 @@ pub fn strip_with_multi_characters(checker: &mut Checker, expr: &Expr, func: &Ex
return;
};
if value.len() > 1 && value.chars().unique().count() != value.len() {
let num_chars = value.chars().count();
if num_chars > 1 && num_chars != value.chars().unique().count() {
checker.diagnostics.push(Diagnostic::new(
StripWithMultiCharacters,
Range::from_located(expr),

View File

@@ -13,7 +13,7 @@ impl Violation for UnreliableCallableCheck {
#[derive_message_formats]
fn message(&self) -> String {
format!(
" Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use \
"Using `hasattr(x, '__call__')` to test if x is callable is unreliable. Use \
`callable(x)` for consistent results."
)
}

View File

@@ -75,7 +75,7 @@ impl Violation for UnusedLoopControlVariable {
if matches!(certainty, Certainty::Certain) && rename.is_some() {
Some(|UnusedLoopControlVariable { name, rename, .. }| {
let rename = rename.as_ref().unwrap();
format!("Rename unused `{name}` to `_{rename}`")
format!("Rename unused `{name}` to `{rename}`")
})
} else {
None

View File

@@ -19,7 +19,7 @@ impl Violation for UselessContextlibSuppress {
}
}
/// B005
/// B022
pub fn useless_contextlib_suppress(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
if args.is_empty()
&& checker.resolve_call_path(func).map_or(false, |call_path| {

View File

@@ -16,7 +16,7 @@ impl Violation for UselessExpression {
}
}
// B018
/// B018
pub fn useless_expression(checker: &mut Checker, body: &[Stmt]) {
for stmt in body {
if let StmtKind::Expr { value } = &stmt.node {

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_bugbear/mod.rs
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
expression: diagnostics
---
- kind:
@@ -62,4 +62,24 @@ expression: diagnostics
column: 18
fix: ~
parent: ~
- kind:
StripWithMultiCharacters: ~
location:
row: 22
column: 0
end_location:
row: 22
column: 13
fix: ~
parent: ~
- kind:
StripWithMultiCharacters: ~
location:
row: 24
column: 0
end_location:
row: 24
column: 35
fix: ~
parent: ~

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