Compare commits

...

67 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
212 changed files with 7092 additions and 1622 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

@@ -15,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,12 @@
# 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))

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.
@@ -73,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.
@@ -170,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

899
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,8 +4,8 @@ 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 = "d94d0ac72072eb60bd9363e69b96ff1d5eb401b3" }
rustpython-parser = { features = ["lalrpop"], git = "https://github.com/RustPython/RustPython.git", rev = "d94d0ac72072eb60bd9363e69b96ff1d5eb401b3" }
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"

View File

@@ -1060,3 +1060,5 @@ are:
"""
Freely Distributable
"""
- flake8-django, licensed under the GPL license.

147
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)
@@ -231,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.245'
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
@@ -393,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)
@@ -426,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`
@@ -620,7 +634,7 @@ 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:
@@ -639,6 +653,25 @@ Ruff supports two command-line flags that alter its exit code behavior:
`--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
@@ -683,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 | |
@@ -702,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` | 🛠 |
@@ -721,13 +754,12 @@ For more, see [pycodestyle](https://pypi.org/project/pycodestyle/) on PyPI.
| 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 | |
| E704 | multiple-statements-on-one-line-def | Multiple statements on one line (def) | |
| 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 | bare-except | Do not use bare `except` | |
| 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}` | |
@@ -749,7 +781,7 @@ For more, see [mccabe](https://pypi.org/project/mccabe/) on PyPI.
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| C901 | [function-is-too-complex](https://github.com/charliermarsh/ruff/blob/main/docs/rules/function-is-too-complex.md) | `{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)
@@ -818,7 +850,7 @@ For more, see [pydocstyle](https://pypi.org/project/pydocstyle/) on PyPI.
| 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}") | 🛠 |
@@ -828,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)
@@ -845,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 | 🛠 |
@@ -934,7 +966,7 @@ For more, see [flake8-bandit](https://pypi.org/project/flake8-bandit/) on PyPI.
| 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: "{}" | |
| 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. | |
@@ -1017,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) | 🛠 |
@@ -1028,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)
@@ -1058,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.
@@ -1127,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` | |
@@ -1151,6 +1193,8 @@ 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)
@@ -1217,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 `{condition}` 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 | 🛠 |
@@ -1226,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()` | 🛠 |
@@ -1248,7 +1293,7 @@ For more, see [flake8-tidy-imports](https://pypi.org/project/flake8-tidy-imports
| Code | Name | Message | Fix |
| ---- | ---- | ------- | --- |
| TID251 | banned-api | `{name}` is banned: {message} | |
| 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)
@@ -1321,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` | |
@@ -1703,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/)
@@ -1801,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/)
@@ -2521,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
@@ -3480,8 +3546,7 @@ 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).
Generally, this is reserved for relative imports (`from . import module`).
**Default value**: `[]`
@@ -3517,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`
@@ -3533,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.245"
version = "0.0.246"
edition = "2021"
[dependencies]

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.245"
version = "0.0.246"
authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
edition = "2021"
rust-version = "1.65.0"
@@ -39,8 +39,8 @@ 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.245", path = "../ruff_macros" }
ruff_python = { version = "0.0.245", 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 = { workspace = true }
rustpython-parser = { workspace = true }

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

@@ -38,13 +38,13 @@ class Foo(metaclass=BazMeta):
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()

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

@@ -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,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,4 +3,5 @@ line-length = 88
[tool.ruff.isort]
lines-after-imports = 3
lines-between-types = 2
known-local-folder = ["ruff"]

View File

@@ -44,3 +44,13 @@ 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

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

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

@@ -35,9 +35,9 @@ 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_pyi, flake8_pytest_style, flake8_raise, flake8_return,
flake8_self, flake8_simplify, flake8_tidy_imports, flake8_type_checking,
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,
};
@@ -464,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, || {
@@ -566,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,
@@ -771,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);
}
@@ -1480,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,
@@ -1490,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) {
@@ -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,7 +2392,7 @@ 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);
}
@@ -2438,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
@@ -2532,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
@@ -2785,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);
}
}
@@ -3121,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),
@@ -3771,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);
}
@@ -3907,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 {
@@ -5085,7 +5142,7 @@ impl<'a> Checker<'a> {
|| 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
@@ -5110,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();
@@ -5246,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
@@ -5288,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

@@ -38,16 +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);

View File

@@ -40,10 +40,7 @@ pub fn check_tokens(
|| settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineSemicolon)
|| settings.rules.enabled(&Rule::UselessSemicolon)
|| settings
.rules
.enabled(&Rule::MultipleStatementsOnOneLineDef);
|| settings.rules.enabled(&Rule::UselessSemicolon);
let enforce_invalid_escape_sequence = settings.rules.enabled(&Rule::InvalidEscapeSequence);
let enforce_implicit_string_concatenation = settings
.rules
@@ -117,7 +114,7 @@ pub fn check_tokens(
}
}
// E701, E702, E703, E704
// E701, E702, E703
if enforce_compound_statements {
diagnostics.extend(
pycodestyle::rules::compound_statements(tokens)

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

@@ -282,10 +282,6 @@ mod tests {
pattern: "examples/*".to_string(),
prefix: RuleCodePrefix::F841.into(),
},
PatternPrefixPair {
pattern: "*.pyi".to_string(),
prefix: RuleCodePrefix::E704.into(),
},
];
assert_eq!(actual, expected);

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;
@@ -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)]
@@ -217,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)?;
@@ -244,7 +247,7 @@ pub fn add_noqa_to_path(path: &Path, settings: &Settings) -> Result<usize> {
error,
} = check_path(
path,
None,
package,
&contents,
tokens,
&locator,
@@ -341,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;
@@ -419,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

@@ -63,7 +63,6 @@ ruff_macros::define_rule_mapping!(
E701 => rules::pycodestyle::rules::MultipleStatementsOnOneLineColon,
E702 => rules::pycodestyle::rules::MultipleStatementsOnOneLineSemicolon,
E703 => rules::pycodestyle::rules::UselessSemicolon,
E704 => rules::pycodestyle::rules::MultipleStatementsOnOneLineDef,
E711 => rules::pycodestyle::rules::NoneComparison,
E712 => rules::pycodestyle::rules::TrueFalseComparison,
E713 => rules::pycodestyle::rules::NotInTest,
@@ -203,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,
@@ -252,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,
@@ -263,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,
@@ -282,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,
@@ -344,7 +344,7 @@ ruff_macros::define_rule_mapping!(
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,
@@ -354,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,
@@ -447,6 +447,8 @@ ruff_macros::define_rule_mapping!(
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,
@@ -474,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
@@ -554,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)]
@@ -613,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,
@@ -751,7 +760,7 @@ impl Rule {
| Rule::LineTooLong
| Rule::MixedSpacesAndTabs
| Rule::NoNewLineAtEndOfFile
| Rule::PEP3120UnnecessaryCodingComment
| Rule::UTF8EncodingDeclaration
| Rule::ShebangMissingExecutableFile
| Rule::ShebangNotExecutable
| Rule::ShebangNewline
@@ -774,7 +783,6 @@ impl Rule {
| Rule::TrailingCommaOnBareTupleProhibited
| Rule::MultipleStatementsOnOneLineColon
| Rule::UselessSemicolon
| Rule::MultipleStatementsOnOneLineDef
| Rule::MultipleStatementsOnOneLineSemicolon
| Rule::TrailingCommaProhibited => &LintSource::Tokens,
Rule::IOError => &LintSource::Io,
@@ -898,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

@@ -35,18 +35,12 @@ define_violation!(
/// ## 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 {
pub string: String,
}
pub struct HardcodedSQLExpression;
);
impl Violation for HardcodedSQLExpression {
#[derive_message_formats]
fn message(&self) -> String {
let HardcodedSQLExpression { string } = self;
format!(
"Possible SQL injection vector through string-based query construction: \"{}\"",
string.escape_debug()
)
format!("Possible SQL injection vector through string-based query construction")
}
}
@@ -102,7 +96,7 @@ 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 { string },
HardcodedSQLExpression,
Range::from_located(expr),
));
}

View File

@@ -3,8 +3,7 @@ source: crates/ruff/src/rules/flake8_bandit/mod.rs
expression: diagnostics
---
- kind:
HardcodedSQLExpression:
string: "\"SELECT %s FROM table\" % (var,)"
HardcodedSQLExpression: ~
location:
row: 2
column: 9
@@ -14,8 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT var FROM \" + table"
HardcodedSQLExpression: ~
location:
row: 3
column: 9
@@ -25,8 +23,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT \" + val + \" FROM \" + table"
HardcodedSQLExpression: ~
location:
row: 4
column: 9
@@ -36,8 +33,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT {} FROM table;\".format(var)"
HardcodedSQLExpression: ~
location:
row: 5
column: 9
@@ -47,8 +43,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"SELECT * FROM table WHERE var = {var}\""
HardcodedSQLExpression: ~
location:
row: 6
column: 9
@@ -58,8 +53,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM table WHERE var = %s\" % (var,)"
HardcodedSQLExpression: ~
location:
row: 8
column: 9
@@ -69,8 +63,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM table WHERE VAR = \" + var"
HardcodedSQLExpression: ~
location:
row: 9
column: 9
@@ -80,8 +73,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM \" + table + \"WHERE var = \" + var"
HardcodedSQLExpression: ~
location:
row: 10
column: 9
@@ -91,8 +83,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"DELETE FROM table WHERE var = {}\".format(var)"
HardcodedSQLExpression: ~
location:
row: 11
column: 9
@@ -102,8 +93,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"DELETE FROM table WHERE var = {var}\""
HardcodedSQLExpression: ~
location:
row: 12
column: 10
@@ -113,8 +103,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"INSERT INTO table VALUES (%s)\" % (var,)"
HardcodedSQLExpression: ~
location:
row: 14
column: 10
@@ -124,8 +113,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"INSERT INTO TABLE VALUES (\" + var + \")\""
HardcodedSQLExpression: ~
location:
row: 15
column: 10
@@ -135,8 +123,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"INSERT INTO {} VALUES ({})\".format(table, var)"
HardcodedSQLExpression: ~
location:
row: 16
column: 10
@@ -146,8 +133,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"INSERT INTO {table} VALUES var = {var}\""
HardcodedSQLExpression: ~
location:
row: 17
column: 10
@@ -157,8 +143,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"UPDATE %s SET var = %s\" % (table, var)"
HardcodedSQLExpression: ~
location:
row: 19
column: 10
@@ -168,8 +153,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"UPDATE \" + table + \" SET var = \" + var"
HardcodedSQLExpression: ~
location:
row: 20
column: 10
@@ -179,8 +163,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"UPDATE {} SET var = {}\".format(table, var)"
HardcodedSQLExpression: ~
location:
row: 21
column: 10
@@ -190,8 +173,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"UPDATE {table} SET var = {var}\""
HardcodedSQLExpression: ~
location:
row: 22
column: 10
@@ -201,8 +183,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select %s from table\" % (var,)"
HardcodedSQLExpression: ~
location:
row: 24
column: 10
@@ -212,8 +193,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select var from \" + table"
HardcodedSQLExpression: ~
location:
row: 25
column: 10
@@ -223,8 +203,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select \" + val + \" from \" + table"
HardcodedSQLExpression: ~
location:
row: 26
column: 10
@@ -234,8 +213,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"select {} from table;\".format(var)"
HardcodedSQLExpression: ~
location:
row: 27
column: 10
@@ -245,8 +223,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"select * from table where var = {var}\""
HardcodedSQLExpression: ~
location:
row: 28
column: 10
@@ -256,8 +233,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from table where var = %s\" % (var,)"
HardcodedSQLExpression: ~
location:
row: 30
column: 10
@@ -267,8 +243,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from table where var = \" + var"
HardcodedSQLExpression: ~
location:
row: 31
column: 10
@@ -278,8 +253,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from \" + table + \"where var = \" + var"
HardcodedSQLExpression: ~
location:
row: 32
column: 10
@@ -289,8 +263,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"delete from table where var = {}\".format(var)"
HardcodedSQLExpression: ~
location:
row: 33
column: 10
@@ -300,8 +273,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"delete from table where var = {var}\""
HardcodedSQLExpression: ~
location:
row: 34
column: 10
@@ -311,8 +283,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"insert into table values (%s)\" % (var,)"
HardcodedSQLExpression: ~
location:
row: 36
column: 10
@@ -322,8 +293,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"insert into table values (\" + var + \")\""
HardcodedSQLExpression: ~
location:
row: 37
column: 10
@@ -333,8 +303,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"insert into {} values ({})\".format(table, var)"
HardcodedSQLExpression: ~
location:
row: 38
column: 10
@@ -344,8 +313,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"insert into {table} values var = {var}\""
HardcodedSQLExpression: ~
location:
row: 39
column: 10
@@ -355,8 +323,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"update %s set var = %s\" % (table, var)"
HardcodedSQLExpression: ~
location:
row: 41
column: 10
@@ -366,8 +333,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"update \" + table + \" set var = \" + var"
HardcodedSQLExpression: ~
location:
row: 42
column: 10
@@ -377,8 +343,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"update {} set var = {}\".format(table, var)"
HardcodedSQLExpression: ~
location:
row: 43
column: 10
@@ -388,8 +353,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"update {table} set var = {var}\""
HardcodedSQLExpression: ~
location:
row: 44
column: 10
@@ -399,8 +363,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"\\n SELECT *\\n FROM table\\n WHERE var = %s\\n \" % var"
HardcodedSQLExpression: ~
location:
row: 48
column: 11
@@ -410,8 +373,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"\\n SELECT *\\n FROM TABLE\\n WHERE var =\\n \" + var"
HardcodedSQLExpression: ~
location:
row: 55
column: 11
@@ -421,8 +383,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"\\n SELECT *\\n FROM table\\n WHERE var = {}\\n \".format(var)"
HardcodedSQLExpression: ~
location:
row: 62
column: 11
@@ -432,8 +393,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"\\n SELECT *\\n FROM table\\n WHERE var = {var}\\n \""
HardcodedSQLExpression: ~
location:
row: 69
column: 11
@@ -443,8 +403,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"SELECT *FROM tableWHERE var = {var}\""
HardcodedSQLExpression: ~
location:
row: 77
column: 8
@@ -454,8 +413,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT * FROM table WHERE var = %s\" % var"
HardcodedSQLExpression: ~
location:
row: 83
column: 25
@@ -465,8 +423,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "f\"SELECT * FROM table WHERE var = {var}\""
HardcodedSQLExpression: ~
location:
row: 84
column: 25
@@ -476,8 +433,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT * FROM table WHERE var = {}\".format(var)"
HardcodedSQLExpression: ~
location:
row: 85
column: 25
@@ -487,8 +443,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
HardcodedSQLExpression:
string: "\"SELECT * FROM table WHERE var = %s\" % var"
HardcodedSQLExpression: ~
location:
row: 86
column: 29

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: ~

View File

@@ -1,93 +1,244 @@
---
source: src/rules/flake8_comprehensions/mod.rs
source: crates/ruff/src/rules/flake8_comprehensions/mod.rs
expression: diagnostics
---
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 2
row: 3
column: 0
end_location:
row: 2
row: 3
column: 26
fix: ~
fix:
content:
- (x + 1 for x in nums)
location:
row: 3
column: 0
end_location:
row: 3
column: 26
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 3
row: 4
column: 0
end_location:
row: 3
row: 4
column: 27
fix: ~
fix:
content:
- (str(x) for x in nums)
location:
row: 4
column: 0
end_location:
row: 4
column: 27
parent: ~
- kind:
UnnecessaryMap:
obj_type: list
location:
row: 4
row: 5
column: 0
end_location:
row: 4
row: 5
column: 32
fix: ~
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 4
column: 5
end_location:
row: 4
column: 31
fix: ~
fix:
content:
- "[x * 2 for x in nums]"
location:
row: 5
column: 0
end_location:
row: 5
column: 32
parent: ~
- kind:
UnnecessaryMap:
obj_type: set
location:
row: 5
row: 6
column: 0
end_location:
row: 5
row: 6
column: 36
fix: ~
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 5
column: 4
end_location:
row: 5
column: 35
fix: ~
fix:
content:
- "{x % 2 == 0 for x in nums}"
location:
row: 6
column: 0
end_location:
row: 6
column: 36
parent: ~
- kind:
UnnecessaryMap:
obj_type: dict
location:
row: 6
row: 7
column: 0
end_location:
row: 6
row: 7
column: 36
fix: ~
fix:
content:
- "{v: v**2 for v in nums}"
location:
row: 7
column: 0
end_location:
row: 7
column: 36
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 6
column: 5
row: 8
column: 0
end_location:
row: 6
row: 8
column: 26
fix:
content:
- "(\"const\" for _ in nums)"
location:
row: 8
column: 0
end_location:
row: 8
column: 26
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 9
column: 0
end_location:
row: 9
column: 24
fix:
content:
- (3.0 for _ in nums)
location:
row: 9
column: 0
end_location:
row: 9
column: 24
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 10
column: 12
end_location:
row: 10
column: 63
fix:
content:
- "(x in nums and \"1\" or \"0\" for x in range(123))"
location:
row: 10
column: 12
end_location:
row: 10
column: 63
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 11
column: 4
end_location:
row: 11
column: 44
fix:
content:
- "(isinstance(v, dict) for v in nums)"
location:
row: 11
column: 4
end_location:
row: 11
column: 44
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 12
column: 13
end_location:
row: 12
column: 35
fix:
content:
- (v for v in nums)
location:
row: 12
column: 13
end_location:
row: 12
column: 35
parent: ~
- kind:
UnnecessaryMap:
obj_type: set
location:
row: 15
column: 7
end_location:
row: 15
column: 43
fix:
content:
- " {x % 2 == 0 for x in nums} "
location:
row: 15
column: 7
end_location:
row: 15
column: 43
parent: ~
- kind:
UnnecessaryMap:
obj_type: dict
location:
row: 16
column: 7
end_location:
row: 16
column: 43
fix:
content:
- " {v: v**2 for v in nums} "
location:
row: 16
column: 7
end_location:
row: 16
column: 43
parent: ~
- kind:
UnnecessaryMap:
obj_type: generator
location:
row: 21
column: 0
end_location:
row: 21
column: 24
fix: ~
parent: ~

View File

@@ -0,0 +1,27 @@
//! Rules from [django-flake8](https://pypi.org/project/flake8-django/)
pub(crate) mod rules;
#[cfg(test)]
mod tests {
use std::path::Path;
use anyhow::Result;
use test_case::test_case;
use crate::registry::Rule;
use crate::test::test_path;
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::ModelStringFieldNullable, Path::new("DJ001.py"); "DJ001")]
#[test_case(Rule::ModelDunderStr, Path::new("DJ008.py"); "DJ008")]
#[test_case(Rule::ReceiverDecoratorChecker, Path::new("DJ013.py"); "DJ013")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_django").join(path).as_path(),
&settings::Settings::for_rule(rule_code),
)?;
assert_yaml_snapshot!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -0,0 +1,20 @@
use rustpython_parser::ast::Expr;
use crate::checkers::ast::Checker;
/// Return `true` if a Python class appears to be a Django model based on a base class.
pub fn is_model(checker: &Checker, base: &Expr) -> bool {
checker.resolve_call_path(base).map_or(false, |call_path| {
call_path.as_slice() == ["django", "db", "models", "Model"]
})
}
pub fn get_model_field_name<'a>(checker: &'a Checker, expr: &'a Expr) -> Option<&'a str> {
checker.resolve_call_path(expr).and_then(|call_path| {
let call_path = call_path.as_slice();
if !call_path.starts_with(&["django", "db", "models"]) {
return None;
}
call_path.last().copied()
})
}

View File

@@ -0,0 +1,8 @@
mod helpers;
mod model_dunder_str;
mod model_string_field_nullable;
mod receiver_decorator_checker;
pub use model_dunder_str::{model_dunder_str, ModelDunderStr};
pub use model_string_field_nullable::{model_string_field_nullable, ModelStringFieldNullable};
pub use receiver_decorator_checker::{receiver_decorator_checker, ReceiverDecoratorChecker};

View File

@@ -0,0 +1,123 @@
use rustpython_parser::ast::{Constant, Expr, StmtKind};
use rustpython_parser::ast::{ExprKind, Stmt};
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 super::helpers;
define_violation!(
/// ## What it does
/// Checks that `__str__` method is defined in Django models.
///
/// ## Why is this bad?
/// Django models should define `__str__` method to return a string representation
/// of the model instance, as Django calls this method to display the object in
/// the Django Admin and elsewhere.
///
/// Models without `__str__` method will display a non-meaningful representation
/// of the object in the Django Admin.
///
/// ## Example
/// ```python
/// from django.db import models
///
/// class MyModel(models.Model):
/// field = models.CharField(max_length=255)
/// ```
///
/// Use instead:
/// ```python
/// from django.db import models
///
/// class MyModel(models.Model):
/// field = models.CharField(max_length=255)
///
/// def __str__(self):
/// return f"{self.field}"
/// ```
pub struct ModelDunderStr;
);
impl Violation for ModelDunderStr {
#[derive_message_formats]
fn message(&self) -> String {
format!("Model does not define `__str__` method")
}
}
/// DJ008
pub fn model_dunder_str(
checker: &Checker,
bases: &[Expr],
body: &[Stmt],
class_location: &Stmt,
) -> Option<Diagnostic> {
if !checker_applies(checker, bases, body) {
return None;
}
if !has_dunder_method(body) {
return Some(Diagnostic::new(
ModelDunderStr,
Range::from_located(class_location),
));
}
None
}
fn has_dunder_method(body: &[Stmt]) -> bool {
body.iter().any(|val| match &val.node {
StmtKind::FunctionDef { name, .. } => {
if name == "__str__" {
return true;
}
false
}
_ => false,
})
}
fn checker_applies(checker: &Checker, bases: &[Expr], body: &[Stmt]) -> bool {
for base in bases.iter() {
if is_model_abstract(body) {
continue;
}
if helpers::is_model(checker, base) {
return true;
}
}
false
}
/// Check if class is abstract, in terms of Django model inheritance.
fn is_model_abstract(body: &[Stmt]) -> bool {
for element in body.iter() {
let StmtKind::ClassDef {name, body, ..} = &element.node else {
continue
};
if name != "Meta" {
continue;
}
for element in body.iter() {
let StmtKind::Assign {targets, value, ..} = &element.node else {
continue;
};
for target in targets.iter() {
let ExprKind::Name {id , ..} = &target.node else {
continue;
};
if id != "abstract" {
continue;
}
let ExprKind::Constant{value: Constant::Bool(true), ..} = &value.node else {
continue;
};
return true;
}
}
}
false
}

View File

@@ -0,0 +1,124 @@
use super::helpers;
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::Diagnostic;
use crate::violation::Violation;
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::Constant::Bool;
use rustpython_parser::ast::{Expr, ExprKind, Stmt, StmtKind};
define_violation!(
/// ## What it does
/// Checks nullable string-based fields (like `CharField` and `TextField`)
/// in Django models.
///
/// ## Why is this bad?
/// If a string-based field is nullable, then your model will have two possible
/// representations for "no data": `None` and the empty string. This can lead to
/// confusion, as clients of the API have to check for both `None` and the
/// empty string when trying to determine if the field has data.
///
/// The Django convention is to use the empty string in lieu of `None` for
/// string-based fields.
///
/// ## Example
/// ```python
/// from django.db import models
///
/// class MyModel(models.Model):
/// field = models.CharField(max_length=255, null=True)
/// ```
///
/// Use instead:
/// ```python
/// from django.db import models
///
/// class MyModel(models.Model):
/// field = models.CharField(max_length=255, default="")
/// ```
pub struct ModelStringFieldNullable {
pub field_name: String,
}
);
impl Violation for ModelStringFieldNullable {
#[derive_message_formats]
fn message(&self) -> String {
let ModelStringFieldNullable { field_name } = self;
format!("Avoid using `null=True` on string-based fields such as {field_name}")
}
}
const NOT_NULL_TRUE_FIELDS: [&str; 6] = [
"CharField",
"TextField",
"SlugField",
"EmailField",
"FilePathField",
"URLField",
];
/// DJ001
pub fn model_string_field_nullable(
checker: &Checker,
bases: &[Expr],
body: &[Stmt],
) -> Vec<Diagnostic> {
if !bases.iter().any(|base| helpers::is_model(checker, base)) {
return vec![];
}
let mut errors = Vec::new();
for statement in body.iter() {
let StmtKind::Assign {value, ..} = &statement.node else {
continue
};
if let Some(field_name) = check_nullable_field(checker, value) {
errors.push(Diagnostic::new(
ModelStringFieldNullable {
field_name: field_name.to_string(),
},
Range::from_located(value),
));
}
}
errors
}
fn check_nullable_field<'a>(checker: &'a Checker, value: &'a Expr) -> Option<&'a str> {
let ExprKind::Call {func, keywords, ..} = &value.node else {
return None;
};
let Some(valid_field_name) = helpers::get_model_field_name(checker, func) else {
return None;
};
if !NOT_NULL_TRUE_FIELDS.contains(&valid_field_name) {
return None;
}
let mut null_key = false;
let mut blank_key = false;
let mut unique_key = false;
for keyword in keywords.iter() {
let ExprKind::Constant {value: Bool(true), ..} = &keyword.node.value.node else {
continue
};
let Some(argument) = &keyword.node.arg else {
continue
};
match argument.as_str() {
"blank" => blank_key = true,
"null" => null_key = true,
"unique" => unique_key = true,
_ => continue,
}
}
if blank_key && unique_key {
return None;
}
if !null_key {
return None;
}
Some(valid_field_name)
}

View File

@@ -0,0 +1,75 @@
use rustpython_parser::ast::{Expr, ExprKind};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::{CallPath, Range};
use crate::registry::Diagnostic;
use crate::violation::Violation;
define_violation!(
/// ## What it does
/// Checks that Django's `@receiver` decorator is listed first, prior to
/// any other decorators.
///
/// ## Why is this bad?
/// Django's `@receiver` decorator is special in that it does not return
/// a wrapped function. Rather, `@receiver` connects the decorated function
/// to a signal. If any other decorators are listed before `@receiver`,
/// the decorated function will not be connected to the signal.
///
/// ## Example
/// ```python
/// from django.dispatch import receiver
/// from django.db.models.signals import post_save
///
/// @transaction.atomic
/// @receiver(post_save, sender=MyModel)
/// def my_handler(sender, instance, created, **kwargs):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// from django.dispatch import receiver
/// from django.db.models.signals import post_save
///
/// @receiver(post_save, sender=MyModel)
/// @transaction.atomic
/// def my_handler(sender, instance, created, **kwargs):
/// pass
/// ```
pub struct ReceiverDecoratorChecker;
);
impl Violation for ReceiverDecoratorChecker {
#[derive_message_formats]
fn message(&self) -> String {
format!("`@receiver` decorator must be on top of all the other decorators")
}
}
/// DJ013
pub fn receiver_decorator_checker<'a, F>(
decorator_list: &'a [Expr],
resolve_call_path: F,
) -> Option<Diagnostic>
where
F: Fn(&'a Expr) -> Option<CallPath<'a>>,
{
for (i, decorator) in decorator_list.iter().enumerate() {
if i == 0 {
continue;
}
let ExprKind::Call{ func, ..} = &decorator.node else {
continue;
};
if resolve_call_path(func).map_or(false, |call_path| {
call_path.as_slice() == ["django", "dispatch", "receiver"]
}) {
return Some(Diagnostic::new(
ReceiverDecoratorChecker,
Range::from_located(decorator),
));
}
}
None
}

View File

@@ -0,0 +1,137 @@
---
source: crates/ruff/src/rules/flake8_django/mod.rs
expression: diagnostics
---
- kind:
ModelStringFieldNullable:
field_name: CharField
location:
row: 7
column: 16
end_location:
row: 7
column: 59
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: TextField
location:
row: 8
column: 16
end_location:
row: 8
column: 59
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: SlugField
location:
row: 9
column: 16
end_location:
row: 9
column: 59
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: EmailField
location:
row: 10
column: 17
end_location:
row: 10
column: 61
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: FilePathField
location:
row: 11
column: 20
end_location:
row: 11
column: 67
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: URLField
location:
row: 12
column: 15
end_location:
row: 12
column: 57
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: CharField
location:
row: 16
column: 16
end_location:
row: 16
column: 64
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: CharField
location:
row: 17
column: 16
end_location:
row: 17
column: 56
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: SlugField
location:
row: 18
column: 16
end_location:
row: 18
column: 59
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: EmailField
location:
row: 19
column: 17
end_location:
row: 19
column: 61
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: FilePathField
location:
row: 20
column: 20
end_location:
row: 20
column: 67
fix: ~
parent: ~
- kind:
ModelStringFieldNullable:
field_name: URLField
location:
row: 21
column: 15
end_location:
row: 21
column: 57
fix: ~
parent: ~

View File

@@ -0,0 +1,35 @@
---
source: crates/ruff/src/rules/flake8_django/mod.rs
expression: diagnostics
---
- kind:
ModelDunderStr: ~
location:
row: 6
column: 0
end_location:
row: 18
column: 16
fix: ~
parent: ~
- kind:
ModelDunderStr: ~
location:
row: 21
column: 0
end_location:
row: 33
column: 16
fix: ~
parent: ~
- kind:
ModelDunderStr: ~
location:
row: 36
column: 0
end_location:
row: 47
column: 16
fix: ~
parent: ~

View File

@@ -0,0 +1,15 @@
---
source: crates/ruff/src/rules/flake8_django/mod.rs
expression: diagnostics
---
- kind:
ReceiverDecoratorChecker: ~
location:
row: 15
column: 1
end_location:
row: 15
column: 35
fix: ~
parent: ~

View File

@@ -24,10 +24,9 @@ define_violation!(
/// `__init__.py` file is typically meant to be a regular package, and
/// the absence of the `__init__.py` file is probably an oversight.
///
/// Note that namespace packages can be specified via the
/// [`namespace-packages`](https://github.com/charliermarsh/ruff#namespace-packages)
/// configuration option. Adding a namespace package to the configuration
/// will suppress this violation for a given package.
/// ## Options
///
/// * `namespace-packages`
pub struct ImplicitNamespacePackage {
pub filename: String,
}

View File

@@ -13,10 +13,10 @@ mod tests {
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::DupeClassFieldDefinitions, Path::new("PIE794.py"); "PIE794")]
#[test_case(Rule::NoUnnecessaryDictKwargs, Path::new("PIE804.py"); "PIE804")]
#[test_case(Rule::UnnecessaryDictKwargs, Path::new("PIE804.py"); "PIE804")]
#[test_case(Rule::SingleStartsEndsWith, Path::new("PIE810.py"); "PIE810")]
#[test_case(Rule::NoUnnecessaryPass, Path::new("PIE790.py"); "PIE790")]
#[test_case(Rule::NoUnnecessarySpread, Path::new("PIE800.py"); "PIE800")]
#[test_case(Rule::UnnecessaryPass, Path::new("PIE790.py"); "PIE790")]
#[test_case(Rule::UnnecessarySpread, Path::new("PIE800.py"); "PIE800")]
#[test_case(Rule::PreferListBuiltin, Path::new("PIE807.py"); "PIE807")]
#[test_case(Rule::PreferUniqueEnums, Path::new("PIE796.py"); "PIE796")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -16,9 +16,9 @@ use crate::registry::Diagnostic;
use crate::violation::{AlwaysAutofixableViolation, Violation};
define_violation!(
pub struct NoUnnecessaryPass;
pub struct UnnecessaryPass;
);
impl AlwaysAutofixableViolation for NoUnnecessaryPass {
impl AlwaysAutofixableViolation for UnnecessaryPass {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary `pass` statement")
@@ -59,9 +59,9 @@ impl Violation for PreferUniqueEnums {
}
define_violation!(
pub struct NoUnnecessarySpread;
pub struct UnnecessarySpread;
);
impl Violation for NoUnnecessarySpread {
impl Violation for UnnecessarySpread {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary spread `**`")
@@ -82,9 +82,9 @@ impl Violation for SingleStartsEndsWith {
}
define_violation!(
pub struct NoUnnecessaryDictKwargs;
pub struct UnnecessaryDictKwargs;
);
impl Violation for NoUnnecessaryDictKwargs {
impl Violation for UnnecessaryDictKwargs {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unnecessary `dict` kwargs")
@@ -124,7 +124,7 @@ pub fn no_unnecessary_pass(checker: &mut Checker, body: &[Stmt]) {
) {
if matches!(pass_stmt.node, StmtKind::Pass) {
let mut diagnostic =
Diagnostic::new(NoUnnecessaryPass, Range::from_located(pass_stmt));
Diagnostic::new(UnnecessaryPass, Range::from_located(pass_stmt));
if checker.patch(diagnostic.kind.rule()) {
if let Some(index) = match_trailing_comment(pass_stmt, checker.locator) {
diagnostic.amend(Fix::deletion(
@@ -275,7 +275,7 @@ pub fn no_unnecessary_spread(checker: &mut Checker, keys: &[Option<Expr>], value
// We only care about when the key is None which indicates a spread `**`
// inside a dict.
if let ExprKind::Dict { .. } = value.node {
let diagnostic = Diagnostic::new(NoUnnecessarySpread, Range::from_located(value));
let diagnostic = Diagnostic::new(UnnecessarySpread, Range::from_located(value));
checker.diagnostics.push(diagnostic);
}
}
@@ -307,7 +307,7 @@ pub fn no_unnecessary_dict_kwargs(checker: &mut Checker, expr: &Expr, kwargs: &[
(keys.len() == 1 && keys[0].is_none())
{
let diagnostic =
Diagnostic::new(NoUnnecessaryDictKwargs, Range::from_located(expr));
Diagnostic::new(UnnecessaryDictKwargs, Range::from_located(expr));
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_pie/mod.rs
source: crates/ruff/src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 4
column: 4
@@ -21,7 +21,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 9
column: 4
@@ -39,7 +39,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 14
column: 4
@@ -57,7 +57,7 @@ expression: diagnostics
column: 10
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 21
column: 4
@@ -75,7 +75,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 28
column: 4
@@ -93,7 +93,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 35
column: 4
@@ -111,7 +111,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 42
column: 4
@@ -129,7 +129,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 50
column: 4
@@ -147,7 +147,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 58
column: 4
@@ -165,7 +165,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 65
column: 4
@@ -183,7 +183,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 74
column: 4
@@ -201,7 +201,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 79
column: 4
@@ -219,7 +219,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 83
column: 4
@@ -237,7 +237,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 87
column: 4
@@ -255,7 +255,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 92
column: 4
@@ -273,7 +273,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 96
column: 4
@@ -291,7 +291,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NoUnnecessaryPass: ~
UnnecessaryPass: ~
location:
row: 101
column: 4

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_pie/mod.rs
source: crates/ruff/src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessarySpread: ~
UnnecessarySpread: ~
location:
row: 1
column: 13
@@ -13,7 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
UnnecessarySpread: ~
location:
row: 3
column: 14
@@ -23,7 +23,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
UnnecessarySpread: ~
location:
row: 5
column: 10
@@ -33,7 +33,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessarySpread: ~
UnnecessarySpread: ~
location:
row: 7
column: 18

View File

@@ -1,9 +1,9 @@
---
source: src/rules/flake8_pie/mod.rs
source: crates/ruff/src/rules/flake8_pie/mod.rs
expression: diagnostics
---
- kind:
NoUnnecessaryDictKwargs: ~
UnnecessaryDictKwargs: ~
location:
row: 1
column: 0
@@ -13,7 +13,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
UnnecessaryDictKwargs: ~
location:
row: 3
column: 0
@@ -23,7 +23,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
UnnecessaryDictKwargs: ~
location:
row: 5
column: 0
@@ -33,7 +33,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
UnnecessaryDictKwargs: ~
location:
row: 7
column: 0
@@ -43,7 +43,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NoUnnecessaryDictKwargs: ~
UnnecessaryDictKwargs: ~
location:
row: 9
column: 0

View File

@@ -14,6 +14,10 @@ mod tests {
#[test_case(Rule::PrefixTypeParams, Path::new("PYI001.pyi"))]
#[test_case(Rule::PrefixTypeParams, Path::new("PYI001.py"))]
#[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.pyi"))]
#[test_case(Rule::UnrecognizedPlatformCheck, Path::new("PYI007.py"))]
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.pyi"))]
#[test_case(Rule::UnrecognizedPlatformName, Path::new("PYI008.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,7 @@
pub use prefix_type_params::{prefix_type_params, PrefixTypeParams};
pub use unrecognized_platform::{
unrecognized_platform, UnrecognizedPlatformCheck, UnrecognizedPlatformName,
};
mod prefix_type_params;
mod unrecognized_platform;

View File

@@ -0,0 +1,154 @@
use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprKind};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::registry::{Diagnostic, Rule};
use crate::violation::Violation;
define_violation!(
/// ## What it does
/// Check for unrecognized `sys.platform` checks. Platform checks should be
/// simple string comparisons.
///
/// **Note**: this rule is only enabled in `.pyi` stub files.
///
/// ## Why is this bad?
/// Some `sys.platform` checks are too complex for type checkers to
/// understand, and thus result in false positives. `sys.platform` checks
/// should be simple string comparisons, like `sys.platform == "linux"`.
///
/// ## Example
/// ```python
/// if sys.platform.startswith("linux"):
/// # Linux specific definitions
/// else:
/// # Posix specific definitions
/// ```
///
/// Instead, use a simple string comparison, such as `==` or `!=`:
/// ```python
/// if sys.platform == "linux":
/// # Linux specific definitions
/// else:
/// # Posix specific definitions
/// ```
///
/// ## References
/// - [PEP 484](https://peps.python.org/pep-0484/#version-and-platform-checking)
pub struct UnrecognizedPlatformCheck;
);
impl Violation for UnrecognizedPlatformCheck {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unrecognized sys.platform check")
}
}
define_violation!(
/// ## What it does
/// Check for unrecognized platform names in `sys.platform` checks.
///
/// **Note**: this rule is only enabled in `.pyi` stub files.
///
/// ## Why is this bad?
/// If a `sys.platform` check compares to a platform name outside of a
/// small set of known platforms (e.g. "linux", "win32", etc.), it's likely
/// a typo or a platform name that is not recognized by type checkers.
///
/// The list of known platforms is: "linux", "win32", "cygwin", "darwin".
///
/// ## Example
/// ```python
/// if sys.platform == "linus":
/// ...
/// ```
///
/// Use instead:
/// ```python
/// if sys.platform == "linux":
/// ...
/// ```
///
/// ## References
/// - [PEP 484](https://peps.python.org/pep-0484/#version-and-platform-checking)
pub struct UnrecognizedPlatformName {
pub platform: String,
}
);
impl Violation for UnrecognizedPlatformName {
#[derive_message_formats]
fn message(&self) -> String {
let UnrecognizedPlatformName { platform } = self;
format!("Unrecognized platform `{platform}`")
}
}
/// PYI007, PYI008
pub fn unrecognized_platform(
checker: &mut Checker,
expr: &Expr,
left: &Expr,
ops: &[Cmpop],
comparators: &[Expr],
) {
let ([op], [right]) = (ops, comparators) else {
return;
};
let diagnostic_unrecognized_platform_check =
Diagnostic::new(UnrecognizedPlatformCheck, Range::from_located(expr));
if !checker.resolve_call_path(left).map_or(false, |call_path| {
call_path.as_slice() == ["sys", "platform"]
}) {
return;
}
// "in" might also make sense but we don't currently have one.
if !matches!(op, Cmpop::Eq | Cmpop::NotEq)
&& checker
.settings
.rules
.enabled(&Rule::UnrecognizedPlatformCheck)
{
checker
.diagnostics
.push(diagnostic_unrecognized_platform_check);
return;
}
match &right.node {
ExprKind::Constant {
value: Constant::Str(value),
..
} => {
// Other values are possible but we don't need them right now.
// This protects against typos.
if !["linux", "win32", "cygwin", "darwin"].contains(&value.as_str())
&& checker
.settings
.rules
.enabled(&Rule::UnrecognizedPlatformName)
{
checker.diagnostics.push(Diagnostic::new(
UnrecognizedPlatformName {
platform: value.clone(),
},
Range::from_located(right),
));
}
}
_ => {
if checker
.settings
.rules
.enabled(&Rule::UnrecognizedPlatformCheck)
{
checker
.diagnostics
.push(diagnostic_unrecognized_platform_check);
}
}
}
}

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,35 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
- kind:
UnrecognizedPlatformCheck: ~
location:
row: 7
column: 3
end_location:
row: 7
column: 28
fix: ~
parent: ~
- kind:
UnrecognizedPlatformCheck: ~
location:
row: 9
column: 3
end_location:
row: 9
column: 19
fix: ~
parent: ~
- kind:
UnrecognizedPlatformCheck: ~
location:
row: 11
column: 3
end_location:
row: 11
column: 24
fix: ~
parent: ~

View File

@@ -0,0 +1,6 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
[]

View File

@@ -0,0 +1,16 @@
---
source: crates/ruff/src/rules/flake8_pyi/mod.rs
expression: diagnostics
---
- kind:
UnrecognizedPlatformName:
platform: linus
location:
row: 3
column: 19
end_location:
row: 3
column: 26
fix: ~
parent: ~

View File

@@ -1,16 +1,17 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{Constant, Expr, ExprContext, ExprKind};
use super::super::types;
use super::helpers::{is_pytest_parametrize, split_names};
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::helpers::{create_expr, unparse_expr};
use crate::ast::types::Range;
use crate::checkers::ast::Checker;
use crate::fix::Fix;
use crate::registry::{Diagnostic, Rule};
use crate::source_code::Generator;
use crate::violation::{AlwaysAutofixableViolation, Violation};
use super::super::types;
use super::helpers::{is_pytest_parametrize, split_names};
define_violation!(
pub struct ParametrizeNamesWrongType {
pub expected: types::ParametrizeNameType,
@@ -99,24 +100,25 @@ fn check_names(checker: &mut Checker, expr: &Expr) {
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(
&create_expr(ExprKind::Tuple {
elts: names
.iter()
.map(|&name| {
create_expr(ExprKind::Constant {
value: Constant::Str(name.to_string()),
kind: None,
})
})
.collect(),
ctx: ExprContext::Load,
}),
1,
);
diagnostic.amend(Fix::replacement(
generator.generate(),
format!(
"({})",
unparse_expr(
&create_expr(ExprKind::Tuple {
elts: names
.iter()
.map(|&name| {
create_expr(ExprKind::Constant {
value: Constant::Str(name.to_string()),
kind: None,
})
})
.collect(),
ctx: ExprContext::Load,
}),
checker.stylist,
)
),
expr.location,
expr.end_location.unwrap(),
));
@@ -224,16 +226,17 @@ fn check_names(checker: &mut Checker, expr: &Expr) {
Range::from_located(expr),
);
if checker.patch(diagnostic.kind.rule()) {
let mut generator: Generator = checker.stylist.into();
generator.unparse_expr(
&create_expr(ExprKind::Tuple {
elts: elts.clone(),
ctx: ExprContext::Load,
}),
1, // so tuple is generated with parentheses
);
diagnostic.amend(Fix::replacement(
generator.generate(),
format!(
"({})",
unparse_expr(
&create_expr(ExprKind::Tuple {
elts: elts.clone(),
ctx: ExprContext::Load,
}),
checker.stylist,
)
),
expr.location,
expr.end_location.unwrap(),
));

View File

@@ -14,13 +14,16 @@ use crate::violation::AlwaysAutofixableViolation;
define_violation!(
/// ## What it does
/// Checks for inline strings that use single quotes or double quotes,
/// depending on the value of the [`inline-quotes`](https://github.com/charliermarsh/ruff#inline-quotes)
/// setting.
/// depending on the value of the [`flake8-quotes.inline-quotes`] option.
///
/// ## Why is this bad?
/// Consistency is good. Use either single or double quotes for inline
/// strings, but be consistent.
///
/// ## Options
///
/// * `flake8-quotes.inline-quotes`
///
/// ## Example
/// ```python
/// foo = 'bar'
@@ -56,13 +59,17 @@ impl AlwaysAutofixableViolation for BadQuotesInlineString {
define_violation!(
/// ## What it does
/// Checks for multiline strings that use single quotes or double quotes,
/// depending on the value of the [`multiline-quotes`](https://github.com/charliermarsh/ruff#multiline-quotes)
/// depending on the value of the [`flake8-quotes.multiline-quotes`]
/// setting.
///
/// ## Why is this bad?
/// Consistency is good. Use either single or double quotes for multiline
/// strings, but be consistent.
///
/// ## Options
///
/// * `flake8-quotes.multiline-quotes`
///
/// ## Example
/// ```python
/// foo = '''
@@ -101,13 +108,17 @@ impl AlwaysAutofixableViolation for BadQuotesMultilineString {
define_violation!(
/// ## What it does
/// Checks for docstrings that use single quotes or double quotes, depending on the value of the [`docstring-quotes`](https://github.com/charliermarsh/ruff#docstring-quotes)
/// setting.
/// Checks for docstrings that use single quotes or double quotes, depending
/// on the value of the [`flake8-quotes.docstring-quotes`] setting.
///
/// ## Why is this bad?
/// Consistency is good. Use either single or double quotes for docstring
/// strings, but be consistent.
///
/// ## Options
///
/// * `flake8-quotes.docstring-quotes`
///
/// ## Example
/// ```python
/// '''

View File

@@ -21,15 +21,23 @@ impl Violation for PrivateMemberAccess {
}
}
const VALID_IDS: [&str; 3] = ["self", "cls", "mcs"];
/// SLF001
pub fn private_member_access(checker: &mut Checker, expr: &Expr) {
if let ExprKind::Attribute { value, attr, .. } = &expr.node {
if !attr.ends_with("__") && (attr.starts_with('_') || attr.starts_with("__")) {
let call_path = collect_call_path(value);
if VALID_IDS.iter().any(|id| call_path.as_slice() == [*id]) {
return;
if let ExprKind::Call { func, .. } = &value.node {
let call_path = collect_call_path(func);
if call_path.as_slice() == ["super"] {
return;
}
} else {
let call_path = collect_call_path(value);
if call_path.as_slice() == ["self"]
|| call_path.as_slice() == ["cls"]
|| call_path.as_slice() == ["mcs"]
{
return;
}
}
checker.diagnostics.push(Diagnostic::new(

View File

@@ -13,8 +13,8 @@ mod tests {
use crate::{assert_yaml_snapshot, settings};
#[test_case(Rule::DuplicateIsinstanceCall, Path::new("SIM101.py"); "SIM101")]
#[test_case(Rule::NestedIfStatements, Path::new("SIM102.py"); "SIM102")]
#[test_case(Rule::ReturnBoolConditionDirectly, Path::new("SIM103.py"); "SIM103")]
#[test_case(Rule::CollapsibleIf, Path::new("SIM102.py"); "SIM102")]
#[test_case(Rule::NeedlessBool, Path::new("SIM103.py"); "SIM103")]
#[test_case(Rule::UseContextlibSuppress, Path::new("SIM105.py"); "SIM105")]
#[test_case(Rule::ReturnInTryExceptFinally, Path::new("SIM107.py"); "SIM107")]
#[test_case(Rule::UseTernaryOperator, Path::new("SIM108.py"); "SIM108")]
@@ -37,6 +37,7 @@ mod tests {
#[test_case(Rule::AndFalse, Path::new("SIM223.py"); "SIM223")]
#[test_case(Rule::YodaConditions, Path::new("SIM300.py"); "SIM300")]
#[test_case(Rule::DictGetWithDefault, Path::new("SIM401.py"); "SIM401")]
#[test_case(Rule::IfWithSameArms, Path::new("SIM114.py"); "SIM114")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -1,6 +1,6 @@
use ruff_macros::{define_violation, derive_message_formats};
use rustpython_parser::ast::{
Comprehension, Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind, Unaryop,
Cmpop, Comprehension, Constant, Expr, ExprContext, ExprKind, Location, Stmt, StmtKind, Unaryop,
};
use crate::ast::helpers::{create_expr, create_stmt, unparse_stmt};
@@ -260,6 +260,36 @@ pub fn convert_for_loop_to_any_all(checker: &mut Checker, stmt: &Stmt, sibling:
} = &loop_info.test.node
{
*operand.clone()
} else if let ExprKind::Compare {
left,
ops,
comparators,
} = &loop_info.test.node
{
if ops.len() == 1 && comparators.len() == 1 {
let op = match &ops[0] {
Cmpop::Eq => Cmpop::NotEq,
Cmpop::NotEq => Cmpop::Eq,
Cmpop::Lt => Cmpop::GtE,
Cmpop::LtE => Cmpop::Gt,
Cmpop::Gt => Cmpop::LtE,
Cmpop::GtE => Cmpop::Lt,
Cmpop::Is => Cmpop::IsNot,
Cmpop::IsNot => Cmpop::Is,
Cmpop::In => Cmpop::NotIn,
Cmpop::NotIn => Cmpop::In,
};
create_expr(ExprKind::Compare {
left: left.clone(),
ops: vec![op],
comparators: vec![comparators[0].clone()],
})
} else {
create_expr(ExprKind::UnaryOp {
op: Unaryop::Not,
operand: Box::new(loop_info.test.clone()),
})
}
} else {
create_expr(ExprKind::UnaryOp {
op: Unaryop::Not,

View File

@@ -3,7 +3,7 @@ use rustpython_parser::ast::{Cmpop, Constant, Expr, ExprContext, ExprKind, Stmt,
use ruff_macros::{define_violation, derive_message_formats};
use crate::ast::comparable::ComparableExpr;
use crate::ast::comparable::{ComparableExpr, ComparableStmt};
use crate::ast::helpers::{
contains_call_path, contains_effect, create_expr, create_stmt, first_colon_range, has_comments,
has_comments_in, unparse_expr, unparse_stmt,
@@ -15,12 +15,30 @@ use crate::registry::Diagnostic;
use crate::rules::flake8_simplify::rules::fix_if;
use crate::violation::{AutofixKind, Availability, Violation};
fn compare_expr(expr1: &ComparableExpr, expr2: &ComparableExpr) -> bool {
expr1.eq(expr2)
}
fn compare_stmt(stmt1: &ComparableStmt, stmt2: &ComparableStmt) -> bool {
stmt1.eq(stmt2)
}
fn compare_body(body1: &[Stmt], body2: &[Stmt]) -> bool {
if body1.len() != body2.len() {
return false;
}
body1
.iter()
.zip(body2.iter())
.all(|(stmt1, stmt2)| compare_stmt(&stmt1.into(), &stmt2.into()))
}
define_violation!(
pub struct NestedIfStatements {
pub struct CollapsibleIf {
pub fixable: bool,
}
);
impl Violation for NestedIfStatements {
impl Violation for CollapsibleIf {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
@@ -29,7 +47,7 @@ impl Violation for NestedIfStatements {
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let NestedIfStatements { fixable, .. } = self;
let CollapsibleIf { fixable, .. } = self;
if *fixable {
Some(|_| format!("Combine `if` statements using `and`"))
} else {
@@ -39,26 +57,24 @@ impl Violation for NestedIfStatements {
}
define_violation!(
pub struct ReturnBoolConditionDirectly {
pub struct NeedlessBool {
pub condition: String,
pub fixable: bool,
}
);
impl Violation for ReturnBoolConditionDirectly {
impl Violation for NeedlessBool {
const AUTOFIX: Option<AutofixKind> = Some(AutofixKind::new(Availability::Sometimes));
#[derive_message_formats]
fn message(&self) -> String {
let ReturnBoolConditionDirectly { condition, .. } = self;
let NeedlessBool { condition, .. } = self;
format!("Return the condition `{condition}` directly")
}
fn autofix_title_formatter(&self) -> Option<fn(&Self) -> String> {
let ReturnBoolConditionDirectly { fixable, .. } = self;
let NeedlessBool { fixable, .. } = self;
if *fixable {
Some(|ReturnBoolConditionDirectly { condition, .. }| {
format!("Replace with `return {condition}`")
})
Some(|NeedlessBool { condition, .. }| format!("Replace with `return {condition}`"))
} else {
None
}
@@ -92,6 +108,36 @@ impl Violation for UseTernaryOperator {
}
}
define_violation!(
/// ### What it does
/// Checks for `if` branches with identical arm bodies.
///
/// ### Why is this bad?
/// If multiple arms of an `if` statement have the same body, using `or`
/// better signals the intent of the statement.
///
/// ### Example
/// ```python
/// if x = 1:
/// print("Hello")
/// elif x = 2:
/// print("Hello")
/// ```
///
/// Use instead:
/// ```python
/// if x = 1 or x = 2:
/// print("Hello")
/// ```
pub struct IfWithSameArms;
);
impl Violation for IfWithSameArms {
#[derive_message_formats]
fn message(&self) -> String {
format!("Combine `if` branches using logical `or` operator")
}
}
define_violation!(
pub struct DictGetWithDefault {
pub contents: String,
@@ -211,7 +257,7 @@ pub fn nested_if_statements(
);
let mut diagnostic = Diagnostic::new(
NestedIfStatements { fixable },
CollapsibleIf { fixable },
colon.map_or_else(
|| Range::from_located(stmt),
|colon| Range::new(stmt.location, colon.end_location),
@@ -288,7 +334,7 @@ pub fn return_bool_condition_directly(checker: &mut Checker, stmt: &Stmt) {
&& (matches!(test.node, ExprKind::Compare { .. }) || checker.is_builtin("bool"));
let mut diagnostic = Diagnostic::new(
ReturnBoolConditionDirectly { condition, fixable },
NeedlessBool { condition, fixable },
Range::from_located(stmt),
);
if fixable && checker.patch(diagnostic.kind.rule()) {
@@ -451,8 +497,76 @@ pub fn use_ternary_operator(checker: &mut Checker, stmt: &Stmt, parent: Option<&
checker.diagnostics.push(diagnostic);
}
fn compare_expr(expr1: &ComparableExpr, expr2: &ComparableExpr) -> bool {
expr1.eq(expr2)
fn get_if_body_pairs<'a>(
test: &'a Expr,
body: &'a [Stmt],
orelse: &'a [Stmt],
) -> Vec<(&'a Expr, &'a [Stmt])> {
let mut pairs = vec![(test, body)];
let mut orelse = orelse;
loop {
if orelse.len() != 1 {
break;
}
let StmtKind::If { test, body, orelse: orelse_orelse, .. } = &orelse[0].node else {
break;
};
pairs.push((test, body));
orelse = orelse_orelse;
}
pairs
}
/// SIM114
pub fn if_with_same_arms(checker: &mut Checker, stmt: &Stmt, parent: Option<&Stmt>) {
let StmtKind::If { test, body, orelse } = &stmt.node else {
return;
};
// It's part of a bigger if-elif block:
// https://github.com/MartinThoma/flake8-simplify/issues/115
if let Some(StmtKind::If {
orelse: parent_orelse,
..
}) = parent.map(|parent| &parent.node)
{
if parent_orelse.len() == 1 && stmt == &parent_orelse[0] {
// TODO(charlie): These two cases have the same AST:
//
// if True:
// pass
// elif a:
// b = 1
// else:
// b = 2
//
// if True:
// pass
// else:
// if a:
// b = 1
// else:
// b = 2
//
// We want to flag the latter, but not the former. Right now, we flag neither.
return;
}
}
let if_body_pairs = get_if_body_pairs(test, body, orelse);
for i in 0..(if_body_pairs.len() - 1) {
let (test, body) = &if_body_pairs[i];
let (.., next_body) = &if_body_pairs[i + 1];
if compare_body(body, next_body) {
checker.diagnostics.push(Diagnostic::new(
IfWithSameArms,
Range::new(
if i == 0 { stmt.location } else { test.location },
next_body.last().unwrap().end_location.unwrap(),
),
));
}
}
}
/// SIM401

View File

@@ -5,9 +5,9 @@ pub use ast_bool_op::{
pub use ast_expr::{use_capital_environment_variables, UseCapitalEnvironmentVariables};
pub use ast_for::{convert_for_loop_to_any_all, ConvertLoopToAll, ConvertLoopToAny};
pub use ast_if::{
nested_if_statements, return_bool_condition_directly, use_dict_get_with_default,
use_ternary_operator, DictGetWithDefault, NestedIfStatements, ReturnBoolConditionDirectly,
UseTernaryOperator,
if_with_same_arms, nested_if_statements, return_bool_condition_directly,
use_dict_get_with_default, use_ternary_operator, CollapsibleIf, DictGetWithDefault,
IfWithSameArms, NeedlessBool, UseTernaryOperator,
};
pub use ast_ifexp::{
explicit_false_true_in_ifexpr, explicit_true_false_in_ifexpr, twisted_arms_in_ifexpr,

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 2
@@ -24,7 +24,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 7
@@ -46,7 +46,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 15
@@ -67,7 +67,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: false
location:
row: 20
@@ -78,7 +78,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 26
@@ -100,7 +100,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 51
@@ -131,7 +131,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 67
@@ -162,7 +162,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 83
@@ -185,7 +185,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 90
@@ -208,7 +208,7 @@ expression: diagnostics
column: 0
parent: ~
- kind:
NestedIfStatements:
CollapsibleIf:
fixable: true
location:
row: 117

View File

@@ -3,7 +3,7 @@ source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
ReturnBoolConditionDirectly:
NeedlessBool:
condition: a
fixable: true
location:
@@ -23,7 +23,7 @@ expression: diagnostics
column: 20
parent: ~
- kind:
ReturnBoolConditionDirectly:
NeedlessBool:
condition: a == b
fixable: true
location:
@@ -43,7 +43,7 @@ expression: diagnostics
column: 20
parent: ~
- kind:
ReturnBoolConditionDirectly:
NeedlessBool:
condition: b
fixable: true
location:
@@ -63,7 +63,7 @@ expression: diagnostics
column: 20
parent: ~
- kind:
ReturnBoolConditionDirectly:
NeedlessBool:
condition: b
fixable: true
location:
@@ -83,7 +83,7 @@ expression: diagnostics
column: 24
parent: ~
- kind:
ReturnBoolConditionDirectly:
NeedlessBool:
condition: a
fixable: false
location:
@@ -95,7 +95,7 @@ expression: diagnostics
fix: ~
parent: ~
- kind:
ReturnBoolConditionDirectly:
NeedlessBool:
condition: a
fixable: false
location:

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_simplify/mod.rs
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
@@ -108,4 +108,42 @@ expression: diagnostics
row: 157
column: 15
parent: ~
- kind:
ConvertLoopToAll:
all: return all(x in y for x in iterable)
location:
row: 162
column: 4
end_location:
row: 164
column: 24
fix:
content:
- return all(x in y for x in iterable)
location:
row: 162
column: 4
end_location:
row: 165
column: 15
parent: ~
- kind:
ConvertLoopToAll:
all: return all(x <= y for x in iterable)
location:
row: 170
column: 4
end_location:
row: 172
column: 24
fix:
content:
- return all(x <= y for x in iterable)
location:
row: 170
column: 4
end_location:
row: 173
column: 15
parent: ~

View File

@@ -0,0 +1,85 @@
---
source: crates/ruff/src/rules/flake8_simplify/mod.rs
expression: diagnostics
---
- kind:
IfWithSameArms: ~
location:
row: 2
column: 0
end_location:
row: 5
column: 5
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 7
column: 0
end_location:
row: 12
column: 22
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 14
column: 0
end_location:
row: 21
column: 26
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 23
column: 0
end_location:
row: 36
column: 26
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 24
column: 4
end_location:
row: 29
column: 26
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 31
column: 4
end_location:
row: 36
column: 26
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 38
column: 0
end_location:
row: 56
column: 8
fix: ~
parent: ~
- kind:
IfWithSameArms: ~
location:
row: 62
column: 5
end_location:
row: 65
column: 14
fix: ~
parent: ~

View File

@@ -20,6 +20,23 @@ pub struct ApiBan {
}
define_violation!(
/// ## What it does
/// Checks for banned imports.
///
/// ## Why is this bad?
/// Projects may want to ensure that specific modules or module members are
/// not be imported or accessed.
///
/// Security or other company policies may be a reason to impose
/// restrictions on importing external Python libraries. In some cases,
/// projects may adopt conventions around the use of certain modules or
/// module members that are not enforceable by the language itself.
///
/// This rule enforces certain import conventions project-wide in an
/// automatic way.
///
/// ## Options
/// * `flake8-tidy-imports.banned-api`
pub struct BannedApi {
pub name: String,
pub message: String,

View File

@@ -48,11 +48,9 @@ define_violation!(
/// > from .sibling import example
/// > ```
///
/// Note that degree of strictness packages can be specified via the
/// [`strictness`](https://github.com/charliermarsh/ruff#strictness)
/// configuration option, which allows banning all relative imports (`strictness = "all"`)
/// or only those that extend into the parent module or beyond (`strictness = "parents"`,
/// the default).
/// ## Options
///
/// * `flake8-tidy-imports.ban-relative-imports`
///
/// ## Example
/// ```python

View File

@@ -29,6 +29,7 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"); "TCH004_9")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_10.py"); "TCH004_10")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_11.py"); "TCH004_11")]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_12.py"); "TCH004_12")]
#[test_case(Rule::EmptyTypeCheckingBlock, Path::new("TCH005.py"); "TCH005")]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"); "strict")]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -0,0 +1,16 @@
---
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:
RuntimeImportInTypeCheckingBlock:
full_name: collections.abc.Callable
location:
row: 6
column: 32
end_location:
row: 6
column: 40
fix: ~
parent: ~

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_type_checking/mod.rs
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
[]

View File

@@ -1,5 +1,5 @@
---
source: src/rules/flake8_type_checking/mod.rs
source: crates/ruff/src/rules/flake8_type_checking/mod.rs
expression: diagnostics
---
- kind:

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