Compare commits

...

134 Commits

Author SHA1 Message Date
Charlie Marsh
c21a5912b9 Run release on tag creation 2023-02-12 22:33:01 -05:00
Charlie Marsh
48a5cd1dd9 Revert "perf: Use custom allocator (#2768)" (#2841)
This is causing wheel creation to fail on some of our more exotic build targets: https://github.com/charliermarsh/ruff/actions/runs/4159524132.

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

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

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

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

    ## Options

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

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

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

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

The original implementation in flake8-pyi is shown below.

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

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

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

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

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

List all top-level config keys:

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

List all config keys in a specific section:

    $ ruff config mccabe
    max-complexity

Describe a specific config option:

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

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

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

* Windows: mimalloc
* Unix: tikv-jemallocator

## Performance:

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

@@ -1,12 +1,8 @@
name: mkdocs
on:
push:
paths:
- README.md
- mkdocs.template.yml
- .github/workflows/docs.yaml
branches: [main]
release:
types: [published]
workflow_dispatch:
jobs:
@@ -19,7 +15,7 @@ jobs:
- uses: actions/setup-python@v4
- name: "Install dependencies"
run: |
pip install "mkdocs~=1.4.2" "mkdocs-material~=9.0.6"
pip install -r docs/requirements.txt
- name: "Copy README File"
run: |
python scripts/transform_readme.py --target mkdocs

View File

@@ -2,8 +2,9 @@ name: "[ruff] Release"
on:
workflow_dispatch:
release:
types: [published]
push:
tags:
- '**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

2
.gitignore vendored
View File

@@ -3,6 +3,8 @@
crates/ruff/resources/test/cpython
docs/*
!docs/rules
!docs/assets
!docs/requirements.txt
mkdocs.yml
.overrides

View File

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

View File

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

View File

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

900
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,11 @@
members = ["crates/*"]
default-members = ["crates/ruff", "crates/ruff_cli"]
[workspace.dependencies]
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "61b48f108982d865524f86624a9d5bc2ae3bccef" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "61b48f108982d865524f86624a9d5bc2ae3bccef" }
[profile.release]
panic = "abort"
lto = "thin"

27
LICENSE
View File

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

269
README.md
View File

@@ -137,6 +137,7 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/).
1. [flake8-comprehensions (C4)](#flake8-comprehensions-c4)
1. [flake8-datetimez (DTZ)](#flake8-datetimez-dtz)
1. [flake8-debugger (T10)](#flake8-debugger-t10)
1. [flake8-django (DJ)](#flake8-django-dj)
1. [flake8-errmsg (EM)](#flake8-errmsg-em)
1. [flake8-executable (EXE)](#flake8-executable-exe)
1. [flake8-implicit-str-concat (ISC)](#flake8-implicit-str-concat-isc)
@@ -145,6 +146,7 @@ This README is also available as [documentation](https://beta.ruff.rs/docs/).
1. [flake8-no-pep420 (INP)](#flake8-no-pep420-inp)
1. [flake8-pie (PIE)](#flake8-pie-pie)
1. [flake8-print (T20)](#flake8-print-t20)
1. [flake8-pyi (PYI)](#flake8-pyi-pyi)
1. [flake8-pytest-style (PT)](#flake8-pytest-style-pt)
1. [flake8-quotes (Q)](#flake8-quotes-q)
1. [flake8-return (RET)](#flake8-return-ret)
@@ -230,11 +232,22 @@ Ruff also works with [pre-commit](https://pre-commit.com):
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.244'
rev: 'v0.0.246'
hooks:
- id: ruff
```
Or, to enable autofix:
```yaml
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.246'
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
```
<!-- End section: Installation and Usage -->
## Configuration
@@ -392,6 +405,7 @@ Usage: ruff [OPTIONS] <COMMAND>
Commands:
check Run Ruff on the given files or directories (default)
rule Explain a rule
config List or describe the available configuration options
linter List all supported upstream linters
clean Clear any caches in the current directory and any subdirectories
help Print this message or the help of the given subcommand(s)
@@ -425,6 +439,7 @@ Arguments:
Options:
--fix Attempt to automatically fix lint violations
--show-source Show violations with source code
--show-fixes Show an enumeration of all autofixed lint violations
--diff Avoid writing any fixed files back; instead, output a diff for each changed file to stdout
-w, --watch Run in watch mode by re-running whenever files change
--fix-only Fix any fixable lint violations, but don't report on leftover violations. Implies `--fix`
@@ -456,14 +471,6 @@ File selection:
--respect-gitignore Respect file exclusions via `.gitignore` and other standard ignore files
--force-exclude Enforce exclusions, even for paths passed to Ruff directly on the command-line
Rule configuration:
--target-version <TARGET_VERSION>
The minimum Python version that should be supported
--line-length <LINE_LENGTH>
Set the line-length for length-associated rules and automatic formatting
--dummy-variable-rgx <DUMMY_VARIABLE_RGX>
Regular expression matching the name of dummy variables
Miscellaneous:
-n, --no-cache
Disable cache reads
@@ -627,25 +634,44 @@ configuration.
See the [`isort` documentation](https://pycqa.github.io/isort/docs/configuration/action_comments.html)
for more.
#### Exit codes
### Exit codes
By default, Ruff exits with the following status codes:
- `0` if no violations were found, or if all present violations were fixed automatically.
- `1` if violations were found.
- `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an internal error.
* `0` if no violations were found, or if all present violations were fixed automatically.
* `1` if violations were found.
* `2` if Ruff terminates abnormally due to invalid configuration, invalid CLI options, or an internal error.
This convention mirrors that of tools like ESLint, Prettier, and RuboCop.
Ruff supports two command-line flags that alter its exit code behavior:
- `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
* `--exit-zero` will cause Ruff to exit with a status code of `0` even if violations were found.
Note that Ruff will still exit with a status code of `2` if it terminates abnormally.
- `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
* `--exit-non-zero-on-fix` will cause Ruff to exit with a status code of `1` if violations were
found, _even if_ all such violations were fixed automatically. Note that the use of
`--exit-non-zero-on-fix` can result in a non-zero exit code even if no violations remain after
autofixing.
### Autocompletion
Ruff supports autocompletion for most shells. A shell-specific completion script can be generated
by `ruff completion <SHELL>`, where `<SHELL>` is one of `bash`, `elvish`, `fig`, `fish`,
`powershell`, or `zsh`.
The exact steps required to enable autocompletion will vary by shell. For example instructions,
see the [Poetry](https://python-poetry.org/docs/#enable-tab-completion-for-bash-fish-or-zsh) or
[ripgrep](https://github.com/BurntSushi/ripgrep/blob/master/FAQ.md#complete) documentation.
As an example: to enable autocompletion for Zsh, run
`ruff generate-shell-completion zsh > ~/.zfunc/_ruff`. Then add the following line to your
`~/.zshrc` file, if they're not already present:
```zsh
fpath+=~/.zfunc
autoload -Uz compinit && compinit
```
<!-- End section: Configuration -->
## Supported Rules
@@ -671,7 +697,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| ---- | ---- | ------- | --- |
| F401 | unused-import | `{name}` imported but unused; consider adding to `__all__` or using a redundant alias | 🛠 |
| F402 | import-shadowed-by-loop-var | Import `{name}` from line {line} shadowed by loop variable | |
| F403 | import-star-used | `from {name} import *` used; unable to detect undefined names | |
| F403 | import-star | `from {name} import *` used; unable to detect undefined names | |
| F404 | late-future-import | `from __future__` imports must occur at the beginning of the file | |
| F405 | import-star-usage | `{name}` may be undefined, or defined from star imports: {sources} | |
| F406 | import-star-not-permitted | `from {name} import *` only allowed at module level | |
@@ -690,7 +716,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| F523 | string-dot-format-extra-positional-arguments | `.format` call has unused arguments at position(s): {message} | |
| F524 | string-dot-format-missing-arguments | `.format` call is missing argument(s) for placeholder(s): {message} | |
| F525 | string-dot-format-mixing-automatic | `.format` string mixes automatic and manual numbering | |
| F541 | f-string-missing-placeholders | f-string without any placeholders | 🛠 |
| F541 | [f-string-missing-placeholders](https://github.com/charliermarsh/ruff/blob/main/docs/rules/f-string-missing-placeholders.md) | f-string without any placeholders | 🛠 |
| F601 | multi-value-repeated-key-literal | Dictionary key literal `{name}` repeated | 🛠 |
| F602 | multi-value-repeated-key-variable | Dictionary key `{name}` repeated | 🛠 |
| F621 | expressions-in-star-assignment | Too many expressions in star-unpacking assignment | |
@@ -709,7 +735,7 @@ For more, see [Pyflakes](https://pypi.org/project/pyflakes/) on PyPI.
| F821 | undefined-name | Undefined name `{name}` | |
| F822 | undefined-export | Undefined name `{name}` in `__all__` | |
| F823 | undefined-local | Local variable `{name}` referenced before assignment | |
| F841 | unused-variable | Local variable `{name}` is assigned to but never used | 🛠 |
| F841 | [unused-variable](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unused-variable.md) | Local variable `{name}` is assigned to but never used | 🛠 |
| F842 | unused-annotation | Local variable `{name}` is annotated but never used | |
| F901 | raise-not-implemented | `raise NotImplemented` should be `raise NotImplementedError` | 🛠 |
@@ -725,13 +751,16 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/) on PyPI.
| E401 | multiple-imports-on-one-line | Multiple imports on one line | |
| E402 | module-import-not-at-top-of-file | Module level import not at top of file | |
| E501 | line-too-long | Line too long ({length} > {limit} characters) | |
| E701 | multiple-statements-on-one-line-colon | Multiple statements on one line (colon) | |
| E702 | multiple-statements-on-one-line-semicolon | Multiple statements on one line (semicolon) | |
| E703 | useless-semicolon | Statement ends with an unnecessary semicolon | |
| E711 | none-comparison | Comparison to `None` should be `cond is None` | 🛠 |
| E712 | true-false-comparison | Comparison to `True` should be `cond is True` | 🛠 |
| E713 | not-in-test | Test for membership should be `not in` | 🛠 |
| E714 | not-is-test | Test for object identity should be `is not` | 🛠 |
| E721 | type-comparison | Do not compare types, use `isinstance()` | |
| E722 | do-not-use-bare-except | Do not use bare `except` | |
| E731 | do-not-assign-lambda | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E722 | [bare-except](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bare-except.md) | Do not use bare `except` | |
| E731 | lambda-assignment | Do not assign a `lambda` expression, use a `def` | 🛠 |
| E741 | ambiguous-variable-name | Ambiguous variable name: `{name}` | |
| E742 | ambiguous-class-name | Ambiguous class name: `{name}` | |
| E743 | ambiguous-function-name | Ambiguous function name: `{name}` | |
@@ -752,7 +781,7 @@ For more, see [mccabe](https://pypi.org/project/mccabe/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C901 | function-is-too-complex | `{name}` is too complex ({complexity}) | |
| C901 | [complex-structure](https://github.com/charliermarsh/ruff/blob/main/docs/rules/complex-structure.md) | `{name}` is too complex ({complexity}) | |
### isort (I)
@@ -760,8 +789,8 @@ For more, see [isort](https://pypi.org/project/isort/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| I001 | unsorted-imports | Import block is un-sorted or un-formatted | 🛠 |
| I002 | missing-required-import | Missing required import: `{name}` | 🛠 |
| I001 | [unsorted-imports](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unsorted-imports.md) | Import block is un-sorted or un-formatted | 🛠 |
| I002 | [missing-required-import](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-required-import.md) | Missing required import: `{name}` | 🛠 |
### pep8-naming (N)
@@ -815,13 +844,13 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| D213 | multi-line-summary-second-line | Multi-line docstring summary should start at the second line | 🛠 |
| D214 | section-not-over-indented | Section is over-indented ("{name}") | 🛠 |
| D215 | section-underline-not-over-indented | Section underline is over-indented ("{name}") | 🛠 |
| D300 | uses-triple-quotes | Use """triple double quotes""" | |
| D301 | uses-r-prefix-for-backslashed-content | Use r""" if any backslashes in a docstring | |
| D300 | triple-single-quotes | Use """triple double quotes""" | |
| D301 | escape-sequence-in-docstring | Use r""" if any backslashes in a docstring | |
| D400 | ends-in-period | First line should end with a period | 🛠 |
| D401 | non-imperative-mood | First line of docstring should be in imperative mood: "{first_line}" | |
| D402 | no-signature | First line should not be the function's signature | |
| D403 | first-line-capitalized | First word of the first line should be properly capitalized | |
| D404 | no-this-prefix | First word of the docstring should not be "This" | |
| D404 | docstring-starts-with-this | First word of the docstring should not be "This" | |
| D405 | capitalize-section-name | Section name should be properly capitalized ("{name}") | 🛠 |
| D406 | new-line-after-section-name | Section name should end with a newline ("{name}") | 🛠 |
| D407 | dashed-underline-after-section | Missing dashed underline after section ("{name}") | 🛠 |
@@ -831,12 +860,12 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| D411 | blank-line-before-section | Missing blank line before section ("{name}") | 🛠 |
| D412 | no-blank-lines-between-header-and-content | No blank lines allowed between a section header and its content ("{name}") | 🛠 |
| D413 | blank-line-after-last-section | Missing blank line after last section ("{name}") | 🛠 |
| D414 | non-empty-section | Section has no content ("{name}") | |
| D414 | empty-docstring-section | Section has no content ("{name}") | |
| D415 | ends-in-punctuation | First line should end with a period, question mark, or exclamation point | 🛠 |
| D416 | section-name-ends-in-colon | Section name should end with a colon ("{name}") | 🛠 |
| D417 | document-all-arguments | Missing argument description in the docstring: `{name}` | |
| D418 | skip-docstring | Function decorated with `@overload` shouldn't contain a docstring | |
| D419 | non-empty | Docstring is empty | |
| D417 | undocumented-param | Missing argument description in the docstring: `{name}` | |
| D418 | overload-with-docstring | Function decorated with `@overload` shouldn't contain a docstring | |
| D419 | empty-docstring | Docstring is empty | |
### pyupgrade (UP)
@@ -848,10 +877,10 @@ For more, see [pyupgrade](https://pypi.org/project/pyupgrade/) on PyPI.
| UP003 | type-of-primitive | Use `{}` instead of `type(...)` | 🛠 |
| UP004 | useless-object-inheritance | Class `{name}` inherits from `object` | 🛠 |
| UP005 | deprecated-unittest-alias | `{alias}` is deprecated, use `{target}` | 🛠 |
| UP006 | use-pep585-annotation | Use `{}` instead of `{}` for type annotations | 🛠 |
| UP007 | use-pep604-annotation | Use `X \| Y` for type annotations | 🛠 |
| UP006 | deprecated-collection-type | Use `{}` instead of `{}` for type annotations | 🛠 |
| UP007 | typing-union | Use `X \| Y` for type annotations | 🛠 |
| UP008 | super-call-with-parameters | Use `super()` instead of `super(__class__, self)` | 🛠 |
| UP009 | pep3120-unnecessary-coding-comment | UTF-8 encoding declaration is unnecessary | 🛠 |
| UP009 | utf8-encoding-declaration | UTF-8 encoding declaration is unnecessary | 🛠 |
| UP010 | unnecessary-future-import | Unnecessary `__future__` import `{import}` for target Python version | 🛠 |
| UP011 | lru-cache-without-parameters | Unnecessary parameters to `functools.lru_cache` | 🛠 |
| UP012 | unnecessary-encode-utf8 | Unnecessary call to `encode` as UTF-8 | 🛠 |
@@ -903,17 +932,17 @@ For more, see [flake8-annotations](https://pypi.org/project/flake8-annotations/)
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| ANN001 | missing-type-function-argument | Missing type annotation for function argument `{name}` | |
| ANN002 | missing-type-args | Missing type annotation for `*{name}` | |
| ANN003 | missing-type-kwargs | Missing type annotation for `**{name}` | |
| ANN101 | missing-type-self | Missing type annotation for `{name}` in method | |
| ANN102 | missing-type-cls | Missing type annotation for `{name}` in classmethod | |
| ANN201 | missing-return-type-public-function | Missing return type annotation for public function `{name}` | |
| ANN202 | missing-return-type-private-function | Missing return type annotation for private function `{name}` | |
| ANN204 | missing-return-type-special-method | Missing return type annotation for special method `{name}` | 🛠 |
| ANN205 | missing-return-type-static-method | Missing return type annotation for staticmethod `{name}` | |
| ANN206 | missing-return-type-class-method | Missing return type annotation for classmethod `{name}` | |
| ANN401 | dynamically-typed-expression | Dynamically typed expressions (typing.Any) are disallowed in `{name}` | |
| ANN001 | [missing-type-function-argument](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-function-argument.md) | Missing type annotation for function argument `{name}` | |
| ANN002 | [missing-type-args](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-args.md) | Missing type annotation for `*{name}` | |
| ANN003 | [missing-type-kwargs](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-kwargs.md) | Missing type annotation for `**{name}` | |
| ANN101 | [missing-type-self](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-self.md) | Missing type annotation for `{name}` in method | |
| ANN102 | [missing-type-cls](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-type-cls.md) | Missing type annotation for `{name}` in classmethod | |
| ANN201 | [missing-return-type-public-function](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-public-function.md) | Missing return type annotation for public function `{name}` | |
| ANN202 | [missing-return-type-private-function](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-private-function.md) | Missing return type annotation for private function `{name}` | |
| ANN204 | [missing-return-type-special-method](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-special-method.md) | Missing return type annotation for special method `{name}` | 🛠 |
| ANN205 | [missing-return-type-static-method](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-static-method.md) | Missing return type annotation for staticmethod `{name}` | |
| ANN206 | [missing-return-type-class-method](https://github.com/charliermarsh/ruff/blob/main/docs/rules/missing-return-type-class-method.md) | Missing return type annotation for classmethod `{name}` | |
| ANN401 | [any-type](https://github.com/charliermarsh/ruff/blob/main/docs/rules/any-type.md) | Dynamically typed expressions (typing.Any) are disallowed in `{name}` | |
### flake8-bandit (S)
@@ -921,8 +950,8 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| S101 | assert-used | Use of `assert` detected | |
| S102 | exec-used | Use of `exec` detected | |
| S101 | assert | Use of `assert` detected | |
| S102 | exec-builtin | Use of `exec` detected | |
| S103 | bad-file-permissions | `os.chmod` setting a permissive mask `{mask:#o}` on file or directory | |
| S104 | hardcoded-bind-all-interfaces | Possible binding to all interfaces | |
| S105 | hardcoded-password-string | Possible hardcoded password: "{}" | |
@@ -930,12 +959,14 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| S107 | hardcoded-password-default | Possible hardcoded password: "{}" | |
| S108 | hardcoded-temp-file | Probable insecure usage of temporary file or directory: "{}" | |
| S110 | try-except-pass | `try`-`except`-`pass` detected, consider logging the exception | |
| S112 | try-except-continue | `try`-`except`-`continue` detected, consider logging the exception | |
| S113 | request-without-timeout | Probable use of requests call with timeout set to `{value}` | |
| S324 | hashlib-insecure-hash-function | Probable use of insecure hash functions in `hashlib`: "{}" | |
| S501 | request-with-no-cert-validation | Probable use of `{string}` call with `verify=False` disabling SSL certificate checks | |
| S506 | unsafe-yaml-load | Probable use of unsafe loader `{name}` with `yaml.load`. Allows instantiation of arbitrary objects. Consider `yaml.safe_load`. | |
| S508 | snmp-insecure-version | The use of SNMPv1 and SNMPv2 is insecure. Use SNMPv3 if able. | |
| S509 | snmp-weak-cryptography | You should not use SNMPv3 without encryption. `noAuthNoPriv` & `authNoPriv` is insecure. | |
| S608 | [hardcoded-sql-expression](https://github.com/charliermarsh/ruff/blob/main/docs/rules/hardcoded-sql-expression.md) | Possible SQL injection vector through string-based query construction | |
| S612 | logging-config-insecure-listen | Use of insecure `logging.config.listen` detected | |
| S701 | jinja2-autoescape-false | Using jinja2 templates with `autoescape=False` is dangerous and can lead to XSS. Ensure `autoescape=True` or use the `select_autoescape` function. | |
@@ -972,13 +1003,13 @@ For more, see [flake8-bugbear](https://pypi.org/project/flake8-bugbear/) on PyPI
| B008 | function-call-argument-default | Do not perform function call `{name}` in argument defaults | |
| B009 | get-attr-with-constant | Do not call `getattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B010 | set-attr-with-constant | Do not call `setattr` with a constant attribute value. It is not any safer than normal property access. | 🛠 |
| B011 | do-not-assert-false | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B011 | assert-false | Do not `assert False` (`python -O` removes these calls), raise `AssertionError()` | 🛠 |
| B012 | jump-statement-in-finally | `{name}` inside `finally` blocks cause exceptions to be silenced | |
| B013 | redundant-tuple-in-exception-handler | A length-one tuple literal is redundant. Write `except {name}` instead of `except ({name},)`. | 🛠 |
| B014 | duplicate-handler-exception | Exception handler with duplicate exception: `{name}` | 🛠 |
| B015 | useless-comparison | Pointless comparison. This comparison does nothing but waste CPU instructions. Either prepend `assert` or remove it. | |
| B016 | cannot-raise-literal | Cannot raise a literal. Did you intend to return it or raise an Exception? | |
| [B017](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | [assert-raises-exception](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | `assertRaises(Exception)` should be considered evil | |
| B017 | [assert-raises-exception](https://github.com/charliermarsh/ruff/blob/main/docs/rules/assert-raises-exception.md) | `assertRaises(Exception)` should be considered evil | |
| B018 | useless-expression | Found useless expression. Either assign it to a variable or remove it. | |
| B019 | cached-instance-method | Use of `functools.lru_cache` or `functools.cache` on methods can lead to memory leaks | |
| B020 | loop-variable-overrides-iterator | Loop control variable `{name}` overrides iterable it iterates | |
@@ -1018,9 +1049,9 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C400 | unnecessary-generator-list | Unnecessary generator (rewrite as a `list` comprehension) | 🛠 |
| C401 | unnecessary-generator-set | Unnecessary generator (rewrite as a `set` comprehension) | 🛠 |
| C402 | unnecessary-generator-dict | Unnecessary generator (rewrite as a `dict` comprehension) | 🛠 |
| C400 | [unnecessary-generator-list](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unnecessary-generator-list.md) | Unnecessary generator (rewrite as a `list` comprehension) | 🛠 |
| C401 | [unnecessary-generator-set](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unnecessary-generator-set.md) | Unnecessary generator (rewrite as a `set` comprehension) | 🛠 |
| C402 | [unnecessary-generator-dict](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unnecessary-generator-dict.md) | Unnecessary generator (rewrite as a `dict` comprehension) | 🛠 |
| C403 | unnecessary-list-comprehension-set | Unnecessary `list` comprehension (rewrite as a `set` comprehension) | 🛠 |
| C404 | unnecessary-list-comprehension-dict | Unnecessary `list` comprehension (rewrite as a `dict` comprehension) | 🛠 |
| C405 | unnecessary-literal-set | Unnecessary `{obj_type}` literal (rewrite as a `set` literal) | 🛠 |
@@ -1029,11 +1060,11 @@ For more, see [flake8-comprehensions](https://pypi.org/project/flake8-comprehens
| C409 | unnecessary-literal-within-tuple-call | Unnecessary `{literal}` literal passed to `tuple()` (rewrite as a `tuple` literal) | 🛠 |
| C410 | unnecessary-literal-within-list-call | Unnecessary `{literal}` literal passed to `list()` (remove the outer call to `list()`) | 🛠 |
| C411 | unnecessary-list-call | Unnecessary `list` call (remove the outer call to `list()`) | 🛠 |
| C413 | unnecessary-call-around-sorted | Unnecessary `{func}` call around `sorted()` | 🛠 |
| C414 | unnecessary-double-cast-or-process | Unnecessary `{inner}` call within `{outer}()` | |
| C413 | [unnecessary-call-around-sorted](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unnecessary-call-around-sorted.md) | Unnecessary `{func}` call around `sorted()` | 🛠 |
| C414 | [unnecessary-double-cast-or-process](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unnecessary-double-cast-or-process.md) | Unnecessary `{inner}` call within `{outer}()` | 🛠 |
| C415 | unnecessary-subscript-reversal | Unnecessary subscript reversal of iterable within `{func}()` | |
| C416 | unnecessary-comprehension | Unnecessary `{obj_type}` comprehension (rewrite using `{obj_type}()`) | 🛠 |
| C417 | unnecessary-map | Unnecessary `map` usage (rewrite using a generator expression) | |
| C417 | [unnecessary-map](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unnecessary-map.md) | Unnecessary `map` usage (rewrite using a generator expression) | 🛠 |
### flake8-datetimez (DTZ)
@@ -1059,6 +1090,16 @@ For more, see [flake8-debugger](https://pypi.org/project/flake8-debugger/) on Py
| ---- | ---- | ------- | --- |
| T100 | debugger | Trace found: `{name}` used | |
### flake8-django (DJ)
For more, see [flake8-django](https://pypi.org/project/flake8-django/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| DJ001 | [model-string-field-nullable](https://github.com/charliermarsh/ruff/blob/main/docs/rules/model-string-field-nullable.md) | Avoid using `null=True` on string-based fields such as {field_name} | |
| DJ008 | [model-dunder-str](https://github.com/charliermarsh/ruff/blob/main/docs/rules/model-dunder-str.md) | Model does not define `__str__` method | |
| DJ013 | [receiver-decorator-checker](https://github.com/charliermarsh/ruff/blob/main/docs/rules/receiver-decorator-checker.md) | `@receiver` decorator must be on top of all the other decorators | |
### flake8-errmsg (EM)
For more, see [flake8-errmsg](https://pypi.org/project/flake8-errmsg/) on PyPI.
@@ -1097,7 +1138,7 @@ For more, see [flake8-import-conventions](https://github.com/joaopalmeiro/flake8
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [ICN001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | [unconventional-import-alias](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | `{name}` should be imported as `{asname}` | |
| ICN001 | [unconventional-import-alias](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unconventional-import-alias.md) | `{name}` should be imported as `{asname}` | |
### flake8-logging-format (G)
@@ -1120,7 +1161,7 @@ For more, see [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420/) on
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [INP001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | [implicit-namespace-package](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
| INP001 | [implicit-namespace-package](https://github.com/charliermarsh/ruff/blob/main/docs/rules/implicit-namespace-package.md) | File `{filename}` is part of an implicit namespace package. Add an `__init__.py`. | |
### flake8-pie (PIE)
@@ -1128,11 +1169,11 @@ For more, see [flake8-pie](https://pypi.org/project/flake8-pie/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PIE790 | no-unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE790 | unnecessary-pass | Unnecessary `pass` statement | 🛠 |
| PIE794 | dupe-class-field-definitions | Class field `{name}` is defined multiple times | 🛠 |
| PIE796 | prefer-unique-enums | Enum contains duplicate value: `{value}` | |
| PIE800 | no-unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | no-unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE800 | unnecessary-spread | Unnecessary spread `**` | |
| PIE804 | unnecessary-dict-kwargs | Unnecessary `dict` kwargs | |
| PIE807 | prefer-list-builtin | Prefer `list` over useless lambda | 🛠 |
| PIE810 | single-starts-ends-with | Call `{attr}` once with a `tuple` | |
@@ -1145,6 +1186,16 @@ For more, see [flake8-print](https://pypi.org/project/flake8-print/) on PyPI.
| T201 | print-found | `print` found | |
| T203 | p-print-found | `pprint` found | |
### flake8-pyi (PYI)
For more, see [flake8-pyi](https://pypi.org/project/flake8-pyi/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PYI001 | [prefix-type-params](https://github.com/charliermarsh/ruff/blob/main/docs/rules/prefix-type-params.md) | Name of private `{kind}` must start with _ | |
| PYI007 | [unrecognized-platform-check](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unrecognized-platform-check.md) | Unrecognized sys.platform check | |
| PYI008 | [unrecognized-platform-name](https://github.com/charliermarsh/ruff/blob/main/docs/rules/unrecognized-platform-name.md) | Unrecognized platform `{platform}` | |
### flake8-pytest-style (PT)
For more, see [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/) on PyPI.
@@ -1183,10 +1234,10 @@ For more, see [flake8-quotes](https://pypi.org/project/flake8-quotes/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [Q000](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | [bad-quotes-inline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | Double quotes found but single quotes preferred | 🛠 |
| [Q001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | [bad-quotes-multiline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | Double quote multiline found but single quotes preferred | 🛠 |
| [Q002](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | [bad-quotes-docstring](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | Double quote docstring found but single quotes preferred | 🛠 |
| [Q003](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoid-quote-escape.md) | [avoid-quote-escape](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoid-quote-escape.md) | Change outer quotes to avoid escaping inner quotes | 🛠 |
| Q000 | [bad-quotes-inline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-inline-string.md) | Double quotes found but single quotes preferred | 🛠 |
| Q001 | [bad-quotes-multiline-string](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-multiline-string.md) | Double quote multiline found but single quotes preferred | 🛠 |
| Q002 | [bad-quotes-docstring](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-quotes-docstring.md) | Double quote docstring found but single quotes preferred | 🛠 |
| Q003 | [avoidable-escaped-quote](https://github.com/charliermarsh/ruff/blob/main/docs/rules/avoidable-escaped-quote.md) | Change outer quotes to avoid escaping inner quotes | 🛠 |
### flake8-return (RET)
@@ -1210,8 +1261,8 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on Py
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| SIM101 | duplicate-isinstance-call | Multiple `isinstance` calls for `{name}`, merge into a single call | 🛠 |
| SIM102 | nested-if-statements | Use a single `if` statement instead of nested `if` statements | 🛠 |
| SIM103 | return-bool-condition-directly | Return the condition `{cond}` directly | 🛠 |
| SIM102 | collapsible-if | Use a single `if` statement instead of nested `if` statements | 🛠 |
| SIM103 | needless-bool | Return the condition `{condition}` directly | 🛠 |
| SIM105 | use-contextlib-suppress | Use `contextlib.suppress({exception})` instead of try-except-pass | |
| SIM107 | return-in-try-except-finally | Don't use `return` in `try`/`except` and `finally` | |
| SIM108 | use-ternary-operator | Use ternary operator `{contents}` instead of if-else-block | 🛠 |
@@ -1219,6 +1270,7 @@ For more, see [flake8-simplify](https://pypi.org/project/flake8-simplify/) on Py
| SIM110 | convert-loop-to-any | Use `{any}` instead of `for` loop | 🛠 |
| SIM111 | convert-loop-to-all | Use `{all}` instead of `for` loop | 🛠 |
| SIM112 | use-capital-environment-variables | Use capitalized environment variable `{expected}` instead of `{original}` | 🛠 |
| SIM114 | [if-with-same-arms](https://github.com/charliermarsh/ruff/blob/main/docs/rules/if-with-same-arms.md) | Combine `if` branches using logical `or` operator | |
| SIM115 | open-file-with-context-handler | Use context handler for opening files | |
| SIM117 | multiple-with-statements | Use a single `with` statement with multiple contexts instead of nested `with` statements | 🛠 |
| SIM118 | key-in-dict | Use `{key} in {dict}` instead of `{key} in {dict}.keys()` | 🛠 |
@@ -1241,8 +1293,8 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TID251 | banned-api | `{name}` is banned: {message} | |
| TID252 | relative-imports | Relative imports from parent modules are banned | |
| TID251 | [banned-api](https://github.com/charliermarsh/ruff/blob/main/docs/rules/banned-api.md) | `{name}` is banned: {message} | |
| TID252 | [relative-imports](https://github.com/charliermarsh/ruff/blob/main/docs/rules/relative-imports.md) | Relative imports from parent modules are banned | 🛠 |
### flake8-type-checking (TCH)
@@ -1306,7 +1358,7 @@ For more, see [eradicate](https://pypi.org/project/eradicate/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [ERA001](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | [commented-out-code](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | Found commented-out code | 🛠 |
| ERA001 | [commented-out-code](https://github.com/charliermarsh/ruff/blob/main/docs/rules/commented-out-code.md) | Found commented-out code | 🛠 |
### pandas-vet (PD)
@@ -1314,7 +1366,7 @@ For more, see [pandas-vet](https://pypi.org/project/pandas-vet/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PD002 | use-of-inplace-argument | `inplace=True` should be avoided; it has inconsistent behavior | 🛠 |
| PD002 | [use-of-inplace-argument](https://github.com/charliermarsh/ruff/blob/main/docs/rules/use-of-inplace-argument.md) | `inplace=True` should be avoided; it has inconsistent behavior | 🛠 |
| PD003 | use-of-dot-is-null | `.isna` is preferred to `.isnull`; functionality is equivalent | |
| PD004 | use-of-dot-not-null | `.notna` is preferred to `.notnull`; functionality is equivalent | |
| PD007 | use-of-dot-ix | `.ix` is deprecated; use more explicit `.loc` or `.iloc` | |
@@ -1353,13 +1405,15 @@ For more, see [Pylint](https://pypi.org/project/pylint/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| PLE0100 | [yield-in-init](https://github.com/charliermarsh/ruff/blob/main/docs/rules/yield-in-init.md) | `__init__` method is a generator | |
| PLE0117 | nonlocal-without-binding | Nonlocal name `{name}` found without binding | |
| PLE0118 | used-prior-global-declaration | Name `{name}` is used prior to global declaration on line {line} | |
| PLE0604 | invalid-all-object | Invalid object in `__all__`, must contain only strings | |
| PLE0605 | invalid-all-format | Invalid format for `__all__`, must be `tuple` or `list` | |
| PLE1142 | await-outside-async | `await` should be used within an async function | |
| PLE1307 | [bad-string-format-type](https://github.com/charliermarsh/ruff/blob/main/docs/rules/bad-string-format-type.md) | Format type does not match argument type | |
| PLE1310 | bad-str-strip-call | String `{strip}` call contains duplicate characters (did you mean `{removal}`?) | |
| PLE2502 | bidirectional-unicode | Avoid using bidirectional unicode | |
| PLE2502 | bidirectional-unicode | Contains control characters that can permit obfuscated code | |
#### Refactor (PLR)
@@ -1389,7 +1443,7 @@ For more, see [tryceratops](https://pypi.org/project/tryceratops/1.1.0/) on PyPI
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| [TRY002](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | [raise-vanilla-class](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | Create your own exception | |
| TRY002 | [raise-vanilla-class](https://github.com/charliermarsh/ruff/blob/main/docs/rules/raise-vanilla-class.md) | Create your own exception | |
| TRY003 | raise-vanilla-args | Avoid specifying long messages outside the exception class | |
| TRY004 | prefer-type-error | Prefer `TypeError` exception for invalid type | 🛠 |
| TRY200 | reraise-no-cause | Use `raise from` to specify exception cause | |
@@ -1422,7 +1476,7 @@ For more, see [flake8-self](https://pypi.org/project/flake8-self/) on PyPI.
| RUF002 | ambiguous-unicode-character-docstring | Docstring contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF003 | ambiguous-unicode-character-comment | Comment contains ambiguous unicode character '{confusable}' (did you mean '{representant}'?) | 🛠 |
| RUF004 | keyword-argument-before-star-argument | Keyword argument `{name}` must come after starred arguments | |
| RUF005 | unpack-instead-of-concatenating-to-collection-literal | Consider `{expr}` instead of concatenation | |
| RUF005 | unpack-instead-of-concatenating-to-collection-literal | Consider `{expr}` instead of concatenation | 🛠 |
| RUF100 | unused-noqa | Unused blanket `noqa` directive | 🛠 |
<!-- End auto-generated sections. -->
@@ -1694,6 +1748,7 @@ natively, including:
* [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
* [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
* [flake8-debugger](https://pypi.org/project/flake8-debugger/)
* [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
* [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
* [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
* [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
@@ -1704,6 +1759,7 @@ natively, including:
* [flake8-no-pep420](https://pypi.org/project/flake8-no-pep420)
* [flake8-pie](https://pypi.org/project/flake8-pie/)
* [flake8-print](https://pypi.org/project/flake8-print/)
* [flake8-pyi](https://pypi.org/project/flake8-pyi/)
* [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
* [flake8-quotes](https://pypi.org/project/flake8-quotes/)
* [flake8-raise](https://pypi.org/project/flake8-raise/)
@@ -1791,6 +1847,7 @@ Today, Ruff can be used to replace Flake8 when used with any of the following pl
* [flake8-comprehensions](https://pypi.org/project/flake8-comprehensions/)
* [flake8-datetimez](https://pypi.org/project/flake8-datetimez/)
* [flake8-debugger](https://pypi.org/project/flake8-debugger/)
* [flake8-django](https://pypi.org/project/flake8-django/) ([#2817](https://github.com/charliermarsh/ruff/issues/2817))
* [flake8-docstrings](https://pypi.org/project/flake8-docstrings/)
* [flake8-eradicate](https://pypi.org/project/flake8-eradicate/)
* [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
@@ -1975,6 +2032,15 @@ unfixable = ["B", "SIM", "TRY", "RUF"]
If you find a case where Ruff's autofix breaks your code, please file an Issue!
### How can I disable Ruff's color output?
Ruff's color output is powered by the [`colored`](https://crates.io/crates/colored) crate, which
attempts to automatically detect whether the output stream supports color. However, you can force
colors off by setting the `NO_COLOR` environment variable to any value (e.g., `NO_COLOR=1`).
[`colored`](https://crates.io/crates/colored) also supports the the `CLICOLOR` and `CLICOLOR_FORCE`
environment variables (see the [spec](https://bixense.com/clicolors/)).
<!-- End section: FAQ -->
## Contributing
@@ -2502,6 +2568,25 @@ select = ["E", "F", "B", "Q"]
---
#### [`show-fixes`](#show-fixes)
Whether to show an enumeration of all autofixed lint violations
(overridden by the `--show-fixes` command-line flag).
**Default value**: `false`
**Type**: `bool`
**Example usage**:
```toml
[tool.ruff]
# By default, always enumerate fixed violations.
show-fixes = true
```
---
#### [`show-source`](#show-source)
Whether to show source code snippets when reporting lint violations
@@ -3396,7 +3481,7 @@ alias (e.g., `import A as B`) to wrap such that every line contains
exactly one member. For example, this formatting would be retained,
rather than condensing to a single line:
```py
```python
from .utils import (
test_directory as test_directory,
test_id as test_id
@@ -3458,6 +3543,24 @@ known-first-party = ["src"]
---
#### [`known-local-folder`](#known-local-folder)
A list of modules to consider being a local folder.
Generally, this is reserved for relative imports (`from . import module`).
**Default value**: `[]`
**Type**: `list[str]`
**Example usage**:
```toml
[tool.ruff.isort]
known-local-folder = ["src"]
```
---
#### [`known-third-party`](#known-third-party)
A list of modules to consider third-party, regardless of whether they
@@ -3479,7 +3582,7 @@ known-third-party = ["src"]
#### [`lines-after-imports`](#lines-after-imports)
The number of blank lines to place after imports.
-1 for automatic determination.
Use `-1` for automatic determination.
**Default value**: `-1`
@@ -3495,6 +3598,24 @@ lines-after-imports = 1
---
#### [`lines-between-types`](#lines-between-types)
The number of lines to place between "direct" and `import from` imports.
**Default value**: `0`
**Type**: `int`
**Example usage**:
```toml
[tool.ruff.isort]
# Use a single line between direct and from import
lines-between-types = 1
```
---
#### [`no-lines-before`](#no-lines-before)
A list of sections that should _not_ be delineated from the previous

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.244"
version = "0.0.246"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -30,7 +30,7 @@ globset = { version = "0.4.9" }
ignore = { version = "0.4.18" }
imperative = { version = "1.0.3" }
itertools = { version = "0.10.5" }
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "f2f0b7a487a8725d161fe8b3ed73a6758b21e177" }
libcst = { workspace = true }
log = { version = "0.4.17" }
natord = { version = "1.0.9" }
nohash-hasher = { version = "0.2.0" }
@@ -39,11 +39,11 @@ num-traits = "0.2.15"
once_cell = { version = "1.16.0" }
path-absolutize = { version = "3.0.14", features = ["once_cell_cache", "use_unix_paths_on_wasm"] }
regex = { version = "1.6.0" }
ruff_macros = { version = "0.0.244", path = "../ruff_macros" }
ruff_python = { version = "0.0.244", path = "../ruff_python" }
ruff_macros = { version = "0.0.246", path = "../ruff_macros" }
ruff_python = { version = "0.0.246", path = "../ruff_python" }
rustc-hash = { version = "1.1.0" }
rustpython-common = { git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "adc23253e4b58980b407ba2760dbe61681d752fc" }
rustpython-common = { workspace = true }
rustpython-parser = { workspace = true }
schemars = { version = "0.8.11" }
semver = { version = "1.0.16" }
serde = { version = "1.0.147", features = ["derive"] }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -33,16 +33,18 @@ class Foo(metaclass=BazMeta):
def get_bar():
if self.bar._private: # SLF001
return None
if self.bar()._private: # SLF001
return None
return self.bar
def public_func(self):
pass
super().public_func()
def _private_func(self):
pass
super()._private_func()
def __really_private_func(self, arg):
pass
super().__really_private_func(arg)
foo = Foo()
@@ -51,9 +53,11 @@ 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

View File

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

View File

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

View File

@@ -1,12 +1,30 @@
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
# OK
import other
import other.example
from other import example
# TID252
from . import sibling
from .sibling import example
from .. import parent
from ..parent import example
from ... import grandparent
from ...grandparent import example
from .parent import hello
from .\
parent import \
hello_world
from \
..parent\
import \
world_hello
# TID252 (without autofix; too many levels up)
from ..... import ultragrantparent
from ...... import ultragrantparent
from ....... import ultragrantparent
from ......... import ultragrantparent
from ........................... import ultragrantparent
from .....parent import ultragrantparent
from .........parent import ultragrantparent
from ...........................parent import ultragrantparent

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -568,6 +568,17 @@ pub fn collect_arg_names<'a>(arguments: &'a Arguments) -> FxHashSet<&'a str> {
arg_names
}
/// Given an [`Expr`] that can be callable or not (like a decorator, which could
/// be used with or without explicit call syntax), return the underlying
/// callable.
pub fn map_callable(decorator: &Expr) -> &Expr {
if let ExprKind::Call { func, .. } = &decorator.node {
func
} else {
decorator
}
}
/// Returns `true` if a statement or expression includes at least one comment.
pub fn has_comments<T>(located: &Located<T>, locator: &Locator) -> bool {
let start = if match_leading_content(located, locator) {

View File

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

View File

@@ -6,8 +6,6 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use nohash_hasher::IntMap;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_common::cformat::{CFormatError, CFormatErrorType};
use rustpython_parser::ast::{
@@ -17,6 +15,9 @@ use rustpython_parser::ast::{
use rustpython_parser::parser;
use smallvec::smallvec;
use ruff_python::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python::typing::TYPING_EXTENSIONS;
use crate::ast::helpers::{
binding_range, collect_call_path, extract_handler_names, from_relative_import, to_module_path,
};
@@ -30,16 +31,15 @@ use crate::ast::typing::{match_annotated_subscript, Callable, SubscriptKind};
use crate::ast::visitor::{walk_excepthandler, Visitor};
use crate::ast::{branch_detection, cast, helpers, operations, typing, visitor};
use crate::docstrings::definition::{Definition, DefinitionKind, Docstring, Documentable};
use crate::noqa::Directive;
use crate::registry::{Diagnostic, Rule};
use crate::rules::{
flake8_2020, flake8_annotations, flake8_bandit, flake8_blind_except, flake8_boolean_trap,
flake8_bugbear, flake8_builtins, flake8_comprehensions, flake8_datetimez, flake8_debugger,
flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions, flake8_logging_format,
flake8_pie, flake8_print, flake8_pytest_style, flake8_raise, flake8_return, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_type_checking, flake8_unused_arguments,
flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle, pydocstyle, pyflakes,
pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
flake8_django, flake8_errmsg, flake8_implicit_str_concat, flake8_import_conventions,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_raise,
flake8_return, flake8_self, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
flake8_unused_arguments, flake8_use_pathlib, mccabe, pandas_vet, pep8_naming, pycodestyle,
pydocstyle, pyflakes, pygrep_hooks, pylint, pyupgrade, ruff, tryceratops,
};
use crate::settings::types::PythonVersion;
use crate::settings::{flags, Settings};
@@ -282,7 +282,7 @@ impl<'a> Checker<'a> {
}
/// Return `true` if a `Rule` is disabled by a `noqa` directive.
pub fn is_ignored(&self, code: &Rule, lineno: usize) -> bool {
pub fn rule_is_ignored(&self, code: &Rule, lineno: usize) -> bool {
// TODO(charlie): `noqa` directives are mostly enforced in `check_lines.rs`.
// However, in rare cases, we need to check them here. For example, when
// removing unused imports, we create a single fix that's applied to all
@@ -293,16 +293,7 @@ impl<'a> Checker<'a> {
if matches!(self.noqa, flags::Noqa::Disabled) {
return false;
}
let noqa_lineno = self.noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = self.locator.slice_source_code_range(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match noqa::extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => noqa::includes(code, &codes),
}
noqa::rule_is_ignored(code, lineno, self.noqa_line_for, self.locator)
}
}
@@ -473,6 +464,15 @@ 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::AmbiguousFunctionName) {
if let Some(diagnostic) =
pycodestyle::rules::ambiguous_function_name(name, || {
@@ -575,7 +575,7 @@ where
flake8_return::rules::function(self, body);
}
if self.settings.rules.enabled(&Rule::FunctionIsTooComplex) {
if self.settings.rules.enabled(&Rule::ComplexStructure) {
if let Some(diagnostic) = mccabe::rules::function_is_too_complex(
stmt,
name,
@@ -780,6 +780,19 @@ where
decorator_list,
body,
} => {
if self.settings.rules.enabled(&Rule::ModelStringFieldNullable) {
self.diagnostics
.extend(flake8_django::rules::model_string_field_nullable(
self, bases, body,
));
}
if self.settings.rules.enabled(&Rule::ModelDunderStr) {
if let Some(diagnostic) =
flake8_django::rules::model_dunder_str(self, bases, body, stmt)
{
self.diagnostics.push(diagnostic);
}
}
if self.settings.rules.enabled(&Rule::UselessObjectInheritance) {
pyupgrade::rules::useless_object_inheritance(self, stmt, name, bases, keywords);
}
@@ -1235,9 +1248,9 @@ where
}
}
if self.settings.rules.enabled(&Rule::ImportStarUsed) {
if self.settings.rules.enabled(&Rule::ImportStar) {
self.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsed {
pyflakes::rules::ImportStar {
name: helpers::format_import_from(
level.as_ref(),
module.as_deref(),
@@ -1300,9 +1313,12 @@ where
if self.settings.rules.enabled(&Rule::RelativeImports) {
if let Some(diagnostic) =
flake8_tidy_imports::relative_imports::banned_relative_import(
self,
stmt,
level.as_ref(),
module.as_deref(),
&self.settings.flake8_tidy_imports.ban_relative_imports,
self.path,
)
{
self.diagnostics.push(diagnostic);
@@ -1486,7 +1502,7 @@ where
if self.settings.rules.enabled(&Rule::IfTuple) {
pyflakes::rules::if_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::NestedIfStatements) {
if self.settings.rules.enabled(&Rule::CollapsibleIf) {
flake8_simplify::rules::nested_if_statements(
self,
stmt,
@@ -1496,11 +1512,14 @@ where
self.current_stmt_parent().map(Into::into),
);
}
if self
.settings
.rules
.enabled(&Rule::ReturnBoolConditionDirectly)
{
if self.settings.rules.enabled(&Rule::IfWithSameArms) {
flake8_simplify::rules::if_with_same_arms(
self,
stmt,
self.current_stmt_parent().map(std::convert::Into::into),
);
}
if self.settings.rules.enabled(&Rule::NeedlessBool) {
flake8_simplify::rules::return_bool_condition_directly(self, stmt);
}
if self.settings.rules.enabled(&Rule::UseTernaryOperator) {
@@ -1537,7 +1556,7 @@ where
if self.settings.rules.enabled(&Rule::AssertTuple) {
pyflakes::rules::assert_tuple(self, stmt, test);
}
if self.settings.rules.enabled(&Rule::DoNotAssertFalse) {
if self.settings.rules.enabled(&Rule::AssertFalse) {
flake8_bugbear::rules::assert_false(
self,
stmt,
@@ -1545,7 +1564,7 @@ where
msg.as_ref().map(|expr| &**expr),
);
}
if self.settings.rules.enabled(&Rule::AssertUsed) {
if self.settings.rules.enabled(&Rule::Assert) {
self.diagnostics
.push(flake8_bandit::rules::assert_used(stmt));
}
@@ -1704,9 +1723,9 @@ where
}
}
StmtKind::Assign { targets, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
if self.settings.rules.enabled(&Rule::LambdaAssignment) {
if let [target] = &targets[..] {
pycodestyle::rules::do_not_assign_lambda(self, target, value, stmt);
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
}
}
@@ -1722,6 +1741,12 @@ where
}
}
if self.settings.rules.enabled(&Rule::PrefixTypeParams) {
if self.path.extension().map_or(false, |ext| ext == "pyi") {
flake8_pyi::rules::prefix_type_params(self, value, targets);
}
}
if self.settings.rules.enabled(&Rule::UselessMetaclassType) {
pyupgrade::rules::useless_metaclass_type(self, stmt, value, targets);
}
@@ -1754,9 +1779,9 @@ where
}
}
StmtKind::AnnAssign { target, value, .. } => {
if self.settings.rules.enabled(&Rule::DoNotAssignLambda) {
if self.settings.rules.enabled(&Rule::LambdaAssignment) {
if let Some(value) = value {
pycodestyle::rules::do_not_assign_lambda(self, target, value, stmt);
pycodestyle::rules::lambda_assignment(self, target, value, stmt);
}
}
}
@@ -2066,7 +2091,7 @@ where
// Ex) Optional[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP604Annotation)
&& self.settings.rules.enabled(&Rule::TypingUnion)
&& (self.settings.target_version >= PythonVersion::Py310
|| (self.settings.target_version >= PythonVersion::Py37
&& self.annotations_future_enabled
@@ -2121,7 +2146,7 @@ where
// Ex) List[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP585Annotation)
&& self.settings.rules.enabled(&Rule::DeprecatedCollectionType)
&& (self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.annotations_future_enabled
@@ -2166,7 +2191,7 @@ where
// Ex) typing.List[...]
if !self.in_deferred_string_type_definition
&& !self.settings.pyupgrade.keep_runtime_typing
&& self.settings.rules.enabled(&Rule::UsePEP585Annotation)
&& self.settings.rules.enabled(&Rule::DeprecatedCollectionType)
&& (self.settings.target_version >= PythonVersion::Py39
|| (self.settings.target_version >= PythonVersion::Py37
&& self.annotations_future_enabled
@@ -2367,12 +2392,12 @@ where
}
// flake8-pie
if self.settings.rules.enabled(&Rule::NoUnnecessaryDictKwargs) {
if self.settings.rules.enabled(&Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::no_unnecessary_dict_kwargs(self, expr, keywords);
}
// flake8-bandit
if self.settings.rules.enabled(&Rule::ExecUsed) {
if self.settings.rules.enabled(&Rule::ExecBuiltin) {
if let Some(diagnostic) = flake8_bandit::rules::exec_used(expr, func) {
self.diagnostics.push(diagnostic);
}
@@ -2405,6 +2430,9 @@ where
self.diagnostics
.extend(flake8_bandit::rules::hardcoded_password_func_arg(keywords));
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
if self
.settings
.rules
@@ -2435,12 +2463,22 @@ where
}
if self.settings.rules.enabled(&Rule::UnnecessaryGeneratorSet) {
flake8_comprehensions::rules::unnecessary_generator_set(
self, expr, func, args, keywords,
self,
expr,
self.current_expr_parent().map(Into::into),
func,
args,
keywords,
);
}
if self.settings.rules.enabled(&Rule::UnnecessaryGeneratorDict) {
flake8_comprehensions::rules::unnecessary_generator_dict(
self, expr, func, args, keywords,
self,
expr,
self.current_expr_parent().map(Into::into),
func,
args,
keywords,
);
}
if self
@@ -2529,7 +2567,13 @@ where
);
}
if self.settings.rules.enabled(&Rule::UnnecessaryMap) {
flake8_comprehensions::rules::unnecessary_map(self, expr, func, args);
flake8_comprehensions::rules::unnecessary_map(
self,
expr,
self.current_expr_parent().map(Into::into),
func,
args,
);
}
// flake8-boolean-trap
@@ -2782,7 +2826,7 @@ where
pyflakes::rules::repeated_keys(self, keys, values);
}
if self.settings.rules.enabled(&Rule::NoUnnecessarySpread) {
if self.settings.rules.enabled(&Rule::UnnecessarySpread) {
flake8_pie::rules::no_unnecessary_spread(self, keys, values);
}
}
@@ -2790,11 +2834,17 @@ where
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
}
if self.settings.rules.enabled(&Rule::YieldInInit) {
pylint::rules::yield_in_init(self, expr);
}
}
ExprKind::YieldFrom { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
pyflakes::rules::yield_outside_function(self, expr);
}
if self.settings.rules.enabled(&Rule::YieldInInit) {
pylint::rules::yield_in_init(self, expr);
}
}
ExprKind::Await { .. } => {
if self.settings.rules.enabled(&Rule::YieldOutsideFunction) {
@@ -2812,6 +2862,9 @@ where
{
pyflakes::rules::f_string_missing_placeholders(expr, values, self);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
ExprKind::BinOp {
left,
@@ -2973,6 +3026,12 @@ where
if self.settings.rules.enabled(&Rule::PrintfStringFormatting) {
pyupgrade::rules::printf_string_formatting(self, expr, left, right);
}
if self.settings.rules.enabled(&Rule::BadStringFormatType) {
pylint::rules::bad_string_format_type(self, expr, right);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
}
ExprKind::BinOp {
@@ -2994,6 +3053,9 @@ where
{
ruff::rules::unpack_instead_of_concatenating_to_collection_literal(self, expr);
}
if self.settings.rules.enabled(&Rule::HardcodedSQLExpression) {
flake8_bandit::rules::hardcoded_sql_expression(self, expr);
}
}
ExprKind::UnaryOp { op, operand } => {
let check_not_in = self.settings.rules.enabled(&Rule::NotInTest);
@@ -3100,6 +3162,23 @@ where
if self.settings.rules.enabled(&Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(self, expr, left, ops, comparators);
}
if self
.settings
.rules
.enabled(&Rule::UnrecognizedPlatformCheck)
|| self.settings.rules.enabled(&Rule::UnrecognizedPlatformName)
{
if self.path.extension().map_or(false, |ext| ext == "pyi") {
flake8_pyi::rules::unrecognized_platform(
self,
expr,
left,
ops,
comparators,
);
}
}
}
ExprKind::Constant {
value: Constant::Str(value),
@@ -3137,9 +3216,6 @@ where
if self.settings.rules.enabled(&Rule::RewriteUnicodeLiteral) {
pyupgrade::rules::rewrite_unicode_literal(self, expr, kind.as_deref());
}
if self.settings.rules.enabled(&Rule::BidirectionalUnicode) {
pylint::rules::bidirectional_unicode(self, expr, value);
}
}
ExprKind::Lambda { args, body, .. } => {
if self.settings.rules.enabled(&Rule::PreferListBuiltin) {
@@ -3521,8 +3597,8 @@ where
ExcepthandlerKind::ExceptHandler {
type_, name, body, ..
} => {
if self.settings.rules.enabled(&Rule::DoNotUseBareExcept) {
if let Some(diagnostic) = pycodestyle::rules::do_not_use_bare_except(
if self.settings.rules.enabled(&Rule::BareExcept) {
if let Some(diagnostic) = pycodestyle::rules::bare_except(
type_.as_deref(),
body,
excepthandler,
@@ -3549,6 +3625,17 @@ where
if self.settings.rules.enabled(&Rule::TryExceptPass) {
flake8_bandit::rules::try_except_pass(
self,
excepthandler,
type_.as_deref(),
name.as_deref(),
body,
self.settings.flake8_bandit.check_typed_exception,
);
}
if self.settings.rules.enabled(&Rule::TryExceptContinue) {
flake8_bandit::rules::try_except_continue(
self,
excepthandler,
type_.as_deref(),
name.as_deref(),
body,
@@ -3742,7 +3829,7 @@ where
}
fn visit_body(&mut self, body: &'b [Stmt]) {
if self.settings.rules.enabled(&Rule::NoUnnecessaryPass) {
if self.settings.rules.enabled(&Rule::UnnecessaryPass) {
flake8_pie::rules::no_unnecessary_pass(self, body);
}
@@ -3855,6 +3942,14 @@ impl<'a> Checker<'a> {
&self.scopes[*(self.scope_stack.last().expect("No current scope found"))]
}
pub fn current_scope_parent(&self) -> Option<&Scope> {
self.scope_stack
.iter()
.rev()
.nth(1)
.map(|index| &self.scopes[*index])
}
pub fn current_scopes(&self) -> impl Iterator<Item = &Scope> {
self.scope_stack
.iter()
@@ -3870,7 +3965,6 @@ impl<'a> Checker<'a> {
if self.in_type_checking_block
|| self.in_annotation
|| self.in_deferred_string_type_definition
|| self.in_deferred_type_definition
{
ExecutionContext::Typing
} else {
@@ -4304,16 +4398,18 @@ impl<'a> Checker<'a> {
} {
let (all_names, all_names_flags) = extract_all_names(self, parent, current);
if self.settings.rules.enabled(&Rule::InvalidAllFormat)
&& matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT)
{
pylint::rules::invalid_all_format(self, expr);
if self.settings.rules.enabled(&Rule::InvalidAllFormat) {
if matches!(all_names_flags, AllNamesFlags::INVALID_FORMAT) {
self.diagnostics
.push(pylint::rules::invalid_all_format(expr));
}
}
if self.settings.rules.enabled(&Rule::InvalidAllObject)
&& matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT)
{
pylint::rules::invalid_all_object(self, expr);
if self.settings.rules.enabled(&Rule::InvalidAllObject) {
if matches!(all_names_flags, AllNamesFlags::INVALID_OBJECT) {
self.diagnostics
.push(pylint::rules::invalid_all_object(expr));
}
}
self.add_binding(
@@ -4578,11 +4674,54 @@ impl<'a> Checker<'a> {
return;
}
// Mark anything referenced in `__all__` as used.
let global_scope = &self.scopes[GLOBAL_SCOPE_INDEX];
let all_names: Option<(&Vec<String>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some((names, binding.range)),
_ => None,
});
let all_bindings: Option<(Vec<usize>, Range)> = all_names.map(|(names, range)| {
(
names
.iter()
.filter_map(|name| global_scope.bindings.get(name.as_str()).copied())
.collect(),
range,
)
});
if let Some((bindings, range)) = all_bindings {
for index in bindings {
self.bindings[index].mark_used(
GLOBAL_SCOPE_INDEX,
range,
ExecutionContext::Runtime,
);
}
}
// Extract `__all__` names from the global scope.
let all_names: Option<(Vec<&str>, Range)> = global_scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index])
.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => {
Some((names.iter().map(String::as_str).collect(), binding.range))
}
_ => None,
});
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let runtime_imports: Vec<Vec<&Binding>> = if !self.settings.flake8_type_checking.strict
&& (self
let runtime_imports: Vec<Vec<&Binding>> = if self.settings.flake8_type_checking.strict {
vec![]
} else {
if self
.settings
.rules
.enabled(&Rule::RuntimeImportInTypeCheckingBlock)
@@ -4597,29 +4736,41 @@ impl<'a> Checker<'a> {
|| self
.settings
.rules
.enabled(&Rule::TypingOnlyStandardLibraryImport))
{
self.scopes
.iter()
.map(|scope| {
scope
.bindings
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
.enabled(&Rule::TypingOnlyStandardLibraryImport)
{
self.scopes
.iter()
.map(|scope| {
scope
.bindings
.values()
.map(|index| &self.bindings[*index])
.filter(|binding| {
flake8_type_checking::helpers::is_valid_runtime_import(binding)
})
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
} else {
vec![]
}
};
let mut diagnostics: Vec<Diagnostic> = vec![];
for (index, stack) in self.dead_scopes.iter().rev() {
let scope = &self.scopes[*index];
// F822
if *index == GLOBAL_SCOPE_INDEX {
if self.settings.rules.enabled(&Rule::UndefinedExport) {
if let Some((names, range)) = &all_names {
diagnostics.extend(pyflakes::rules::undefined_export(
names, range, self.path, scope,
));
}
}
}
// PLW0602
if self
.settings
@@ -4648,35 +4799,6 @@ impl<'a> Checker<'a> {
continue;
}
let all_binding: Option<&Binding> = scope
.bindings
.get("__all__")
.map(|index| &self.bindings[*index]);
let all_names: Option<Vec<&str>> =
all_binding.and_then(|binding| match &binding.kind {
BindingKind::Export(names) => Some(names.iter().map(String::as_str).collect()),
_ => None,
});
if self.settings.rules.enabled(&Rule::UndefinedExport) {
if !scope.import_starred && !self.path.ends_with("__init__.py") {
if let Some(all_binding) = all_binding {
if let Some(names) = &all_names {
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedExport {
name: name.to_string(),
},
all_binding.range,
));
}
}
}
}
}
}
// Look for any bindings that were redefined in another scope, and remain
// unused. Note that we only store references in `redefinitions` if
// the bindings are in different scopes.
@@ -4692,13 +4814,7 @@ impl<'a> Checker<'a> {
| BindingKind::StarImportation(..)
| BindingKind::FutureImportation
) {
// Skip used exports from `__all__`
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default()
{
if binding.used() {
continue;
}
@@ -4728,31 +4844,27 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::ImportStarUsage) {
if scope.import_starred {
if let Some(all_binding) = all_binding {
if let Some(names) = &all_names {
let mut from_list = vec![];
for binding in
scope.bindings.values().map(|index| &self.bindings[*index])
{
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
if let Some((names, range)) = &all_names {
let mut from_list = vec![];
for binding in scope.bindings.values().map(|index| &self.bindings[*index]) {
if let BindingKind::StarImportation(level, module) = &binding.kind {
from_list.push(helpers::format_import_from(
level.as_ref(),
module.as_deref(),
));
}
from_list.sort();
}
from_list.sort();
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
all_binding.range,
));
}
for &name in names {
if !scope.bindings.contains_key(name) {
diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportStarUsage {
name: name.to_string(),
sources: from_list.clone(),
},
*range,
));
}
}
}
@@ -4820,7 +4932,7 @@ impl<'a> Checker<'a> {
let mut ignored: FxHashMap<BindingContext, Vec<UnusedImport>> =
FxHashMap::default();
for (name, index) in &scope.bindings {
for index in scope.bindings.values() {
let binding = &self.bindings[*index];
let full_name = match &binding.kind {
@@ -4830,13 +4942,7 @@ impl<'a> Checker<'a> {
_ => continue,
};
// Skip used exports from `__all__`
if binding.used()
|| all_names
.as_ref()
.map(|names| names.contains(name))
.unwrap_or_default()
{
if binding.used() {
continue;
}
@@ -4853,9 +4959,9 @@ impl<'a> Checker<'a> {
None
};
if self.is_ignored(&Rule::UnusedImport, diagnostic_lineno)
if self.rule_is_ignored(&Rule::UnusedImport, diagnostic_lineno)
|| parent_lineno.map_or(false, |parent_lineno| {
self.is_ignored(&Rule::UnusedImport, parent_lineno)
self.rule_is_ignored(&Rule::UnusedImport, parent_lineno)
})
{
ignored
@@ -4987,10 +5093,7 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::MissingReturnTypeClassMethod)
|| self
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression);
|| self.settings.rules.enabled(&Rule::AnyType);
let enforce_docstrings = self.settings.rules.enabled(&Rule::PublicModule)
|| self.settings.rules.enabled(&Rule::PublicClass)
|| self.settings.rules.enabled(&Rule::PublicMethod)
@@ -5030,16 +5133,16 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::SectionUnderlineNotOverIndented)
|| self.settings.rules.enabled(&Rule::UsesTripleQuotes)
|| self.settings.rules.enabled(&Rule::TripleSingleQuotes)
|| self
.settings
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
.enabled(&Rule::EscapeSequenceInDocstring)
|| self.settings.rules.enabled(&Rule::EndsInPeriod)
|| self.settings.rules.enabled(&Rule::NonImperativeMood)
|| self.settings.rules.enabled(&Rule::NoSignature)
|| self.settings.rules.enabled(&Rule::FirstLineCapitalized)
|| self.settings.rules.enabled(&Rule::NoThisPrefix)
|| self.settings.rules.enabled(&Rule::DocstringStartsWithThis)
|| self.settings.rules.enabled(&Rule::CapitalizeSectionName)
|| self.settings.rules.enabled(&Rule::NewLineAfterSectionName)
|| self
@@ -5064,12 +5167,12 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::BlankLineAfterLastSection)
|| self.settings.rules.enabled(&Rule::NonEmptySection)
|| self.settings.rules.enabled(&Rule::EmptyDocstringSection)
|| self.settings.rules.enabled(&Rule::EndsInPunctuation)
|| self.settings.rules.enabled(&Rule::SectionNameEndsInColon)
|| self.settings.rules.enabled(&Rule::DocumentAllArguments)
|| self.settings.rules.enabled(&Rule::SkipDocstring)
|| self.settings.rules.enabled(&Rule::NonEmpty);
|| self.settings.rules.enabled(&Rule::UndocumentedParam)
|| self.settings.rules.enabled(&Rule::OverloadWithDocstring)
|| self.settings.rules.enabled(&Rule::EmptyDocstring);
let mut overloaded_name: Option<String> = None;
self.definitions.reverse();
@@ -5178,13 +5281,13 @@ impl<'a> Checker<'a> {
{
pydocstyle::rules::multi_line_summary_start(self, &docstring);
}
if self.settings.rules.enabled(&Rule::UsesTripleQuotes) {
if self.settings.rules.enabled(&Rule::TripleSingleQuotes) {
pydocstyle::rules::triple_quotes(self, &docstring);
}
if self
.settings
.rules
.enabled(&Rule::UsesRPrefixForBackslashedContent)
.enabled(&Rule::EscapeSequenceInDocstring)
{
pydocstyle::rules::backslashes(self, &docstring);
}
@@ -5200,13 +5303,13 @@ impl<'a> Checker<'a> {
if self.settings.rules.enabled(&Rule::FirstLineCapitalized) {
pydocstyle::rules::capitalized(self, &docstring);
}
if self.settings.rules.enabled(&Rule::NoThisPrefix) {
if self.settings.rules.enabled(&Rule::DocstringStartsWithThis) {
pydocstyle::rules::starts_with_this(self, &docstring);
}
if self.settings.rules.enabled(&Rule::EndsInPunctuation) {
pydocstyle::rules::ends_with_punctuation(self, &docstring);
}
if self.settings.rules.enabled(&Rule::SkipDocstring) {
if self.settings.rules.enabled(&Rule::OverloadWithDocstring) {
pydocstyle::rules::if_needed(self, &docstring);
}
if self
@@ -5242,9 +5345,9 @@ impl<'a> Checker<'a> {
.settings
.rules
.enabled(&Rule::BlankLineAfterLastSection)
|| self.settings.rules.enabled(&Rule::NonEmptySection)
|| self.settings.rules.enabled(&Rule::EmptyDocstringSection)
|| self.settings.rules.enabled(&Rule::SectionNameEndsInColon)
|| self.settings.rules.enabled(&Rule::DocumentAllArguments)
|| self.settings.rules.enabled(&Rule::UndocumentedParam)
{
pydocstyle::rules::sections(
self,

View File

@@ -11,6 +11,7 @@ use crate::rules::pycodestyle::rules::{
doc_line_too_long, line_too_long, mixed_spaces_and_tabs, no_newline_at_end_of_file,
};
use crate::rules::pygrep_hooks::rules::{blanket_noqa, blanket_type_ignore};
use crate::rules::pylint;
use crate::rules::pyupgrade::rules::unnecessary_coding_comment;
use crate::settings::{flags, Settings};
use crate::source_code::Stylist;
@@ -37,15 +38,12 @@ pub fn check_physical_lines(
let enforce_doc_line_too_long = settings.rules.enabled(&Rule::DocLineTooLong);
let enforce_line_too_long = settings.rules.enabled(&Rule::LineTooLong);
let enforce_no_newline_at_end_of_file = settings.rules.enabled(&Rule::NoNewLineAtEndOfFile);
let enforce_unnecessary_coding_comment = settings
.rules
.enabled(&Rule::PEP3120UnnecessaryCodingComment);
let enforce_unnecessary_coding_comment = settings.rules.enabled(&Rule::UTF8EncodingDeclaration);
let enforce_mixed_spaces_and_tabs = settings.rules.enabled(&Rule::MixedSpacesAndTabs);
let enforce_bidirectional_unicode = settings.rules.enabled(&Rule::BidirectionalUnicode);
let fix_unnecessary_coding_comment = matches!(autofix, flags::Autofix::Enabled)
&& settings
.rules
.should_fix(&Rule::PEP3120UnnecessaryCodingComment);
&& settings.rules.should_fix(&Rule::UTF8EncodingDeclaration);
let fix_shebang_whitespace = matches!(autofix, flags::Autofix::Enabled)
&& settings.rules.should_fix(&Rule::ShebangWhitespace);
@@ -137,6 +135,10 @@ pub fn check_physical_lines(
diagnostics.push(diagnostic);
}
}
if enforce_bidirectional_unicode {
diagnostics.extend(pylint::rules::bidirectional_unicode(index, line));
}
}
if enforce_no_newline_at_end_of_file {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -127,6 +127,7 @@ pub fn defaultSettings() -> Result<JsValue, JsValue> {
per_file_ignores: None,
required_version: None,
respect_gitignore: None,
show_fixes: None,
show_source: None,
src: None,
task_tags: None,

View File

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

View File

@@ -7,10 +7,12 @@ use nohash_hasher::IntMap;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::{FxHashMap, FxHashSet};
use rustpython_parser::ast::Location;
use crate::ast::types::Range;
use crate::registry::{Diagnostic, Rule};
use crate::rule_redirects::get_redirect_target;
use crate::source_code::LineEnding;
use crate::source_code::{LineEnding, Locator};
static NOQA_LINE_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -76,6 +78,25 @@ pub fn includes(needle: &Rule, haystack: &[&str]) -> bool {
.any(|candidate| needle == get_redirect_target(candidate).unwrap_or(candidate))
}
/// Returns `true` if the given [`Rule`] is ignored at the specified `lineno`.
pub fn rule_is_ignored(
code: &Rule,
lineno: usize,
noqa_line_for: &IntMap<usize, usize>,
locator: &Locator,
) -> bool {
let noqa_lineno = noqa_line_for.get(&lineno).unwrap_or(&lineno);
let line = locator.slice_source_code_range(&Range::new(
Location::new(*noqa_lineno, 0),
Location::new(noqa_lineno + 1, 0),
));
match extract_noqa_directive(line) {
Directive::None => false,
Directive::All(..) => true,
Directive::Codes(.., codes) => includes(code, &codes),
}
}
pub fn add_noqa(
path: &Path,
diagnostics: &[Diagnostic],

View File

@@ -60,13 +60,16 @@ ruff_macros::define_rule_mapping!(
E401 => rules::pycodestyle::rules::MultipleImportsOnOneLine,
E402 => rules::pycodestyle::rules::ModuleImportNotAtTopOfFile,
E501 => rules::pycodestyle::rules::LineTooLong,
E701 => rules::pycodestyle::rules::MultipleStatementsOnOneLineColon,
E702 => rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon,
E703 => rules::pycodestyle::rules::UselessSemicolon,
E711 => rules::pycodestyle::rules::NoneComparison,
E712 => rules::pycodestyle::rules::TrueFalseComparison,
E713 => rules::pycodestyle::rules::NotInTest,
E714 => rules::pycodestyle::rules::NotIsTest,
E721 => rules::pycodestyle::rules::TypeComparison,
E722 => rules::pycodestyle::rules::DoNotUseBareExcept,
E731 => rules::pycodestyle::rules::DoNotAssignLambda,
E722 => rules::pycodestyle::rules::BareExcept,
E731 => rules::pycodestyle::rules::LambdaAssignment,
E741 => rules::pycodestyle::rules::AmbiguousVariableName,
E742 => rules::pycodestyle::rules::AmbiguousClassName,
E743 => rules::pycodestyle::rules::AmbiguousFunctionName,
@@ -79,7 +82,7 @@ ruff_macros::define_rule_mapping!(
// pyflakes
F401 => rules::pyflakes::rules::UnusedImport,
F402 => rules::pyflakes::rules::ImportShadowedByLoopVar,
F403 => rules::pyflakes::rules::ImportStarUsed,
F403 => rules::pyflakes::rules::ImportStar,
F404 => rules::pyflakes::rules::LateFutureImport,
F405 => rules::pyflakes::rules::ImportStarUsage,
F406 => rules::pyflakes::rules::ImportStarNotPermitted,
@@ -121,8 +124,10 @@ ruff_macros::define_rule_mapping!(
F842 => rules::pyflakes::rules::UnusedAnnotation,
F901 => rules::pyflakes::rules::RaiseNotImplemented,
// pylint
PLE0100 => rules::pylint::rules::YieldInInit,
PLE0604 => rules::pylint::rules::InvalidAllObject,
PLE0605 => rules::pylint::rules::InvalidAllFormat,
PLE1307 => rules::pylint::rules::BadStringFormatType,
PLE2502 => rules::pylint::rules::BidirectionalUnicode,
PLE1310 => rules::pylint::rules::BadStrStripCall,
PLC0414 => rules::pylint::rules::UselessImportAlias,
@@ -156,7 +161,7 @@ ruff_macros::define_rule_mapping!(
B008 => rules::flake8_bugbear::rules::FunctionCallArgumentDefault,
B009 => rules::flake8_bugbear::rules::GetAttrWithConstant,
B010 => rules::flake8_bugbear::rules::SetAttrWithConstant,
B011 => rules::flake8_bugbear::rules::DoNotAssertFalse,
B011 => rules::flake8_bugbear::rules::AssertFalse,
B012 => rules::flake8_bugbear::rules::JumpStatementInFinally,
B013 => rules::flake8_bugbear::rules::RedundantTupleInExceptionHandler,
B014 => rules::flake8_bugbear::rules::DuplicateHandlerException,
@@ -197,7 +202,7 @@ ruff_macros::define_rule_mapping!(
// flake8-debugger
T100 => rules::flake8_debugger::rules::Debugger,
// mccabe
C901 => rules::mccabe::rules::FunctionIsTooComplex,
C901 => rules::mccabe::rules::ComplexStructure,
// flake8-tidy-imports
TID251 => rules::flake8_tidy_imports::banned_api::BannedApi,
TID252 => rules::flake8_tidy_imports::relative_imports::RelativeImports,
@@ -221,7 +226,7 @@ ruff_macros::define_rule_mapping!(
Q000 => rules::flake8_quotes::rules::BadQuotesInlineString,
Q001 => rules::flake8_quotes::rules::BadQuotesMultilineString,
Q002 => rules::flake8_quotes::rules::BadQuotesDocstring,
Q003 => rules::flake8_quotes::rules::AvoidQuoteEscape,
Q003 => rules::flake8_quotes::rules::AvoidableEscapedQuote,
// flake8-annotations
ANN001 => rules::flake8_annotations::rules::MissingTypeFunctionArgument,
ANN002 => rules::flake8_annotations::rules::MissingTypeArgs,
@@ -233,7 +238,7 @@ ruff_macros::define_rule_mapping!(
ANN204 => rules::flake8_annotations::rules::MissingReturnTypeSpecialMethod,
ANN205 => rules::flake8_annotations::rules::MissingReturnTypeStaticMethod,
ANN206 => rules::flake8_annotations::rules::MissingReturnTypeClassMethod,
ANN401 => rules::flake8_annotations::rules::DynamicallyTypedExpression,
ANN401 => rules::flake8_annotations::rules::AnyType,
// flake8-2020
YTT101 => rules::flake8_2020::rules::SysVersionSlice3Referenced,
YTT102 => rules::flake8_2020::rules::SysVersion2Referenced,
@@ -246,10 +251,9 @@ ruff_macros::define_rule_mapping!(
YTT302 => rules::flake8_2020::rules::SysVersionCmpStr10,
YTT303 => rules::flake8_2020::rules::SysVersionSlice1Referenced,
// flake8-simplify
SIM115 => rules::flake8_simplify::rules::OpenFileWithContextHandler,
SIM101 => rules::flake8_simplify::rules::DuplicateIsinstanceCall,
SIM102 => rules::flake8_simplify::rules::NestedIfStatements,
SIM103 => rules::flake8_simplify::rules::ReturnBoolConditionDirectly,
SIM102 => rules::flake8_simplify::rules::CollapsibleIf,
SIM103 => rules::flake8_simplify::rules::NeedlessBool,
SIM105 => rules::flake8_simplify::rules::UseContextlibSuppress,
SIM107 => rules::flake8_simplify::rules::ReturnInTryExceptFinally,
SIM108 => rules::flake8_simplify::rules::UseTernaryOperator,
@@ -257,6 +261,8 @@ ruff_macros::define_rule_mapping!(
SIM110 => rules::flake8_simplify::rules::ConvertLoopToAny,
SIM111 => rules::flake8_simplify::rules::ConvertLoopToAll,
SIM112 => rules::flake8_simplify::rules::UseCapitalEnvironmentVariables,
SIM114 => rules::flake8_simplify::rules::IfWithSameArms,
SIM115 => rules::flake8_simplify::rules::OpenFileWithContextHandler,
SIM117 => rules::flake8_simplify::rules::MultipleWithStatements,
SIM118 => rules::flake8_simplify::rules::KeyInDict,
SIM201 => rules::flake8_simplify::rules::NegateEqualOp,
@@ -276,10 +282,10 @@ ruff_macros::define_rule_mapping!(
UP003 => rules::pyupgrade::rules::TypeOfPrimitive,
UP004 => rules::pyupgrade::rules::UselessObjectInheritance,
UP005 => rules::pyupgrade::rules::DeprecatedUnittestAlias,
UP006 => rules::pyupgrade::rules::UsePEP585Annotation,
UP007 => rules::pyupgrade::rules::UsePEP604Annotation,
UP006 => rules::pyupgrade::rules::DeprecatedCollectionType,
UP007 => rules::pyupgrade::rules::TypingUnion,
UP008 => rules::pyupgrade::rules::SuperCallWithParameters,
UP009 => rules::pyupgrade::rules::PEP3120UnnecessaryCodingComment,
UP009 => rules::pyupgrade::rules::UTF8EncodingDeclaration,
UP010 => rules::pyupgrade::rules::UnnecessaryFutureImport,
UP011 => rules::pyupgrade::rules::LRUCacheWithoutParameters,
UP012 => rules::pyupgrade::rules::UnnecessaryEncodeUTF8,
@@ -332,13 +338,13 @@ ruff_macros::define_rule_mapping!(
D213 => rules::pydocstyle::rules::MultiLineSummarySecondLine,
D214 => rules::pydocstyle::rules::SectionNotOverIndented,
D215 => rules::pydocstyle::rules::SectionUnderlineNotOverIndented,
D300 => rules::pydocstyle::rules::UsesTripleQuotes,
D301 => rules::pydocstyle::rules::UsesRPrefixForBackslashedContent,
D300 => rules::pydocstyle::rules::TripleSingleQuotes,
D301 => rules::pydocstyle::rules::EscapeSequenceInDocstring,
D400 => rules::pydocstyle::rules::EndsInPeriod,
D401 => rules::pydocstyle::rules::NonImperativeMood,
D402 => rules::pydocstyle::rules::NoSignature,
D403 => rules::pydocstyle::rules::FirstLineCapitalized,
D404 => rules::pydocstyle::rules::NoThisPrefix,
D404 => rules::pydocstyle::rules::DocstringStartsWithThis,
D405 => rules::pydocstyle::rules::CapitalizeSectionName,
D406 => rules::pydocstyle::rules::NewLineAfterSectionName,
D407 => rules::pydocstyle::rules::DashedUnderlineAfterSection,
@@ -348,12 +354,12 @@ ruff_macros::define_rule_mapping!(
D411 => rules::pydocstyle::rules::BlankLineBeforeSection,
D412 => rules::pydocstyle::rules::NoBlankLinesBetweenHeaderAndContent,
D413 => rules::pydocstyle::rules::BlankLineAfterLastSection,
D414 => rules::pydocstyle::rules::NonEmptySection,
D414 => rules::pydocstyle::rules::EmptyDocstringSection,
D415 => rules::pydocstyle::rules::EndsInPunctuation,
D416 => rules::pydocstyle::rules::SectionNameEndsInColon,
D417 => rules::pydocstyle::rules::DocumentAllArguments,
D418 => rules::pydocstyle::rules::SkipDocstring,
D419 => rules::pydocstyle::rules::NonEmpty,
D417 => rules::pydocstyle::rules::UndocumentedParam,
D418 => rules::pydocstyle::rules::OverloadWithDocstring,
D419 => rules::pydocstyle::rules::EmptyDocstring,
// pep8-naming
N801 => rules::pep8_naming::rules::InvalidClassName,
N802 => rules::pep8_naming::rules::InvalidFunctionName,
@@ -376,15 +382,17 @@ ruff_macros::define_rule_mapping!(
// eradicate
ERA001 => rules::eradicate::rules::CommentedOutCode,
// flake8-bandit
S101 => rules::flake8_bandit::rules::AssertUsed,
S102 => rules::flake8_bandit::rules::ExecUsed,
S101 => rules::flake8_bandit::rules::Assert,
S102 => rules::flake8_bandit::rules::ExecBuiltin,
S103 => rules::flake8_bandit::rules::BadFilePermissions,
S104 => rules::flake8_bandit::rules::HardcodedBindAllInterfaces,
S105 => rules::flake8_bandit::rules::HardcodedPasswordString,
S106 => rules::flake8_bandit::rules::HardcodedPasswordFuncArg,
S107 => rules::flake8_bandit::rules::HardcodedPasswordDefault,
S608 => rules::flake8_bandit::rules::HardcodedSQLExpression,
S108 => rules::flake8_bandit::rules::HardcodedTempFile,
S110 => rules::flake8_bandit::rules::TryExceptPass,
S112 => rules::flake8_bandit::rules::TryExceptContinue,
S113 => rules::flake8_bandit::rules::RequestWithoutTimeout,
S324 => rules::flake8_bandit::rules::HashlibInsecureHashFunction,
S501 => rules::flake8_bandit::rules::RequestWithNoCertValidation,
@@ -437,6 +445,10 @@ ruff_macros::define_rule_mapping!(
EM101 => rules::flake8_errmsg::rules::RawStringInException,
EM102 => rules::flake8_errmsg::rules::FStringInException,
EM103 => rules::flake8_errmsg::rules::DotFormatInException,
// flake8-pyi
PYI001 => rules::flake8_pyi::rules::PrefixTypeParams,
PYI007 => rules::flake8_pyi::rules::UnrecognizedPlatformCheck,
PYI008 => rules::flake8_pyi::rules::UnrecognizedPlatformName,
// flake8-pytest-style
PT001 => rules::flake8_pytest_style::rules::IncorrectFixtureParenthesesStyle,
PT002 => rules::flake8_pytest_style::rules::FixturePositionalArgs,
@@ -464,11 +476,11 @@ ruff_macros::define_rule_mapping!(
PT025 => rules::flake8_pytest_style::rules::ErroneousUseFixturesOnFixture,
PT026 => rules::flake8_pytest_style::rules::UseFixturesWithoutParameters,
// flake8-pie
PIE790 => rules::flake8_pie::rules::NoUnnecessaryPass,
PIE790 => rules::flake8_pie::rules::UnnecessaryPass,
PIE794 => rules::flake8_pie::rules::DupeClassFieldDefinitions,
PIE796 => rules::flake8_pie::rules::PreferUniqueEnums,
PIE800 => rules::flake8_pie::rules::NoUnnecessarySpread,
PIE804 => rules::flake8_pie::rules::NoUnnecessaryDictKwargs,
PIE800 => rules::flake8_pie::rules::UnnecessarySpread,
PIE804 => rules::flake8_pie::rules::UnnecessaryDictKwargs,
PIE807 => rules::flake8_pie::rules::PreferListBuiltin,
PIE810 => rules::flake8_pie::rules::SingleStartsEndsWith,
// flake8-commas
@@ -544,6 +556,10 @@ ruff_macros::define_rule_mapping!(
RUF004 => rules::ruff::rules::KeywordArgumentBeforeStarArgument,
RUF005 => rules::ruff::rules::UnpackInsteadOfConcatenatingToCollectionLiteral,
RUF100 => rules::ruff::rules::UnusedNOQA,
// flake8-django
DJ001 => rules::flake8_django::rules::ModelStringFieldNullable,
DJ008 => rules::flake8_django::rules::ModelDunderStr,
DJ013 => rules::flake8_django::rules::ReceiverDecoratorChecker,
);
#[derive(EnumIter, Debug, PartialEq, Eq, RuleNamespace)]
@@ -603,6 +619,9 @@ pub enum Linter {
/// [flake8-debugger](https://pypi.org/project/flake8-debugger/)
#[prefix = "T10"]
Flake8Debugger,
/// [flake8-django](https://pypi.org/project/flake8-django/)
#[prefix = "DJ"]
Flake8Django,
/// [flake8-errmsg](https://pypi.org/project/flake8-errmsg/)
#[prefix = "EM"]
Flake8ErrMsg,
@@ -627,6 +646,9 @@ pub enum Linter {
/// [flake8-print](https://pypi.org/project/flake8-print/)
#[prefix = "T20"]
Flake8Print,
/// [flake8-pyi](https://pypi.org/project/flake8-pyi/)
#[prefix = "PYI"]
Flake8Pyi,
/// [flake8-pytest-style](https://pypi.org/project/flake8-pytest-style/)
#[prefix = "PT"]
Flake8PytestStyle,
@@ -738,16 +760,17 @@ impl Rule {
| Rule::LineTooLong
| Rule::MixedSpacesAndTabs
| Rule::NoNewLineAtEndOfFile
| Rule::PEP3120UnnecessaryCodingComment
| Rule::UTF8EncodingDeclaration
| Rule::ShebangMissingExecutableFile
| Rule::ShebangNotExecutable
| Rule::ShebangNewline
| Rule::BidirectionalUnicode
| Rule::ShebangPython
| Rule::ShebangWhitespace => &LintSource::PhysicalLines,
Rule::AmbiguousUnicodeCharacterComment
| Rule::AmbiguousUnicodeCharacterDocstring
| Rule::AmbiguousUnicodeCharacterString
| Rule::AvoidQuoteEscape
| Rule::AvoidableEscapedQuote
| Rule::BadQuotesDocstring
| Rule::BadQuotesInlineString
| Rule::BadQuotesMultilineString
@@ -758,6 +781,9 @@ impl Rule {
| Rule::SingleLineImplicitStringConcatenation
| Rule::TrailingCommaMissing
| Rule::TrailingCommaOnBareTupleProhibited
| Rule::MultipleStatementsOnOneLineColon
| Rule::UselessSemicolon
| Rule::MultipleStatementsOnOneLineSemicolon
| Rule::TrailingCommaProhibited => &LintSource::Tokens,
Rule::IOError => &LintSource::Io,
Rule::UnsortedImports | Rule::MissingRequiredImport => &LintSource::Imports,
@@ -843,6 +869,28 @@ mod tests {
use super::{Linter, Rule, RuleNamespace};
#[test]
fn test_rule_naming_convention() {
// The disallowed rule names are defined in a separate file so that they can also be picked up by add_rule.py.
let patterns: Vec<_> = include_str!("../resources/test/disallowed_rule_names.txt")
.trim()
.split('\n')
.map(|line| {
glob::Pattern::new(line).expect("malformed pattern in disallowed_rule_names.txt")
})
.collect();
for rule in Rule::iter() {
let rule_name = rule.as_ref();
for pattern in &patterns {
assert!(
!pattern.matches(rule_name),
"{rule_name} does not match naming convention, see CONTRIBUTING.md"
);
}
}
}
#[test]
fn check_code_serialization() {
for rule in Rule::iter() {
@@ -858,7 +906,7 @@ mod tests {
for rule in Rule::iter() {
let code = rule.code();
let (linter, rest) =
Linter::parse_code(code).unwrap_or_else(|| panic!("couldn't parse {:?}", code));
Linter::parse_code(code).unwrap_or_else(|| panic!("couldn't parse {code:?}"));
assert_eq!(code, format!("{}{rest}", linter.common_prefix()));
}
}

View File

@@ -10,14 +10,14 @@ use crate::source_code::Locator;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for commented-out Python code.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// Commented-out code is dead code, and is often included inadvertently.
/// It should be removed.
///
/// ### Example
/// ## Example
/// ```python
/// # print('foo')
/// ```

View File

@@ -31,7 +31,7 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
@@ -59,7 +59,7 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
@@ -131,7 +131,7 @@ mod tests {
Rule::MissingReturnTypeSpecialMethod,
Rule::MissingReturnTypeStaticMethod,
Rule::MissingReturnTypeClassMethod,
Rule::DynamicallyTypedExpression,
Rule::AnyType,
])
},
)?;
@@ -148,7 +148,7 @@ mod tests {
allow_star_arg_any: true,
..Default::default()
},
..Settings::for_rules(vec![Rule::DynamicallyTypedExpression])
..Settings::for_rules(vec![Rule::AnyType])
},
)?;
assert_yaml_snapshot!(diagnostics);

View File

@@ -16,6 +16,25 @@ use crate::visibility;
use crate::visibility::Visibility;
define_violation!(
/// ## What it does
/// Checks that function arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// ## Example
/// ```python
/// def foo(x):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(x: int):
/// ...
/// ```
pub struct MissingTypeFunctionArgument {
pub name: String,
}
@@ -29,6 +48,25 @@ impl Violation for MissingTypeFunctionArgument {
}
define_violation!(
/// ## What it does
/// Checks that function `*args` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// ## Example
/// ```python
/// def foo(*args):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(*args: int):
/// ...
/// ```
pub struct MissingTypeArgs {
pub name: String,
}
@@ -42,6 +80,25 @@ impl Violation for MissingTypeArgs {
}
define_violation!(
/// ## What it does
/// Checks that function `**kwargs` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// ## Example
/// ```python
/// def foo(**kwargs):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(**kwargs: int):
/// ...
/// ```
pub struct MissingTypeKwargs {
pub name: String,
}
@@ -55,6 +112,30 @@ impl Violation for MissingTypeKwargs {
}
define_violation!(
/// ## What it does
/// Checks that instance method `self` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// Note that many type checkers will infer the type of `self` automatically, so this
/// annotation is not strictly necessary.
///
/// ## Example
/// ```python
/// class Foo:
/// def bar(self):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def bar(self: "Foo"):
/// ...
/// ```
pub struct MissingTypeSelf {
pub name: String,
}
@@ -68,6 +149,32 @@ impl Violation for MissingTypeSelf {
}
define_violation!(
/// ## What it does
/// Checks that class method `cls` arguments have type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the types of function arguments. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any provided arguments match expectation.
///
/// Note that many type checkers will infer the type of `cls` automatically, so this
/// annotation is not strictly necessary.
///
/// ## Example
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls: Type["Foo"]):
/// ...
/// ```
pub struct MissingTypeCls {
pub name: String,
}
@@ -81,6 +188,25 @@ impl Violation for MissingTypeCls {
}
define_violation!(
/// ## What it does
/// Checks that public functions and methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// def add(a, b):
/// return a + b
/// ```
///
/// Use instead:
/// ```python
/// def add(a: int, b: int) -> int:
/// return a + b
/// ```
pub struct MissingReturnTypePublicFunction {
pub name: String,
}
@@ -94,6 +220,25 @@ impl Violation for MissingReturnTypePublicFunction {
}
define_violation!(
/// ## What it does
/// Checks that private functions and methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// def _add(a, b):
/// return a + b
/// ```
///
/// Use instead:
/// ```python
/// def _add(a: int, b: int) -> int:
/// return a + b
/// ```
pub struct MissingReturnTypePrivateFunction {
pub name: String,
}
@@ -107,6 +252,38 @@ impl Violation for MissingReturnTypePrivateFunction {
}
define_violation!(
/// ## What it does
/// Checks that "special" methods, like `__init__`, `__new__`, and `__call__`, have
/// return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// Note that type checkers often allow you to omit the return type annotation for
/// `__init__` methods, as long as at least one argument has a type annotation. To
/// opt-in to this behavior, use the `mypy-init-return` setting in your `pyproject.toml`
/// or `ruff.toml` file:
///
/// ```toml
/// [tool.ruff.flake8-annotations]
/// mypy-init-return = true
/// ```
///
/// ## Example
/// ```python
/// class Foo:
/// def __init__(self, x: int):
/// self.x = x
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// def __init__(self, x: int) -> None:
/// self.x = x
/// ```
pub struct MissingReturnTypeSpecialMethod {
pub name: String,
}
@@ -124,6 +301,29 @@ impl AlwaysAutofixableViolation for MissingReturnTypeSpecialMethod {
}
define_violation!(
/// ## What it does
/// Checks that static methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// class Foo:
/// @staticmethod
/// def bar():
/// return 1
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// @staticmethod
/// def bar() -> int:
/// return 1
/// ```
pub struct MissingReturnTypeStaticMethod {
pub name: String,
}
@@ -137,6 +337,29 @@ impl Violation for MissingReturnTypeStaticMethod {
}
define_violation!(
/// ## What it does
/// Checks that class methods have return type annotations.
///
/// ## Why is this bad?
/// Type annotations are a good way to document the return types of functions. They also
/// help catch bugs, when used alongside a type checker, by ensuring that the types of
/// any returned values, and the types expected by callers, match expectation.
///
/// ## Example
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls):
/// return 1
/// ```
///
/// Use instead:
/// ```python
/// class Foo:
/// @classmethod
/// def bar(cls) -> int:
/// return 1
/// ```
pub struct MissingReturnTypeClassMethod {
pub name: String,
}
@@ -150,14 +373,42 @@ impl Violation for MissingReturnTypeClassMethod {
}
define_violation!(
pub struct DynamicallyTypedExpression {
/// ## What it does
/// Checks that an expression is annotated with a more specific type than
/// `Any`.
///
/// ## Why is this bad?
/// `Any` is a special type indicating an unconstrained type. When an
/// expression is annotated with type `Any`, type checkers will allow all
/// operations on it.
///
/// It's better to be explicit about the type of an expression, and to use
/// `Any` as an "escape hatch" only when it is really needed.
///
/// ## Example
/// ```python
/// def foo(x: Any):
/// ...
/// ```
///
/// Use instead:
/// ```python
/// def foo(x: int):
/// ...
/// ```
///
/// ## References
/// * [PEP 484](https://www.python.org/dev/peps/pep-0484/#the-any-type)
/// * [`typing.Any`](https://docs.python.org/3/library/typing.html#typing.Any)
/// * [Mypy: The Any type](https://mypy.readthedocs.io/en/stable/kinds_of_types.html#the-any-type)
pub struct AnyType {
pub name: String,
}
);
impl Violation for DynamicallyTypedExpression {
impl Violation for AnyType {
#[derive_message_formats]
fn message(&self) -> String {
let DynamicallyTypedExpression { name } = self;
let AnyType { name } = self;
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
}
@@ -192,7 +443,7 @@ fn check_dynamically_typed<F>(
{
if checker.match_typing_expr(annotation, "Any") {
diagnostics.push(Diagnostic::new(
DynamicallyTypedExpression { name: func() },
AnyType { name: func() },
Range::from_located(annotation),
));
};
@@ -238,11 +489,7 @@ pub fn definition(
// ANN401 for dynamically typed arguments
if let Some(annotation) = &arg.node.annotation {
has_any_typed_arg = true;
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
check_dynamically_typed(
checker,
annotation,
@@ -275,11 +522,7 @@ pub fn definition(
if let Some(expr) = &arg.node.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
let name = &arg.node.arg;
check_dynamically_typed(
checker,
@@ -310,11 +553,7 @@ pub fn definition(
if let Some(expr) = &arg.node.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
let name = &arg.node.arg;
check_dynamically_typed(
checker,
@@ -372,11 +611,7 @@ pub fn definition(
// ANN201, ANN202, ANN401
if let Some(expr) = &returns {
has_typed_return = true;
if checker
.settings
.rules
.enabled(&Rule::DynamicallyTypedExpression)
{
if checker.settings.rules.enabled(&Rule::AnyType) {
check_dynamically_typed(checker, expr, || name.to_string(), &mut diagnostics);
}
} else if !(

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_annotations/mod.rs
source: crates/ruff/src/rules/flake8_annotations/mod.rs
expression: diagnostics
---
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 10
@@ -14,7 +14,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo
location:
row: 15
@@ -25,7 +25,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 40
@@ -36,7 +36,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo_method
location:
row: 44

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_annotations/mod.rs
source: crates/ruff/src/rules/flake8_annotations/mod.rs
expression: diagnostics
---
- kind:
@@ -91,7 +91,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 44
@@ -102,7 +102,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo
location:
row: 49
@@ -113,7 +113,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*args"
location:
row: 54
@@ -124,7 +124,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**kwargs"
location:
row: 54
@@ -135,7 +135,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*args"
location:
row: 59
@@ -146,7 +146,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**kwargs"
location:
row: 64
@@ -168,7 +168,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: a
location:
row: 78
@@ -179,7 +179,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: foo
location:
row: 82
@@ -190,7 +190,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*params"
location:
row: 86
@@ -201,7 +201,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**options"
location:
row: 86
@@ -212,7 +212,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "*params"
location:
row: 90
@@ -223,7 +223,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
DynamicallyTypedExpression:
AnyType:
name: "**options"
location:
row: 94

View File

@@ -1,5 +1,7 @@
use rustpython_parser::ast::{Constant, Expr, ExprKind};
use crate::checkers::ast::Checker;
const PASSWORD_NAMES: [&str; 7] = [
"password", "pass", "passwd", "pwd", "secret", "token", "secrete",
];
@@ -20,3 +22,21 @@ pub fn matches_password_name(string: &str) -> bool {
.iter()
.any(|name| string.to_lowercase().contains(name))
}
pub fn is_untyped_exception(type_: Option<&Expr>, checker: &Checker) -> bool {
type_.map_or(true, |type_| {
if let ExprKind::Tuple { elts, .. } = &type_.node {
elts.iter().any(|type_| {
checker.resolve_call_path(type_).map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
})
} else {
checker.resolve_call_path(type_).map_or(false, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
}
})
}

View File

@@ -15,13 +15,14 @@ mod tests {
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::AssertUsed, Path::new("S101.py"); "S101")]
#[test_case(Rule::ExecUsed, Path::new("S102.py"); "S102")]
#[test_case(Rule::Assert, Path::new("S101.py"); "S101")]
#[test_case(Rule::ExecBuiltin, Path::new("S102.py"); "S102")]
#[test_case(Rule::BadFilePermissions, Path::new("S103.py"); "S103")]
#[test_case(Rule::HardcodedBindAllInterfaces, Path::new("S104.py"); "S104")]
#[test_case(Rule::HardcodedPasswordString, Path::new("S105.py"); "S105")]
#[test_case(Rule::HardcodedPasswordFuncArg, Path::new("S106.py"); "S106")]
#[test_case(Rule::HardcodedPasswordDefault, Path::new("S107.py"); "S107")]
#[test_case(Rule::HardcodedSQLExpression, Path::new("S608.py"); "S608")]
#[test_case(Rule::HardcodedTempFile, Path::new("S108.py"); "S108")]
#[test_case(Rule::RequestWithoutTimeout, Path::new("S113.py"); "S113")]
#[test_case(Rule::HashlibInsecureHashFunction, Path::new("S324.py"); "S324")]
@@ -32,6 +33,7 @@ mod tests {
#[test_case(Rule::LoggingConfigInsecureListen, Path::new("S612.py"); "S612")]
#[test_case(Rule::Jinja2AutoescapeFalse, Path::new("S701.py"); "S701")]
#[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 diagnostics = test_path(

View File

@@ -6,9 +6,9 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct AssertUsed;
pub struct Assert;
);
impl Violation for AssertUsed {
impl Violation for Assert {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `assert` detected")
@@ -18,7 +18,7 @@ impl Violation for AssertUsed {
/// S101
pub fn assert_used(stmt: &Located<StmtKind>) -> Diagnostic {
Diagnostic::new(
AssertUsed,
Assert,
Range::new(stmt.location, stmt.location.with_col_offset("assert".len())),
)
}

View File

@@ -6,9 +6,9 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
pub struct ExecUsed;
pub struct ExecBuiltin;
);
impl Violation for ExecUsed {
impl Violation for ExecBuiltin {
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of `exec` detected")
@@ -23,5 +23,5 @@ pub fn exec_used(expr: &Expr, func: &Expr) -> Option<Diagnostic> {
if id != "exec" {
return None;
}
Some(Diagnostic::new(ExecUsed, Range::from_located(expr)))
Some(Diagnostic::new(ExecBuiltin, Range::from_located(expr)))
}

View File

@@ -0,0 +1,105 @@
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, ExprKind, Operator};
use super::super::helpers::string_literal;
use crate::ast::helpers::{any_over_expr, unparse_expr};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
static SQL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?i)(select\s.*from\s|delete\s+from\s|insert\s+into\s.*values\s|update\s.*set\s)")
.unwrap()
});
define_violation!(
/// ## What it does
/// Checks for strings that resemble SQL statements involved in some form
/// string building operation.
///
/// ## Why is this bad?
/// SQL injection is a common attack vector for web applications. Directly
/// interpolating user input into SQL statements should always be avoided.
/// Instead, favor parameterized queries, in which the SQL statement is
/// provided separately from its parameters, as supported by `psycopg3`
/// and other database drivers and ORMs.
///
/// ## Example
/// ```python
/// query = "DELETE FROM foo WHERE id = '%s'" % identifier
/// ```
///
/// ## References
/// * [B608: Test for SQL injection](https://bandit.readthedocs.io/en/latest/plugins/b608_hardcoded_sql_expressions.html)
/// * [psycopg3: Server-side binding](https://www.psycopg.org/psycopg3/docs/basic/from_pg2.html#server-side-binding)
pub struct HardcodedSQLExpression;
);
impl Violation for HardcodedSQLExpression {
#[derive_message_formats]
fn message(&self) -> String {
format!("Possible SQL injection vector through string-based query construction")
}
}
fn has_string_literal(expr: &Expr) -> bool {
string_literal(expr).is_some()
}
fn matches_sql_statement(string: &str) -> bool {
SQL_REGEX.is_match(string)
}
fn unparse_string_format_expression(checker: &mut Checker, expr: &Expr) -> Option<String> {
match &expr.node {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
ExprKind::BinOp {
op: Operator::Add | Operator::Mod,
..
} => {
let Some(parent) = checker.current_expr_parent() else {
if any_over_expr(expr, &has_string_literal) {
return Some(unparse_expr(expr, checker.stylist));
}
return None;
};
// Only evaluate the full BinOp, not the nested components.
let ExprKind::BinOp { .. } = &parent.node else {
if any_over_expr(expr, &has_string_literal) {
return Some(unparse_expr(expr, checker.stylist));
}
return None;
};
None
}
ExprKind::Call { func, .. } => {
let ExprKind::Attribute{ attr, value, .. } = &func.node else {
return None;
};
// "select * from table where val = {}".format(...)
if attr == "format" && string_literal(value).is_some() {
return Some(unparse_expr(expr, checker.stylist));
};
None
}
// f"select * from table where val = {val}"
ExprKind::JoinedStr { .. } => Some(unparse_expr(expr, checker.stylist)),
_ => None,
}
}
/// S608
pub fn hardcoded_sql_expression(checker: &mut Checker, expr: &Expr) {
match unparse_string_format_expression(checker, expr) {
Some(string) if matches_sql_statement(&string) => {
checker.diagnostics.push(Diagnostic::new(
HardcodedSQLExpression,
Range::from_located(expr),
));
}
_ => (),
}
}

View File

@@ -1,6 +1,6 @@
pub use assert_used::{assert_used, AssertUsed};
pub use assert_used::{assert_used, Assert};
pub use bad_file_permissions::{bad_file_permissions, BadFilePermissions};
pub use exec_used::{exec_used, ExecUsed};
pub use exec_used::{exec_used, ExecBuiltin};
pub use hardcoded_bind_all_interfaces::{
hardcoded_bind_all_interfaces, HardcodedBindAllInterfaces,
};
@@ -9,6 +9,7 @@ pub use hardcoded_password_func_arg::{hardcoded_password_func_arg, HardcodedPass
pub use hardcoded_password_string::{
assign_hardcoded_password_string, compare_to_hardcoded_password_string, HardcodedPasswordString,
};
pub use hardcoded_sql_expression::{hardcoded_sql_expression, HardcodedSQLExpression};
pub use hardcoded_tmp_directory::{hardcoded_tmp_directory, HardcodedTempFile};
pub use hashlib_insecure_hash_functions::{
hashlib_insecure_hash_functions, HashlibInsecureHashFunction,
@@ -23,6 +24,7 @@ pub use request_with_no_cert_validation::{
pub use request_without_timeout::{request_without_timeout, RequestWithoutTimeout};
pub use snmp_insecure_version::{snmp_insecure_version, SnmpInsecureVersion};
pub use snmp_weak_cryptography::{snmp_weak_cryptography, SnmpWeakCryptography};
pub use try_except_continue::{try_except_continue, TryExceptContinue};
pub use try_except_pass::{try_except_pass, TryExceptPass};
pub use unsafe_yaml_load::{unsafe_yaml_load, UnsafeYAMLLoad};
@@ -33,6 +35,7 @@ mod hardcoded_bind_all_interfaces;
mod hardcoded_password_default;
mod hardcoded_password_func_arg;
mod hardcoded_password_string;
mod hardcoded_sql_expression;
mod hardcoded_tmp_directory;
mod hashlib_insecure_hash_functions;
mod jinja2_autoescape_false;
@@ -41,5 +44,6 @@ mod request_with_no_cert_validation;
mod request_without_timeout;
mod snmp_insecure_version;
mod snmp_weak_cryptography;
mod try_except_continue;
mod try_except_pass;
mod unsafe_yaml_load;

View File

@@ -0,0 +1,39 @@
use rustpython_parser::ast::{Excepthandler, Expr, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::rules::flake8_bandit::helpers::is_untyped_exception;
use crate::violation::Violation;
define_violation!(
pub struct TryExceptContinue;
);
impl Violation for TryExceptContinue {
#[derive_message_formats]
fn message(&self) -> String {
format!("`try`-`except`-`continue` detected, consider logging the exception")
}
}
/// S112
pub fn try_except_continue(
checker: &mut Checker,
excepthandler: &Excepthandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
check_typed_exception: bool,
) {
if body.len() == 1
&& body[0].node == StmtKind::Continue
&& (check_typed_exception || is_untyped_exception(type_, checker))
{
checker.diagnostics.push(Diagnostic::new(
TryExceptContinue,
Range::from_located(excepthandler),
));
}
}

View File

@@ -1,9 +1,11 @@
use rustpython_parser::ast::{Excepthandler, Expr, Stmt, StmtKind};
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, Stmt, StmtKind};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::rules::flake8_bandit::helpers::is_untyped_exception;
use crate::violation::Violation;
define_violation!(
@@ -19,6 +21,7 @@ impl Violation for TryExceptPass {
/// S110
pub fn try_except_pass(
checker: &mut Checker,
excepthandler: &Excepthandler,
type_: Option<&Expr>,
_name: Option<&str>,
body: &[Stmt],
@@ -26,17 +29,11 @@ pub fn try_except_pass(
) {
if body.len() == 1
&& body[0].node == StmtKind::Pass
&& (check_typed_exception
|| type_.map_or(true, |type_| {
checker.resolve_call_path(type_).map_or(true, |call_path| {
call_path.as_slice() == ["", "Exception"]
|| call_path.as_slice() == ["", "BaseException"]
})
}))
&& (check_typed_exception || is_untyped_exception(type_, checker))
{
checker.diagnostics.push(Diagnostic::new(
TryExceptPass,
Range::from_located(&body[0]),
Range::from_located(excepthandler),
));
}
}

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
AssertUsed: ~
Assert: ~
location:
row: 2
column: 0
@@ -13,7 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
AssertUsed: ~
Assert: ~
location:
row: 8
column: 4
@@ -23,7 +23,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
AssertUsed: ~
Assert: ~
location:
row: 11
column: 4

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
ExecUsed: ~
ExecBuiltin: ~
location:
row: 3
column: 4
@@ -13,7 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
ExecUsed: ~
ExecBuiltin: ~
location:
row: 5
column: 0

View File

@@ -1,12 +1,12 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
TryExceptPass: ~
location:
row: 4
column: 4
row: 3
column: 0
end_location:
row: 4
column: 8
@@ -15,8 +15,8 @@ expression: diagnostics
- kind:
TryExceptPass: ~
location:
row: 9
column: 4
row: 8
column: 0
end_location:
row: 9
column: 8

View File

@@ -1,12 +1,12 @@
---
source: src/rules/flake8_bandit/mod.rs
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
TryExceptPass: ~
location:
row: 4
column: 4
row: 3
column: 0
end_location:
row: 4
column: 8
@@ -15,8 +15,8 @@ expression: diagnostics
- kind:
TryExceptPass: ~
location:
row: 9
column: 4
row: 8
column: 0
end_location:
row: 9
column: 8
@@ -25,8 +25,8 @@ expression: diagnostics
- kind:
TryExceptPass: ~
location:
row: 14
column: 4
row: 13
column: 0
end_location:
row: 14
column: 8

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
TryExceptContinue: ~
location:
row: 3
column: 0
end_location:
row: 4
column: 12
fix: ~
parent: ~
- kind:
TryExceptContinue: ~
location:
row: 8
column: 0
end_location:
row: 9
column: 12
fix: ~
parent: ~
- kind:
TryExceptContinue: ~
location:
row: 13
column: 0
end_location:
row: 14
column: 12
fix: ~
parent: ~
- kind:
TryExceptContinue: ~
location:
row: 18
column: 0
end_location:
row: 19
column: 12
fix: ~
parent: ~

View File

@@ -0,0 +1,455 @@
---
source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
HardcodedSQLExpression: ~
location:
row: 2
column: 9
end_location:
row: 2
column: 40
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 3
column: 9
end_location:
row: 3
column: 35
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 4
column: 9
end_location:
row: 4
column: 43
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 5
column: 9
end_location:
row: 5
column: 44
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 6
column: 9
end_location:
row: 6
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 8
column: 9
end_location:
row: 8
column: 52
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 9
column: 9
end_location:
row: 9
column: 47
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 10
column: 9
end_location:
row: 10
column: 54
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 11
column: 9
end_location:
row: 11
column: 55
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 12
column: 10
end_location:
row: 12
column: 48
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 14
column: 10
end_location:
row: 14
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 15
column: 10
end_location:
row: 15
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 16
column: 10
end_location:
row: 16
column: 57
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 17
column: 10
end_location:
row: 17
column: 51
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 19
column: 10
end_location:
row: 19
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 20
column: 10
end_location:
row: 20
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 21
column: 10
end_location:
row: 21
column: 53
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 22
column: 10
end_location:
row: 22
column: 43
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 24
column: 10
end_location:
row: 24
column: 41
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 25
column: 10
end_location:
row: 25
column: 36
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 26
column: 10
end_location:
row: 26
column: 44
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 27
column: 10
end_location:
row: 27
column: 45
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 28
column: 10
end_location:
row: 28
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 30
column: 10
end_location:
row: 30
column: 53
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 31
column: 10
end_location:
row: 31
column: 48
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 32
column: 10
end_location:
row: 32
column: 55
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 33
column: 10
end_location:
row: 33
column: 56
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 34
column: 10
end_location:
row: 34
column: 48
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 36
column: 10
end_location:
row: 36
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 37
column: 10
end_location:
row: 37
column: 50
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 38
column: 10
end_location:
row: 38
column: 57
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 39
column: 10
end_location:
row: 39
column: 51
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 41
column: 10
end_location:
row: 41
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 42
column: 10
end_location:
row: 42
column: 49
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 43
column: 10
end_location:
row: 43
column: 53
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 44
column: 10
end_location:
row: 44
column: 43
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 48
column: 11
end_location:
row: 52
column: 13
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 55
column: 11
end_location:
row: 59
column: 13
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 62
column: 11
end_location:
row: 66
column: 19
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 69
column: 11
end_location:
row: 73
column: 7
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 77
column: 8
end_location:
row: 79
column: 28
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 83
column: 25
end_location:
row: 83
column: 67
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 84
column: 25
end_location:
row: 84
column: 65
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 85
column: 25
end_location:
row: 85
column: 73
fix: ~
parent: ~
- kind:
HardcodedSQLExpression: ~
location:
row: 86
column: 29
end_location:
row: 86
column: 71
fix: ~
parent: ~

View File

@@ -23,7 +23,7 @@ mod tests {
#[test_case(Rule::FunctionCallArgumentDefault, Path::new("B006_B008.py"); "B008")]
#[test_case(Rule::GetAttrWithConstant, Path::new("B009_B010.py"); "B009")]
#[test_case(Rule::SetAttrWithConstant, Path::new("B009_B010.py"); "B010")]
#[test_case(Rule::DoNotAssertFalse, Path::new("B011.py"); "B011")]
#[test_case(Rule::AssertFalse, Path::new("B011.py"); "B011")]
#[test_case(Rule::JumpStatementInFinally, Path::new("B012.py"); "B012")]
#[test_case(Rule::RedundantTupleInExceptionHandler, Path::new("B013.py"); "B013")]
#[test_case(Rule::DuplicateHandlerException, Path::new("B014.py"); "B014")]

View File

@@ -9,9 +9,9 @@ use crate::registry::Diagnostic;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
pub struct DoNotAssertFalse;
pub struct AssertFalse;
);
impl AlwaysAutofixableViolation for DoNotAssertFalse {
impl AlwaysAutofixableViolation for AssertFalse {
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not `assert False` (`python -O` removes these calls), raise `AssertionError()`")
@@ -61,7 +61,7 @@ pub fn assert_false(checker: &mut Checker, stmt: &Stmt, test: &Expr, msg: Option
return;
};
let mut diagnostic = Diagnostic::new(DoNotAssertFalse, Range::from_located(test));
let mut diagnostic = Diagnostic::new(AssertFalse, Range::from_located(test));
if checker.patch(diagnostic.kind.rule()) {
diagnostic.amend(Fix::replacement(
unparse_stmt(&assertion_error(msg), checker.stylist),

View File

@@ -7,17 +7,17 @@ use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
/// ### What it does
/// ## What it does
/// Checks for `self.assertRaises(Exception)`.
///
/// ### Why is this bad?
/// ## Why is this bad?
/// `assertRaises(Exception)` can lead to your test passing even if the
/// code being tested is never executed due to a typo.
///
/// Either assert for a more specific exception (builtin or custom), use
/// `assertRaisesRegex` or the context manager form of `assertRaises`.
///
/// ### Example
/// ## Example
/// ```python
/// self.assertRaises(Exception, foo)
/// ```

View File

@@ -2,7 +2,7 @@ pub use abstract_base_class::{
abstract_base_class, AbstractBaseClassWithoutAbstractMethod,
EmptyMethodWithoutAbstractDecorator,
};
pub use assert_false::{assert_false, DoNotAssertFalse};
pub use assert_false::{assert_false, AssertFalse};
pub use assert_raises_exception::{assert_raises_exception, AssertRaisesException};
pub use assignment_to_os_environ::{assignment_to_os_environ, AssignmentToOsEnviron};
pub use cached_instance_method::{cached_instance_method, CachedInstanceMethod};

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_bugbear/mod.rs
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
expression: diagnostics
---
- kind:
DoNotAssertFalse: ~
AssertFalse: ~
location:
row: 8
column: 7
@@ -21,7 +21,7 @@ expression: diagnostics
column: 12
parent: ~
- kind:
DoNotAssertFalse: ~
AssertFalse: ~
location:
row: 10
column: 7

View File

@@ -1,9 +1,10 @@
use anyhow::{bail, Result};
use itertools::Itertools;
use libcst_native::{
Arg, AssignEqual, Call, Codegen, CodegenState, Dict, DictComp, DictElement, Element, Expr,
Expression, LeftCurlyBrace, LeftParen, LeftSquareBracket, List, ListComp, Name,
ParenthesizableWhitespace, RightCurlyBrace, RightParen, RightSquareBracket, Set, SetComp,
SimpleString, SimpleWhitespace, Tuple,
Arg, AssignEqual, AssignTargetExpression, Call, Codegen, CodegenState, CompFor, Dict, DictComp,
DictElement, Element, Expr, Expression, GeneratorExp, LeftCurlyBrace, LeftParen,
LeftSquareBracket, List, ListComp, Name, ParenthesizableWhitespace, RightCurlyBrace,
RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace, Tuple,
};
use crate::ast::types::Range;
@@ -78,6 +79,7 @@ pub fn fix_unnecessary_generator_set(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_parser::ast::Expr,
parent: Option<&rustpython_parser::ast::Expr>,
) -> Result<Fix> {
// Expr(Call(GeneratorExp)))) -> Expr(SetComp)))
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
@@ -112,8 +114,18 @@ pub fn fix_unnecessary_generator_set(
};
tree.codegen(&mut state);
let mut content = state.to_string();
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if let Some(parent_element) = parent {
if let &rustpython_parser::ast::ExprKind::FormattedValue { .. } = &parent_element.node {
content = format!(" {content} ");
}
}
Ok(Fix::replacement(
state.to_string(),
content,
expr.location,
expr.end_location.unwrap(),
))
@@ -125,6 +137,7 @@ pub fn fix_unnecessary_generator_dict(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_parser::ast::Expr,
parent: Option<&rustpython_parser::ast::Expr>,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
let mut tree = match_module(module_text)?;
@@ -175,8 +188,18 @@ pub fn fix_unnecessary_generator_dict(
};
tree.codegen(&mut state);
let mut content = state.to_string();
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if let Some(parent_element) = parent {
if let &rustpython_parser::ast::ExprKind::FormattedValue { .. } = &parent_element.node {
content = format!(" {content} ");
}
}
Ok(Fix::replacement(
state.to_string(),
content,
expr.location,
expr.end_location.unwrap(),
))
@@ -742,6 +765,7 @@ pub fn fix_unnecessary_call_around_sorted(
if outer_name.value == "list" {
body.value = Expression::Call(inner_call.clone());
} else {
// If the `reverse` argument is used
let args = if inner_call.args.iter().any(|arg| {
matches!(
arg.keyword,
@@ -751,7 +775,46 @@ pub fn fix_unnecessary_call_around_sorted(
})
)
}) {
inner_call.args.clone()
// Negate the `reverse` argument
inner_call
.args
.clone()
.into_iter()
.map(|mut arg| {
if matches!(
arg.keyword,
Some(Name {
value: "reverse",
..
})
) {
if let Expression::Name(ref val) = arg.value {
if val.value == "True" {
// TODO: even better would be to drop the argument, as False is the default
arg.value = Expression::Name(Box::new(Name {
value: "False",
lpar: vec![],
rpar: vec![],
}));
arg
} else if val.value == "False" {
arg.value = Expression::Name(Box::new(Name {
value: "True",
lpar: vec![],
rpar: vec![],
}));
arg
} else {
arg
}
} else {
arg
}
} else {
arg
}
})
.collect_vec()
} else {
let mut args = inner_call.args.clone();
args.push(Arg {
@@ -802,6 +865,45 @@ pub fn fix_unnecessary_call_around_sorted(
))
}
/// (C414) Convert `sorted(list(foo))` to `sorted(foo)`
pub fn fix_unnecessary_double_cast_or_process(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_parser::ast::Expr,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
let mut tree = match_module(module_text)?;
let body = match_expr(&mut tree)?;
let mut outer_call = match_call(body)?;
let inner_call = match &outer_call.args[..] {
[arg] => {
if let Expression::Call(call) = &arg.value {
&call.args
} else {
bail!("Expected Expression::Call ");
}
}
_ => {
bail!("Expected one argument in outer function call");
}
};
outer_call.args = inner_call.clone();
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
Ok(Fix::replacement(
state.to_string(),
expr.location,
expr.end_location.unwrap(),
))
}
/// (C416) Convert `[i for i in x]` to `list(x)`.
pub fn fix_unnecessary_comprehension(
locator: &Locator,
@@ -875,3 +977,173 @@ pub fn fix_unnecessary_comprehension(
expr.end_location.unwrap(),
))
}
/// (C417) Convert `map(lambda x: x * 2, bar)` to `(x * 2 for x in bar)`.
pub fn fix_unnecessary_map(
locator: &Locator,
stylist: &Stylist,
expr: &rustpython_parser::ast::Expr,
parent: Option<&rustpython_parser::ast::Expr>,
kind: &str,
) -> Result<Fix> {
let module_text = locator.slice_source_code_range(&Range::from_located(expr));
let mut tree = match_module(module_text)?;
let mut body = match_expr(&mut tree)?;
let call = match_call(body)?;
let arg = match_arg(call)?;
let (args, lambda_func) = match &arg.value {
Expression::Call(outer_call) => {
let inner_lambda = outer_call.args.first().unwrap().value.clone();
match &inner_lambda {
Expression::Lambda(..) => (outer_call.args.clone(), inner_lambda),
_ => {
bail!("Expected a lambda function")
}
}
}
Expression::Lambda(..) => (call.args.clone(), arg.value.clone()),
_ => {
bail!("Expected a lambda or call")
}
};
let Expression::Lambda(func_body) = &lambda_func else {
bail!("Expected a lambda")
};
if args.len() == 2 {
if func_body.params.params.iter().any(|f| f.default.is_some()) {
bail!("Currently not supporting default values");
}
let mut args_str = func_body
.params
.params
.iter()
.map(|f| f.name.value)
.join(", ");
if args_str.is_empty() {
args_str = "_".to_string();
}
let compfor = Box::new(CompFor {
target: AssignTargetExpression::Name(Box::new(Name {
value: args_str.as_str(),
lpar: vec![],
rpar: vec![],
})),
iter: args.last().unwrap().value.clone(),
ifs: vec![],
inner_for_in: None,
asynchronous: None,
whitespace_before: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
whitespace_after_for: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
" ",
)),
whitespace_before_in: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(
" ",
)),
whitespace_after_in: ParenthesizableWhitespace::SimpleWhitespace(SimpleWhitespace(" ")),
});
match kind {
"generator" => {
body.value = Expression::GeneratorExp(Box::new(GeneratorExp {
elt: func_body.body.clone(),
for_in: compfor,
lpar: vec![LeftParen::default()],
rpar: vec![RightParen::default()],
}));
}
"list" => {
body.value = Expression::ListComp(Box::new(ListComp {
elt: func_body.body.clone(),
for_in: compfor,
lbracket: LeftSquareBracket::default(),
rbracket: RightSquareBracket::default(),
lpar: vec![],
rpar: vec![],
}));
}
"set" => {
body.value = Expression::SetComp(Box::new(SetComp {
elt: func_body.body.clone(),
for_in: compfor,
lpar: vec![],
rpar: vec![],
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
}));
}
"dict" => {
let (key, value) = if let Expression::Tuple(tuple) = func_body.body.as_ref() {
if tuple.elements.len() != 2 {
bail!("Expected two elements")
}
let Some(Element::Simple { value: key, .. }) = &tuple.elements.get(0) else {
bail!(
"Expected tuple to contain a key as the first element"
);
};
let Some(Element::Simple { value, .. }) = &tuple.elements.get(1) else {
bail!(
"Expected tuple to contain a key as the second element"
);
};
(key, value)
} else {
bail!("Expected tuple for dict comprehension")
};
body.value = Expression::DictComp(Box::new(DictComp {
for_in: compfor,
lpar: vec![],
rpar: vec![],
key: Box::new(key.clone()),
value: Box::new(value.clone()),
lbrace: LeftCurlyBrace::default(),
rbrace: RightCurlyBrace::default(),
whitespace_before_colon: ParenthesizableWhitespace::default(),
whitespace_after_colon: ParenthesizableWhitespace::SimpleWhitespace(
SimpleWhitespace(" "),
),
}));
}
_ => {
bail!("Expected generator, list, set or dict");
}
}
let mut state = CodegenState {
default_newline: stylist.line_ending(),
default_indent: stylist.indentation(),
..CodegenState::default()
};
tree.codegen(&mut state);
let mut content = state.to_string();
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if kind == "set" || kind == "dict" {
if let Some(parent_element) = parent {
if let &rustpython_parser::ast::ExprKind::FormattedValue { .. } =
&parent_element.node
{
content = format!(" {content} ");
}
}
}
Ok(Fix::replacement(
content,
expr.location,
expr.end_location.unwrap(),
))
} else {
bail!("Should have two arguments");
}
}

View File

@@ -10,6 +10,29 @@ use crate::rules::flake8_comprehensions::fixes;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Checks for unnecessary `list` or `reversed` calls around `sorted`
/// calls.
///
/// ## Why is this bad?
/// It is unnecessary to use `list` around `sorted`, as the latter already
/// returns a list.
///
/// It is also unnecessary to use `reversed` around `sorted`, as the latter
/// has a `reverse` argument that can be used in lieu of an additional
/// `reversed` call.
///
/// In both cases, it's clearer to avoid the redundant call.
///
/// ## Examples
/// ```python
/// reversed(sorted(iterable))
/// ```
///
/// Use instead:
/// ```python
/// sorted(iterable, reverse=True)
/// ```
pub struct UnnecessaryCallAroundSorted {
pub func: String,
}

View File

@@ -1,24 +1,66 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Expr, ExprKind};
use super::helpers;
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
use crate::rules::flake8_comprehensions::fixes;
use crate::violation::AlwaysAutofixableViolation;
use super::helpers;
define_violation!(
/// ## What it does
/// Checks for unnecessary `list`, `reversed`, `set`, `sorted`, and `tuple`
/// call within `list`, `set`, `sorted`, and `tuple` calls.
///
/// ## Why is this bad?
/// It's unnecessary to double-cast or double-process iterables by wrapping
/// the listed functions within an additional `list`, `set`, `sorted`, or
/// `tuple` call. Doing so is redundant and can be confusing for readers.
///
/// ## Examples
/// ```python
/// list(tuple(iterable))
/// ```
///
/// Use instead:
/// ```python
/// list(iterable)
/// ```
///
/// This rule applies to a variety of functions, including `list`, `reversed`,
/// `set`, `sorted`, and `tuple`. For example:
/// * Instead of `list(list(iterable))`, use `list(iterable)`.
/// * Instead of `list(tuple(iterable))`, use `list(iterable)`.
/// * Instead of `tuple(list(iterable))`, use `tuple(iterable)`.
/// * Instead of `tuple(tuple(iterable))`, use `tuple(iterable)`.
/// * Instead of `set(set(iterable))`, use `set(iterable)`.
/// * Instead of `set(list(iterable))`, use `set(iterable)`.
/// * Instead of `set(tuple(iterable))`, use `set(iterable)`.
/// * Instead of `set(sorted(iterable))`, use `set(iterable)`.
/// * Instead of `set(reversed(iterable))`, use `set(iterable)`.
/// * Instead of `sorted(list(iterable))`, use `sorted(iterable)`.
/// * Instead of `sorted(tuple(iterable))`, use `sorted(iterable)`.
/// * Instead of `sorted(sorted(iterable))`, use `sorted(iterable)`.
/// * Instead of `sorted(reversed(iterable))`, use `sorted(iterable)`.
pub struct UnnecessaryDoubleCastOrProcess {
pub inner: String,
pub outer: String,
}
);
impl Violation for UnnecessaryDoubleCastOrProcess {
impl AlwaysAutofixableViolation for UnnecessaryDoubleCastOrProcess {
#[derive_message_formats]
fn message(&self) -> String {
let UnnecessaryDoubleCastOrProcess { inner, outer } = self;
format!("Unnecessary `{inner}` call within `{outer}()`")
}
fn autofix_title(&self) -> String {
let UnnecessaryDoubleCastOrProcess { inner, .. } = self;
format!("Remove the inner `{inner}` call")
}
}
/// C414
@@ -28,7 +70,7 @@ pub fn unnecessary_double_cast_or_process(
func: &Expr,
args: &[Expr],
) {
fn diagnostic(inner: &str, outer: &str, location: Range) -> Diagnostic {
fn create_diagnostic(inner: &str, outer: &str, location: Range) -> Diagnostic {
Diagnostic::new(
UnnecessaryDoubleCastOrProcess {
inner: inner.to_string(),
@@ -63,27 +105,23 @@ pub fn unnecessary_double_cast_or_process(
}
// Ex) set(tuple(...))
if (outer == "set" || outer == "sorted")
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted")
{
checker
.diagnostics
.push(diagnostic(inner, outer, Range::from_located(expr)));
return;
}
// Ex) list(tuple(...))
if (outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple") {
checker
.diagnostics
.push(diagnostic(inner, outer, Range::from_located(expr)));
return;
}
// Ex) set(set(...))
if outer == "set" && inner == "set" {
checker
.diagnostics
.push(diagnostic(inner, outer, Range::from_located(expr)));
if ((outer == "set" || outer == "sorted")
&& (inner == "list" || inner == "tuple" || inner == "reversed" || inner == "sorted"))
|| (outer == "set" && inner == "set")
|| ((outer == "list" || outer == "tuple") && (inner == "list" || inner == "tuple"))
{
let mut diagnostic = create_diagnostic(inner, outer, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
if let Ok(fix) = fixes::fix_unnecessary_double_cast_or_process(
checker.locator,
checker.stylist,
expr,
) {
diagnostic.amend(fix);
}
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -10,6 +10,24 @@ use crate::rules::flake8_comprehensions::fixes;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Checks for unnecessary generators that can be rewritten as `dict`
/// comprehensions.
///
/// ## Why is this bad?
/// It is unnecessary to use `dict` around a generator expression, since
/// there are equivalent comprehensions for these types. Using a
/// comprehension is clearer and more idiomatic.
///
/// ## Examples
/// ```python
/// dict((x, f(x)) for x in foo)
/// ```
///
/// Use instead:
/// ```python
/// {x: f(x) for x in foo}
/// ```
pub struct UnnecessaryGeneratorDict;
);
impl AlwaysAutofixableViolation for UnnecessaryGeneratorDict {
@@ -27,6 +45,7 @@ impl AlwaysAutofixableViolation for UnnecessaryGeneratorDict {
pub fn unnecessary_generator_dict(
checker: &mut Checker,
expr: &Expr,
parent: Option<&Expr>,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
@@ -44,6 +63,7 @@ pub fn unnecessary_generator_dict(
checker.locator,
checker.stylist,
expr,
parent,
) {
Ok(fix) => {
diagnostic.amend(fix);

View File

@@ -10,6 +10,24 @@ use crate::rules::flake8_comprehensions::fixes;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Checks for unnecessary generators that can be rewritten as `list`
/// comprehensions.
///
/// ## Why is this bad?
/// It is unnecessary to use `list` around a generator expression, since
/// there are equivalent comprehensions for these types. Using a
/// comprehension is clearer and more idiomatic.
///
/// ## Examples
/// ```python
/// list(f(x) for x in foo)
/// ```
///
/// Use instead:
/// ```python
/// [f(x) for x in foo]
/// ```
pub struct UnnecessaryGeneratorList;
);
impl AlwaysAutofixableViolation for UnnecessaryGeneratorList {

View File

@@ -10,6 +10,24 @@ use crate::rules::flake8_comprehensions::fixes;
use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Checks for unnecessary generators that can be rewritten as `set`
/// comprehensions.
///
/// ## Why is this bad?
/// It is unnecessary to use `set` around a generator expression, since
/// there are equivalent comprehensions for these types. Using a
/// comprehension is clearer and more idiomatic.
///
/// ## Examples
/// ```python
/// set(f(x) for x in foo)
/// ```
///
/// Use instead:
/// ```python
/// {f(x) for x in foo}
/// ```
pub struct UnnecessaryGeneratorSet;
);
impl AlwaysAutofixableViolation for UnnecessaryGeneratorSet {
@@ -27,6 +45,7 @@ impl AlwaysAutofixableViolation for UnnecessaryGeneratorSet {
pub fn unnecessary_generator_set(
checker: &mut Checker,
expr: &Expr,
parent: Option<&Expr>,
func: &Expr,
args: &[Expr],
keywords: &[Keyword],
@@ -40,7 +59,12 @@ pub fn unnecessary_generator_set(
if let ExprKind::GeneratorExp { .. } = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
match fixes::fix_unnecessary_generator_set(checker.locator, checker.stylist, expr) {
match fixes::fix_unnecessary_generator_set(
checker.locator,
checker.stylist,
expr,
parent,
) {
Ok(fix) => {
diagnostic.amend(fix);
}

View File

@@ -1,18 +1,50 @@
use ruff_macros::{define_violation, derive_message_formats};
use log::error;
use rustpython_parser::ast::{Expr, ExprKind};
use super::helpers;
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
use crate::rules::flake8_comprehensions::fixes;
use crate::violation::{AutofixKind, Availability, Violation};
use super::helpers;
define_violation!(
/// ## What it does
/// Checks for unnecessary `map` calls with `lambda` functions.
///
/// ## Why is this bad?
/// Using `map(func, iterable)` when `func` is a `lambda` is slower than
/// using a generator expression or a comprehension, as the latter approach
/// avoids the function call overhead, in addition to being more readable.
///
/// ## Examples
/// ```python
/// map(lambda x: x + 1, iterable)
/// ```
///
/// Use instead:
/// ```python
/// (x + 1 for x in iterable)
/// ```
///
/// This rule also applies to `map` calls within `list`, `set`, and `dict`
/// calls. For example:
/// * Instead of `list(map(lambda num: num * 2, nums))`, use
/// `[num * 2 for num in nums]`.
/// * Instead of `set(map(lambda num: num % 2 == 0, nums))`, use
/// `{num % 2 == 0 for num in nums}`.
/// * Instead of `dict(map(lambda v: (v, v ** 2), values))`, use
/// `{v: v ** 2 for v in values}`.
pub struct UnnecessaryMap {
pub obj_type: String,
}
);
impl Violation for UnnecessaryMap {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
let UnnecessaryMap { obj_type } = self;
@@ -22,11 +54,27 @@ impl Violation for UnnecessaryMap {
format!("Unnecessary `map` usage (rewrite using a `{obj_type}` comprehension)")
}
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
Some(|UnnecessaryMap { obj_type }| {
if obj_type == "generator" {
format!("Replace `map` using a generator expression")
} else {
format!("Replace `map` using a `{obj_type}` comprehension")
}
})
}
}
/// C417
pub fn unnecessary_map(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[Expr]) {
fn diagnostic(kind: &str, location: Range) -> Diagnostic {
pub fn unnecessary_map(
checker: &mut Checker,
expr: &Expr,
parent: Option<&Expr>,
func: &Expr,
args: &[Expr],
) {
fn create_diagnostic(kind: &str, location: Range) -> Diagnostic {
Diagnostic::new(
UnnecessaryMap {
obj_type: kind.to_string(),
@@ -44,10 +92,34 @@ pub fn unnecessary_map(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[
return;
}
// Exclude the parent if already matched by other arms
if let Some(parent) = parent {
if let ExprKind::Call { func: f, .. } = &parent.node {
if let Some(id_parent) = helpers::function_name(f) {
if id_parent == "dict" || id_parent == "set" || id_parent == "list" {
return;
}
}
};
};
if args.len() == 2 && matches!(&args[0].node, ExprKind::Lambda { .. }) {
checker
.diagnostics
.push(diagnostic("generator", Range::from_located(expr)));
let mut diagnostic = create_diagnostic("generator", Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
match fixes::fix_unnecessary_map(
checker.locator,
checker.stylist,
expr,
parent,
"generator",
) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
checker.diagnostics.push(diagnostic);
}
}
"list" | "set" => {
@@ -57,13 +129,29 @@ pub fn unnecessary_map(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[
if let Some(arg) = args.first() {
if let ExprKind::Call { func, args, .. } = &arg.node {
if args.len() != 2 {
return;
}
let Some(argument) = helpers::first_argument_with_matching_function("map", func, args) else {
return;
};
if let ExprKind::Lambda { .. } = argument {
checker
.diagnostics
.push(diagnostic(id, Range::from_located(expr)));
let mut diagnostic = create_diagnostic(id, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
match fixes::fix_unnecessary_map(
checker.locator,
checker.stylist,
expr,
parent,
id,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
checker.diagnostics.push(diagnostic);
}
}
}
@@ -81,9 +169,22 @@ pub fn unnecessary_map(checker: &mut Checker, expr: &Expr, func: &Expr, args: &[
if let ExprKind::Lambda { body, .. } = &argument {
if matches!(&body.node, ExprKind::Tuple { elts, .. } | ExprKind::List { elts, .. } if elts.len() == 2)
{
checker
.diagnostics
.push(diagnostic(id, Range::from_located(expr)));
let mut diagnostic = create_diagnostic(id, Range::from_located(expr));
if checker.patch(diagnostic.kind.rule()) {
match fixes::fix_unnecessary_map(
checker.locator,
checker.stylist,
expr,
parent,
id,
) {
Ok(fix) => {
diagnostic.amend(fix);
}
Err(e) => error!("Failed to generate fix: {e}"),
}
}
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_comprehensions/mod.rs
source: crates/ruff/src/rules/flake8_comprehensions/mod.rs
expression: diagnostics
---
- kind:
@@ -40,4 +40,58 @@ expression: diagnostics
row: 4
column: 1
parent: ~
- kind:
UnnecessaryGeneratorSet: ~
location:
row: 5
column: 7
end_location:
row: 5
column: 48
fix:
content:
- " {a if a < 6 else 0 for a in range(3)} "
location:
row: 5
column: 7
end_location:
row: 5
column: 48
parent: ~
- kind:
UnnecessaryGeneratorSet: ~
location:
row: 6
column: 16
end_location:
row: 6
column: 57
fix:
content:
- "{a if a < 6 else 0 for a in range(3)}"
location:
row: 6
column: 16
end_location:
row: 6
column: 57
parent: ~
- kind:
UnnecessaryGeneratorSet: ~
location:
row: 7
column: 15
end_location:
row: 7
column: 39
fix:
content:
- " {a for a in range(3)} "
location:
row: 7
column: 15
end_location:
row: 7
column: 39
parent: ~

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_comprehensions/mod.rs
source: crates/ruff/src/rules/flake8_comprehensions/mod.rs
expression: diagnostics
---
- kind:
@@ -40,4 +40,40 @@ expression: diagnostics
row: 4
column: 1
parent: ~
- kind:
UnnecessaryGeneratorDict: ~
location:
row: 6
column: 7
end_location:
row: 6
column: 37
fix:
content:
- " {x: x for x in range(3)} "
location:
row: 6
column: 7
end_location:
row: 6
column: 37
parent: ~
- kind:
UnnecessaryGeneratorDict: ~
location:
row: 7
column: 15
end_location:
row: 7
column: 45
fix:
content:
- " {x: x for x in range(3)} "
location:
row: 7
column: 15
end_location:
row: 7
column: 45
parent: ~

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_comprehensions/mod.rs
source: crates/ruff/src/rules/flake8_comprehensions/mod.rs
expression: diagnostics
---
- kind:
@@ -70,7 +70,7 @@ expression: diagnostics
column: 33
fix:
content:
- "sorted(x, reverse=True)"
- "sorted(x, reverse=False)"
location:
row: 6
column: 0
@@ -78,4 +78,61 @@ expression: diagnostics
row: 6
column: 33
parent: ~
- kind:
UnnecessaryCallAroundSorted:
func: reversed
location:
row: 7
column: 0
end_location:
row: 7
column: 50
fix:
content:
- "sorted(x, key=lambda e: e, reverse=False)"
location:
row: 7
column: 0
end_location:
row: 7
column: 50
parent: ~
- kind:
UnnecessaryCallAroundSorted:
func: reversed
location:
row: 8
column: 0
end_location:
row: 8
column: 50
fix:
content:
- "sorted(x, reverse=False, key=lambda e: e)"
location:
row: 8
column: 0
end_location:
row: 8
column: 50
parent: ~
- kind:
UnnecessaryCallAroundSorted:
func: reversed
location:
row: 9
column: 0
end_location:
row: 9
column: 34
fix:
content:
- "sorted(x, reverse=True)"
location:
row: 9
column: 0
end_location:
row: 9
column: 34
parent: ~

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_comprehensions/mod.rs
source: crates/ruff/src/rules/flake8_comprehensions/mod.rs
expression: diagnostics
---
- kind:
@@ -12,7 +12,15 @@ expression: diagnostics
end_location:
row: 2
column: 13
fix: ~
fix:
content:
- list(x)
location:
row: 2
column: 0
end_location:
row: 2
column: 13
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -24,7 +32,15 @@ expression: diagnostics
end_location:
row: 3
column: 14
fix: ~
fix:
content:
- list(x)
location:
row: 3
column: 0
end_location:
row: 3
column: 14
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -36,7 +52,15 @@ expression: diagnostics
end_location:
row: 4
column: 14
fix: ~
fix:
content:
- tuple(x)
location:
row: 4
column: 0
end_location:
row: 4
column: 14
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -48,7 +72,15 @@ expression: diagnostics
end_location:
row: 5
column: 15
fix: ~
fix:
content:
- tuple(x)
location:
row: 5
column: 0
end_location:
row: 5
column: 15
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -60,7 +92,15 @@ expression: diagnostics
end_location:
row: 6
column: 11
fix: ~
fix:
content:
- set(x)
location:
row: 6
column: 0
end_location:
row: 6
column: 11
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -72,7 +112,15 @@ expression: diagnostics
end_location:
row: 7
column: 12
fix: ~
fix:
content:
- set(x)
location:
row: 7
column: 0
end_location:
row: 7
column: 12
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -84,7 +132,15 @@ expression: diagnostics
end_location:
row: 8
column: 13
fix: ~
fix:
content:
- set(x)
location:
row: 8
column: 0
end_location:
row: 8
column: 13
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -96,7 +152,15 @@ expression: diagnostics
end_location:
row: 9
column: 14
fix: ~
fix:
content:
- set(x)
location:
row: 9
column: 0
end_location:
row: 9
column: 14
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -108,7 +172,15 @@ expression: diagnostics
end_location:
row: 10
column: 16
fix: ~
fix:
content:
- set(x)
location:
row: 10
column: 0
end_location:
row: 10
column: 16
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -120,7 +192,15 @@ expression: diagnostics
end_location:
row: 11
column: 15
fix: ~
fix:
content:
- sorted(x)
location:
row: 11
column: 0
end_location:
row: 11
column: 15
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -132,7 +212,15 @@ expression: diagnostics
end_location:
row: 12
column: 16
fix: ~
fix:
content:
- sorted(x)
location:
row: 12
column: 0
end_location:
row: 12
column: 16
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -144,7 +232,15 @@ expression: diagnostics
end_location:
row: 13
column: 17
fix: ~
fix:
content:
- sorted(x)
location:
row: 13
column: 0
end_location:
row: 13
column: 17
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
@@ -156,6 +252,37 @@ expression: diagnostics
end_location:
row: 14
column: 19
fix: ~
fix:
content:
- sorted(x)
location:
row: 14
column: 0
end_location:
row: 14
column: 19
parent: ~
- kind:
UnnecessaryDoubleCastOrProcess:
inner: list
outer: tuple
location:
row: 15
column: 0
end_location:
row: 20
column: 1
fix:
content:
- tuple(
- " [x, 3, \"hell\"\\"
- " \"o\"]"
- " )"
location:
row: 15
column: 0
end_location:
row: 20
column: 1
parent: ~

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