Compare commits

...

49 Commits

Author SHA1 Message Date
Charlie Marsh
a7755d7a8d Bump version to v0.1.15 (#9690) 2024-01-29 17:44:05 -05:00
Charlie Marsh
11449acfd9 Avoid marking InitVar as a typing-only annotation (#9688)
## Summary

Given:

```python
from dataclasses import InitVar, dataclass

@dataclass
class C:
    i: int
    j: int = None
    database: InitVar[DatabaseType] = None

    def __post_init__(self, database):
        if self.j is None and database is not None:
            self.j = database.lookup('j')

c = C(10, database=my_database)
```

We should avoid marking `InitVar` as typing-only, since it _is_ required
by the dataclass at runtime.

Note that by default, we _already_ don't flag this, since the
`@dataclass` member is needed at runtime too -- so it's only a problem
with `strict` mode.

Closes https://github.com/astral-sh/ruff/issues/9666.
2024-01-29 16:27:20 -05:00
Zanie Blue
4ccbacd44b Error if the NURSERY selector is used with preview (#9682)
Changes our warning for combined use of `--preview` and `--select
NURSERY` to a hard error.

This should go out _before_ #9680 where we will ban use of `NURSERY`
outside of preview as well (see #9683).

Part of https://github.com/astral-sh/ruff/issues/7992
2024-01-29 13:33:46 -06:00
Charlie Marsh
05a2f52206 Document literal-membership fix safety conditions (#9677)
## Summary

This seems safe to me. See
https://github.com/astral-sh/ruff/issues/8482#issuecomment-1859299411.

## Test Plan

`cargo test`
2024-01-29 17:48:40 +00:00
Charlie Marsh
a6f7100b55 [pycodestyle] Allow dtype comparisons in type-comparison (#9676)
## Summary

Per https://github.com/astral-sh/ruff/issues/9570:

> `dtype` are a bit of a strange beast, but definitely best thought of
as instances, not classes, and they are meant to be comparable not just
to their own class, but also to the corresponding scalar types (e.g.,
`x.dtype == np.float32`) and strings (e.g., `x.dtype == ['i1,i4']`;
basically, `__eq__` always tries to do `dtype(other)`.

This PR thus allows comparisons to `dtype` in preview.

Closes https://github.com/astral-sh/ruff/issues/9570.

## Test Plan

`cargo test`
2024-01-29 12:39:01 -05:00
Charlie Marsh
50122d2308 [flake8-pytest-style] Add fix safety documentation for duplicate-parameterize-test-cases (#9678)
See:
https://github.com/astral-sh/ruff/issues/8482#issuecomment-1859299411.
2024-01-29 17:33:22 +00:00
Mikko Leppänen
ad2cfa3dba [flake8-return] Consider exception suppress for unnecessary assignment (#9673)
## Summary

This review contains a fix for
[RET504](https://docs.astral.sh/ruff/rules/unnecessary-assign/)
(unnecessary-assign)

The problem is that Ruff suggests combining a return statement inside
contextlib.suppress. Even though it is an unsafe fix it might lead to an
invalid code that is not equivalent to the original one.

See: https://github.com/astral-sh/ruff/issues/5909

## Test Plan

```bash
cargo test
```
2024-01-29 12:29:05 -05:00
Micha Reiser
0045032905 Set source type: Stub for black tests with options (#9674) 2024-01-29 15:54:30 +01:00
Charlie Marsh
bea8f2ee3a Detect automagic-like assignments in notebooks (#9653)
## Summary

Given a statement like `colors = 6`, we currently treat the cell as an
automagic (since `colors` is an automagic) -- i.e., we assume it's
equivalent to `%colors = 6`. This PR adds some additional detection
whereby if the statement is an _assignment_, we avoid treating it as
such. I audited the list of automagics, and I believe this is safe for
all of them.

Closes https://github.com/astral-sh/ruff/issues/8526.

Closes https://github.com/astral-sh/ruff/issues/9648.

## Test Plan

`cargo test`
2024-01-29 12:55:44 +00:00
dependabot[bot]
c8074b0e18 Bump serde_with from 3.5.0 to 3.5.1 (#9672)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 10:10:22 +00:00
dependabot[bot]
740d8538de Bump chrono from 0.4.31 to 0.4.33 (#9671)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 11:01:46 +01:00
dependabot[bot]
341d7447a0 Bump serde_json from 1.0.111 to 1.0.113 (#9670)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 11:00:55 +01:00
dependabot[bot]
2e8aff647b Bump rayon from 1.8.0 to 1.8.1 (#9669)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 11:00:41 +01:00
dependabot[bot]
e555cf0ae4 Bump wasm-bindgen-test from 0.3.39 to 0.3.40 (#9668)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-29 10:59:58 +01:00
Mikko Leppänen
b9139a31d5 [flake8-pie] Omit bound tuples passed to .startswith or .endswith (#9661)
## Summary

This review contains a fix for
[PIE810](https://docs.astral.sh/ruff/rules/multiple-starts-ends-with/)
(multiple-starts-ends-with)

The problem is that ruff suggests combining multiple startswith/endswith
calls into a single call even though there might be a call with tuple of
strs. This leads to calling startswith/endswith with tuple of tuple of
strs which is incorrect and violates startswith/endswith conctract and
results in runtime failure.

However the following will be valid and fixed correctly => 
```python
x = ("hello", "world")
y = "h"
z = "w"
msg = "hello world"

if msg.startswith(x) or msg.startswith(y) or msg.startswith(z) :
      sys.exit(1)
```
```
ruff --fix --select PIE810 --unsafe-fixes
```
=> 
```python
if msg.startswith(x) or msg.startswith((y,z)):
      sys.exit(1)
```

See: https://github.com/astral-sh/ruff/issues/8906

## Test Plan

```bash
cargo test
```
2024-01-28 19:29:04 +00:00
Charlie Marsh
7329bf459c Avoid panic when fixing inlined else blocks (#9657)
Closes https://github.com/astral-sh/ruff/issues/9655.
2024-01-27 14:15:33 +00:00
Charlie Marsh
157d5bacfc [pydocstyle] Re-implement last-line-after-section (D413) (#9654)
## Summary

This rule was just incorrect, it didn't match the examples in the docs.
(It's a very rarely-used rule since it's not included in any of the
conventions.)

Closes https://github.com/astral-sh/ruff/issues/9452.
2024-01-26 19:30:59 +00:00
Mikko Leppänen
d496c164d3 [ruff] Guard against use of default_factory as a keyword argument (RUF026) (#9651)
## Summary

Add a rule for defaultdict(default_factory=callable). Instead suggest
using defaultdict(callable).

See: https://github.com/astral-sh/ruff/issues/9509

If a user tries to bind a "non-callable" to default_factory, the rule
ignores it. Another option would be to warn that it's probably not what
you want. Because Python allows the following:

```python 
from collections import defaultdict

defaultdict(default_factory=1)
```
this raises after you actually try to use it:

```python
dd = defaultdict(default_factory=1)
dd[1]
```
=> 
```bash
KeyError: 1
```

Instead using callable directly in the constructor it will raise (not
being a callable):

```python 
from collections import defaultdict

defaultdict(1)
```
=> 
```bash
TypeError: first argument must be callable or None
```




## Test Plan

```bash
cargo test
```
2024-01-26 19:10:05 +00:00
Charlie Marsh
b61b0edeea Add Pydantic's BaseConfig to default-copy list (#9650)
Closes https://github.com/astral-sh/ruff/issues/9647.
2024-01-26 14:54:48 +00:00
Charlie Marsh
79a0ddc112 Avoid rendering display-only rules as fixable (#9649)
Closes https://github.com/astral-sh/ruff/issues/9505.

The `ERA` rule is no longer marked as fixable:

![Screenshot 2024-01-26 at 9 17
48 AM](https://github.com/astral-sh/ruff/assets/1309177/fdc6217f-38ff-4098-b6ca-37ff51b710ab)
2024-01-26 09:47:01 -05:00
Micha Reiser
91046e4c81 Preserve indent around multiline strings (#9637) 2024-01-26 08:18:30 +01:00
Micha Reiser
5fe0fdd0a8 Delete is_node_with_body method (#9643) 2024-01-25 14:41:13 +00:00
Steve C
ffd13e65ae [flake8-return] - Add fixes for (RET505, RET506, RET507, RET508) (#9595) 2024-01-25 08:28:32 +01:00
Steve C
dba2cb79cb [pylint] Implement too-many-nested-blocks (PLR1702) (#9172)
## Summary

Implement
[`PLR1702`/`too-many-nested-blocks`](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/too-many-nested-blocks.html)

See: #970 

## Test Plan

`cargo test`
2024-01-24 19:30:01 +00:00
Mikko Leppänen
45628a5883 [flake8-return] Take NoReturn annotation into account when analyzing implicit returns (#9636)
## Summary

When we are analyzing the implicit return rule this change add an
additional check to verify if the call expression has been annotated
with NoReturn type from typing module.

See: https://github.com/astral-sh/ruff/issues/5474

## Test Plan

```bash
cargo test
```
2024-01-24 17:19:26 +00:00
Andrew Gallant
fc3e2664f9 help: enable auto-wrapping of help output (#9633)
Previously, without the 'wrap_help' feature enabled, Clap would not do
any auto-wrapping of help text. For help text with long lines, this
tends to lead to non-ideal formatting. It can be especially difficult to
read when the width of the terminal is smaller.

This commit enables 'wrap_help', which will automatically cause Clap to
query the terminal size and wrap according to that. Or, if the terminal
size cannot be determined, it will default to a maximum line width of
100.

Ref https://github.com/astral-sh/ruff/pull/9599#discussion_r1464992692
2024-01-24 10:51:07 -05:00
Charlie Marsh
b0d6fd7343 Generate custom JSON schema for dynamic setting (#9632)
## Summary

If you paste in the TOML for our default configuration (from the docs),
it's rejected by our JSON Schema:

![Screenshot 2024-01-23 at 10 08
09 PM](https://github.com/astral-sh/ruff/assets/1309177/7b4ea6e8-07db-4590-bd1e-73a01a35d747)

It seems like the issue is with:

```toml
# Set the line length limit used when formatting code snippets in
# docstrings.
#
# This only has an effect when the `docstring-code-format` setting is
# enabled.
docstring-code-line-length = "dynamic"
```

Specifically, since that value uses a custom Serde implementation, I
guess Schemars bails out? This PR adds a custom representation to allow
`"dynamic"` (but no other strings):

![Screenshot 2024-01-23 at 10 27
21 PM](https://github.com/astral-sh/ruff/assets/1309177/ab7809d4-b077-44e9-8f98-ed893aaefe5d)

This seems like it should work but I don't have a great way to test it.

Closes https://github.com/astral-sh/ruff/issues/9630.
2024-01-24 04:26:02 +00:00
Charlie Marsh
87821252d7 Update SchemaStore script to install npm dependencies (#9631)
## Summary

SchemaStore now depends on some Prettier plugins, so we need to install
Prettier and its plugins as part of the project (rather than relying on
a global install).

## Test Plan

Ran the script!
2024-01-23 23:19:36 -05:00
Akira Noda
57313d9d63 [pylint] Implement assigning-non-slot (E0237) (#9623)
## Summary

Implement [assigning-non-slot /
E0237](https://pylint.readthedocs.io/en/latest/user_guide/messages/error/assigning-non-slot.html)

related #970

## Test Plan

`cargo test`
2024-01-24 02:50:22 +00:00
Mikko Leppänen
eab1a6862b [ruff] Detect unnecessary dict comprehensions for iterables (RUF025) (#9613)
## Summary

Checks for unnecessary `dict` comprehension when creating a new
dictionary from iterable. Suggest to replace with
`dict.fromkeys(iterable)`

See: https://github.com/astral-sh/ruff/issues/9592

## Test Plan

```bash
cargo test
```
2024-01-24 02:15:38 +00:00
Micha Reiser
395bf3dc98 Fix the input for black's line ranges test file (#9622) 2024-01-23 10:40:23 +00:00
Charlie Marsh
47b8a897e7 [flake8-simplify] Support inverted returns in needless-bool (SIM103) (#9619)
Closes https://github.com/astral-sh/ruff/issues/9618.
2024-01-23 03:26:53 +00:00
dependabot[bot]
e81976f754 Bump ignore from 0.4.21 to 0.4.22 (#9606)
Bumps [ignore](https://github.com/BurntSushi/ripgrep) from 0.4.21 to
0.4.22.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="2c3897585d"><code>2c38975</code></a>
ignore-0.4.22</li>
<li><a
href="c8e4a84519"><code>c8e4a84</code></a>
cli: prefix all non-fatal error messages with 'rg: '</li>
<li><a
href="b9c774937f"><code>b9c7749</code></a>
ignore: fix reference cycle for compiled matchers</li>
<li><a
href="67dd809a80"><code>67dd809</code></a>
ignore: add some 'allow(dead_code)' annotations</li>
<li><a
href="e0a85678e1"><code>e0a8567</code></a>
complete/fish: improve shell completions for fish</li>
<li><a
href="56c7ad175a"><code>56c7ad1</code></a>
ignore/types: add Lean</li>
<li><a
href="2a4dba3fbf"><code>2a4dba3</code></a>
ignore/types: add meson.options</li>
<li><a
href="daa157b5f9"><code>daa157b</code></a>
core: actually implement --sortr=path</li>
<li><a
href="0096c74c11"><code>0096c74</code></a>
grep-0.3.1</li>
<li><a
href="8c48355b03"><code>8c48355</code></a>
deps: bump grep-printer to 0.2.1</li>
<li>Additional commits viewable in <a
href="https://github.com/BurntSushi/ripgrep/compare/ignore-0.4.21...ignore-0.4.22">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ignore&package-manager=cargo&previous-version=0.4.21&new-version=0.4.22)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 08:00:16 -05:00
Alex Waygood
f5061dbb8e Add a rule/autofix to sort __slots__ and __match_args__ (#9564)
## Summary

This PR introduces a new rule to sort `__slots__` and `__match_args__`
according to a [natural sort](https://en.wikipedia.org/wiki/Natural_sort_order), as was
requested in https://github.com/astral-sh/ruff/issues/1198#issuecomment-1881418365.

The implementation here generalises some of the machinery introduced in
3aae16f1bd
so that different kinds of sorts can be applied to lists of string
literals. (We use an "isort-style" sort for `__all__`, but that isn't
really appropriate for `__slots__` and `__match_args__`, where nearly
all items will be snake_case.) Several sections of code have been moved
from `sort_dunder_all.rs` to a new module, `sorting_helpers.rs`, which
`sort_dunder_all.rs` and `sort_dunder_slots.rs` both make use of.

`__match_args__` is very similar to `__all__`, in that it can only be a
tuple or a list. `__slots__` differs from the other two, however, in
that it can be any iterable of strings. If slots is a dictionary, the
values are used by the builtin `help()` function as per-attribute
docstrings that show up in the output of `help()`. (There's no
particular use-case for making `__slots__` a set, but it's perfectly
legal at runtime, so there's no reason for us not to handle it in this
rule.)

Note that we don't do an autofix for multiline `__slots__` if `__slots__` is a dictionary: that's out of scope. Everything else, we can nearly always fix, however.

## Test Plan

`cargo test` / `cargo insta review`.

I also ran this rule on CPython, and the diff looked pretty good

---

Co-authored-by: Micha Reiser <micha@reiser.io>
2024-01-22 12:21:55 +00:00
dependabot[bot]
a3d667ed04 Bump shlex from 1.2.0 to 1.3.0 (#9609)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 08:39:48 +00:00
dependabot[bot]
76a069d0c9 Bump serde_with from 3.4.0 to 3.5.0 (#9608)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 08:39:28 +00:00
dependabot[bot]
b994fb3ce0 Bump smallvec from 1.11.2 to 1.13.1 (#9607)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 09:31:09 +01:00
dependabot[bot]
40a0231189 Bump serde_json from 1.0.109 to 1.0.111 (#9605)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-22 09:29:46 +01:00
Steve C
9c8a4d927e [flake8-simplify] Add fix for if-with-same-arms (SIM114) (#9591)
## Summary

 add fix for `if-with-same-arms` / `SIM114`

Also preserves comments!

## Test Plan

`cargo test`
2024-01-22 04:37:18 +00:00
trag1c
836e4406a5 Corrected code block hyperlink (#9602)
## Summary

Apparently MkDocs doesn't like when reference-style links have
formatting inside :)

<details>
<summary>Screenshots (before and after the change)</summary>
<img width="1235" alt="61353"
src="https://github.com/astral-sh/ruff/assets/77130613/e32a82bd-0c5d-4edb-998f-b53659a6c54d">

<img width="1237" alt="15526"
src="https://github.com/astral-sh/ruff/assets/77130613/bdafcda5-eb9c-4af6-af03-b4849c1e5c81">
</details>
2024-01-21 22:57:34 -05:00
Steve C
e54ed28ba9 [pylint] Add fix for collapsible-else-if (PLR5501) (#9594)
## Summary

adds a fix for `collapsible-else-if` / `PLR5501`

## Test Plan

`cargo test`
2024-01-21 19:53:15 -05:00
Tom Kuson
1e4b421a00 [ruff] Implement mutable-fromkeys-value (RUF024) (#9597)
## Summary

Implement rule `mutable-fromkeys-value` (`RUF023`).

Autofixes

```python
dict.fromkeys(foo, [])
```

to

```python
{key: [] for key in foo}
```

The fix is marked as unsafe as it changes runtime behaviour. It also
uses `key` as the comprehension variable, which may not always be
desired.

Closes #4613.

## Test Plan

`cargo test`
2024-01-22 00:22:02 +00:00
Charlie Marsh
a1f3cda190 Include global --config when determining namespace packages (#9603)
## Summary

When determining whether _any_ settings have namespace packages, we need
to consider the global settings (as would be provided via `--config`).
This was a subtle fallout of a refactor.

Closes https://github.com/astral-sh/ruff/issues/9579.

## Test Plan

Tested locally by compiling Ruff and running against this
[namespace-test](https://github.com/gokay05/namespace-test) repo.
2024-01-21 19:10:43 -05:00
Steve C
837984168a [pycodestyle] Add fix for multiple-imports-on-one-line (E401) (#9518)
## Summary

Add autofix for `multiple_imports_on_one_line`, `E401`

## Test Plan

`cargo test`
2024-01-21 15:33:38 -05:00
Charlie Marsh
b64aa1e86d Split pycodestyle import rules into separate files (#9600) 2024-01-21 19:30:00 +00:00
Steve C
9e5f3f1b1b [pylint] Add fix for useless-else-on-loop (PLW0120) (#9590)
## Summary

adds fix for `useless-else-on-loop` / `PLW0120`.

## Test Plan

`cargo test`
2024-01-21 11:17:58 -05:00
Robert Craigie
9dc59cbb81 Docs: fix isort rule code (#9598)
## Summary

Fixes a typo in the docs, the isort rule code in an example was not
correct.
2024-01-21 15:30:14 +00:00
Zanie Blue
a42600e9a2 Always unset the required-version option during ecosystem checks (#9593)
Uses our existing configuration overrides to unset the
`required-version` option in ecosystem projects during checks.

The downside to this approach, is we will now update the config file of
_every_ project (with a config file). This roughly normalizes the configuration file, as we
don't preserve comments and such. We could instead do a more targeted
approach applying this override to projects which we know use this
setting 🤷‍♀️
2024-01-20 23:07:11 -06:00
Steve C
49a445a23d [pylint] Implement potential-index-error (PLE0643) (#9545)
## Summary

add `potential-index-error` rule (`PLE0643`)

See: #970 

## Test Plan

`cargo test`
2024-01-21 03:59:48 +00:00
159 changed files with 9195 additions and 1811 deletions

View File

@@ -1,5 +1,52 @@
# Changelog
## 0.1.15
### Preview features
- Error when `NURSERY` selector is used with `--preview` ([#9682](https://github.com/astral-sh/ruff/pull/9682))
- Preserve indentation around multiline strings in formatter ([#9637](https://github.com/astral-sh/ruff/pull/9637))
- \[`flake8-return`\] Add fixes for all rules (`RET505`, `RET506`, `RET507`, `RET508`) ([#9595](https://github.com/astral-sh/ruff/pull/9595))
- \[`flake8-simplify`\] Add fix for `if-with-same-arms` (`SIM114`) ([#9591](https://github.com/astral-sh/ruff/pull/9591))
- \[`pycodestyle`\] Add fix for `multiple-imports-on-one-line` (`E401`) ([#9518](https://github.com/astral-sh/ruff/pull/9518))
- \[`pylint`\] Add fix for `collapsible-else-if` (`PLR5501`) ([#9594](https://github.com/astral-sh/ruff/pull/9594))
- \[`pylint`\] Add fix for `useless-else-on-loop` (`PLW0120`) ([#9590](https://github.com/astral-sh/ruff/pull/9590))
- \[`pylint`\] Implement `assigning-non-slot` (`E0237`) ([#9623](https://github.com/astral-sh/ruff/pull/9623))
- \[`pylint`\] Implement `potential-index-error` (`PLE0643`) ([#9545](https://github.com/astral-sh/ruff/pull/9545))
- \[`pylint`\] Implement `too-many-nested-blocks` (`PLR1702`) ([#9172](https://github.com/astral-sh/ruff/pull/9172))
- \[`ruff`\] Add rule to sort `__slots__` and `__match_args__` ([#9564](https://github.com/astral-sh/ruff/pull/9564))
- \[`ruff`\] Detect unnecessary `dict` comprehensions for iterables (`RUF025`) ([#9613](https://github.com/astral-sh/ruff/pull/9613))
- \[`ruff`\] Guard against use of `default_factory` as a keyword argument (`RUF026`) ([#9651](https://github.com/astral-sh/ruff/pull/9651))
- \[`ruff`\] Implement `mutable-fromkeys-value` (`RUF024`) ([#9597](https://github.com/astral-sh/ruff/pull/9597))
### CLI
- Enable auto-wrapping of `--help` output ([#9633](https://github.com/astral-sh/ruff/pull/9633))
### Bug fixes
- Avoid rendering display-only rules as fixable ([#9649](https://github.com/astral-sh/ruff/pull/9649))
- Detect automagic-like assignments in notebooks ([#9653](https://github.com/astral-sh/ruff/pull/9653))
- Generate custom JSON schema for dynamic setting ([#9632](https://github.com/astral-sh/ruff/pull/9632))
- \[`flake8-no-pep420`\] Include global `--config` when determining namespace packages ([#9603](https://github.com/astral-sh/ruff/pull/9603))
- \[`flake8-pie`\] Omit bound tuples passed to `.startswith` or `.endswith` ([#9661](https://github.com/astral-sh/ruff/pull/9661))
- \[`flake8-return`\] Avoid panic when fixing inlined else blocks ([#9657](https://github.com/astral-sh/ruff/pull/9657))
- \[`flake8-return`\] Consider exception suppression in unnecessary assignment ([#9673](https://github.com/astral-sh/ruff/pull/9673))
- \[`flake8-return`\] Take `NoReturn` annotation into account when analyzing implicit returns ([#9636](https://github.com/astral-sh/ruff/pull/9636))
- \[`flake8-simplify`\] Support inverted returns in `needless-bool` (`SIM103`) ([#9619](https://github.com/astral-sh/ruff/pull/9619))
- \[`flake8-type-checking`\] Add Pydantic's `BaseConfig` to default-copy list ([#9650](https://github.com/astral-sh/ruff/pull/9650))
- \[`flake8-type-checking`\] Avoid marking `InitVar` as a typing-only annotation ([#9688](https://github.com/astral-sh/ruff/pull/9688))
- \[`pycodestyle`\] Allow `dtype` comparisons in `type-comparison` ([#9676](https://github.com/astral-sh/ruff/pull/9676))
- \[`pydocstyle`\] Re-implement `last-line-after-section` (`D413`) ([#9654](https://github.com/astral-sh/ruff/pull/9654))
### Documentation
- \[`flake8-pytest-style`\] Add fix safety documentation for `duplicate-parameterize-test-cases` ([#9678](https://github.com/astral-sh/ruff/pull/9678))
- \[`pylint`\] Document `literal-membership` fix safety conditions ([#9677](https://github.com/astral-sh/ruff/pull/9677))
- \[`isort`\] Fix reference to `isort` rule code ([#9598](https://github.com/astral-sh/ruff/pull/9598))
### Other changes
## 0.1.14
### Preview features

67
Cargo.lock generated
View File

@@ -273,14 +273,14 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.31"
version = "0.4.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets 0.48.5",
"windows-targets 0.52.0",
]
[[package]]
@@ -330,6 +330,7 @@ dependencies = [
"anstyle",
"clap_lex",
"strsim",
"terminal_size",
]
[[package]]
@@ -956,9 +957,9 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.21"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1"
dependencies = [
"crossbeam-deque",
"globset",
@@ -1855,9 +1856,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.8.0"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1"
checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051"
dependencies = [
"either",
"rayon-core",
@@ -1865,9 +1866,9 @@ dependencies = [
[[package]]
name = "rayon-core"
version = "1.12.0"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed"
checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
dependencies = [
"crossbeam-deque",
"crossbeam-utils",
@@ -2004,7 +2005,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.1.14"
version = "0.1.15"
dependencies = [
"anyhow",
"argfile",
@@ -2164,7 +2165,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.14"
version = "0.1.15"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2416,7 +2417,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.14"
version = "0.1.15"
dependencies = [
"anyhow",
"clap",
@@ -2683,9 +2684,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.109"
version = "1.0.113"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb0652c533506ad7a2e353cce269330d6afd8bdfb6d75e0ace5b35aacbd7b9e9"
checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
dependencies = [
"itoa",
"ryu",
@@ -2712,9 +2713,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.4.0"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64cd236ccc1b7a29e7e2739f27c0b2dd199804abc4290e32f59f3b68d6405c23"
checksum = "f5c9fdb6b00a489875b22efd4b78fe2b363b72265cc5f6eb2e2b9ee270e6140c"
dependencies = [
"serde",
"serde_with_macros",
@@ -2722,9 +2723,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.4.0"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93634eb5f75a2323b16de4748022ac4297f9e76b6dced2be287a099f41b5e788"
checksum = "dbff351eb4b33600a2e138dfa0b10b65a238ea8ff8fb2387c422c5022a3e8298"
dependencies = [
"darling",
"proc-macro2",
@@ -2752,9 +2753,9 @@ dependencies = [
[[package]]
name = "shlex"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "similar"
@@ -2770,9 +2771,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "smallvec"
version = "1.11.2"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "spin"
@@ -2891,6 +2892,16 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
dependencies = [
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "terminfo"
version = "0.8.0"
@@ -3417,9 +3428,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.39"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461"
dependencies = [
"cfg-if",
"js-sys",
@@ -3458,9 +3469,9 @@ checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.39"
version = "0.3.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403"
checksum = "139bd73305d50e1c1c4333210c0db43d989395b64a237bd35c10ef3832a7f70c"
dependencies = [
"console_error_panic_hook",
"js-sys",
@@ -3472,9 +3483,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.39"
version = "0.3.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
checksum = "70072aebfe5da66d2716002c729a14e4aec4da0e23cc2ea66323dac541c93928"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -20,7 +20,7 @@ assert_cmd = { version = "2.0.13" }
bincode = { version = "1.3.3" }
bitflags = { version = "2.4.1" }
cachedir = { version = "0.3.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
chrono = { version = "0.4.33", default-features = false, features = ["clock"] }
clap = { version = "4.4.13", features = ["derive"] }
clap_complete_command = { version = "0.5.1" }
clearscreen = { version = "2.0.0" }
@@ -40,7 +40,7 @@ fs-err = { version ="2.11.0"}
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
hexf-parse = { version ="0.2.1"}
ignore = { version = "0.4.21" }
ignore = { version = "0.4.22" }
imara-diff ={ version = "0.1.5"}
imperative = { version = "1.0.4" }
indicatif ={ version = "0.17.7"}
@@ -69,7 +69,7 @@ pyproject-toml = { version = "0.8.1" }
quick-junit = { version = "0.3.5" }
quote = { version = "1.0.23" }
rand = { version = "0.8.5" }
rayon = { version = "1.8.0" }
rayon = { version = "1.8.1" }
regex = { version = "1.10.2" }
result-like = { version = "0.5.0" }
rustc-hash = { version = "1.1.0" }
@@ -78,13 +78,13 @@ seahash = { version ="4.1.0"}
semver = { version = "1.0.21" }
serde = { version = "1.0.195", features = ["derive"] }
serde-wasm-bindgen = { version = "0.6.3" }
serde_json = { version = "1.0.109" }
serde_json = { version = "1.0.113" }
serde_test = { version = "1.0.152" }
serde_with = { version = "3.4.0", default-features = false, features = ["macros"] }
serde_with = { version = "3.5.1", default-features = false, features = ["macros"] }
shellexpand = { version = "3.0.0" }
shlex = { version ="1.2.0"}
shlex = { version ="1.3.0"}
similar = { version = "2.4.0", features = ["inline"] }
smallvec = { version = "1.11.2" }
smallvec = { version = "1.13.1" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
@@ -107,7 +107,7 @@ url = { version = "2.5.0" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
walkdir = { version = "2.3.2" }
wasm-bindgen = { version = "0.2.84" }
wasm-bindgen-test = { version = "0.3.39" }
wasm-bindgen-test = { version = "0.3.40" }
wild = { version = "2" }
[workspace.lints.rust]

View File

@@ -150,7 +150,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com/) hook via [`ruff
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.1.14
rev: v0.1.15
hooks:
# Run the linter.
- id: ruff

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.1.14"
version = "0.1.15"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -30,7 +30,7 @@ bincode = { workspace = true }
bitflags = { workspace = true }
cachedir = { workspace = true }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
clap = { workspace = true, features = ["derive", "env", "wrap_help"] }
clap_complete_command = { workspace = true }
clearscreen = { workspace = true }
colored = { workspace = true }

View File

@@ -878,15 +878,12 @@ fn nursery_group_selector_preview_enabled() {
assert_cmd_snapshot!(cmd
.pass_stdin("I=42\n"), @r###"
success: false
exit_code: 1
exit_code: 2
----- stdout -----
-:1:1: CPY001 Missing copyright notice at top of file
-:1:2: E225 [*] Missing whitespace around operator
Found 2 errors.
[*] 1 fixable with the `--fix` option.
----- stderr -----
warning: The `NURSERY` selector has been deprecated.
ruff failed
Cause: The `NURSERY` selector is deprecated and cannot be used with preview mode enabled.
"###);
}

View File

@@ -25,7 +25,7 @@ ruff_python_trivia = { path = "../ruff_python_trivia" }
ruff_workspace = { path = "../ruff_workspace", features = ["schemars"]}
anyhow = { workspace = true }
clap = { workspace = true }
clap = { workspace = true, features = ["wrap_help"] }
ignore = { workspace = true }
imara-diff = { workspace = true }
indicatif = { workspace = true }

View File

@@ -114,12 +114,15 @@ pub(super) fn main(args: &Args) -> Result<()> {
/// Returns the output of `ruff help`.
fn help_text() -> String {
args::Args::command().render_help().to_string()
args::Args::command()
.term_width(79)
.render_help()
.to_string()
}
/// Returns the output of a given subcommand (e.g., `ruff help check`).
fn subcommand_help_text(subcommand: &str) -> Result<String> {
let mut cmd = args::Args::command();
let mut cmd = args::Args::command().term_width(79);
// The build call is necessary for the help output to contain `Usage: ruff
// check` instead of `Usage: check` see https://github.com/clap-rs/clap/issues/4685

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.14"
version = "0.1.15"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -9,6 +9,22 @@ obj.startswith(foo) or obj.startswith("foo")
# error
obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
def func():
msg = "hello world"
x = "h"
y = ("h", "e", "l", "l", "o")
z = "w"
if msg.startswith(x) or msg.startswith(y) or msg.startswith(z): # Error
print("yes")
def func():
msg = "hello world"
if msg.startswith(("h", "e", "l", "l", "o")) or msg.startswith("h") or msg.startswith("w"): # Error
print("yes")
# ok
obj.startswith(("foo", "bar"))
# ok
@@ -17,3 +33,46 @@ obj.endswith(("foo", "bar"))
obj.startswith("foo") or obj.endswith("bar")
# ok
obj.startswith("foo") or abc.startswith("bar")
def func():
msg = "hello world"
x = "h"
y = ("h", "e", "l", "l", "o")
if msg.startswith(x) or msg.startswith(y): # OK
print("yes")
def func():
msg = "hello world"
y = ("h", "e", "l", "l", "o")
if msg.startswith(y): # OK
print("yes")
def func():
msg = "hello world"
y = ("h", "e", "l", "l", "o")
if msg.startswith(y) or msg.startswith(y): # OK
print("yes")
def func():
msg = "hello world"
y = ("h", "e", "l", "l", "o")
x = ("w", "o", "r", "l", "d")
if msg.startswith(y) or msg.startswith(x) or msg.startswith("h"): # OK
print("yes")
def func():
msg = "hello world"
y = ("h", "e", "l", "l", "o")
x = ("w", "o", "r", "l", "d")
if msg.startswith(y) or msg.endswith(x) or msg.startswith("h"): # OK
print("yes")

View File

@@ -1,14 +1,15 @@
import _thread
import builtins
import os
import posix
from posix import abort
import sys as std_sys
import typing
import typing_extensions
import _thread
import _winapi
from posix import abort
from typing import NoReturn
import _winapi
import pytest
import typing_extensions
from pytest import xfail as py_xfail
###
@@ -326,3 +327,44 @@ def end_of_file():
if False:
return 1
x = 2 \
# function return type annotation NoReturn
def foo(x: int) -> int:
def bar() -> NoReturn:
abort()
if x == 5:
return 5
bar()
def foo(string: str) -> str:
def raises(value: str) -> NoReturn:
raise RuntimeError("something went wrong")
match string:
case "a":
return "first"
case "b":
return "second"
case "c":
return "third"
case _:
raises(string)
def foo() -> int:
def baz() -> int:
return 1
def bar() -> NoReturn:
a = 1 + 2
raise AssertionError("Very bad")
if baz() > 3:
return 1
bar()

View File

@@ -363,3 +363,46 @@ def foo():
def mavko_debari(P_kbar):
D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
return D
# contextlib suppress in with statement
import contextlib
def foo():
x = 2
with contextlib.suppress(Exception):
x = x + 1
return x
def foo(data):
with open("in.txt") as file_out, contextlib.suppress(IOError):
file_out.write(data)
data = 10
return data
def foo(data):
with open("in.txt") as file_out:
file_out.write(data)
with contextlib.suppress(IOError):
data = 10
return data
def foo():
y = 1
x = 2
with contextlib.suppress(Exception):
x = 1
y = y + 2
return y # RET504
def foo():
y = 1
if y > 0:
with contextlib.suppress(Exception):
y = 2
return y

View File

@@ -131,6 +131,51 @@ def bar3(x, y, z):
return None
def bar4(x):
if True:
return
else:
# comment
pass
def bar5():
if True:
return
else: # comment
pass
def bar6():
if True:
return
else\
:\
# comment
pass
def bar7():
if True:
return
else\
: # comment
pass
def bar8():
if True:
return
else: pass
def bar9():
if True:
return
else:\
pass
x = 0
if x == 1:

View File

@@ -4,6 +4,11 @@ if a:
elif c:
b
if a: # we preserve comments, too!
b
elif c: # but not on the second branch
b
if x == 1:
for _ in range(20):
print("hello")
@@ -63,6 +68,10 @@ elif result.eofs == "F":
errors = 1
elif result.eofs == "E":
errors = 1
elif result.eofs == "X":
errors = 1
elif result.eofs == "C":
errors = 1
# OK
@@ -70,7 +79,7 @@ def complicated_calc(*arg, **kwargs):
return 42
def foo(p):
def func(p):
if p == 2:
return complicated_calc(microsecond=0)
elif p == 3:
@@ -103,7 +112,7 @@ elif c:
x = 1
def foo():
def func():
a = True
b = False
if a > b: # end-of-line
@@ -114,3 +123,23 @@ def foo():
return 4
elif b is None:
return 4
def func():
"""Ensure that the named expression is parenthesized when merged."""
a = True
b = False
if a > b: # end-of-line
return 3
elif a := 1:
return 3
if a: # we preserve comments, too!
b
elif c: # but not on the second branch
b
if a: b # here's a comment
elif c: b

View File

@@ -0,0 +1,18 @@
"""Test: avoid marking an `InitVar` as typing-only."""
from __future__ import annotations
from dataclasses import FrozenInstanceError, InitVar, dataclass
from pathlib import Path
@dataclass
class C:
i: int
j: int = None
database: InitVar[Path] = None
err: FrozenInstanceError = None
def __post_init__(self, database):
...

View File

@@ -1,5 +1,6 @@
#: E401
import os, sys
#: Okay
import os
import sys
@@ -59,3 +60,21 @@ import foo
a = 1
import bar
#: E401
import re as regex, string # also with a comment!
import re as regex, string; x = 1
x = 1; import re as regex, string
def blah():
import datetime as dt, copy
def nested_and_tested():
import builtins, textwrap as tw
x = 1; import re as regex, string
import re as regex, string; x = 1
if True: import re as regex, string

View File

@@ -126,3 +126,15 @@ class Foo:
# Okay
if type(value) is str:
...
import numpy as np
#: Okay
x.dtype == float
#: Okay
np.dtype(int) == float
#: E721
dtype == float

View File

@@ -0,0 +1,59 @@
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value
"""
def func():
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value
"""
def func():
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value
"""
def func():
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value
"""
def func():
"""Do something.
Args:
x: the value
with a hanging indent
Returns:
the value"""

View File

@@ -49,6 +49,17 @@ def not_ok1():
pass
def not_ok1_with_comments():
if 1:
pass
else:
# inner comment
if 2:
pass
else:
pass # final pass comment
# Regression test for https://github.com/apache/airflow/blob/f1e1cdcc3b2826e68ba133f350300b5065bbca33/airflow/models/dag.py#L1737
def not_ok2():
if True:
@@ -61,3 +72,28 @@ def not_ok2():
else:
print(4)
def not_ok3():
if 1:
pass
else:
if 2: pass
else: pass
def not_ok4():
if 1:
pass
else:
if 2: pass
else:
pass
def not_ok5():
if 1:
pass
else:
if 2:
pass
else: pass

View File

@@ -0,0 +1,56 @@
class StudentA:
__slots__ = ("name",)
def __init__(self, name, surname):
self.name = name
self.surname = surname # [assigning-non-slot]
self.setup()
def setup(self):
pass
class StudentB:
__slots__ = ("name", "surname")
def __init__(self, name, middle_name):
self.name = name
self.middle_name = middle_name # [assigning-non-slot]
self.setup()
def setup(self):
pass
class StudentC:
__slots__ = ("name", "surname")
def __init__(self, name, surname):
self.name = name
self.surname = surname # OK
self.setup()
def setup(self):
pass
class StudentD(object):
__slots__ = ("name", "surname")
def __init__(self, name, middle_name):
self.name = name
self.middle_name = middle_name # [assigning-non-slot]
self.setup()
def setup(self):
pass
class StudentE(StudentD):
def __init__(self, name, middle_name):
self.name = name
self.middle_name = middle_name # OK
self.setup()
def setup(self):
pass

View File

@@ -0,0 +1,9 @@
print([1, 2, 3][3]) # PLE0643
print([1, 2, 3][-4]) # PLE0643
print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
print([1, 2, 3][2]) # OK
print([1, 2, 3][0]) # OK
print([1, 2, 3][-3]) # OK
print([1, 2, 3][3:]) # OK

View File

@@ -0,0 +1,21 @@
def correct_fruits(fruits) -> bool:
if len(fruits) > 1: # PLR1702
if "apple" in fruits:
if "orange" in fruits:
count = fruits["orange"]
if count % 2:
if "kiwi" in fruits:
if count == 2:
return True
return False
# Ok
def correct_fruits(fruits) -> bool:
if len(fruits) > 1:
if "apple" in fruits:
if "orange" in fruits:
count = fruits["orange"]
if count % 2:
if "kiwi" in fruits:
return True
return False

View File

@@ -135,3 +135,14 @@ def test_break_in_match():
else:
return True
return False
def test_retain_comment():
"""Retain the comment within the `else` block"""
for j in range(10):
pass
else:
# [useless-else-on-loop]
print("fat chance")
for j in range(10):
break

View File

@@ -67,3 +67,15 @@ class G(F):
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
from pydantic import BaseConfig
class H(BaseModel):
class Config(BaseConfig):
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []

View File

@@ -306,3 +306,5 @@ __all__ = (
__all__ = [
"foo", (), "bar"
]
__all__ = "foo", "an" "implicitly_concatenated_second_item", not_a_string_literal

View File

@@ -0,0 +1,234 @@
#########################
# Single-line definitions
#########################
class Klass:
__slots__ = ["d", "c", "b", "a"] # a comment that is untouched
__match_args__ = ("d", "c", "b", "a")
# Quoting style is retained,
# but unnecessary parens are not
__slots__: set = {'b', "c", ((('a')))}
# Trailing commas are also not retained for single-line definitions
# (but they are in multiline definitions)
__match_args__: tuple = ("b", "c", "a",)
class Klass2:
if bool():
__slots__ = {"x": "docs for x", "m": "docs for m", "a": "docs for a"}
else:
__slots__ = "foo3", "foo2", "foo1" # NB: an implicit tuple (without parens)
__match_args__: list[str] = ["the", "three", "little", "pigs"]
__slots__ = ("parenthesized_item"), "in", ("an_unparenthesized_tuple")
# we use natural sort,
# not alphabetical sort or "isort-style" sort
__slots__ = {"aadvark237", "aadvark10092", "aadvark174", "aadvark532"}
############################
# Neat multiline definitions
############################
class Klass3:
__slots__ = (
"d0",
"c0", # a comment regarding 'c0'
"b0",
# a comment regarding 'a0':
"a0"
)
__match_args__ = [
"d",
"c", # a comment regarding 'c'
"b",
# a comment regarding 'a':
"a"
]
##########################################
# Messier multiline __all__ definitions...
##########################################
class Klass4:
# comment0
__slots__ = ("d", "a", # comment1
# comment2
"f", "b",
"strangely", # comment3
# comment4
"formatted",
# comment5
) # comment6
# comment7
__match_args__ = [ # comment0
# comment1
# comment2
"dx", "cx", "bx", "ax" # comment3
# comment4
# comment5
# comment6
] # comment7
# from cpython/Lib/pathlib/__init__.py
class PurePath:
__slots__ = (
# The `_raw_paths` slot stores unnormalized string paths. This is set
# in the `__init__()` method.
'_raw_paths',
# The `_drv`, `_root` and `_tail_cached` slots store parsed and
# normalized parts of the path. They are set when any of the `drive`,
# `root` or `_tail` properties are accessed for the first time. The
# three-part division corresponds to the result of
# `os.path.splitroot()`, except that the tail is further split on path
# separators (i.e. it is a list of strings), and that the root and
# tail are normalized.
'_drv', '_root', '_tail_cached',
# The `_str` slot stores the string representation of the path,
# computed from the drive, root and tail when `__str__()` is called
# for the first time. It's used to implement `_str_normcase`
'_str',
# The `_str_normcase_cached` slot stores the string path with
# normalized case. It is set when the `_str_normcase` property is
# accessed for the first time. It's used to implement `__eq__()`
# `__hash__()`, and `_parts_normcase`
'_str_normcase_cached',
# The `_parts_normcase_cached` slot stores the case-normalized
# string path after splitting on path separators. It's set when the
# `_parts_normcase` property is accessed for the first time. It's used
# to implement comparison methods like `__lt__()`.
'_parts_normcase_cached',
# The `_hash` slot stores the hash of the case-normalized string
# path. It's set when `__hash__()` is called for the first time.
'_hash',
)
# From cpython/Lib/pickletools.py
class ArgumentDescriptor(object):
__slots__ = (
# name of descriptor record, also a module global name; a string
'name',
# length of argument, in bytes; an int; UP_TO_NEWLINE and
# TAKEN_FROM_ARGUMENT{1,4,8} are negative values for variable-length
# cases
'n',
# a function taking a file-like object, reading this kind of argument
# from the object at the current position, advancing the current
# position by n bytes, and returning the value of the argument
'reader',
# human-readable docs for this arg descriptor; a string
'doc',
)
####################################
# Should be flagged, but not fixed
####################################
# from cpython/Lib/test/test_inspect.py.
# Multiline dicts are out of scope.
class SlotUser:
__slots__ = {'power': 'measured in kilowatts',
'distance': 'measured in kilometers'}
class Klass5:
__match_args__ = (
"look",
(
"a_veeeeeeeeeeeeeeeeeeery_long_parenthesized_item"
),
)
__slots__ = (
"b",
((
"c"
)),
"a"
)
__slots__ = ("don't" "care" "about", "__slots__" "with", "concatenated" "strings")
###################################
# These should all not get flagged:
###################################
class Klass6:
__slots__ = ()
__match_args__ = []
__slots__ = ("single_item",)
__match_args__ = (
"single_item_multiline",
)
__slots__ = {"single_item",}
__slots__ = {"single_item_no_trailing_comma": "docs for that"}
__match_args__ = [
"single_item_multiline_no_trailing_comma"
]
__slots__ = ("not_a_tuple_just_a_string")
__slots__ = ["a", "b", "c", "d"]
__slots__ += ["e", "f", "g"]
__slots__ = ("a", "b", "c", "d")
if bool():
__slots__ += ("e", "f", "g")
else:
__slots__ += ["alpha", "omega"]
__slots__ = {"not": "sorted", "but": "includes", **a_kwarg_splat}
__slots__ = ("b", "a", "e", "d")
__slots__ = ["b", "a", "e", "d"]
__match_args__ = ["foo", "bar", "antipasti"]
class Klass6:
__slots__ = (9, 8, 7)
__match_args__ = ( # This is just an empty tuple,
# but,
# it's very well
) # documented
# We don't deduplicate elements;
# this just ensures that duplicate elements aren't unnecessarily
# reordered by an autofix:
__slots__ = (
"duplicate_element", # comment1
"duplicate_element", # comment3
"duplicate_element", # comment2
"duplicate_element", # comment0
)
__slots__ = "foo", "an" "implicitly_concatenated_second_item", not_a_string_literal
__slots__ =[
[]
]
__slots__ = [
()
]
__match_args__ = (
()
)
__match_args__ = (
[]
)
__slots__ = (
(),
)
__slots__ = (
[],
)
__match_args__ = (
"foo", [], "bar"
)
__match_args__ = [
"foo", (), "bar"
]
__match_args__ = {"a", "set", "for", "__match_args__", "is invalid"}
__match_args__ = {"this": "is", "also": "invalid"}

View File

@@ -0,0 +1,32 @@
pierogi_fillings = [
"cabbage",
"strawberry",
"cheese",
"blueberry",
]
# Errors.
dict.fromkeys(pierogi_fillings, [])
dict.fromkeys(pierogi_fillings, list())
dict.fromkeys(pierogi_fillings, {})
dict.fromkeys(pierogi_fillings, set())
dict.fromkeys(pierogi_fillings, {"pre": "populated!"})
dict.fromkeys(pierogi_fillings, dict())
# Okay.
dict.fromkeys(pierogi_fillings)
dict.fromkeys(pierogi_fillings, None)
dict.fromkeys(pierogi_fillings, 1)
dict.fromkeys(pierogi_fillings)
dict.fromkeys(pierogi_fillings, ("blessed", "tuples", "don't", "mutate"))
dict.fromkeys(pierogi_fillings, "neither do strings")
class MysteryBox: ...
dict.fromkeys(pierogi_fillings, MysteryBox)
bar.fromkeys(pierogi_fillings, [])
def bad_dict() -> None:
dict = MysteryBox()
dict.fromkeys(pierogi_fillings, [])

View File

@@ -0,0 +1,92 @@
# Violation cases: RUF025
def func():
numbers = [1, 2, 3]
{n: None for n in numbers} # RUF025
def func():
for key, value in {n: 1 for n in [1, 2, 3]}.items(): # RUF025
pass
def func():
{n: 1.1 for n in [1, 2, 3]} # RUF025
def func():
{n: complex(3, 5) for n in [1, 2, 3]} # RUF025
def func():
def f(data):
return data
f({c: "a" for c in "12345"}) # RUF025
def func():
{n: True for n in [1, 2, 2]} # RUF025
def func():
{n: b"hello" for n in (1, 2, 2)} # RUF025
def func():
{n: ... for n in [1, 2, 3]} # RUF025
def func():
{n: False for n in {1: "a", 2: "b"}} # RUF025
def func():
{(a, b): 1 for (a, b) in [(1, 2), (3, 4)]} # RUF025
def func():
def f():
return 1
a = f()
{n: a for n in [1, 2, 3]} # RUF025
def func():
values = ["a", "b", "c"]
[{n: values for n in [1, 2, 3]}] # RUF025
# Non-violation cases: RUF025
def func():
{n: 1 for n in [1, 2, 3, 4, 5] if n < 3} # OK
def func():
{n: 1 for c in [1, 2, 3, 4, 5] for n in [1, 2, 3] if c < 3} # OK
def func():
def f():
pass
{n: f() for n in [1, 2, 3]} # OK
def func():
{n: n for n in [1, 2, 3, 4, 5]} # OK
def func():
def f():
return {n: 1 for c in [1, 2, 3, 4, 5] for n in [1, 2, 3]} # OK
f()
def func():
{(a, b): a + b for (a, b) in [(1, 2), (3, 4)]} # OK

View File

@@ -0,0 +1,120 @@
from collections import defaultdict
# Violation cases: RUF026
def func():
defaultdict(default_factory=None) # RUF026
def func():
defaultdict(default_factory=int) # RUF026
def func():
defaultdict(default_factory=float) # RUF026
def func():
defaultdict(default_factory=dict) # RUF026
def func():
defaultdict(default_factory=list) # RUF026
def func():
defaultdict(default_factory=tuple) # RUF026
def func():
def foo():
pass
defaultdict(default_factory=foo) # RUF026
def func():
defaultdict(default_factory=lambda: 1) # RUF026
def func():
from collections import deque
defaultdict(default_factory=deque) # RUF026
def func():
class MyCallable:
def __call__(self):
pass
defaultdict(default_factory=MyCallable()) # RUF026
def func():
defaultdict(default_factory=tuple, member=1) # RUF026
def func():
defaultdict(member=1, default_factory=tuple) # RUF026
def func():
defaultdict(member=1, default_factory=tuple,) # RUF026
def func():
defaultdict(
member=1,
default_factory=tuple,
) # RUF026
def func():
defaultdict(
default_factory=tuple,
member=1,
) # RUF026
# Non-violation cases: RUF026
def func():
defaultdict(default_factory=1) # OK
def func():
defaultdict(default_factory="wdefwef") # OK
def func():
defaultdict(default_factory=[1, 2, 3]) # OK
def func():
defaultdict() # OK
def func():
defaultdict(int) # OK
def func():
defaultdict(list) # OK
def func():
defaultdict(dict) # OK
def func():
defaultdict(dict, defaultdict=list) # OK
def func():
def constant_factory(value):
return lambda: value
defaultdict(constant_factory("<missing>"))

View File

@@ -125,6 +125,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SliceCopy) {
refurb::rules::slice_copy(checker, subscript);
}
if checker.enabled(Rule::PotentialIndexError) {
pylint::rules::potential_index_error(checker, value, slice);
}
pandas_vet::rules::subscript(checker, value, expr);
}
@@ -974,9 +977,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::SslInsecureVersion) {
flake8_bandit::rules::ssl_insecure_version(checker, call);
}
if checker.enabled(Rule::MutableFromkeysValue) {
ruff::rules::mutable_fromkeys_value(checker, call);
}
if checker.enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_extend_call(checker, call);
}
if checker.enabled(Rule::DefaultFactoryKwarg) {
ruff::rules::default_factory_kwarg(checker, call);
}
}
Expr::Dict(dict) => {
if checker.any_enabled(&[
@@ -1416,11 +1426,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryDictIndexLookup) {
pylint::rules::unnecessary_dict_index_lookup_comprehension(checker, expr);
}
if checker.enabled(Rule::UnnecessaryComprehension) {
flake8_comprehensions::rules::unnecessary_dict_comprehension(
checker, expr, key, value, generators,
);
}
if checker.enabled(Rule::UnnecessaryDictComprehensionForIterable) {
ruff::rules::unnecessary_dict_comprehension_for_iterable(checker, dict_comp);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Expr(expr));
}

View File

@@ -507,6 +507,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NoSlotsInNamedtupleSubclass) {
flake8_slots::rules::no_slots_in_namedtuple_subclass(checker, stmt, class_def);
}
if checker.enabled(Rule::NonSlotAssignment) {
pylint::rules::non_slot_assignment(checker, class_def);
}
if checker.enabled(Rule::SingleStringSlots) {
pylint::rules::single_string_slots(checker, class_def);
}
@@ -1069,13 +1072,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
ruff::rules::sort_dunder_all_aug_assign(checker, aug_assign);
}
}
Stmt::If(
if_ @ ast::StmtIf {
test,
elif_else_clauses,
..
},
) => {
Stmt::If(if_ @ ast::StmtIf { test, .. }) => {
if checker.enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.enabled(Rule::EmptyTypeCheckingBlock) {
if typing::is_type_checking_block(if_, &checker.semantic) {
flake8_type_checking::rules::empty_type_checking_block(checker, if_);
@@ -1092,7 +1092,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
);
}
if checker.enabled(Rule::IfWithSameArms) {
flake8_simplify::rules::if_with_same_arms(checker, checker.locator, if_);
flake8_simplify::rules::if_with_same_arms(checker, if_);
}
if checker.enabled(Rule::NeedlessBool) {
flake8_simplify::rules::needless_bool(checker, if_);
@@ -1117,9 +1117,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pyupgrade::rules::outdated_version_block(checker, if_);
}
if checker.enabled(Rule::CollapsibleElseIf) {
if let Some(diagnostic) = pylint::rules::collapsible_else_if(elif_else_clauses) {
checker.diagnostics.push(diagnostic);
}
pylint::rules::collapsible_else_if(checker, stmt);
}
if checker.enabled(Rule::CheckAndRemoveFromSet) {
refurb::rules::check_and_remove_from_set(checker, if_);
@@ -1210,6 +1208,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1237,6 +1238,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
Stmt::While(while_stmt @ ast::StmtWhile { body, orelse, .. }) => {
if checker.enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.enabled(Rule::FunctionUsesLoopVariable) {
flake8_bugbear::rules::function_uses_loop_variable(checker, &Node::Stmt(stmt));
}
@@ -1260,6 +1264,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
range: _,
},
) => {
if checker.enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.any_enabled(&[
Rule::EnumerateForLoop,
Rule::IncorrectDictIterator,
@@ -1324,6 +1331,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
finalbody,
..
}) => {
if checker.enabled(Rule::TooManyNestedBlocks) {
pylint::rules::too_many_nested_blocks(checker, stmt);
}
if checker.enabled(Rule::JumpStatementInFinally) {
flake8_bugbear::rules::jump_statement_in_finally(checker, finalbody);
}
@@ -1458,6 +1468,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_assign(checker, assign);
}
if checker.enabled(Rule::UnsortedDunderSlots) {
ruff::rules::sort_dunder_slots_assign(checker, assign);
}
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnprefixedTypeParam,
@@ -1531,6 +1544,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::UnsortedDunderAll) {
ruff::rules::sort_dunder_all_ann_assign(checker, assign_stmt);
}
if checker.enabled(Rule::UnsortedDunderSlots) {
ruff::rules::sort_dunder_slots_ann_assign(checker, assign_stmt);
}
if checker.source_type.is_stub() {
if let Some(value) = value {
if checker.enabled(Rule::AssignmentDefaultInStub) {

View File

@@ -721,6 +721,21 @@ where
AnnotationContext::RuntimeEvaluated => {
self.visit_runtime_evaluated_annotation(annotation);
}
AnnotationContext::TypingOnly
if flake8_type_checking::helpers::is_dataclass_meta_annotation(
annotation,
self.semantic(),
) =>
{
if let Expr::Subscript(subscript) = &**annotation {
// Ex) `InitVar[str]`
self.visit_runtime_required_annotation(&subscript.value);
self.visit_annotation(&subscript.slice);
} else {
// Ex) `InitVar`
self.visit_runtime_required_annotation(annotation);
}
}
AnnotationContext::TypingOnly => self.visit_annotation(annotation),
}

View File

@@ -224,11 +224,13 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E0116") => (RuleGroup::Stable, rules::pylint::rules::ContinueInFinally),
(Pylint, "E0117") => (RuleGroup::Stable, rules::pylint::rules::NonlocalWithoutBinding),
(Pylint, "E0118") => (RuleGroup::Stable, rules::pylint::rules::LoadBeforeGlobalDeclaration),
(Pylint, "E0237") => (RuleGroup::Stable, rules::pylint::rules::NonSlotAssignment),
(Pylint, "E0241") => (RuleGroup::Stable, rules::pylint::rules::DuplicateBases),
(Pylint, "E0302") => (RuleGroup::Stable, rules::pylint::rules::UnexpectedSpecialMethodSignature),
(Pylint, "E0307") => (RuleGroup::Stable, rules::pylint::rules::InvalidStrReturnType),
(Pylint, "E0604") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllObject),
(Pylint, "E0605") => (RuleGroup::Stable, rules::pylint::rules::InvalidAllFormat),
(Pylint, "E0643") => (RuleGroup::Preview, rules::pylint::rules::PotentialIndexError),
(Pylint, "E0704") => (RuleGroup::Preview, rules::pylint::rules::MisplacedBareRaise),
(Pylint, "E1132") => (RuleGroup::Preview, rules::pylint::rules::RepeatedKeywordArgument),
(Pylint, "E1142") => (RuleGroup::Stable, rules::pylint::rules::AwaitOutsideAsync),
@@ -259,6 +261,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
(Pylint, "R1701") => (RuleGroup::Stable, rules::pylint::rules::RepeatedIsinstanceCalls),
(Pylint, "R1702") => (RuleGroup::Preview, rules::pylint::rules::TooManyNestedBlocks),
(Pylint, "R1704") => (RuleGroup::Preview, rules::pylint::rules::RedefinedArgumentFromLocal),
(Pylint, "R1711") => (RuleGroup::Stable, rules::pylint::rules::UselessReturn),
(Pylint, "R1714") => (RuleGroup::Stable, rules::pylint::rules::RepeatedEqualityComparison),
@@ -925,6 +928,10 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
(Ruff, "021") => (RuleGroup::Preview, rules::ruff::rules::ParenthesizeChainedOperators),
(Ruff, "022") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderAll),
(Ruff, "023") => (RuleGroup::Preview, rules::ruff::rules::UnsortedDunderSlots),
(Ruff, "024") => (RuleGroup::Preview, rules::ruff::rules::MutableFromkeysValue),
(Ruff, "025") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryDictComprehensionForIterable),
(Ruff, "026") => (RuleGroup::Preview, rules::ruff::rules::DefaultFactoryKwarg),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -795,6 +795,23 @@ mod tests {
Ok(())
}
#[test]
fn test_undefined_name() -> Result<(), NotebookError> {
let actual = notebook_path("undefined_name.ipynb");
let expected = notebook_path("undefined_name.ipynb");
let TestedNotebook {
messages,
source_notebook,
..
} = assert_notebook_path(
&actual,
expected,
&settings::LinterSettings::for_rule(Rule::UndefinedName),
)?;
assert_messages!(messages, actual, source_notebook);
Ok(())
}
#[test]
fn test_json_consistency() -> Result<()> {
let actual_path = notebook_path("before_fix.ipynb");

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_index::Indexer;
use ruff_source_file::Locator;
@@ -30,14 +30,16 @@ use super::super::detection::comment_contains_code;
#[violation]
pub struct CommentedOutCode;
impl AlwaysFixableViolation for CommentedOutCode {
impl Violation for CommentedOutCode {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::None;
#[derive_message_formats]
fn message(&self) -> String {
format!("Found commented-out code")
}
fn fix_title(&self) -> String {
"Remove commented-out code".to_string()
fn fix_title(&self) -> Option<String> {
Some(format!("Remove commented-out code"))
}
}
@@ -65,7 +67,6 @@ pub(crate) fn commented_out_code(
// Verify that the comment is on its own line, and that it contains code.
if is_standalone_comment(line) && comment_contains_code(line, &settings.task_tags[..]) {
let mut diagnostic = Diagnostic::new(CommentedOutCode, *range);
diagnostic.set_fix(Fix::display_only_edit(Edit::range_deletion(
locator.full_lines_range(*range),
)));

View File

@@ -9,4 +9,3 @@ EXE001_1.py:1:1: EXE001 Shebang is present but file is not executable
3 | if __name__ == '__main__':
|

View File

@@ -7,5 +7,3 @@ EXE002_1.py:1:1: EXE002 The file is executable but no shebang is present
| EXE002
2 | print('I should be executable.')
|

View File

@@ -3,6 +3,7 @@ use std::iter;
use itertools::Either::{Left, Right};
use ruff_python_semantic::{analyze, SemanticModel};
use ruff_text_size::{Ranged, TextRange};
use ruff_python_ast::{self as ast, Arguments, BoolOp, Expr, ExprContext, Identifier};
@@ -36,6 +37,14 @@ use crate::checkers::ast::Checker;
/// print("Greetings!")
/// ```
///
/// ## Fix safety
/// This rule's fix is unsafe, as in some cases, it will be unable to determine
/// whether the argument to an existing `.startswith` or `.endswith` call is a
/// tuple. For example, given `msg.startswith(x) or msg.startswith(y)`, if `x`
/// or `y` is a tuple, and the semantic model is unable to detect it as such,
/// the rule will suggest `msg.startswith((x, y))`, which will error at
/// runtime.
///
/// ## References
/// - [Python documentation: `str.startswith`](https://docs.python.org/3/library/stdtypes.html#str.startswith)
/// - [Python documentation: `str.endswith`](https://docs.python.org/3/library/stdtypes.html#str.endswith)
@@ -84,10 +93,14 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
continue;
};
if !(args.len() == 1 && keywords.is_empty()) {
if !keywords.is_empty() {
continue;
}
let [arg] = args.as_slice() else {
continue;
};
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() else {
continue;
};
@@ -99,6 +112,13 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
continue;
};
// If the argument is bound to a tuple, skip it, since we don't want to suggest
// `startswith((x, y))` where `x` or `y` are tuples. (Tuple literals are okay, since we
// inline them below.)
if is_bound_to_tuple(arg, checker.semantic()) {
continue;
}
duplicates
.entry((attr.as_str(), arg_name.as_str()))
.or_insert_with(Vec::new)
@@ -149,7 +169,7 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
Right(iter::once(*value))
}
})
.map(Clone::clone)
.cloned()
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
@@ -202,3 +222,18 @@ pub(crate) fn multiple_starts_ends_with(checker: &mut Checker, expr: &Expr) {
}
}
}
/// Returns `true` if the expression definitively resolves to a tuple (e.g., `x` in `x = (1, 2)`).
fn is_bound_to_tuple(arg: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Name(ast::ExprName { id, .. }) = arg else {
return false;
};
let Some(binding_id) = semantic.lookup_symbol(id.as_str()) else {
return false;
};
let binding = semantic.binding(binding_id);
analyze::typing::is_tuple(binding, semantic)
}

View File

@@ -89,7 +89,7 @@ PIE810.py:10:1: PIE810 [*] Call `startswith` once with a `tuple`
10 | obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE810
11 |
12 | # ok
12 | def func():
|
= help: Merge into a single `startswith` call
@@ -100,7 +100,47 @@ PIE810.py:10:1: PIE810 [*] Call `startswith` once with a `tuple`
10 |-obj.endswith(foo) or obj.startswith(foo) or obj.startswith("foo")
10 |+obj.endswith(foo) or obj.startswith((foo, "foo"))
11 11 |
12 12 | # ok
13 13 | obj.startswith(("foo", "bar"))
12 12 | def func():
13 13 | msg = "hello world"
PIE810.py:19:8: PIE810 [*] Call `startswith` once with a `tuple`
|
17 | z = "w"
18 |
19 | if msg.startswith(x) or msg.startswith(y) or msg.startswith(z): # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE810
20 | print("yes")
|
= help: Merge into a single `startswith` call
Unsafe fix
16 16 | y = ("h", "e", "l", "l", "o")
17 17 | z = "w"
18 18 |
19 |- if msg.startswith(x) or msg.startswith(y) or msg.startswith(z): # Error
19 |+ if msg.startswith((x, z)) or msg.startswith(y): # Error
20 20 | print("yes")
21 21 |
22 22 | def func():
PIE810.py:25:8: PIE810 [*] Call `startswith` once with a `tuple`
|
23 | msg = "hello world"
24 |
25 | if msg.startswith(("h", "e", "l", "l", "o")) or msg.startswith("h") or msg.startswith("w"): # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PIE810
26 | print("yes")
|
= help: Merge into a single `startswith` call
Unsafe fix
22 22 | def func():
23 23 | msg = "hello world"
24 24 |
25 |- if msg.startswith(("h", "e", "l", "l", "o")) or msg.startswith("h") or msg.startswith("w"): # Error
25 |+ if msg.startswith(("h", "e", "l", "l", "o", "h", "w")): # Error
26 26 | print("yes")
27 27 |
28 28 | # ok

View File

@@ -226,6 +226,10 @@ impl Violation for PytestParametrizeValuesWrongType {
/// ...
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as tests that rely on mutable global
/// state may be affected by removing duplicate test cases.
///
/// ## References
/// - [`pytest` documentation: How to parametrize fixtures and test functions](https://docs.pytest.org/en/latest/how-to/parametrize.html#pytest-mark-parametrize)
#[violation]

View File

@@ -11,10 +11,11 @@ mod tests {
use anyhow::Result;
use test_case::test_case;
use crate::assert_messages;
use crate::registry::Rule;
use crate::settings::types::PreviewMode;
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::{assert_messages, settings};
#[test_case(Rule::UnnecessaryReturnNone, Path::new("RET501.py"))]
#[test_case(Rule::ImplicitReturnValue, Path::new("RET502.py"))]
@@ -33,4 +34,25 @@ mod tests {
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Rule::SuperfluousElseReturn, Path::new("RET505.py"))]
#[test_case(Rule::SuperfluousElseRaise, Path::new("RET506.py"))]
#[test_case(Rule::SuperfluousElseContinue, Path::new("RET507.py"))]
#[test_case(Rule::SuperfluousElseBreak, Path::new("RET508.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("flake8_return").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
}

View File

@@ -1,9 +1,10 @@
use anyhow::Result;
use std::ops::Add;
use ruff_python_ast::{self as ast, ElifElseClause, Expr, Stmt};
use ruff_text_size::{Ranged, TextRange, TextSize};
use ruff_diagnostics::{AlwaysFixableViolation, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, FixAvailability, Violation};
use ruff_diagnostics::{Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
@@ -11,13 +12,17 @@ use ruff_python_ast::helpers::{is_const_false, is_const_true};
use ruff_python_ast::stmt_if::elif_else_range;
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::whitespace::indentation;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_semantic::SemanticModel;
use ruff_python_trivia::is_python_whitespace;
use ruff_python_trivia::{is_python_whitespace, SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use crate::checkers::ast::Checker;
use crate::fix::edits;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_return::helpers::end_of_last_statement;
use crate::rules::pyupgrade::fixes::adjust_indentation;
use super::super::branch::Branch;
use super::super::helpers::result_exists;
@@ -210,11 +215,17 @@ pub struct SuperfluousElseReturn {
}
impl Violation for SuperfluousElseReturn {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let SuperfluousElseReturn { branch } = self;
format!("Unnecessary `{branch}` after `return` statement")
}
fn fix_title(&self) -> Option<String> {
let SuperfluousElseReturn { branch } = self;
Some(format!("Remove unnecessary `{branch}`"))
}
}
/// ## What it does
@@ -248,11 +259,17 @@ pub struct SuperfluousElseRaise {
}
impl Violation for SuperfluousElseRaise {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let SuperfluousElseRaise { branch } = self;
format!("Unnecessary `{branch}` after `raise` statement")
}
fn fix_title(&self) -> Option<String> {
let SuperfluousElseRaise { branch } = self;
Some(format!("Remove unnecessary `{branch}`"))
}
}
/// ## What it does
@@ -288,11 +305,17 @@ pub struct SuperfluousElseContinue {
}
impl Violation for SuperfluousElseContinue {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let SuperfluousElseContinue { branch } = self;
format!("Unnecessary `{branch}` after `continue` statement")
}
fn fix_title(&self) -> Option<String> {
let SuperfluousElseContinue { branch } = self;
Some(format!("Remove unnecessary `{branch}`"))
}
}
/// ## What it does
@@ -328,11 +351,17 @@ pub struct SuperfluousElseBreak {
}
impl Violation for SuperfluousElseBreak {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
let SuperfluousElseBreak { branch } = self;
format!("Unnecessary `{branch}` after `break` statement")
}
fn fix_title(&self) -> Option<String> {
let SuperfluousElseBreak { branch } = self;
Some(format!("Remove unnecessary `{branch}`"))
}
}
/// RET501
@@ -368,9 +397,11 @@ fn implicit_return_value(checker: &mut Checker, stack: &Stack) {
}
}
/// Return `true` if the `func` is a known function that never returns.
/// Return `true` if the `func` appears to be non-returning.
fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(func).is_some_and(|call_path| {
// First, look for known functions that never return from the standard library and popular
// libraries.
if semantic.resolve_call_path(func).is_some_and(|call_path| {
matches!(
call_path.as_slice(),
["" | "builtins" | "sys" | "_thread" | "pytest", "exit"]
@@ -379,7 +410,32 @@ fn is_noreturn_func(func: &Expr, semantic: &SemanticModel) -> bool {
| ["_winapi", "ExitProcess"]
| ["pytest", "fail" | "skip" | "xfail"]
) || semantic.match_typing_call_path(&call_path, "assert_never")
})
}) {
return true;
}
// Second, look for `NoReturn` annotations on the return type.
let Some(func_binding) = semantic.lookup_attribute(func) else {
return false;
};
let Some(node_id) = semantic.binding(func_binding).source else {
return false;
};
let Stmt::FunctionDef(ast::StmtFunctionDef { returns, .. }) = semantic.statement(node_id)
else {
return false;
};
let Some(returns) = returns.as_ref() else {
return false;
};
let Some(call_path) = semantic.resolve_call_path(returns) else {
return false;
};
semantic.match_typing_call_path(&call_path, "NoReturn")
}
/// RET503
@@ -575,42 +631,82 @@ fn superfluous_else_node(
};
for child in if_elif_body {
if child.is_return_stmt() {
let diagnostic = Diagnostic::new(
let mut diagnostic = Diagnostic::new(
SuperfluousElseReturn { branch },
elif_else_range(elif_else, checker.locator().contents())
.unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_else(
elif_else,
checker.locator(),
checker.indexer(),
checker.stylist(),
)
});
}
checker.diagnostics.push(diagnostic);
}
return true;
} else if child.is_break_stmt() {
let diagnostic = Diagnostic::new(
let mut diagnostic = Diagnostic::new(
SuperfluousElseBreak { branch },
elif_else_range(elif_else, checker.locator().contents())
.unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_else(
elif_else,
checker.locator(),
checker.indexer(),
checker.stylist(),
)
});
}
checker.diagnostics.push(diagnostic);
}
return true;
} else if child.is_raise_stmt() {
let diagnostic = Diagnostic::new(
let mut diagnostic = Diagnostic::new(
SuperfluousElseRaise { branch },
elif_else_range(elif_else, checker.locator().contents())
.unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_else(
elif_else,
checker.locator(),
checker.indexer(),
checker.stylist(),
)
});
}
checker.diagnostics.push(diagnostic);
}
return true;
} else if child.is_continue_stmt() {
let diagnostic = Diagnostic::new(
let mut diagnostic = Diagnostic::new(
SuperfluousElseContinue { branch },
elif_else_range(elif_else, checker.locator().contents())
.unwrap_or_else(|| elif_else.range()),
);
if checker.enabled(diagnostic.kind.rule()) {
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_else(
elif_else,
checker.locator(),
checker.indexer(),
checker.stylist(),
)
});
}
checker.diagnostics.push(diagnostic);
}
return true;
@@ -641,7 +737,7 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
// Traverse the function body, to collect the stack.
let stack = {
let mut visitor = ReturnVisitor::default();
let mut visitor = ReturnVisitor::new(checker.semantic());
for stmt in body {
visitor.visit_stmt(stmt);
}
@@ -688,3 +784,83 @@ pub(crate) fn function(checker: &mut Checker, body: &[Stmt], returns: Option<&Ex
}
}
}
/// Generate a [`Fix`] to remove an `else` or `elif` clause.
fn remove_else(
elif_else: &ElifElseClause,
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Result<Fix> {
if elif_else.test.is_some() {
// Ex) `elif` -> `if`
Ok(Fix::safe_edit(Edit::deletion(
elif_else.start(),
elif_else.start() + TextSize::from(2),
)))
} else {
// the start of the line where the `else`` is
let else_line_start = locator.line_start(elif_else.start());
// making a tokenizer to find the Colon for the `else`, not always on the same line!
let mut else_line_tokenizer =
SimpleTokenizer::starts_at(elif_else.start(), locator.contents());
// find the Colon for the `else`
let Some(else_colon) =
else_line_tokenizer.find(|token| token.kind == SimpleTokenKind::Colon)
else {
return Err(anyhow::anyhow!("Cannot find `:` in `else` statement"));
};
// get the indentation of the `else`, since that is the indent level we want to end with
let Some(desired_indentation) = indentation(locator, elif_else) else {
return Err(anyhow::anyhow!("Compound statement cannot be inlined"));
};
// If the statement is on the same line as the `else`, just remove the `else: `.
// Ex) `else: return True` -> `return True`
let Some(first) = elif_else.body.first() else {
return Err(anyhow::anyhow!("`else` statement has no body"));
};
if indexer.in_multi_statement_line(first, locator) {
return Ok(Fix::safe_edit(Edit::deletion(
elif_else.start(),
first.start(),
)));
}
// we're deleting the `else`, and it's Colon, and the rest of the line(s) they're on,
// so here we get the last position of the line the Colon is on
let else_colon_end = locator.full_line_end(else_colon.end());
// if there is a comment on the same line as the Colon, let's keep it
// and give it the proper indentation once we unindent it
let else_comment_after_colon = else_line_tokenizer
.find(|token| token.kind.is_comment())
.and_then(|token| {
if token.kind == SimpleTokenKind::Comment && token.start() < else_colon_end {
return Some(format!(
"{desired_indentation}{}{}",
locator.slice(token),
stylist.line_ending().as_str(),
));
}
None
})
.unwrap_or(String::new());
let indented = adjust_indentation(
TextRange::new(else_colon_end, elif_else.end()),
desired_indentation,
locator,
stylist,
)?;
Ok(Fix::safe_edit(Edit::replacement(
format!("{else_comment_after_colon}{indented}"),
else_line_start,
elif_else.end(),
)))
}
}

View File

@@ -1,403 +1,456 @@
---
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
---
RET503.py:20:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:21:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
18 | # if/elif/else
19 | def x(y):
20 | if not y:
19 | # if/elif/else
20 | def x(y):
21 | if not y:
| _____^
21 | | return 1
22 | | return 1
| |________________^ RET503
22 | # error
23 | # error
|
= help: Add explicit `return` statement
Unsafe fix
19 19 | def x(y):
20 20 | if not y:
21 21 | return 1
22 |+ return None
22 23 | # error
23 24 |
20 20 | def x(y):
21 21 | if not y:
22 22 | return 1
23 |+ return None
23 24 | # error
24 25 |
25 26 |
RET503.py:27:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:28:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
25 | def x(y):
26 | if not y:
27 | print() # error
26 | def x(y):
27 | if not y:
28 | print() # error
| ^^^^^^^ RET503
28 | else:
29 | return 2
29 | else:
30 | return 2
|
= help: Add explicit `return` statement
Unsafe fix
25 25 | def x(y):
26 26 | if not y:
27 27 | print() # error
28 |+ return None
28 29 | else:
29 30 | return 2
30 31 |
26 26 | def x(y):
27 27 | if not y:
28 28 | print() # error
29 |+ return None
29 30 | else:
30 31 | return 2
31 32 |
RET503.py:36:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:37:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
34 | return 1
35 |
36 | print() # error
35 | return 1
36 |
37 | print() # error
| ^^^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
34 34 | return 1
35 35 |
36 36 | print() # error
37 |+ return None
37 38 |
35 35 | return 1
36 36 |
37 37 | print() # error
38 |+ return None
38 39 |
39 40 | # for
39 40 |
40 41 | # for
RET503.py:41:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:42:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
39 | # for
40 | def x(y):
41 | for i in range(10):
40 | # for
41 | def x(y):
42 | for i in range(10):
| _____^
42 | | if i > 10:
43 | | return i
43 | | if i > 10:
44 | | return i
| |____________________^ RET503
44 | # error
45 | # error
|
= help: Add explicit `return` statement
Unsafe fix
41 41 | for i in range(10):
42 42 | if i > 10:
43 43 | return i
44 |+ return None
44 45 | # error
45 46 |
42 42 | for i in range(10):
43 43 | if i > 10:
44 44 | return i
45 |+ return None
45 46 | # error
46 47 |
47 48 |
RET503.py:52:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:53:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
50 | return i
51 | else:
52 | print() # error
51 | return i
52 | else:
53 | print() # error
| ^^^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
50 50 | return i
51 51 | else:
52 52 | print() # error
53 |+ return None
53 54 |
51 51 | return i
52 52 | else:
53 53 | print() # error
54 |+ return None
54 55 |
55 56 | # A nonexistent function
55 56 |
56 57 | # A nonexistent function
RET503.py:59:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:60:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
57 | if x > 0:
58 | return False
59 | no_such_function() # error
58 | if x > 0:
59 | return False
60 | no_such_function() # error
| ^^^^^^^^^^^^^^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
57 57 | if x > 0:
58 58 | return False
59 59 | no_such_function() # error
60 |+ return None
60 61 |
58 58 | if x > 0:
59 59 | return False
60 60 | no_such_function() # error
61 |+ return None
61 62 |
62 63 | # A function that does return the control
62 63 |
63 64 | # A function that does return the control
RET503.py:66:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:67:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
64 | if x > 0:
65 | return False
66 | print("", end="") # error
65 | if x > 0:
66 | return False
67 | print("", end="") # error
| ^^^^^^^^^^^^^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
64 64 | if x > 0:
65 65 | return False
66 66 | print("", end="") # error
67 |+ return None
67 68 |
65 65 | if x > 0:
66 66 | return False
67 67 | print("", end="") # error
68 |+ return None
68 69 |
69 70 | ###
69 70 |
70 71 | ###
RET503.py:82:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:83:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
80 | # last line in while loop
81 | def x(y):
82 | while i > 0:
81 | # last line in while loop
82 | def x(y):
83 | while i > 0:
| _____^
83 | | if y > 0:
84 | | return 1
85 | | y += 1
84 | | if y > 0:
85 | | return 1
86 | | y += 1
| |______________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
83 83 | if y > 0:
84 84 | return 1
85 85 | y += 1
86 |+ return None
86 87 |
84 84 | if y > 0:
85 85 | return 1
86 86 | y += 1
87 |+ return None
87 88 |
88 89 | # exclude empty functions
88 89 |
89 90 | # exclude empty functions
RET503.py:113:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:114:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
111 | # return value within loop
112 | def bar1(x, y, z):
113 | for i in x:
112 | # return value within loop
113 | def bar1(x, y, z):
114 | for i in x:
| _____^
114 | | if i > y:
115 | | break
116 | | return z
115 | | if i > y:
116 | | break
117 | | return z
| |________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
114 114 | if i > y:
115 115 | break
116 116 | return z
117 |+ return None
117 118 |
115 115 | if i > y:
116 116 | break
117 117 | return z
118 |+ return None
118 119 |
119 120 | def bar3(x, y, z):
119 120 |
120 121 | def bar3(x, y, z):
RET503.py:120:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:121:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
119 | def bar3(x, y, z):
120 | for i in x:
120 | def bar3(x, y, z):
121 | for i in x:
| _____^
121 | | if i > y:
122 | | if z:
123 | | break
124 | | else:
125 | | return z
126 | | return None
122 | | if i > y:
123 | | if z:
124 | | break
125 | | else:
126 | | return z
127 | | return None
| |___________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
124 124 | else:
125 125 | return z
126 126 | return None
127 |+ return None
127 128 |
125 125 | else:
126 126 | return z
127 127 | return None
128 |+ return None
128 129 |
129 130 | def bar1(x, y, z):
129 130 |
130 131 | def bar1(x, y, z):
RET503.py:130:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:131:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
129 | def bar1(x, y, z):
130 | for i in x:
130 | def bar1(x, y, z):
131 | for i in x:
| _____^
131 | | if i < y:
132 | | continue
133 | | return z
132 | | if i < y:
133 | | continue
134 | | return z
| |________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
131 131 | if i < y:
132 132 | continue
133 133 | return z
134 |+ return None
134 135 |
132 132 | if i < y:
133 133 | continue
134 134 | return z
135 |+ return None
135 136 |
136 137 | def bar3(x, y, z):
136 137 |
137 138 | def bar3(x, y, z):
RET503.py:137:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:138:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
136 | def bar3(x, y, z):
137 | for i in x:
137 | def bar3(x, y, z):
138 | for i in x:
| _____^
138 | | if i < y:
139 | | if z:
140 | | continue
141 | | else:
142 | | return z
143 | | return None
139 | | if i < y:
140 | | if z:
141 | | continue
142 | | else:
143 | | return z
144 | | return None
| |___________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
141 141 | else:
142 142 | return z
143 143 | return None
144 |+ return None
144 145 |
142 142 | else:
143 143 | return z
144 144 | return None
145 |+ return None
145 146 |
146 147 | def prompts(self, foo):
146 147 |
147 148 | def prompts(self, foo):
RET503.py:274:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:275:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
272 | return False
273 |
274 | for value in values:
273 | return False
274 |
275 | for value in values:
| _____^
275 | | print(value)
276 | | print(value)
| |____________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
273 273 |
274 274 | for value in values:
275 275 | print(value)
276 |+ return None
276 277 |
274 274 |
275 275 | for value in values:
276 276 | print(value)
277 |+ return None
277 278 |
278 279 | def while_true():
278 279 |
279 280 | def while_true():
RET503.py:291:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:292:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
289 | return 1
290 | case 1:
291 | print() # error
290 | return 1
291 | case 1:
292 | print() # error
| ^^^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
289 289 | return 1
290 290 | case 1:
291 291 | print() # error
292 |+ return None
292 293 |
290 290 | return 1
291 291 | case 1:
292 292 | print() # error
293 |+ return None
293 294 |
294 295 | def foo(baz: str) -> str:
294 295 |
295 296 | def foo(baz: str) -> str:
RET503.py:300:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:301:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
298 | def end_of_statement():
299 | def example():
300 | if True:
299 | def end_of_statement():
300 | def example():
301 | if True:
| _________^
301 | | return ""
302 | | return ""
| |_____________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
299 299 | def example():
300 300 | if True:
301 301 | return ""
302 |+ return None
302 303 |
300 300 | def example():
301 301 | if True:
302 302 | return ""
303 |+ return None
303 304 |
304 305 | def example():
304 305 |
305 306 | def example():
RET503.py:305:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:306:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
304 | def example():
305 | if True:
305 | def example():
306 | if True:
| _________^
306 | | return ""
307 | | return ""
| |_____________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
304 304 | def example():
305 305 | if True:
306 306 | return ""
307 |+ return None
307 308 |
305 305 | def example():
306 306 | if True:
307 307 | return ""
308 |+ return None
308 309 |
309 310 | def example():
309 310 |
310 311 | def example():
RET503.py:310:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:311:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
309 | def example():
310 | if True:
310 | def example():
311 | if True:
| _________^
311 | | return "" # type: ignore
312 | | return "" # type: ignore
| |_____________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
309 309 | def example():
310 310 | if True:
311 311 | return "" # type: ignore
312 |+ return None
312 313 |
310 310 | def example():
311 311 | if True:
312 312 | return "" # type: ignore
313 |+ return None
313 314 |
314 315 | def example():
314 315 |
315 316 | def example():
RET503.py:315:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:316:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
314 | def example():
315 | if True:
315 | def example():
316 | if True:
| _________^
316 | | return "" ;
317 | | return "" ;
| |_____________________^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
314 314 | def example():
315 315 | if True:
316 316 | return "" ;
317 |+ return None
317 318 |
315 315 | def example():
316 316 | if True:
317 317 | return "" ;
318 |+ return None
318 319 |
319 320 | def example():
319 320 |
320 321 | def example():
RET503.py:320:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:321:9: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
319 | def example():
320 | if True:
320 | def example():
321 | if True:
| _________^
321 | | return "" \
322 | | return "" \
| |_____________________^ RET503
322 | ; # type: ignore
323 | ; # type: ignore
|
= help: Add explicit `return` statement
Unsafe fix
320 320 | if True:
321 321 | return "" \
322 322 | ; # type: ignore
323 |+ return None
323 324 |
321 321 | if True:
322 322 | return "" \
323 323 | ; # type: ignore
324 |+ return None
324 325 |
325 326 | def end_of_file():
325 326 |
326 327 | def end_of_file():
RET503.py:328:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
RET503.py:329:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
326 | if False:
327 | return 1
328 | x = 2 \
327 | if False:
328 | return 1
329 | x = 2 \
| ^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
326 326 | if False:
327 327 | return 1
328 328 | x = 2 \
329 |+
330 |+ return None
328 328 | return 1
329 329 | x = 2 \
330 330 |
331 |+ return None
331 332 |
332 333 |
333 334 | # function return type annotation NoReturn
RET503.py:339:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
337 | if x == 5:
338 | return 5
339 | bar()
| ^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
337 337 | if x == 5:
338 338 | return 5
339 339 | bar()
340 |+ return None
340 341 |
341 342 |
342 343 | def foo(string: str) -> str:
RET503.py:354:13: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
352 | return "third"
353 | case _:
354 | raises(string)
| ^^^^^^^^^^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
352 352 | return "third"
353 353 | case _:
354 354 | raises(string)
355 |+ return None
355 356 |
356 357 |
357 358 | def foo() -> int:
RET503.py:370:5: RET503 [*] Missing explicit `return` at the end of function able to return non-`None` value
|
368 | if baz() > 3:
369 | return 1
370 | bar()
| ^^^^^ RET503
|
= help: Add explicit `return` statement
Unsafe fix
368 368 | if baz() > 3:
369 369 | return 1
370 370 | bar()
371 |+ return None

View File

@@ -217,5 +217,28 @@ RET504.py:365:12: RET504 [*] Unnecessary assignment to `D` before `return` state
364 |- D=0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
365 |- return D
364 |+ return 0.4853881 + 3.6006116*P - 0.0117368*(P-1.3822)**2
366 365 |
367 366 |
368 367 | # contextlib suppress in with statement
RET504.py:400:12: RET504 [*] Unnecessary assignment to `y` before `return` statement
|
398 | x = 1
399 | y = y + 2
400 | return y # RET504
| ^ RET504
|
= help: Remove unnecessary assignment
Unsafe fix
396 396 | x = 2
397 397 | with contextlib.suppress(Exception):
398 398 | x = 1
399 |- y = y + 2
400 |- return y # RET504
399 |+ return y + 2
401 400 |
402 401 |
403 402 | def foo():

View File

@@ -10,6 +10,7 @@ RET505.py:8:5: RET505 Unnecessary `elif` after `return` statement
9 | b = 2
10 | return w
|
= help: Remove unnecessary `elif`
RET505.py:23:5: RET505 Unnecessary `elif` after `return` statement
|
@@ -20,6 +21,7 @@ RET505.py:23:5: RET505 Unnecessary `elif` after `return` statement
24 | c = 2
25 | else:
|
= help: Remove unnecessary `elif`
RET505.py:41:5: RET505 Unnecessary `elif` after `return` statement
|
@@ -30,6 +32,7 @@ RET505.py:41:5: RET505 Unnecessary `elif` after `return` statement
42 | b = 2
43 | return w
|
= help: Remove unnecessary `elif`
RET505.py:53:5: RET505 Unnecessary `else` after `return` statement
|
@@ -40,6 +43,7 @@ RET505.py:53:5: RET505 Unnecessary `else` after `return` statement
54 | b = 2
55 | return z
|
= help: Remove unnecessary `else`
RET505.py:64:9: RET505 Unnecessary `else` after `return` statement
|
@@ -50,6 +54,7 @@ RET505.py:64:9: RET505 Unnecessary `else` after `return` statement
65 | c = 3
66 | return x
|
= help: Remove unnecessary `else`
RET505.py:79:5: RET505 Unnecessary `else` after `return` statement
|
@@ -60,6 +65,7 @@ RET505.py:79:5: RET505 Unnecessary `else` after `return` statement
80 | c = 3
81 | return
|
= help: Remove unnecessary `else`
RET505.py:89:9: RET505 Unnecessary `else` after `return` statement
|
@@ -70,6 +76,7 @@ RET505.py:89:9: RET505 Unnecessary `else` after `return` statement
90 | b = 2
91 | else:
|
= help: Remove unnecessary `else`
RET505.py:99:5: RET505 Unnecessary `else` after `return` statement
|
@@ -80,5 +87,68 @@ RET505.py:99:5: RET505 Unnecessary `else` after `return` statement
100 | try:
101 | return False
|
= help: Remove unnecessary `else`
RET505.py:137:5: RET505 Unnecessary `else` after `return` statement
|
135 | if True:
136 | return
137 | else:
| ^^^^ RET505
138 | # comment
139 | pass
|
= help: Remove unnecessary `else`
RET505.py:145:5: RET505 Unnecessary `else` after `return` statement
|
143 | if True:
144 | return
145 | else: # comment
| ^^^^ RET505
146 | pass
|
= help: Remove unnecessary `else`
RET505.py:152:5: RET505 Unnecessary `else` after `return` statement
|
150 | if True:
151 | return
152 | else\
| ^^^^ RET505
153 | :\
154 | # comment
|
= help: Remove unnecessary `else`
RET505.py:161:5: RET505 Unnecessary `else` after `return` statement
|
159 | if True:
160 | return
161 | else\
| ^^^^ RET505
162 | : # comment
163 | pass
|
= help: Remove unnecessary `else`
RET505.py:169:5: RET505 Unnecessary `else` after `return` statement
|
167 | if True:
168 | return
169 | else: pass
| ^^^^ RET505
|
= help: Remove unnecessary `else`
RET505.py:175:5: RET505 Unnecessary `else` after `return` statement
|
173 | if True:
174 | return
175 | else:\
| ^^^^ RET505
176 | pass
|
= help: Remove unnecessary `else`

View File

@@ -10,6 +10,7 @@ RET506.py:8:5: RET506 Unnecessary `elif` after `raise` statement
9 | b = 2
10 | raise Exception(w)
|
= help: Remove unnecessary `elif`
RET506.py:23:5: RET506 Unnecessary `elif` after `raise` statement
|
@@ -20,6 +21,7 @@ RET506.py:23:5: RET506 Unnecessary `elif` after `raise` statement
24 | raise Exception(y)
25 | else:
|
= help: Remove unnecessary `elif`
RET506.py:34:5: RET506 Unnecessary `else` after `raise` statement
|
@@ -30,6 +32,7 @@ RET506.py:34:5: RET506 Unnecessary `else` after `raise` statement
35 | b = 2
36 | raise Exception(z)
|
= help: Remove unnecessary `else`
RET506.py:45:9: RET506 Unnecessary `else` after `raise` statement
|
@@ -40,6 +43,7 @@ RET506.py:45:9: RET506 Unnecessary `else` after `raise` statement
46 | c = 3
47 | raise Exception(x)
|
= help: Remove unnecessary `else`
RET506.py:60:5: RET506 Unnecessary `else` after `raise` statement
|
@@ -50,6 +54,7 @@ RET506.py:60:5: RET506 Unnecessary `else` after `raise` statement
61 | c = 3
62 | raise Exception(y)
|
= help: Remove unnecessary `else`
RET506.py:70:9: RET506 Unnecessary `else` after `raise` statement
|
@@ -60,6 +65,7 @@ RET506.py:70:9: RET506 Unnecessary `else` after `raise` statement
71 | b = 2
72 | else:
|
= help: Remove unnecessary `else`
RET506.py:80:5: RET506 Unnecessary `else` after `raise` statement
|
@@ -70,5 +76,6 @@ RET506.py:80:5: RET506 Unnecessary `else` after `raise` statement
81 | try:
82 | raise Exception(False)
|
= help: Remove unnecessary `else`

View File

@@ -10,6 +10,7 @@ RET507.py:8:9: RET507 Unnecessary `elif` after `continue` statement
9 | continue
10 | else:
|
= help: Remove unnecessary `elif`
RET507.py:22:9: RET507 Unnecessary `elif` after `continue` statement
|
@@ -20,6 +21,7 @@ RET507.py:22:9: RET507 Unnecessary `elif` after `continue` statement
23 | c = 2
24 | else:
|
= help: Remove unnecessary `elif`
RET507.py:36:9: RET507 Unnecessary `else` after `continue` statement
|
@@ -29,6 +31,7 @@ RET507.py:36:9: RET507 Unnecessary `else` after `continue` statement
| ^^^^ RET507
37 | a = z
|
= help: Remove unnecessary `else`
RET507.py:47:13: RET507 Unnecessary `else` after `continue` statement
|
@@ -39,6 +42,7 @@ RET507.py:47:13: RET507 Unnecessary `else` after `continue` statement
48 | c = 3
49 | continue
|
= help: Remove unnecessary `else`
RET507.py:63:9: RET507 Unnecessary `else` after `continue` statement
|
@@ -49,6 +53,7 @@ RET507.py:63:9: RET507 Unnecessary `else` after `continue` statement
64 | c = 3
65 | continue
|
= help: Remove unnecessary `else`
RET507.py:74:13: RET507 Unnecessary `else` after `continue` statement
|
@@ -59,6 +64,7 @@ RET507.py:74:13: RET507 Unnecessary `else` after `continue` statement
75 | b = 2
76 | else:
|
= help: Remove unnecessary `else`
RET507.py:85:9: RET507 Unnecessary `else` after `continue` statement
|
@@ -69,5 +75,6 @@ RET507.py:85:9: RET507 Unnecessary `else` after `continue` statement
86 | try:
87 | return
|
= help: Remove unnecessary `else`

View File

@@ -10,6 +10,7 @@ RET508.py:8:9: RET508 Unnecessary `elif` after `break` statement
9 | break
10 | else:
|
= help: Remove unnecessary `elif`
RET508.py:22:9: RET508 Unnecessary `elif` after `break` statement
|
@@ -20,6 +21,7 @@ RET508.py:22:9: RET508 Unnecessary `elif` after `break` statement
23 | c = 2
24 | else:
|
= help: Remove unnecessary `elif`
RET508.py:33:9: RET508 Unnecessary `else` after `break` statement
|
@@ -29,6 +31,7 @@ RET508.py:33:9: RET508 Unnecessary `else` after `break` statement
| ^^^^ RET508
34 | a = z
|
= help: Remove unnecessary `else`
RET508.py:44:13: RET508 Unnecessary `else` after `break` statement
|
@@ -39,6 +42,7 @@ RET508.py:44:13: RET508 Unnecessary `else` after `break` statement
45 | c = 3
46 | break
|
= help: Remove unnecessary `else`
RET508.py:60:9: RET508 Unnecessary `else` after `break` statement
|
@@ -49,6 +53,7 @@ RET508.py:60:9: RET508 Unnecessary `else` after `break` statement
61 | c = 3
62 | break
|
= help: Remove unnecessary `else`
RET508.py:71:13: RET508 Unnecessary `else` after `break` statement
|
@@ -59,6 +64,7 @@ RET508.py:71:13: RET508 Unnecessary `else` after `break` statement
72 | b = 2
73 | else:
|
= help: Remove unnecessary `else`
RET508.py:82:9: RET508 Unnecessary `else` after `break` statement
|
@@ -69,6 +75,7 @@ RET508.py:82:9: RET508 Unnecessary `else` after `break` statement
83 | try:
84 | return
|
= help: Remove unnecessary `else`
RET508.py:158:13: RET508 Unnecessary `else` after `break` statement
|
@@ -78,5 +85,6 @@ RET508.py:158:13: RET508 Unnecessary `else` after `break` statement
| ^^^^ RET508
159 | a = z
|
= help: Remove unnecessary `else`

View File

@@ -0,0 +1,322 @@
---
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
---
RET505.py:8:5: RET505 [*] Unnecessary `elif` after `return` statement
|
6 | a = 1
7 | return y
8 | elif z:
| ^^^^ RET505
9 | b = 2
10 | return w
|
= help: Remove unnecessary `elif`
Safe fix
5 5 | if x: # [no-else-return]
6 6 | a = 1
7 7 | return y
8 |- elif z:
8 |+ if z:
9 9 | b = 2
10 10 | return w
11 11 | else:
RET505.py:23:5: RET505 [*] Unnecessary `elif` after `return` statement
|
21 | b = 2
22 | return
23 | elif z:
| ^^^^ RET505
24 | c = 2
25 | else:
|
= help: Remove unnecessary `elif`
Safe fix
20 20 | else:
21 21 | b = 2
22 22 | return
23 |- elif z:
23 |+ if z:
24 24 | c = 2
25 25 | else:
26 26 | c = 3
RET505.py:41:5: RET505 [*] Unnecessary `elif` after `return` statement
|
39 | a = 1
40 | return y
41 | elif z:
| ^^^^ RET505
42 | b = 2
43 | return w
|
= help: Remove unnecessary `elif`
Safe fix
38 38 | if x: # [no-else-return]
39 39 | a = 1
40 40 | return y
41 |- elif z:
41 |+ if z:
42 42 | b = 2
43 43 | return w
44 44 | else:
RET505.py:53:5: RET505 [*] Unnecessary `else` after `return` statement
|
51 | a = 1
52 | return y
53 | else:
| ^^^^ RET505
54 | b = 2
55 | return z
|
= help: Remove unnecessary `else`
Safe fix
50 50 | if x: # [no-else-return]
51 51 | a = 1
52 52 | return y
53 |- else:
54 |- b = 2
55 |- return z
53 |+ b = 2
54 |+ return z
56 55 |
57 56 |
58 57 | def foo3(x, y, z):
RET505.py:64:9: RET505 [*] Unnecessary `else` after `return` statement
|
62 | b = 2
63 | return y
64 | else:
| ^^^^ RET505
65 | c = 3
66 | return x
|
= help: Remove unnecessary `else`
Safe fix
61 61 | if y: # [no-else-return]
62 62 | b = 2
63 63 | return y
64 |- else:
65 |- c = 3
66 |- return x
64 |+ c = 3
65 |+ return x
67 66 | else:
68 67 | d = 4
69 68 | return z
RET505.py:79:5: RET505 [*] Unnecessary `else` after `return` statement
|
77 | b = 2
78 | return
79 | else:
| ^^^^ RET505
80 | c = 3
81 | return
|
= help: Remove unnecessary `else`
Safe fix
76 76 | else:
77 77 | b = 2
78 78 | return
79 |- else:
80 |- c = 3
79 |+ c = 3
81 80 | return
82 81 |
83 82 |
RET505.py:89:9: RET505 [*] Unnecessary `else` after `return` statement
|
87 | a = 4
88 | return
89 | else:
| ^^^^ RET505
90 | b = 2
91 | else:
|
= help: Remove unnecessary `else`
Safe fix
86 86 | if y: # [no-else-return]
87 87 | a = 4
88 88 | return
89 |- else:
90 |- b = 2
89 |+ b = 2
91 90 | else:
92 91 | c = 3
93 92 | return
RET505.py:99:5: RET505 [*] Unnecessary `else` after `return` statement
|
97 | if x: # [no-else-return]
98 | return True
99 | else:
| ^^^^ RET505
100 | try:
101 | return False
|
= help: Remove unnecessary `else`
Safe fix
96 96 | def bar4(x):
97 97 | if x: # [no-else-return]
98 98 | return True
99 |- else:
100 |- try:
101 |- return False
102 |- except ValueError:
103 |- return None
99 |+ try:
100 |+ return False
101 |+ except ValueError:
102 |+ return None
104 103 |
105 104 |
106 105 | ###
RET505.py:137:5: RET505 [*] Unnecessary `else` after `return` statement
|
135 | if True:
136 | return
137 | else:
| ^^^^ RET505
138 | # comment
139 | pass
|
= help: Remove unnecessary `else`
Safe fix
134 134 | def bar4(x):
135 135 | if True:
136 136 | return
137 |- else:
138 |- # comment
139 |- pass
137 |+ # comment
138 |+ pass
140 139 |
141 140 |
142 141 | def bar5():
RET505.py:145:5: RET505 [*] Unnecessary `else` after `return` statement
|
143 | if True:
144 | return
145 | else: # comment
| ^^^^ RET505
146 | pass
|
= help: Remove unnecessary `else`
Safe fix
142 142 | def bar5():
143 143 | if True:
144 144 | return
145 |- else: # comment
146 |- pass
145 |+ # comment
146 |+ pass
147 147 |
148 148 |
149 149 | def bar6():
RET505.py:152:5: RET505 [*] Unnecessary `else` after `return` statement
|
150 | if True:
151 | return
152 | else\
| ^^^^ RET505
153 | :\
154 | # comment
|
= help: Remove unnecessary `else`
Safe fix
149 149 | def bar6():
150 150 | if True:
151 151 | return
152 |- else\
153 |- :\
154 |- # comment
155 |- pass
152 |+ # comment
153 |+ pass
156 154 |
157 155 |
158 156 | def bar7():
RET505.py:161:5: RET505 [*] Unnecessary `else` after `return` statement
|
159 | if True:
160 | return
161 | else\
| ^^^^ RET505
162 | : # comment
163 | pass
|
= help: Remove unnecessary `else`
Safe fix
158 158 | def bar7():
159 159 | if True:
160 160 | return
161 |- else\
162 |- : # comment
163 |- pass
161 |+ # comment
162 |+ pass
164 163 |
165 164 |
166 165 | def bar8():
RET505.py:169:5: RET505 [*] Unnecessary `else` after `return` statement
|
167 | if True:
168 | return
169 | else: pass
| ^^^^ RET505
|
= help: Remove unnecessary `else`
Safe fix
166 166 | def bar8():
167 167 | if True:
168 168 | return
169 |- else: pass
169 |+ pass
170 170 |
171 171 |
172 172 | def bar9():
RET505.py:175:5: RET505 [*] Unnecessary `else` after `return` statement
|
173 | if True:
174 | return
175 | else:\
| ^^^^ RET505
176 | pass
|
= help: Remove unnecessary `else`
Safe fix
172 172 | def bar9():
173 173 | if True:
174 174 | return
175 |- else:\
176 |- pass
175 |+ pass
177 176 |
178 177 |
179 178 | x = 0

View File

@@ -0,0 +1,166 @@
---
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
---
RET506.py:8:5: RET506 [*] Unnecessary `elif` after `raise` statement
|
6 | a = 1
7 | raise Exception(y)
8 | elif z:
| ^^^^ RET506
9 | b = 2
10 | raise Exception(w)
|
= help: Remove unnecessary `elif`
Safe fix
5 5 | if x: # [no-else-raise]
6 6 | a = 1
7 7 | raise Exception(y)
8 |- elif z:
8 |+ if z:
9 9 | b = 2
10 10 | raise Exception(w)
11 11 | else:
RET506.py:23:5: RET506 [*] Unnecessary `elif` after `raise` statement
|
21 | b = 2
22 | raise Exception(x)
23 | elif z:
| ^^^^ RET506
24 | raise Exception(y)
25 | else:
|
= help: Remove unnecessary `elif`
Safe fix
20 20 | else:
21 21 | b = 2
22 22 | raise Exception(x)
23 |- elif z:
23 |+ if z:
24 24 | raise Exception(y)
25 25 | else:
26 26 | c = 3
RET506.py:34:5: RET506 [*] Unnecessary `else` after `raise` statement
|
32 | a = 1
33 | raise Exception(y)
34 | else:
| ^^^^ RET506
35 | b = 2
36 | raise Exception(z)
|
= help: Remove unnecessary `else`
Safe fix
31 31 | if x: # [no-else-raise]
32 32 | a = 1
33 33 | raise Exception(y)
34 |- else:
35 |- b = 2
36 |- raise Exception(z)
34 |+ b = 2
35 |+ raise Exception(z)
37 36 |
38 37 |
39 38 | def foo3(x, y, z):
RET506.py:45:9: RET506 [*] Unnecessary `else` after `raise` statement
|
43 | b = 2
44 | raise Exception(y)
45 | else:
| ^^^^ RET506
46 | c = 3
47 | raise Exception(x)
|
= help: Remove unnecessary `else`
Safe fix
42 42 | if y: # [no-else-raise]
43 43 | b = 2
44 44 | raise Exception(y)
45 |- else:
46 |- c = 3
47 |- raise Exception(x)
45 |+ c = 3
46 |+ raise Exception(x)
48 47 | else:
49 48 | d = 4
50 49 | raise Exception(z)
RET506.py:60:5: RET506 [*] Unnecessary `else` after `raise` statement
|
58 | b = 2
59 | raise Exception(x)
60 | else:
| ^^^^ RET506
61 | c = 3
62 | raise Exception(y)
|
= help: Remove unnecessary `else`
Safe fix
57 57 | else:
58 58 | b = 2
59 59 | raise Exception(x)
60 |- else:
61 |- c = 3
60 |+ c = 3
62 61 | raise Exception(y)
63 62 |
64 63 |
RET506.py:70:9: RET506 [*] Unnecessary `else` after `raise` statement
|
68 | a = 4
69 | raise Exception(x)
70 | else:
| ^^^^ RET506
71 | b = 2
72 | else:
|
= help: Remove unnecessary `else`
Safe fix
67 67 | if y: # [no-else-raise]
68 68 | a = 4
69 69 | raise Exception(x)
70 |- else:
71 |- b = 2
70 |+ b = 2
72 71 | else:
73 72 | c = 3
74 73 | raise Exception(y)
RET506.py:80:5: RET506 [*] Unnecessary `else` after `raise` statement
|
78 | if x: # [no-else-raise]
79 | raise Exception(True)
80 | else:
| ^^^^ RET506
81 | try:
82 | raise Exception(False)
|
= help: Remove unnecessary `else`
Safe fix
77 77 | def bar4(x):
78 78 | if x: # [no-else-raise]
79 79 | raise Exception(True)
80 |- else:
81 |- try:
82 |- raise Exception(False)
83 |- except ValueError:
84 |- raise Exception(None)
80 |+ try:
81 |+ raise Exception(False)
82 |+ except ValueError:
83 |+ raise Exception(None)
85 84 |
86 85 |
87 86 | ###

View File

@@ -0,0 +1,163 @@
---
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
---
RET507.py:8:9: RET507 [*] Unnecessary `elif` after `continue` statement
|
6 | if i < y: # [no-else-continue]
7 | continue
8 | elif i < w:
| ^^^^ RET507
9 | continue
10 | else:
|
= help: Remove unnecessary `elif`
Safe fix
5 5 | for i in x:
6 6 | if i < y: # [no-else-continue]
7 7 | continue
8 |- elif i < w:
8 |+ if i < w:
9 9 | continue
10 10 | else:
11 11 | a = z
RET507.py:22:9: RET507 [*] Unnecessary `elif` after `continue` statement
|
20 | b = 2
21 | continue
22 | elif z:
| ^^^^ RET507
23 | c = 2
24 | else:
|
= help: Remove unnecessary `elif`
Safe fix
19 19 | else:
20 20 | b = 2
21 21 | continue
22 |- elif z:
22 |+ if z:
23 23 | c = 2
24 24 | else:
25 25 | c = 3
RET507.py:36:9: RET507 [*] Unnecessary `else` after `continue` statement
|
34 | if i < y: # [no-else-continue]
35 | continue
36 | else:
| ^^^^ RET507
37 | a = z
|
= help: Remove unnecessary `else`
Safe fix
33 33 | for i in x:
34 34 | if i < y: # [no-else-continue]
35 35 | continue
36 |- else:
37 |- a = z
36 |+ a = z
38 37 |
39 38 |
40 39 | def foo3(x, y, z):
RET507.py:47:13: RET507 [*] Unnecessary `else` after `continue` statement
|
45 | b = 2
46 | continue
47 | else:
| ^^^^ RET507
48 | c = 3
49 | continue
|
= help: Remove unnecessary `else`
Safe fix
44 44 | if z: # [no-else-continue]
45 45 | b = 2
46 46 | continue
47 |- else:
48 |- c = 3
49 |- continue
47 |+ c = 3
48 |+ continue
50 49 | else:
51 50 | d = 4
52 51 | continue
RET507.py:63:9: RET507 [*] Unnecessary `else` after `continue` statement
|
61 | b = 2
62 | continue
63 | else:
| ^^^^ RET507
64 | c = 3
65 | continue
|
= help: Remove unnecessary `else`
Safe fix
60 60 | else:
61 61 | b = 2
62 62 | continue
63 |- else:
64 |- c = 3
63 |+ c = 3
65 64 | continue
66 65 |
67 66 |
RET507.py:74:13: RET507 [*] Unnecessary `else` after `continue` statement
|
72 | a = 4
73 | continue
74 | else:
| ^^^^ RET507
75 | b = 2
76 | else:
|
= help: Remove unnecessary `else`
Safe fix
71 71 | if y: # [no-else-continue]
72 72 | a = 4
73 73 | continue
74 |- else:
75 |- b = 2
74 |+ b = 2
76 75 | else:
77 76 | c = 3
78 77 | continue
RET507.py:85:9: RET507 [*] Unnecessary `else` after `continue` statement
|
83 | if x: # [no-else-continue]
84 | continue
85 | else:
| ^^^^ RET507
86 | try:
87 | return
|
= help: Remove unnecessary `else`
Safe fix
82 82 | for i in range(10):
83 83 | if x: # [no-else-continue]
84 84 | continue
85 |- else:
86 |- try:
87 |- return
88 |- except ValueError:
89 |- continue
85 |+ try:
86 |+ return
87 |+ except ValueError:
88 |+ continue
90 89 |
91 90 |
92 91 | def bar1(x, y, z):

View File

@@ -0,0 +1,181 @@
---
source: crates/ruff_linter/src/rules/flake8_return/mod.rs
---
RET508.py:8:9: RET508 [*] Unnecessary `elif` after `break` statement
|
6 | if i > y: # [no-else-break]
7 | break
8 | elif i > w:
| ^^^^ RET508
9 | break
10 | else:
|
= help: Remove unnecessary `elif`
Safe fix
5 5 | for i in x:
6 6 | if i > y: # [no-else-break]
7 7 | break
8 |- elif i > w:
8 |+ if i > w:
9 9 | break
10 10 | else:
11 11 | a = z
RET508.py:22:9: RET508 [*] Unnecessary `elif` after `break` statement
|
20 | b = 2
21 | break
22 | elif z:
| ^^^^ RET508
23 | c = 2
24 | else:
|
= help: Remove unnecessary `elif`
Safe fix
19 19 | else:
20 20 | b = 2
21 21 | break
22 |- elif z:
22 |+ if z:
23 23 | c = 2
24 24 | else:
25 25 | c = 3
RET508.py:33:9: RET508 [*] Unnecessary `else` after `break` statement
|
31 | if i > y: # [no-else-break]
32 | break
33 | else:
| ^^^^ RET508
34 | a = z
|
= help: Remove unnecessary `else`
Safe fix
30 30 | for i in x:
31 31 | if i > y: # [no-else-break]
32 32 | break
33 |- else:
34 |- a = z
33 |+ a = z
35 34 |
36 35 |
37 36 | def foo3(x, y, z):
RET508.py:44:13: RET508 [*] Unnecessary `else` after `break` statement
|
42 | b = 2
43 | break
44 | else:
| ^^^^ RET508
45 | c = 3
46 | break
|
= help: Remove unnecessary `else`
Safe fix
41 41 | if z: # [no-else-break]
42 42 | b = 2
43 43 | break
44 |- else:
45 |- c = 3
46 |- break
44 |+ c = 3
45 |+ break
47 46 | else:
48 47 | d = 4
49 48 | break
RET508.py:60:9: RET508 [*] Unnecessary `else` after `break` statement
|
58 | b = 2
59 | break
60 | else:
| ^^^^ RET508
61 | c = 3
62 | break
|
= help: Remove unnecessary `else`
Safe fix
57 57 | else:
58 58 | b = 2
59 59 | break
60 |- else:
61 |- c = 3
60 |+ c = 3
62 61 | break
63 62 |
64 63 |
RET508.py:71:13: RET508 [*] Unnecessary `else` after `break` statement
|
69 | a = 4
70 | break
71 | else:
| ^^^^ RET508
72 | b = 2
73 | else:
|
= help: Remove unnecessary `else`
Safe fix
68 68 | if y: # [no-else-break]
69 69 | a = 4
70 70 | break
71 |- else:
72 |- b = 2
71 |+ b = 2
73 72 | else:
74 73 | c = 3
75 74 | break
RET508.py:82:9: RET508 [*] Unnecessary `else` after `break` statement
|
80 | if x: # [no-else-break]
81 | break
82 | else:
| ^^^^ RET508
83 | try:
84 | return
|
= help: Remove unnecessary `else`
Safe fix
79 79 | for i in range(10):
80 80 | if x: # [no-else-break]
81 81 | break
82 |- else:
83 |- try:
84 |- return
85 |- except ValueError:
86 |- break
82 |+ try:
83 |+ return
84 |+ except ValueError:
85 |+ break
87 86 |
88 87 |
89 88 | ###
RET508.py:158:13: RET508 [*] Unnecessary `else` after `break` statement
|
156 | if i > w:
157 | break
158 | else:
| ^^^^ RET508
159 | a = z
|
= help: Remove unnecessary `else`
Safe fix
155 155 | else:
156 156 | if i > w:
157 157 | break
158 |- else:
159 |- a = z
158 |+ a = z

View File

@@ -3,34 +3,48 @@ use rustc_hash::FxHashSet;
use ruff_python_ast::visitor;
use ruff_python_ast::visitor::Visitor;
use ruff_python_semantic::SemanticModel;
#[derive(Default)]
pub(super) struct Stack<'a> {
pub(super) struct Stack<'data> {
/// The `return` statements in the current function.
pub(super) returns: Vec<&'a ast::StmtReturn>,
pub(super) returns: Vec<&'data ast::StmtReturn>,
/// The `elif` or `else` statements in the current function.
pub(super) elifs_elses: Vec<(&'a [Stmt], &'a ElifElseClause)>,
pub(super) elifs_elses: Vec<(&'data [Stmt], &'data ElifElseClause)>,
/// The non-local variables in the current function.
pub(super) non_locals: FxHashSet<&'a str>,
pub(super) non_locals: FxHashSet<&'data str>,
/// Whether the current function is a generator.
pub(super) is_generator: bool,
/// The `assignment`-to-`return` statement pairs in the current function.
/// TODO(charlie): Remove the extra [`Stmt`] here, which is necessary to support statement
/// removal for the `return` statement.
pub(super) assignment_return: Vec<(&'a ast::StmtAssign, &'a ast::StmtReturn, &'a Stmt)>,
pub(super) assignment_return:
Vec<(&'data ast::StmtAssign, &'data ast::StmtReturn, &'data Stmt)>,
}
#[derive(Default)]
pub(super) struct ReturnVisitor<'a> {
pub(super) struct ReturnVisitor<'semantic, 'data> {
/// The semantic model of the current file.
semantic: &'semantic SemanticModel<'data>,
/// The current stack of nodes.
pub(super) stack: Stack<'a>,
pub(super) stack: Stack<'data>,
/// The preceding sibling of the current node.
sibling: Option<&'a Stmt>,
sibling: Option<&'data Stmt>,
/// The parent nodes of the current node.
parents: Vec<&'a Stmt>,
parents: Vec<&'data Stmt>,
}
impl<'a> Visitor<'a> for ReturnVisitor<'a> {
impl<'semantic, 'data> ReturnVisitor<'semantic, 'data> {
pub(super) fn new(semantic: &'semantic SemanticModel<'data>) -> Self {
Self {
semantic,
stack: Stack::default(),
sibling: None,
parents: Vec::new(),
}
}
}
impl<'semantic, 'a> Visitor<'a> for ReturnVisitor<'semantic, 'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
match stmt {
Stmt::ClassDef(ast::StmtClassDef { decorator_list, .. }) => {
@@ -95,11 +109,17 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
// x = f.read()
// return x
// ```
Stmt::With(ast::StmtWith { body, .. }) => {
if let Some(stmt_assign) = body.last().and_then(Stmt::as_assign_stmt) {
self.stack
.assignment_return
.push((stmt_assign, stmt_return, stmt));
Stmt::With(with) => {
if let Some(stmt_assign) =
with.body.last().and_then(Stmt::as_assign_stmt)
{
if !has_conditional_body(with, self.semantic) {
self.stack.assignment_return.push((
stmt_assign,
stmt_return,
stmt,
));
}
}
}
_ => {}
@@ -142,3 +162,47 @@ impl<'a> Visitor<'a> for ReturnVisitor<'a> {
self.sibling = sibling;
}
}
/// RET504
/// If the last statement is a `return` statement, and the second-to-last statement is a
/// `with` statement that suppresses an exception, then we should not analyze the `return`
/// statement for unnecessary assignments. Otherwise we will suggest removing the assignment
/// and the `with` statement, which would change the behavior of the code.
///
/// Example:
/// ```python
/// def foo(data):
/// with suppress(JSONDecoderError):
/// data = data.decode()
/// return data
/// Returns `true` if the [`With`] statement is known to have a conditional body. In other words:
/// if the [`With`] statement's body may or may not run.
///
/// For example, in the following, it's unsafe to inline the `return` into the `with`, since if
/// `data.decode()` fails, the behavior of the program will differ. (As-is, the function will return
/// the input `data`; if we inline the `return`, the function will return `None`.)
///
/// ```python
/// def func(data):
/// with suppress(JSONDecoderError):
/// data = data.decode()
/// return data
/// ```
fn has_conditional_body(with: &ast::StmtWith, semantic: &SemanticModel) -> bool {
with.items.iter().any(|item| {
let ast::WithItem {
context_expr: Expr::Call(ast::ExprCall { func, .. }),
..
} = item
else {
return false;
};
if let Some(call_path) = semantic.resolve_call_path(func) {
if call_path.as_slice() == ["contextlib", "suppress"] {
return true;
}
}
false
})
}

View File

@@ -60,6 +60,7 @@ mod tests {
#[test_case(Rule::YodaConditions, Path::new("SIM300.py"))]
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
#[test_case(Rule::IfWithSameArms, Path::new("SIM114.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",

View File

@@ -1,8 +1,15 @@
use ruff_diagnostics::{Diagnostic, Violation};
use std::borrow::Cow;
use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::comparable::ComparableStmt;
use ruff_python_ast::parenthesize::parenthesized_range;
use ruff_python_ast::stmt_if::{if_elif_branches, IfElifBranch};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_index::Indexer;
use ruff_python_trivia::{SimpleTokenKind, SimpleTokenizer};
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
@@ -32,14 +39,20 @@ use crate::checkers::ast::Checker;
pub struct IfWithSameArms;
impl Violation for IfWithSameArms {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Combine `if` branches using logical `or` operator")
}
fn fix_title(&self) -> Option<String> {
Some("Combine `if` branches".to_string())
}
}
/// SIM114
pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_if: &ast::StmtIf) {
pub(crate) fn if_with_same_arms(checker: &mut Checker, stmt_if: &ast::StmtIf) {
let mut branches_iter = if_elif_branches(stmt_if).peekable();
while let Some(current_branch) = branches_iter.next() {
let Some(following_branch) = branches_iter.peek() else {
@@ -63,26 +76,101 @@ pub(crate) fn if_with_same_arms(checker: &mut Checker, locator: &Locator, stmt_i
let first_comments = checker
.indexer()
.comment_ranges()
.comments_in_range(body_range(&current_branch, locator))
.comments_in_range(body_range(&current_branch, checker.locator()))
.iter()
.map(|range| locator.slice(*range));
.map(|range| checker.locator().slice(*range));
let second_comments = checker
.indexer()
.comment_ranges()
.comments_in_range(body_range(following_branch, locator))
.comments_in_range(body_range(following_branch, checker.locator()))
.iter()
.map(|range| locator.slice(*range));
.map(|range| checker.locator().slice(*range));
if !first_comments.eq(second_comments) {
continue;
}
checker.diagnostics.push(Diagnostic::new(
let mut diagnostic = Diagnostic::new(
IfWithSameArms,
TextRange::new(current_branch.start(), following_branch.end()),
));
);
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
merge_branches(
stmt_if,
&current_branch,
following_branch,
checker.locator(),
checker.indexer(),
)
});
}
checker.diagnostics.push(diagnostic);
}
}
/// Generate a [`Fix`] to merge two [`IfElifBranch`] branches.
fn merge_branches(
stmt_if: &ast::StmtIf,
current_branch: &IfElifBranch,
following_branch: &IfElifBranch,
locator: &Locator,
indexer: &Indexer,
) -> Result<Fix> {
// Identify the colon (`:`) at the end of the current branch's test.
let Some(current_branch_colon) =
SimpleTokenizer::starts_at(current_branch.test.end(), locator.contents())
.find(|token| token.kind == SimpleTokenKind::Colon)
else {
return Err(anyhow::anyhow!("Expected colon after test"));
};
let mut following_branch_tokenizer =
SimpleTokenizer::starts_at(following_branch.test.end(), locator.contents());
// Identify the colon (`:`) at the end of the following branch's test.
let Some(following_branch_colon) =
following_branch_tokenizer.find(|token| token.kind == SimpleTokenKind::Colon)
else {
return Err(anyhow::anyhow!("Expected colon after test"));
};
let main_edit = Edit::deletion(
locator.full_line_end(current_branch_colon.end()),
locator.full_line_end(following_branch_colon.end()),
);
// If the test isn't parenthesized, consider parenthesizing it.
let following_branch_test = if let Some(range) = parenthesized_range(
following_branch.test.into(),
stmt_if.into(),
indexer.comment_ranges(),
locator.contents(),
) {
Cow::Borrowed(locator.slice(range))
} else if matches!(
following_branch.test,
Expr::BoolOp(ast::ExprBoolOp {
op: ast::BoolOp::Or,
..
}) | Expr::Lambda(_)
| Expr::NamedExpr(_)
) {
Cow::Owned(format!("({})", locator.slice(following_branch.test)))
} else {
Cow::Borrowed(locator.slice(following_branch.test))
};
Ok(Fix::safe_edits(
main_edit,
[Edit::insertion(
format!(" or {following_branch_test}"),
current_branch_colon.start(),
)],
))
}
/// Return the [`TextRange`] of an [`IfElifBranch`]'s body (from the end of the test to the end of
/// the body).
fn body_range(branch: &IfElifBranch, locator: &Locator) -> TextRange {

View File

@@ -5,6 +5,7 @@ use ruff_python_semantic::analyze::typing::{is_sys_version_block, is_type_checki
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
/// ## What it does
/// Checks for `if` statements that can be replaced with `bool`.
@@ -30,7 +31,8 @@ use crate::checkers::ast::Checker;
/// - [Python documentation: Truth Value Testing](https://docs.python.org/3/library/stdtypes.html#truth-value-testing)
#[violation]
pub struct NeedlessBool {
condition: String,
condition: SourceCodeSnippet,
replacement: Option<SourceCodeSnippet>,
}
impl Violation for NeedlessBool {
@@ -38,13 +40,24 @@ impl Violation for NeedlessBool {
#[derive_message_formats]
fn message(&self) -> String {
let NeedlessBool { condition } = self;
format!("Return the condition `{condition}` directly")
let NeedlessBool { condition, .. } = self;
if let Some(condition) = condition.full_display() {
format!("Return the condition `{condition}` directly")
} else {
format!("Return the condition directly")
}
}
fn fix_title(&self) -> Option<String> {
let NeedlessBool { condition } = self;
Some(format!("Replace with `return {condition}`"))
let NeedlessBool { replacement, .. } = self;
if let Some(replacement) = replacement
.as_ref()
.and_then(SourceCodeSnippet::full_display)
{
Some(format!("Replace with `{replacement}`"))
} else {
Some(format!("Inline condition"))
}
}
}
@@ -90,11 +103,20 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
return;
};
// If the branches have the same condition, abort (although the code could be
// simplified).
if if_return == else_return {
return;
}
// Determine whether the return values are inverted, as in:
// ```python
// if x > 0:
// return False
// else:
// return True
// ```
let inverted = match (if_return, else_return) {
(Bool::True, Bool::False) => false,
(Bool::False, Bool::True) => true,
// If the branches have the same condition, abort (although the code could be
// simplified).
_ => return,
};
// Avoid suggesting ternary for `if sys.version_info >= ...`-style checks.
if is_sys_version_block(stmt_if, checker.semantic()) {
@@ -106,33 +128,38 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
return;
}
let condition = checker.generator().expr(if_test);
let mut diagnostic = Diagnostic::new(NeedlessBool { condition }, range);
if matches!(if_return, Bool::True)
&& matches!(else_return, Bool::False)
&& !checker.indexer().has_comments(&range, checker.locator())
&& (if_test.is_compare_expr() || checker.semantic().is_builtin("bool"))
{
if if_test.is_compare_expr() {
// If the condition is a comparison, we can replace it with the condition.
let condition = checker.locator().slice(if_test);
let replacement = if checker.indexer().has_comments(&range, checker.locator()) {
None
} else {
// If the return values are inverted, wrap the condition in a `not`.
if inverted {
let node = ast::StmtReturn {
value: Some(Box::new(Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::Not,
operand: Box::new(if_test.clone()),
range: TextRange::default(),
}))),
range: TextRange::default(),
};
Some(checker.generator().stmt(&node.into()))
} else if if_test.is_compare_expr() {
// If the condition is a comparison, we can replace it with the condition, since we
// know it's a boolean.
let node = ast::StmtReturn {
value: Some(Box::new(if_test.clone())),
range: TextRange::default(),
};
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().stmt(&node.into()),
range,
)));
} else {
// Otherwise, we need to wrap the condition in a call to `bool`. (We've already
// verified, above, that `bool` is a builtin.)
let node = ast::ExprName {
Some(checker.generator().stmt(&node.into()))
} else if checker.semantic().is_builtin("bool") {
// Otherwise, we need to wrap the condition in a call to `bool`.
let func_node = ast::ExprName {
id: "bool".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
};
let node1 = ast::ExprCall {
func: Box::new(node.into()),
let value_node = ast::ExprCall {
func: Box::new(func_node.into()),
arguments: Arguments {
args: vec![if_test.clone()],
keywords: vec![],
@@ -140,15 +167,28 @@ pub(crate) fn needless_bool(checker: &mut Checker, stmt_if: &ast::StmtIf) {
},
range: TextRange::default(),
};
let node2 = ast::StmtReturn {
value: Some(Box::new(node1.into())),
let return_node = ast::StmtReturn {
value: Some(Box::new(value_node.into())),
range: TextRange::default(),
};
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
checker.generator().stmt(&node2.into()),
range,
)));
};
Some(checker.generator().stmt(&return_node.into()))
} else {
None
}
};
let mut diagnostic = Diagnostic::new(
NeedlessBool {
condition: SourceCodeSnippet::from_str(condition),
replacement: replacement.clone().map(SourceCodeSnippet::new),
},
range,
);
if let Some(replacement) = replacement {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
replacement,
range,
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -12,7 +12,7 @@ SIM103.py:3:5: SIM103 [*] Return the condition `a` directly
6 | | return False
| |____________________^ SIM103
|
= help: Replace with `return a`
= help: Replace with `return bool(a)`
Unsafe fix
1 1 | def f():
@@ -63,7 +63,7 @@ SIM103.py:21:5: SIM103 [*] Return the condition `b` directly
24 | | return False
| |____________________^ SIM103
|
= help: Replace with `return b`
= help: Replace with `return bool(b)`
Unsafe fix
18 18 | # SIM103
@@ -89,7 +89,7 @@ SIM103.py:32:9: SIM103 [*] Return the condition `b` directly
35 | | return False
| |________________________^ SIM103
|
= help: Replace with `return b`
= help: Replace with `return bool(b)`
Unsafe fix
29 29 | if a:
@@ -104,7 +104,7 @@ SIM103.py:32:9: SIM103 [*] Return the condition `b` directly
37 34 |
38 35 | def f():
SIM103.py:57:5: SIM103 Return the condition `a` directly
SIM103.py:57:5: SIM103 [*] Return the condition `a` directly
|
55 | def f():
56 | # SIM103 (but not fixable)
@@ -115,7 +115,20 @@ SIM103.py:57:5: SIM103 Return the condition `a` directly
60 | | return True
| |___________________^ SIM103
|
= help: Replace with `return a`
= help: Replace with `return not a`
Unsafe fix
54 54 |
55 55 | def f():
56 56 | # SIM103 (but not fixable)
57 |- if a:
58 |- return False
59 |- else:
60 |- return True
57 |+ return not a
61 58 |
62 59 |
63 60 | def f():
SIM103.py:83:5: SIM103 Return the condition `a` directly
|
@@ -128,6 +141,6 @@ SIM103.py:83:5: SIM103 Return the condition `a` directly
86 | | return False
| |____________________^ SIM103
|
= help: Replace with `return a`
= help: Inline condition

View File

@@ -10,158 +10,241 @@ SIM114.py:2:1: SIM114 Combine `if` branches using logical `or` operator
5 | | b
| |_____^ SIM114
6 |
7 | if x == 1:
7 | if a: # we preserve comments, too!
|
= help: Combine `if` branches
SIM114.py:7:1: SIM114 Combine `if` branches using logical `or` operator
|
5 | b
6 |
7 | / if x == 1:
8 | | for _ in range(20):
9 | | print("hello")
10 | | elif x == 2:
11 | | for _ in range(20):
12 | | print("hello")
7 | / if a: # we preserve comments, too!
8 | | b
9 | | elif c: # but not on the second branch
10 | | b
| |_____^ SIM114
11 |
12 | if x == 1:
|
= help: Combine `if` branches
SIM114.py:12:1: SIM114 Combine `if` branches using logical `or` operator
|
10 | b
11 |
12 | / if x == 1:
13 | | for _ in range(20):
14 | | print("hello")
15 | | elif x == 2:
16 | | for _ in range(20):
17 | | print("hello")
| |______________________^ SIM114
13 |
14 | if x == 1:
18 |
19 | if x == 1:
|
= help: Combine `if` branches
SIM114.py:14:1: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:19:1: SIM114 Combine `if` branches using logical `or` operator
|
12 | print("hello")
13 |
14 | / if x == 1:
15 | | if True:
16 | | for _ in range(20):
17 | | print("hello")
18 | | elif x == 2:
19 | | if True:
20 | | for _ in range(20):
21 | | print("hello")
| |__________________________^ SIM114
22 |
23 | if x == 1:
|
SIM114.py:23:1: SIM114 Combine `if` branches using logical `or` operator
|
21 | print("hello")
22 |
23 | / if x == 1:
17 | print("hello")
18 |
19 | / if x == 1:
20 | | if True:
21 | | for _ in range(20):
22 | | print("hello")
23 | | elif x == 2:
24 | | if True:
25 | | for _ in range(20):
26 | | print("hello")
27 | | elif False:
28 | | for _ in range(20):
29 | | print("hello")
30 | | elif x == 2:
31 | | if True:
32 | | for _ in range(20):
33 | | print("hello")
34 | | elif False:
35 | | for _ in range(20):
36 | | print("hello")
| |__________________________^ SIM114
37 |
38 | if (
27 |
28 | if x == 1:
|
= help: Combine `if` branches
SIM114.py:24:5: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:28:1: SIM114 Combine `if` branches using logical `or` operator
|
23 | if x == 1:
24 | if True:
26 | print("hello")
27 |
28 | / if x == 1:
29 | | if True:
30 | | for _ in range(20):
31 | | print("hello")
32 | | elif False:
33 | | for _ in range(20):
34 | | print("hello")
35 | | elif x == 2:
36 | | if True:
37 | | for _ in range(20):
38 | | print("hello")
39 | | elif False:
40 | | for _ in range(20):
41 | | print("hello")
| |__________________________^ SIM114
42 |
43 | if (
|
= help: Combine `if` branches
SIM114.py:29:5: SIM114 Combine `if` branches using logical `or` operator
|
28 | if x == 1:
29 | if True:
| _____^
25 | | for _ in range(20):
26 | | print("hello")
27 | | elif False:
28 | | for _ in range(20):
29 | | print("hello")
30 | | for _ in range(20):
31 | | print("hello")
32 | | elif False:
33 | | for _ in range(20):
34 | | print("hello")
| |__________________________^ SIM114
30 | elif x == 2:
31 | if True:
35 | elif x == 2:
36 | if True:
|
= help: Combine `if` branches
SIM114.py:31:5: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:36:5: SIM114 Combine `if` branches using logical `or` operator
|
29 | print("hello")
30 | elif x == 2:
31 | if True:
34 | print("hello")
35 | elif x == 2:
36 | if True:
| _____^
32 | | for _ in range(20):
33 | | print("hello")
34 | | elif False:
35 | | for _ in range(20):
36 | | print("hello")
37 | | for _ in range(20):
38 | | print("hello")
39 | | elif False:
40 | | for _ in range(20):
41 | | print("hello")
| |__________________________^ SIM114
37 |
38 | if (
42 |
43 | if (
|
= help: Combine `if` branches
SIM114.py:38:1: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:43:1: SIM114 Combine `if` branches using logical `or` operator
|
36 | print("hello")
37 |
38 | / if (
39 | | x == 1
40 | | and y == 2
41 | | and z == 3
42 | | and a == 4
43 | | and b == 5
44 | | and c == 6
45 | | and d == 7
46 | | and e == 8
47 | | and f == 9
48 | | and g == 10
49 | | and h == 11
50 | | and i == 12
51 | | and j == 13
52 | | and k == 14
53 | | ):
54 | | pass
55 | | elif 1 == 2:
56 | | pass
41 | print("hello")
42 |
43 | / if (
44 | | x == 1
45 | | and y == 2
46 | | and z == 3
47 | | and a == 4
48 | | and b == 5
49 | | and c == 6
50 | | and d == 7
51 | | and e == 8
52 | | and f == 9
53 | | and g == 10
54 | | and h == 11
55 | | and i == 12
56 | | and j == 13
57 | | and k == 14
58 | | ):
59 | | pass
60 | | elif 1 == 2:
61 | | pass
| |________^ SIM114
57 |
58 | if result.eofs == "O":
62 |
63 | if result.eofs == "O":
|
= help: Combine `if` branches
SIM114.py:62:1: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:67:1: SIM114 Combine `if` branches using logical `or` operator
|
60 | elif result.eofs == "S":
61 | skipped = 1
62 | / elif result.eofs == "F":
63 | | errors = 1
64 | | elif result.eofs == "E":
65 | | errors = 1
65 | elif result.eofs == "S":
66 | skipped = 1
67 | / elif result.eofs == "F":
68 | | errors = 1
69 | | elif result.eofs == "E":
70 | | errors = 1
| |______________^ SIM114
71 | elif result.eofs == "X":
72 | errors = 1
|
= help: Combine `if` branches
SIM114.py:69:1: SIM114 Combine `if` branches using logical `or` operator
|
67 | elif result.eofs == "F":
68 | errors = 1
69 | / elif result.eofs == "E":
70 | | errors = 1
71 | | elif result.eofs == "X":
72 | | errors = 1
| |______________^ SIM114
73 | elif result.eofs == "C":
74 | errors = 1
|
= help: Combine `if` branches
SIM114.py:71:1: SIM114 Combine `if` branches using logical `or` operator
|
69 | elif result.eofs == "E":
70 | errors = 1
71 | / elif result.eofs == "X":
72 | | errors = 1
73 | | elif result.eofs == "C":
74 | | errors = 1
| |______________^ SIM114
|
= help: Combine `if` branches
SIM114.py:109:5: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:118:5: SIM114 Combine `if` branches using logical `or` operator
|
107 | a = True
108 | b = False
109 | if a > b: # end-of-line
116 | a = True
117 | b = False
118 | if a > b: # end-of-line
| _____^
110 | | return 3
111 | | elif a == b:
112 | | return 3
119 | | return 3
120 | | elif a == b:
121 | | return 3
| |________________^ SIM114
113 | elif a < b: # end-of-line
114 | return 4
122 | elif a < b: # end-of-line
123 | return 4
|
= help: Combine `if` branches
SIM114.py:113:5: SIM114 Combine `if` branches using logical `or` operator
SIM114.py:122:5: SIM114 Combine `if` branches using logical `or` operator
|
111 | elif a == b:
112 | return 3
113 | elif a < b: # end-of-line
120 | elif a == b:
121 | return 3
122 | elif a < b: # end-of-line
| _____^
114 | | return 4
115 | | elif b is None:
116 | | return 4
123 | | return 4
124 | | elif b is None:
125 | | return 4
| |________________^ SIM114
|
= help: Combine `if` branches
SIM114.py:132:5: SIM114 Combine `if` branches using logical `or` operator
|
130 | a = True
131 | b = False
132 | if a > b: # end-of-line
| _____^
133 | | return 3
134 | | elif a := 1:
135 | | return 3
| |________________^ SIM114
|
= help: Combine `if` branches
SIM114.py:138:1: SIM114 Combine `if` branches using logical `or` operator
|
138 | / if a: # we preserve comments, too!
139 | | b
140 | | elif c: # but not on the second branch
141 | | b
| |_____^ SIM114
|
= help: Combine `if` branches
SIM114.py:144:1: SIM114 Combine `if` branches using logical `or` operator
|
144 | / if a: b # here's a comment
145 | | elif c: b
| |_________^ SIM114
|
= help: Combine `if` branches

View File

@@ -0,0 +1,446 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM114.py:2:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
1 | # Errors
2 | / if a:
3 | | b
4 | | elif c:
5 | | b
| |_____^ SIM114
6 |
7 | if a: # we preserve comments, too!
|
= help: Combine `if` branches
Safe fix
1 1 | # Errors
2 |-if a:
3 |- b
4 |-elif c:
2 |+if a or c:
5 3 | b
6 4 |
7 5 | if a: # we preserve comments, too!
SIM114.py:7:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
5 | b
6 |
7 | / if a: # we preserve comments, too!
8 | | b
9 | | elif c: # but not on the second branch
10 | | b
| |_____^ SIM114
11 |
12 | if x == 1:
|
= help: Combine `if` branches
Safe fix
4 4 | elif c:
5 5 | b
6 6 |
7 |-if a: # we preserve comments, too!
8 |- b
9 |-elif c: # but not on the second branch
7 |+if a or c: # we preserve comments, too!
10 8 | b
11 9 |
12 10 | if x == 1:
SIM114.py:12:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
10 | b
11 |
12 | / if x == 1:
13 | | for _ in range(20):
14 | | print("hello")
15 | | elif x == 2:
16 | | for _ in range(20):
17 | | print("hello")
| |______________________^ SIM114
18 |
19 | if x == 1:
|
= help: Combine `if` branches
Safe fix
9 9 | elif c: # but not on the second branch
10 10 | b
11 11 |
12 |-if x == 1:
13 |- for _ in range(20):
14 |- print("hello")
15 |-elif x == 2:
12 |+if x == 1 or x == 2:
16 13 | for _ in range(20):
17 14 | print("hello")
18 15 |
SIM114.py:19:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
17 | print("hello")
18 |
19 | / if x == 1:
20 | | if True:
21 | | for _ in range(20):
22 | | print("hello")
23 | | elif x == 2:
24 | | if True:
25 | | for _ in range(20):
26 | | print("hello")
| |__________________________^ SIM114
27 |
28 | if x == 1:
|
= help: Combine `if` branches
Safe fix
16 16 | for _ in range(20):
17 17 | print("hello")
18 18 |
19 |-if x == 1:
20 |- if True:
21 |- for _ in range(20):
22 |- print("hello")
23 |-elif x == 2:
19 |+if x == 1 or x == 2:
24 20 | if True:
25 21 | for _ in range(20):
26 22 | print("hello")
SIM114.py:28:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
26 | print("hello")
27 |
28 | / if x == 1:
29 | | if True:
30 | | for _ in range(20):
31 | | print("hello")
32 | | elif False:
33 | | for _ in range(20):
34 | | print("hello")
35 | | elif x == 2:
36 | | if True:
37 | | for _ in range(20):
38 | | print("hello")
39 | | elif False:
40 | | for _ in range(20):
41 | | print("hello")
| |__________________________^ SIM114
42 |
43 | if (
|
= help: Combine `if` branches
Safe fix
25 25 | for _ in range(20):
26 26 | print("hello")
27 27 |
28 |-if x == 1:
29 |- if True:
30 |- for _ in range(20):
31 |- print("hello")
32 |- elif False:
33 |- for _ in range(20):
34 |- print("hello")
35 |-elif x == 2:
28 |+if x == 1 or x == 2:
36 29 | if True:
37 30 | for _ in range(20):
38 31 | print("hello")
SIM114.py:29:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
28 | if x == 1:
29 | if True:
| _____^
30 | | for _ in range(20):
31 | | print("hello")
32 | | elif False:
33 | | for _ in range(20):
34 | | print("hello")
| |__________________________^ SIM114
35 | elif x == 2:
36 | if True:
|
= help: Combine `if` branches
Safe fix
26 26 | print("hello")
27 27 |
28 28 | if x == 1:
29 |- if True:
30 |- for _ in range(20):
31 |- print("hello")
32 |- elif False:
29 |+ if True or False:
33 30 | for _ in range(20):
34 31 | print("hello")
35 32 | elif x == 2:
SIM114.py:36:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
34 | print("hello")
35 | elif x == 2:
36 | if True:
| _____^
37 | | for _ in range(20):
38 | | print("hello")
39 | | elif False:
40 | | for _ in range(20):
41 | | print("hello")
| |__________________________^ SIM114
42 |
43 | if (
|
= help: Combine `if` branches
Safe fix
33 33 | for _ in range(20):
34 34 | print("hello")
35 35 | elif x == 2:
36 |- if True:
37 |- for _ in range(20):
38 |- print("hello")
39 |- elif False:
36 |+ if True or False:
40 37 | for _ in range(20):
41 38 | print("hello")
42 39 |
SIM114.py:43:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
41 | print("hello")
42 |
43 | / if (
44 | | x == 1
45 | | and y == 2
46 | | and z == 3
47 | | and a == 4
48 | | and b == 5
49 | | and c == 6
50 | | and d == 7
51 | | and e == 8
52 | | and f == 9
53 | | and g == 10
54 | | and h == 11
55 | | and i == 12
56 | | and j == 13
57 | | and k == 14
58 | | ):
59 | | pass
60 | | elif 1 == 2:
61 | | pass
| |________^ SIM114
62 |
63 | if result.eofs == "O":
|
= help: Combine `if` branches
Safe fix
55 55 | and i == 12
56 56 | and j == 13
57 57 | and k == 14
58 |-):
59 |- pass
60 |-elif 1 == 2:
58 |+) or 1 == 2:
61 59 | pass
62 60 |
63 61 | if result.eofs == "O":
SIM114.py:67:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
65 | elif result.eofs == "S":
66 | skipped = 1
67 | / elif result.eofs == "F":
68 | | errors = 1
69 | | elif result.eofs == "E":
70 | | errors = 1
| |______________^ SIM114
71 | elif result.eofs == "X":
72 | errors = 1
|
= help: Combine `if` branches
Safe fix
64 64 | pass
65 65 | elif result.eofs == "S":
66 66 | skipped = 1
67 |-elif result.eofs == "F":
68 |- errors = 1
69 |-elif result.eofs == "E":
67 |+elif result.eofs == "F" or result.eofs == "E":
70 68 | errors = 1
71 69 | elif result.eofs == "X":
72 70 | errors = 1
SIM114.py:69:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
67 | elif result.eofs == "F":
68 | errors = 1
69 | / elif result.eofs == "E":
70 | | errors = 1
71 | | elif result.eofs == "X":
72 | | errors = 1
| |______________^ SIM114
73 | elif result.eofs == "C":
74 | errors = 1
|
= help: Combine `if` branches
Safe fix
66 66 | skipped = 1
67 67 | elif result.eofs == "F":
68 68 | errors = 1
69 |-elif result.eofs == "E":
70 |- errors = 1
71 |-elif result.eofs == "X":
69 |+elif result.eofs == "E" or result.eofs == "X":
72 70 | errors = 1
73 71 | elif result.eofs == "C":
74 72 | errors = 1
SIM114.py:71:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
69 | elif result.eofs == "E":
70 | errors = 1
71 | / elif result.eofs == "X":
72 | | errors = 1
73 | | elif result.eofs == "C":
74 | | errors = 1
| |______________^ SIM114
|
= help: Combine `if` branches
Safe fix
68 68 | errors = 1
69 69 | elif result.eofs == "E":
70 70 | errors = 1
71 |-elif result.eofs == "X":
72 |- errors = 1
73 |-elif result.eofs == "C":
71 |+elif result.eofs == "X" or result.eofs == "C":
74 72 | errors = 1
75 73 |
76 74 |
SIM114.py:118:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
116 | a = True
117 | b = False
118 | if a > b: # end-of-line
| _____^
119 | | return 3
120 | | elif a == b:
121 | | return 3
| |________________^ SIM114
122 | elif a < b: # end-of-line
123 | return 4
|
= help: Combine `if` branches
Safe fix
115 115 | def func():
116 116 | a = True
117 117 | b = False
118 |- if a > b: # end-of-line
119 |- return 3
120 |- elif a == b:
118 |+ if a > b or a == b: # end-of-line
121 119 | return 3
122 120 | elif a < b: # end-of-line
123 121 | return 4
SIM114.py:122:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
120 | elif a == b:
121 | return 3
122 | elif a < b: # end-of-line
| _____^
123 | | return 4
124 | | elif b is None:
125 | | return 4
| |________________^ SIM114
|
= help: Combine `if` branches
Safe fix
119 119 | return 3
120 120 | elif a == b:
121 121 | return 3
122 |- elif a < b: # end-of-line
123 |- return 4
124 |- elif b is None:
122 |+ elif a < b or b is None: # end-of-line
125 123 | return 4
126 124 |
127 125 |
SIM114.py:132:5: SIM114 [*] Combine `if` branches using logical `or` operator
|
130 | a = True
131 | b = False
132 | if a > b: # end-of-line
| _____^
133 | | return 3
134 | | elif a := 1:
135 | | return 3
| |________________^ SIM114
|
= help: Combine `if` branches
Safe fix
129 129 | """Ensure that the named expression is parenthesized when merged."""
130 130 | a = True
131 131 | b = False
132 |- if a > b: # end-of-line
133 |- return 3
134 |- elif a := 1:
132 |+ if a > b or (a := 1): # end-of-line
135 133 | return 3
136 134 |
137 135 |
SIM114.py:138:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
138 | / if a: # we preserve comments, too!
139 | | b
140 | | elif c: # but not on the second branch
141 | | b
| |_____^ SIM114
|
= help: Combine `if` branches
Safe fix
135 135 | return 3
136 136 |
137 137 |
138 |-if a: # we preserve comments, too!
139 |- b
140 |-elif c: # but not on the second branch
138 |+if a or c: # we preserve comments, too!
141 139 | b
142 140 |
143 141 |
SIM114.py:144:1: SIM114 [*] Combine `if` branches using logical `or` operator
|
144 | / if a: b # here's a comment
145 | | elif c: b
| |_________^ SIM114
|
= help: Combine `if` branches
Safe fix
141 141 | b
142 142 |
143 143 |
144 |-if a: b # here's a comment
145 |-elif c: b
144 |+if a or c: b # here's a comment

View File

@@ -2,11 +2,11 @@ use anyhow::Result;
use ruff_diagnostics::Edit;
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::{self as ast, Decorator, Expr};
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_semantic::{
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
analyze, Binding, BindingKind, NodeId, ResolvedReference, ScopeKind, SemanticModel,
};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -104,6 +104,35 @@ fn runtime_required_decorators(
})
}
/// Returns `true` if an annotation will be inspected at runtime by the `dataclasses` module.
///
/// Specifically, detects whether an annotation is to either `dataclasses.InitVar` or
/// `typing.ClassVar` within a `@dataclass` class definition.
///
/// See: <https://docs.python.org/3/library/dataclasses.html#init-only-variables>
pub(crate) fn is_dataclass_meta_annotation(annotation: &Expr, semantic: &SemanticModel) -> bool {
// Determine whether the assignment is in a `@dataclass` class definition.
if let ScopeKind::Class(class_def) = semantic.current_scope().kind {
if class_def.decorator_list.iter().any(|decorator| {
semantic
.resolve_call_path(map_callable(&decorator.expression))
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["dataclasses", "dataclass"])
})
}) {
// Determine whether the annotation is `typing.ClassVar` or `dataclasses.InitVar`.
return semantic
.resolve_call_path(map_subscript(annotation))
.is_some_and(|call_path| {
matches!(call_path.as_slice(), ["dataclasses", "InitVar"])
|| semantic.match_typing_call_path(&call_path, "ClassVar")
});
}
}
false
}
/// Returns `true` if a function is registered as a `singledispatch` interface.
///
/// For example, `fun` below is a `singledispatch` interface:

View File

@@ -39,6 +39,7 @@ mod tests {
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH006_2.py"))]
#[test_case(Rule::TypingOnlyFirstPartyImport, Path::new("TCH001.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("TCH003.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("snapshot.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("TCH002.py"))]
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("quote.py"))]
@@ -75,7 +76,9 @@ mod tests {
}
#[test_case(Rule::TypingOnlyThirdPartyImport, Path::new("strict.py"))]
#[test_case(Rule::TypingOnlyStandardLibraryImport, Path::new("init_var.py"))]
fn strict(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("strict_{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
@@ -86,7 +89,7 @@ mod tests {
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(diagnostics);
assert_messages!(snapshot, diagnostics);
Ok(())
}

View File

@@ -0,0 +1,50 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
init_var.py:5:25: TCH003 [*] Move standard library import `dataclasses.FrozenInstanceError` into a type-checking block
|
3 | from __future__ import annotations
4 |
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
| ^^^^^^^^^^^^^^^^^^^ TCH003
6 | from pathlib import Path
|
= help: Move into type-checking block
Unsafe fix
2 2 |
3 3 | from __future__ import annotations
4 4 |
5 |-from dataclasses import FrozenInstanceError, InitVar, dataclass
5 |+from dataclasses import InitVar, dataclass
6 6 | from pathlib import Path
7 |+from typing import TYPE_CHECKING
8 |+
9 |+if TYPE_CHECKING:
10 |+ from dataclasses import FrozenInstanceError
7 11 |
8 12 |
9 13 | @dataclass
init_var.py:6:21: TCH003 [*] Move standard library import `pathlib.Path` into a type-checking block
|
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
6 | from pathlib import Path
| ^^^^ TCH003
|
= help: Move into type-checking block
Unsafe fix
3 3 | from __future__ import annotations
4 4 |
5 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
6 |-from pathlib import Path
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from pathlib import Path
7 10 |
8 11 |
9 12 | @dataclass

View File

@@ -0,0 +1,25 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
init_var.py:6:21: TCH003 [*] Move standard library import `pathlib.Path` into a type-checking block
|
5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
6 | from pathlib import Path
| ^^^^ TCH003
|
= help: Move into type-checking block
Unsafe fix
3 3 | from __future__ import annotations
4 4 |
5 5 | from dataclasses import FrozenInstanceError, InitVar, dataclass
6 |-from pathlib import Path
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from pathlib import Path
7 10 |
8 11 |
9 12 | @dataclass

View File

@@ -67,6 +67,7 @@ mod tests {
}
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -6,14 +6,14 @@ pub(crate) use compound_statements::*;
pub(crate) use doc_line_too_long::*;
pub(crate) use errors::*;
pub use errors::{IOError, SyntaxError};
pub(crate) use imports::*;
pub(crate) use invalid_escape_sequence::*;
pub(crate) use lambda_assignment::*;
pub(crate) use line_too_long::*;
pub(crate) use literal_comparisons::*;
pub(crate) use missing_newline_at_end_of_file::*;
pub(crate) use mixed_spaces_and_tabs::*;
pub(crate) use module_import_not_at_top_of_file::*;
pub(crate) use multiple_imports_on_one_line::*;
pub(crate) use not_tests::*;
pub(crate) use tab_indentation::*;
pub(crate) use trailing_whitespace::*;
@@ -26,7 +26,6 @@ mod bare_except;
mod compound_statements;
mod doc_line_too_long;
mod errors;
mod imports;
mod invalid_escape_sequence;
mod lambda_assignment;
mod line_too_long;
@@ -34,6 +33,8 @@ mod literal_comparisons;
pub(crate) mod logical_lines;
mod missing_newline_at_end_of_file;
mod mixed_spaces_and_tabs;
mod module_import_not_at_top_of_file;
mod multiple_imports_on_one_line;
mod not_tests;
mod tab_indentation;
mod trailing_whitespace;

View File

@@ -1,38 +1,10 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Alias, PySourceType, Stmt};
use ruff_python_ast::{PySourceType, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Check for multiple imports on one line.
///
/// ## Why is this bad?
/// According to [PEP 8], "imports should usually be on separate lines."
///
/// ## Example
/// ```python
/// import sys, os
/// ```
///
/// Use instead:
/// ```python
/// import os
/// import sys
/// ```
///
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
#[violation]
pub struct MultipleImportsOnOneLine;
impl Violation for MultipleImportsOnOneLine {
#[derive_message_formats]
fn message(&self) -> String {
format!("Multiple imports on one line")
}
}
/// ## What it does
/// Checks for imports that are not at the top of the file. For Jupyter notebooks, this
/// checks for imports that are not at the top of the cell.
@@ -82,15 +54,6 @@ impl Violation for ModuleImportNotAtTopOfFile {
}
}
/// E401
pub(crate) fn multiple_imports_on_one_line(checker: &mut Checker, stmt: &Stmt, names: &[Alias]) {
if names.len() > 1 {
checker
.diagnostics
.push(Diagnostic::new(MultipleImportsOnOneLine, stmt.range()));
}
}
/// E402
pub(crate) fn module_import_not_at_top_of_file(checker: &mut Checker, stmt: &Stmt) {
if checker.semantic().seen_import_boundary() && checker.semantic().at_top_level() {

View File

@@ -0,0 +1,120 @@
use itertools::Itertools;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Alias, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_trivia::indentation_at_offset;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
/// ## What it does
/// Check for multiple imports on one line.
///
/// ## Why is this bad?
/// According to [PEP 8], "imports should usually be on separate lines."
///
/// ## Example
/// ```python
/// import sys, os
/// ```
///
/// Use instead:
/// ```python
/// import os
/// import sys
/// ```
///
/// [PEP 8]: https://peps.python.org/pep-0008/#imports
#[violation]
pub struct MultipleImportsOnOneLine;
impl Violation for MultipleImportsOnOneLine {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Multiple imports on one line")
}
fn fix_title(&self) -> Option<String> {
Some(format!("Split imports"))
}
}
/// E401
pub(crate) fn multiple_imports_on_one_line(checker: &mut Checker, stmt: &Stmt, names: &[Alias]) {
if names.len() > 1 {
let mut diagnostic = Diagnostic::new(MultipleImportsOnOneLine, stmt.range());
if checker.settings.preview.is_enabled() {
diagnostic.set_fix(split_imports(
stmt,
names,
checker.locator(),
checker.indexer(),
checker.stylist(),
));
}
checker.diagnostics.push(diagnostic);
}
}
/// Generate a [`Fix`] to split the imports across multiple statements.
fn split_imports(
stmt: &Stmt,
names: &[Alias],
locator: &Locator,
indexer: &Indexer,
stylist: &Stylist,
) -> Fix {
if indexer.in_multi_statement_line(stmt, locator) {
// Ex) `x = 1; import os, sys` (convert to `x = 1; import os; import sys`)
let replacement = names
.iter()
.map(|alias| {
let Alias {
range: _,
name,
asname,
} = alias;
if let Some(asname) = asname {
format!("import {name} as {asname}")
} else {
format!("import {name}")
}
})
.join("; ");
Fix::safe_edit(Edit::range_replacement(replacement, stmt.range()))
} else {
// Ex) `import os, sys` (convert to `import os\nimport sys`)
let indentation = indentation_at_offset(stmt.start(), locator).unwrap_or_default();
// Generate newline-delimited imports.
let replacement = names
.iter()
.map(|alias| {
let Alias {
range: _,
name,
asname,
} = alias;
if let Some(asname) = asname {
format!("{indentation}import {name} as {asname}")
} else {
format!("{indentation}import {name}")
}
})
.join(stylist.line_ending().as_str());
Fix::safe_edit(Edit::range_replacement(
replacement,
TextRange::new(locator.line_start(stmt.start()), stmt.end()),
))
}
}

View File

@@ -162,7 +162,14 @@ pub(crate) fn preview_type_comparison(checker: &mut Checker, compare: &ast::Expr
.filter(|(_, op)| matches!(op, CmpOp::Eq | CmpOp::NotEq))
.map(|((left, right), _)| (left, right))
{
// If either expression is a type...
if is_type(left, checker.semantic()) || is_type(right, checker.semantic()) {
// And neither is a `dtype`...
if is_dtype(left, checker.semantic()) || is_dtype(right, checker.semantic()) {
continue;
}
// Disallow the comparison.
checker.diagnostics.push(Diagnostic::new(
TypeComparison {
preview: PreviewMode::Enabled,
@@ -295,3 +302,23 @@ fn is_type(expr: &Expr, semantic: &SemanticModel) -> bool {
_ => false,
}
}
/// Returns `true` if the [`Expr`] appears to be a reference to a NumPy dtype, since:
/// > `dtype` are a bit of a strange beast, but definitely best thought of as instances, not
/// > classes, and they are meant to be comparable not just to their own class, but also to the
/// corresponding scalar types (e.g., `x.dtype == np.float32`) and strings (e.g.,
/// `x.dtype == ['i1,i4']`; basically, __eq__ always tries to do `dtype(other)`).
fn is_dtype(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr {
// Ex) `np.dtype(obj)`
Expr::Call(ast::ExprCall { func, .. }) => semantic
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["numpy", "dtype"])),
// Ex) `obj.dtype`
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
// Ex) `obj.dtype`
attr.as_str() == "dtype"
}
_ => false,
}
}

View File

@@ -6,8 +6,87 @@ E40.py:2:1: E401 Multiple imports on one line
1 | #: E401
2 | import os, sys
| ^^^^^^^^^^^^^^ E401
3 | #: Okay
4 | import os
3 |
4 | #: Okay
|
= help: Split imports
E40.py:65:1: E401 Multiple imports on one line
|
64 | #: E401
65 | import re as regex, string # also with a comment!
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
66 | import re as regex, string; x = 1
|
= help: Split imports
E40.py:66:1: E401 Multiple imports on one line
|
64 | #: E401
65 | import re as regex, string # also with a comment!
66 | import re as regex, string; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
67 |
68 | x = 1; import re as regex, string
|
= help: Split imports
E40.py:68:8: E401 Multiple imports on one line
|
66 | import re as regex, string; x = 1
67 |
68 | x = 1; import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
= help: Split imports
E40.py:72:5: E401 Multiple imports on one line
|
71 | def blah():
72 | import datetime as dt, copy
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
73 |
74 | def nested_and_tested():
|
= help: Split imports
E40.py:75:9: E401 Multiple imports on one line
|
74 | def nested_and_tested():
75 | import builtins, textwrap as tw
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
76 |
77 | x = 1; import re as regex, string
|
= help: Split imports
E40.py:77:16: E401 Multiple imports on one line
|
75 | import builtins, textwrap as tw
76 |
77 | x = 1; import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
78 | import re as regex, string; x = 1
|
= help: Split imports
E40.py:78:9: E401 Multiple imports on one line
|
77 | x = 1; import re as regex, string
78 | import re as regex, string; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
79 |
80 | if True: import re as regex, string
|
= help: Split imports
E40.py:80:14: E401 Multiple imports on one line
|
78 | import re as regex, string; x = 1
79 |
80 | if True: import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
= help: Split imports

View File

@@ -1,32 +1,60 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E40.py:55:1: E402 Module level import not at top of file
E40.py:56:1: E402 Module level import not at top of file
|
53 | VERSION = '1.2.3'
54 |
55 | import foo
54 | VERSION = '1.2.3'
55 |
56 | import foo
| ^^^^^^^^^^ E402
56 | #: E402
57 | import foo
57 | #: E402
58 | import foo
|
E40.py:57:1: E402 Module level import not at top of file
E40.py:58:1: E402 Module level import not at top of file
|
55 | import foo
56 | #: E402
57 | import foo
56 | import foo
57 | #: E402
58 | import foo
| ^^^^^^^^^^ E402
58 |
59 | a = 1
59 |
60 | a = 1
|
E40.py:61:1: E402 Module level import not at top of file
E40.py:62:1: E402 Module level import not at top of file
|
59 | a = 1
60 |
61 | import bar
60 | a = 1
61 |
62 | import bar
| ^^^^^^^^^^ E402
63 |
64 | #: E401
|
E40.py:65:1: E402 Module level import not at top of file
|
64 | #: E401
65 | import re as regex, string # also with a comment!
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E402
66 | import re as regex, string; x = 1
|
E40.py:66:1: E402 Module level import not at top of file
|
64 | #: E401
65 | import re as regex, string # also with a comment!
66 | import re as regex, string; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E402
67 |
68 | x = 1; import re as regex, string
|
E40.py:68:8: E402 Module level import not at top of file
|
66 | import re as regex, string; x = 1
67 |
68 | x = 1; import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E402
|

View File

@@ -0,0 +1,180 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E40.py:2:1: E401 [*] Multiple imports on one line
|
1 | #: E401
2 | import os, sys
| ^^^^^^^^^^^^^^ E401
3 |
4 | #: Okay
|
= help: Split imports
Safe fix
1 1 | #: E401
2 |-import os, sys
2 |+import os
3 |+import sys
3 4 |
4 5 | #: Okay
5 6 | import os
E40.py:65:1: E401 [*] Multiple imports on one line
|
64 | #: E401
65 | import re as regex, string # also with a comment!
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
66 | import re as regex, string; x = 1
|
= help: Split imports
Safe fix
62 62 | import bar
63 63 |
64 64 | #: E401
65 |-import re as regex, string # also with a comment!
65 |+import re as regex
66 |+import string # also with a comment!
66 67 | import re as regex, string; x = 1
67 68 |
68 69 | x = 1; import re as regex, string
E40.py:66:1: E401 [*] Multiple imports on one line
|
64 | #: E401
65 | import re as regex, string # also with a comment!
66 | import re as regex, string; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
67 |
68 | x = 1; import re as regex, string
|
= help: Split imports
Safe fix
63 63 |
64 64 | #: E401
65 65 | import re as regex, string # also with a comment!
66 |-import re as regex, string; x = 1
66 |+import re as regex; import string; x = 1
67 67 |
68 68 | x = 1; import re as regex, string
69 69 |
E40.py:68:8: E401 [*] Multiple imports on one line
|
66 | import re as regex, string; x = 1
67 |
68 | x = 1; import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
= help: Split imports
Safe fix
65 65 | import re as regex, string # also with a comment!
66 66 | import re as regex, string; x = 1
67 67 |
68 |-x = 1; import re as regex, string
68 |+x = 1; import re as regex; import string
69 69 |
70 70 |
71 71 | def blah():
E40.py:72:5: E401 [*] Multiple imports on one line
|
71 | def blah():
72 | import datetime as dt, copy
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
73 |
74 | def nested_and_tested():
|
= help: Split imports
Safe fix
69 69 |
70 70 |
71 71 | def blah():
72 |- import datetime as dt, copy
72 |+ import datetime as dt
73 |+ import copy
73 74 |
74 75 | def nested_and_tested():
75 76 | import builtins, textwrap as tw
E40.py:75:9: E401 [*] Multiple imports on one line
|
74 | def nested_and_tested():
75 | import builtins, textwrap as tw
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
76 |
77 | x = 1; import re as regex, string
|
= help: Split imports
Safe fix
72 72 | import datetime as dt, copy
73 73 |
74 74 | def nested_and_tested():
75 |- import builtins, textwrap as tw
75 |+ import builtins
76 |+ import textwrap as tw
76 77 |
77 78 | x = 1; import re as regex, string
78 79 | import re as regex, string; x = 1
E40.py:77:16: E401 [*] Multiple imports on one line
|
75 | import builtins, textwrap as tw
76 |
77 | x = 1; import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
78 | import re as regex, string; x = 1
|
= help: Split imports
Safe fix
74 74 | def nested_and_tested():
75 75 | import builtins, textwrap as tw
76 76 |
77 |- x = 1; import re as regex, string
77 |+ x = 1; import re as regex; import string
78 78 | import re as regex, string; x = 1
79 79 |
80 80 | if True: import re as regex, string
E40.py:78:9: E401 [*] Multiple imports on one line
|
77 | x = 1; import re as regex, string
78 | import re as regex, string; x = 1
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
79 |
80 | if True: import re as regex, string
|
= help: Split imports
Safe fix
75 75 | import builtins, textwrap as tw
76 76 |
77 77 | x = 1; import re as regex, string
78 |- import re as regex, string; x = 1
78 |+ import re as regex; import string; x = 1
79 79 |
80 80 | if True: import re as regex, string
E40.py:80:14: E401 [*] Multiple imports on one line
|
78 | import re as regex, string; x = 1
79 |
80 | if True: import re as regex, string
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E401
|
= help: Split imports
Safe fix
77 77 | x = 1; import re as regex, string
78 78 | import re as regex, string; x = 1
79 79 |
80 |- if True: import re as regex, string
80 |+ if True: import re as regex; import string

View File

@@ -129,4 +129,11 @@ E721.py:59:4: E721 Use `is` and `is not` for type comparisons, or `isinstance()`
61 | #: Okay
|
E721.py:140:1: E721 Use `is` and `is not` for type comparisons, or `isinstance()` for isinstance checks
|
139 | #: E721
140 | dtype == float
| ^^^^^^^^^^^^^^ E721
|

View File

@@ -20,6 +20,7 @@ mod tests {
#[test_case(Rule::BlankLineAfterLastSection, Path::new("sections.py"))]
#[test_case(Rule::NoBlankLineAfterSection, Path::new("sections.py"))]
#[test_case(Rule::BlankLineAfterLastSection, Path::new("D413.py"))]
#[test_case(Rule::BlankLineAfterSummary, Path::new("D.py"))]
#[test_case(Rule::NoBlankLineBeforeSection, Path::new("sections.py"))]
#[test_case(Rule::CapitalizeSectionName, Path::new("sections.py"))]

View File

@@ -1632,9 +1632,13 @@ fn common_section(
}
let line_end = checker.stylist().line_ending().as_str();
let last_line = context.following_lines().last();
if last_line.map_or(true, |line| !line.trim().is_empty()) {
if let Some(next) = next {
if let Some(next) = next {
if context
.following_lines()
.last()
.map_or(true, |line| !line.trim().is_empty())
{
if checker.enabled(Rule::NoBlankLineAfterSection) {
let mut diagnostic = Diagnostic::new(
NoBlankLineAfterSection {
@@ -1649,7 +1653,16 @@ fn common_section(
)));
checker.diagnostics.push(diagnostic);
}
} else {
}
} else {
// The first blank line is the line containing the closing triple quotes, so we need at
// least two.
let num_blank_lines = context
.following_lines()
.rev()
.take_while(|line| line.trim().is_empty())
.count();
if num_blank_lines < 2 {
if checker.enabled(Rule::BlankLineAfterLastSection) {
let mut diagnostic = Diagnostic::new(
BlankLineAfterLastSection {
@@ -1659,7 +1672,11 @@ fn common_section(
);
// Add a newline after the section.
diagnostic.set_fix(Fix::safe_edit(Edit::insertion(
format!("{}{}", line_end, docstring.indentation),
format!(
"{}{}",
line_end.repeat(2 - num_blank_lines),
docstring.indentation
),
context.end(),
)));
checker.diagnostics.push(diagnostic);

View File

@@ -0,0 +1,80 @@
---
source: crates/ruff_linter/src/rules/pydocstyle/mod.rs
---
D413.py:1:1: D413 [*] Missing blank line after last section ("Returns")
|
1 | / """Do something.
2 | |
3 | | Args:
4 | | x: the value
5 | | with a hanging indent
6 | |
7 | | Returns:
8 | | the value
9 | | """
| |___^ D413
|
= help: Add blank line after "Returns"
Safe fix
6 6 |
7 7 | Returns:
8 8 | the value
9 |+
10 |+
9 11 | """
10 12 |
11 13 |
D413.py:13:5: D413 [*] Missing blank line after last section ("Returns")
|
12 | def func():
13 | """Do something.
| _____^
14 | |
15 | | Args:
16 | | x: the value
17 | | with a hanging indent
18 | |
19 | | Returns:
20 | | the value
21 | | """
| |_______^ D413
|
= help: Add blank line after "Returns"
Safe fix
18 18 |
19 19 | Returns:
20 20 | the value
21 |+
21 22 | """
22 23 |
23 24 |
D413.py:52:5: D413 [*] Missing blank line after last section ("Returns")
|
51 | def func():
52 | """Do something.
| _____^
53 | |
54 | | Args:
55 | | x: the value
56 | | with a hanging indent
57 | |
58 | | Returns:
59 | | the value"""
| |____________________^ D413
|
= help: Add blank line after "Returns"
Safe fix
56 56 | with a hanging indent
57 57 |
58 58 | Returns:
59 |- the value"""
59 |+ the value
60 |+
61 |+ """

View File

@@ -19,9 +19,147 @@ sections.py:65:5: D413 [*] Missing blank line after last section ("Returns")
66 66 |
67 |- Returns"""
67 |+ Returns
68 |+ """
68 69 |
69 70 |
70 71 | @expect(_D213)
68 |+
69 |+ """
68 70 |
69 71 |
70 72 | @expect(_D213)
sections.py:120:5: D413 [*] Missing blank line after last section ("Returns")
|
118 | @expect("D413: Missing blank line after last section ('Returns')")
119 | def no_blank_line_after_last_section(): # noqa: D416
120 | """Toggle the gizmo.
| _____^
121 | |
122 | | Returns
123 | | -------
124 | | A value of some sort.
125 | | """
| |_______^ D413
|
= help: Add blank line after "Returns"
Safe fix
122 122 | Returns
123 123 | -------
124 124 | A value of some sort.
125 |+
125 126 | """
126 127 |
127 128 |
sections.py:170:5: D413 [*] Missing blank line after last section ("Returns")
|
168 | @expect("D414: Section has no content ('Returns')")
169 | def section_underline_overindented_and_contentless(): # noqa: D416
170 | """Toggle the gizmo.
| _____^
171 | |
172 | | Returns
173 | | -------
174 | | """
| |_______^ D413
|
= help: Add blank line after "Returns"
Safe fix
171 171 |
172 172 | Returns
173 173 | -------
174 |+
174 175 | """
175 176 |
176 177 |
sections.py:519:5: D413 [*] Missing blank line after last section ("Parameters")
|
518 | def replace_equals_with_dash():
519 | """Equal length equals should be replaced with dashes.
| _____^
520 | |
521 | | Parameters
522 | | ==========
523 | | """
| |_______^ D413
|
= help: Add blank line after "Parameters"
Safe fix
520 520 |
521 521 | Parameters
522 522 | ==========
523 |+
523 524 | """
524 525 |
525 526 |
sections.py:527:5: D413 [*] Missing blank line after last section ("Parameters")
|
526 | def replace_equals_with_dash2():
527 | """Here, the length of equals is not the same.
| _____^
528 | |
529 | | Parameters
530 | | ===========
531 | | """
| |_______^ D413
|
= help: Add blank line after "Parameters"
Safe fix
528 528 |
529 529 | Parameters
530 530 | ===========
531 |+
531 532 | """
532 533 |
533 534 |
sections.py:548:5: D413 [*] Missing blank line after last section ("Args")
|
547 | def lowercase_sub_section_header():
548 | """Below, `returns:` should _not_ be considered a section header.
| _____^
549 | |
550 | | Args:
551 | | Here's a note.
552 | |
553 | | returns:
554 | | """
| |_______^ D413
|
= help: Add blank line after "Args"
Safe fix
551 551 | Here's a note.
552 552 |
553 553 | returns:
554 |+
554 555 | """
555 556 |
556 557 |
sections.py:558:5: D413 [*] Missing blank line after last section ("Returns")
|
557 | def titlecase_sub_section_header():
558 | """Below, `Returns:` should be considered a section header.
| _____^
559 | |
560 | | Args:
561 | | Here's a note.
562 | |
563 | | Returns:
564 | | """
| |_______^ D413
|
= help: Add blank line after "Returns"
Safe fix
561 561 | Here's a note.
562 562 |
563 563 | Returns:
564 |+
564 565 | """

View File

@@ -12,12 +12,13 @@ mod tests {
use rustc_hash::FxHashSet;
use test_case::test_case;
use crate::assert_messages;
use crate::registry::Rule;
use crate::rules::pylint;
use crate::settings::types::PreviewMode;
use crate::settings::types::PythonVersion;
use crate::settings::LinterSettings;
use crate::test::test_path;
use crate::{assert_messages, settings};
#[test_case(Rule::AndOrTernary, Path::new("and_or_ternary.py"))]
#[test_case(Rule::AssertOnStringLiteral, Path::new("assert_on_string_literal.py"))]
@@ -91,6 +92,7 @@ mod tests {
Path::new("named_expr_without_context.py")
)]
#[test_case(Rule::NonlocalWithoutBinding, Path::new("nonlocal_without_binding.py"))]
#[test_case(Rule::NonSlotAssignment, Path::new("non_slot_assignment.py"))]
#[test_case(Rule::PropertyWithParameters, Path::new("property_with_parameters.py"))]
#[test_case(
Rule::RedefinedArgumentFromLocal,
@@ -167,7 +169,9 @@ mod tests {
#[test_case(Rule::NoClassmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(Rule::UnnecessaryDunderCall, Path::new("unnecessary_dunder_call.py"))]
#[test_case(Rule::NoStaticmethodDecorator, Path::new("no_method_decorator.py"))]
#[test_case(Rule::PotentialIndexError, Path::new("potential_index_error.py"))]
#[test_case(Rule::SuperWithoutBrackets, Path::new("super_without_brackets.py"))]
#[test_case(Rule::TooManyNestedBlocks, Path::new("too_many_nested_blocks.py"))]
#[test_case(
Rule::UnnecessaryDictIndexLookup,
Path::new("unnecessary_dict_index_lookup.py")
@@ -190,6 +194,25 @@ mod tests {
Ok(())
}
#[test_case(Rule::UselessElseOnLoop, Path::new("useless_else_on_loop.py"))]
#[test_case(Rule::CollapsibleElseIf, Path::new("collapsible_else_if.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(
"preview__{}_{}",
rule_code.noqa_code(),
path.to_string_lossy()
);
let diagnostics = test_path(
Path::new("pylint").join(path).as_path(),
&settings::LinterSettings {
preview: PreviewMode::Enabled,
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn repeated_isinstance_calls() -> Result<()> {
let diagnostics = test_path(

View File

@@ -1,8 +1,15 @@
use ruff_python_ast::{ElifElseClause, Stmt};
use anyhow::Result;
use ast::whitespace::indentation;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, ElifElseClause, Stmt};
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
use crate::rules::pyupgrade::fixes::adjust_indentation;
/// ## What it does
/// Checks for `else` blocks that consist of a single `if` statement.
@@ -40,27 +47,85 @@ use ruff_macros::{derive_message_formats, violation};
pub struct CollapsibleElseIf;
impl Violation for CollapsibleElseIf {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use `elif` instead of `else` then `if`, to reduce indentation")
}
fn fix_title(&self) -> Option<String> {
Some("Convert to `elif`".to_string())
}
}
/// PLR5501
pub(crate) fn collapsible_else_if(elif_else_clauses: &[ElifElseClause]) -> Option<Diagnostic> {
let Some(ElifElseClause {
body,
test: None,
range,
}) = elif_else_clauses.last()
pub(crate) fn collapsible_else_if(checker: &mut Checker, stmt: &Stmt) {
let Stmt::If(ast::StmtIf {
elif_else_clauses, ..
}) = stmt
else {
return None;
return;
};
if let [first @ Stmt::If(_)] = body.as_slice() {
return Some(Diagnostic::new(
CollapsibleElseIf,
TextRange::new(range.start(), first.start()),
));
let Some(
else_clause @ ElifElseClause {
body, test: None, ..
},
) = elif_else_clauses.last()
else {
return;
};
let [first @ Stmt::If(ast::StmtIf { .. })] = body.as_slice() else {
return;
};
let mut diagnostic = Diagnostic::new(
CollapsibleElseIf,
TextRange::new(else_clause.start(), first.start()),
);
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
convert_to_elif(first, else_clause, checker.locator(), checker.stylist())
});
}
None
checker.diagnostics.push(diagnostic);
}
/// Generate [`Fix`] to convert an `else` block to an `elif` block.
fn convert_to_elif(
first: &Stmt,
else_clause: &ElifElseClause,
locator: &Locator,
stylist: &Stylist,
) -> Result<Fix> {
let inner_if_line_start = locator.line_start(first.start());
let inner_if_line_end = locator.line_end(first.end());
// Identify the indentation of the loop itself (e.g., the `while` or `for`).
let Some(indentation) = indentation(locator, else_clause) else {
return Err(anyhow::anyhow!("`else` is expected to be on its own line"));
};
// Dedent the content from the end of the `else` to the end of the `if`.
let indented = adjust_indentation(
TextRange::new(inner_if_line_start, inner_if_line_end),
indentation,
locator,
stylist,
)?;
// Strip the indent from the first line of the `if` statement, and add `el` to the start.
let Some(unindented) = indented.strip_prefix(indentation) else {
return Err(anyhow::anyhow!("indented block to start with indentation"));
};
let indented = format!("{indentation}el{unindented}");
Ok(Fix::safe_edit(Edit::replacement(
indented,
locator.line_start(else_clause.start()),
inner_if_line_end,
)))
}

View File

@@ -39,8 +39,7 @@ use crate::checkers::ast::Checker;
/// ```
///
/// ## Options
/// - [`namespace-packages`]: List of packages that are defined as namespace
/// packages.
/// - `namespace-packages`
///
/// ## References
/// - [PEP 8: Naming Conventions](https://peps.python.org/pep-0008/#naming-conventions)
@@ -48,7 +47,6 @@ use crate::checkers::ast::Checker;
///
/// [PEP 8]: https://www.python.org/dev/peps/pep-0008/
/// [PEP 420]: https://www.python.org/dev/peps/pep-0420/
/// [`namespace-packages`]: https://beta.ruff.rs/docs/settings/#namespace-packages
#[violation]
pub struct ImportPrivateName {
name: String,

View File

@@ -21,6 +21,12 @@ use crate::checkers::ast::Checker;
/// ```python
/// 1 in {1, 2, 3}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as the use of a `set` literal will
/// error at runtime if the sequence contains unhashable elements (like lists
/// or dictionaries).
///
/// ## References
/// - [Whats New In Python 3.2](https://docs.python.org/3/whatsnew/3.2.html#optimizations)
#[violation]

View File

@@ -41,7 +41,9 @@ pub(crate) use no_method_decorator::*;
pub(crate) use no_self_use::*;
pub(crate) use non_ascii_module_import::*;
pub(crate) use non_ascii_name::*;
pub(crate) use non_slot_assignment::*;
pub(crate) use nonlocal_without_binding::*;
pub(crate) use potential_index_error::*;
pub(crate) use property_with_parameters::*;
pub(crate) use redefined_argument_from_local::*;
pub(crate) use redefined_loop_name::*;
@@ -59,6 +61,7 @@ pub(crate) use too_many_arguments::*;
pub(crate) use too_many_boolean_expressions::*;
pub(crate) use too_many_branches::*;
pub(crate) use too_many_locals::*;
pub(crate) use too_many_nested_blocks::*;
pub(crate) use too_many_positional::*;
pub(crate) use too_many_public_methods::*;
pub(crate) use too_many_return_statements::*;
@@ -123,7 +126,9 @@ mod no_method_decorator;
mod no_self_use;
mod non_ascii_module_import;
mod non_ascii_name;
mod non_slot_assignment;
mod nonlocal_without_binding;
mod potential_index_error;
mod property_with_parameters;
mod redefined_argument_from_local;
mod redefined_loop_name;
@@ -141,6 +146,7 @@ mod too_many_arguments;
mod too_many_boolean_expressions;
mod too_many_branches;
mod too_many_locals;
mod too_many_nested_blocks;
mod too_many_positional;
mod too_many_public_methods;
mod too_many_return_statements;

View File

@@ -0,0 +1,242 @@
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for assignments to attributes that are not defined in `__slots__`.
///
/// ## Why is this bad?
/// When using `__slots__`, only the specified attributes are allowed.
/// Attempting to assign to an attribute that is not defined in `__slots__`
/// will result in an `AttributeError` at runtime.
///
/// ## Known problems
/// This rule can't detect `__slots__` implementations in superclasses, and
/// so limits its analysis to classes that inherit from (at most) `object`.
///
/// ## Example
/// ```python
/// class Student:
/// __slots__ = ("name",)
///
/// def __init__(self, name, surname):
/// self.name = name
/// self.surname = surname # [assigning-non-slot]
/// self.setup()
///
/// def setup(self):
/// pass
/// ```
///
/// Use instead:
/// ```python
/// class Student:
/// __slots__ = ("name", "surname")
///
/// def __init__(self, name, surname):
/// self.name = name
/// self.surname = surname
/// self.setup()
///
/// def setup(self):
/// pass
/// ```
#[violation]
pub struct NonSlotAssignment {
name: String,
}
impl Violation for NonSlotAssignment {
#[derive_message_formats]
fn message(&self) -> String {
let NonSlotAssignment { name } = self;
format!("Attribute `{name}` is not defined in class's `__slots__`")
}
}
/// E0237
pub(crate) fn non_slot_assignment(checker: &mut Checker, class_def: &ast::StmtClassDef) {
// If the class inherits from another class (aside from `object`), then it's possible that
// the parent class defines the relevant `__slots__`.
if !class_def.bases().iter().all(|base| {
checker
.semantic()
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["", "object"]))
}) {
return;
}
for attribute in is_attributes_not_in_slots(&class_def.body) {
checker.diagnostics.push(Diagnostic::new(
NonSlotAssignment {
name: attribute.name.to_string(),
},
attribute.range(),
));
}
}
#[derive(Debug)]
struct AttributeAssignment<'a> {
/// The name of the attribute that is assigned to.
name: &'a str,
/// The range of the attribute that is assigned to.
range: TextRange,
}
impl Ranged for AttributeAssignment<'_> {
fn range(&self) -> TextRange {
self.range
}
}
/// Return a list of attributes that are assigned to but not included in `__slots__`.
fn is_attributes_not_in_slots(body: &[Stmt]) -> Vec<AttributeAssignment> {
// First, collect all the attributes that are assigned to `__slots__`.
let mut slots = FxHashSet::default();
for statement in body {
match statement {
// Ex) `__slots__ = ("name",)`
Stmt::Assign(ast::StmtAssign { targets, value, .. }) => {
let [Expr::Name(ast::ExprName { id, .. })] = targets.as_slice() else {
continue;
};
if id == "__slots__" {
slots.extend(slots_attributes(value));
}
}
// Ex) `__slots__: Tuple[str, ...] = ("name",)`
Stmt::AnnAssign(ast::StmtAnnAssign {
target,
value: Some(value),
..
}) => {
let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else {
continue;
};
if id == "__slots__" {
slots.extend(slots_attributes(value));
}
}
// Ex) `__slots__ += ("name",)`
Stmt::AugAssign(ast::StmtAugAssign { target, value, .. }) => {
let Expr::Name(ast::ExprName { id, .. }) = target.as_ref() else {
continue;
};
if id == "__slots__" {
slots.extend(slots_attributes(value));
}
}
_ => {}
}
}
if slots.is_empty() {
return vec![];
}
// Second, find any assignments that aren't included in `__slots__`.
let mut assignments = vec![];
for statement in body {
let Stmt::FunctionDef(ast::StmtFunctionDef { name, body, .. }) = statement else {
continue;
};
if name == "__init__" {
for statement in body {
match statement {
// Ex) `self.name = name`
Stmt::Assign(ast::StmtAssign { targets, .. }) => {
let [Expr::Attribute(attribute)] = targets.as_slice() else {
continue;
};
let Expr::Name(ast::ExprName { id, .. }) = attribute.value.as_ref() else {
continue;
};
if id == "self" && !slots.contains(attribute.attr.as_str()) {
assignments.push(AttributeAssignment {
name: &attribute.attr,
range: attribute.range(),
});
}
}
// Ex) `self.name: str = name`
Stmt::AnnAssign(ast::StmtAnnAssign { target, .. }) => {
let Expr::Attribute(attribute) = target.as_ref() else {
continue;
};
let Expr::Name(ast::ExprName { id, .. }) = attribute.value.as_ref() else {
continue;
};
if id == "self" && !slots.contains(attribute.attr.as_str()) {
assignments.push(AttributeAssignment {
name: &attribute.attr,
range: attribute.range(),
});
}
}
// Ex) `self.name += name`
Stmt::AugAssign(ast::StmtAugAssign { target, .. }) => {
let Expr::Attribute(attribute) = target.as_ref() else {
continue;
};
let Expr::Name(ast::ExprName { id, .. }) = attribute.value.as_ref() else {
continue;
};
if id == "self" && !slots.contains(attribute.attr.as_str()) {
assignments.push(AttributeAssignment {
name: &attribute.attr,
range: attribute.range(),
});
}
}
_ => {}
}
}
}
}
assignments
}
/// Return an iterator over the attributes enumerated in the given `__slots__` value.
fn slots_attributes(expr: &Expr) -> impl Iterator<Item = &str> {
// Ex) `__slots__ = ("name",)`
let elts_iter = match expr {
Expr::Tuple(ast::ExprTuple { elts, .. })
| Expr::List(ast::ExprList { elts, .. })
| Expr::Set(ast::ExprSet { elts, .. }) => Some(elts.iter().filter_map(|elt| match elt {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => Some(value.to_str()),
_ => None,
})),
_ => None,
};
// Ex) `__slots__ = {"name": ...}`
let keys_iter = match expr {
Expr::Dict(ast::ExprDict { keys, .. }) => Some(keys.iter().filter_map(|key| match key {
Some(Expr::StringLiteral(ast::ExprStringLiteral { value, .. })) => Some(value.to_str()),
_ => None,
})),
_ => None,
};
elts_iter
.into_iter()
.flatten()
.chain(keys_iter.into_iter().flatten())
}

View File

@@ -0,0 +1,73 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for hard-coded sequence accesses that are known to be out of bounds.
///
/// ## Why is this bad?
/// Attempting to access a sequence with an out-of-bounds index will cause an
/// `IndexError` to be raised at runtime. When the sequence and index are
/// defined statically (e.g., subscripts on `list` and `tuple` literals, with
/// integer indexes), such errors can be detected ahead of time.
///
/// ## Example
/// ```python
/// print([0, 1, 2][3])
/// ```
#[violation]
pub struct PotentialIndexError;
impl Violation for PotentialIndexError {
#[derive_message_formats]
fn message(&self) -> String {
format!("Potential IndexError")
}
}
/// PLE0643
pub(crate) fn potential_index_error(checker: &mut Checker, value: &Expr, slice: &Expr) {
// Determine the length of the sequence.
let length = match value {
Expr::Tuple(ast::ExprTuple { elts, .. }) | Expr::List(ast::ExprList { elts, .. }) => {
match i64::try_from(elts.len()) {
Ok(length) => length,
Err(_) => return,
}
}
_ => {
return;
}
};
// Determine the index value.
let index = match slice {
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(number_value),
..
}) => number_value.as_i64(),
Expr::UnaryOp(ast::ExprUnaryOp {
op: ast::UnaryOp::USub,
operand,
..
}) => match operand.as_ref() {
Expr::NumberLiteral(ast::ExprNumberLiteral {
value: ast::Number::Int(number_value),
..
}) => number_value.as_i64().map(|number| -number),
_ => return,
},
_ => return,
};
// Emit a diagnostic if the index is out of bounds. If the index can't be represented as an
// `i64`, but the length _can_, then the index is definitely out of bounds.
if index.map_or(true, |index| index >= length || index < -length) {
checker
.diagnostics
.push(Diagnostic::new(PotentialIndexError, slice.range()));
}
}

View File

@@ -0,0 +1,132 @@
use ast::ExceptHandler;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Stmt};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for functions or methods with too many nested blocks.
///
/// By default, this rule allows up to five nested blocks.
/// This can be configured using the [`pylint.max-nested-blocks`] option.
///
/// ## Why is this bad?
/// Functions or methods with too many nested blocks are harder to understand
/// and maintain.
///
/// ## Options
/// - `pylint.max-nested-blocks`
#[violation]
pub struct TooManyNestedBlocks {
nested_blocks: usize,
max_nested_blocks: usize,
}
impl Violation for TooManyNestedBlocks {
#[derive_message_formats]
fn message(&self) -> String {
let TooManyNestedBlocks {
nested_blocks,
max_nested_blocks,
} = self;
format!("Too many nested blocks ({nested_blocks} > {max_nested_blocks})")
}
}
/// PLR1702
pub(crate) fn too_many_nested_blocks(checker: &mut Checker, stmt: &Stmt) {
// Only enforce nesting within functions or methods.
if !checker.semantic().current_scope().kind.is_function() {
return;
}
// If the statement isn't a leaf node, we don't want to emit a diagnostic, since the diagnostic
// will be emitted on the leaves.
if has_nested_block(stmt) {
return;
}
let max_nested_blocks = checker.settings.pylint.max_nested_blocks;
// Traverse up the hierarchy, identifying the root node and counting the number of nested
// blocks between the root and this leaf.
let (count, root_id) =
checker
.semantic()
.current_statement_ids()
.fold((0, None), |(count, ancestor_id), id| {
let stmt = checker.semantic().statement(id);
if is_nested_block(stmt) {
(count + 1, Some(id))
} else {
(count, ancestor_id)
}
});
let Some(root_id) = root_id else {
return;
};
// If the number of nested blocks is less than the maximum, we don't want to emit a diagnostic.
if count <= max_nested_blocks {
return;
}
checker.diagnostics.push(Diagnostic::new(
TooManyNestedBlocks {
nested_blocks: count,
max_nested_blocks,
},
checker.semantic().statement(root_id).range(),
));
}
/// Returns `true` if the given statement is a nested block.
fn is_nested_block(stmt: &Stmt) -> bool {
matches!(
stmt,
Stmt::If(_) | Stmt::While(_) | Stmt::For(_) | Stmt::Try(_) | Stmt::With(_)
)
}
/// Returns `true` if the given statement is a leaf node.
fn has_nested_block(stmt: &Stmt) -> bool {
match stmt {
Stmt::If(ast::StmtIf {
body,
elif_else_clauses,
..
}) => {
body.iter().any(is_nested_block)
|| elif_else_clauses
.iter()
.any(|elif_else| elif_else.body.iter().any(is_nested_block))
}
Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
body.iter().any(is_nested_block) || orelse.iter().any(is_nested_block)
}
Stmt::For(ast::StmtFor { body, orelse, .. }) => {
body.iter().any(is_nested_block) || orelse.iter().any(is_nested_block)
}
Stmt::Try(ast::StmtTry {
body,
handlers,
orelse,
finalbody,
..
}) => {
body.iter().any(is_nested_block)
|| handlers.iter().any(|handler| match handler {
ExceptHandler::ExceptHandler(ast::ExceptHandlerExceptHandler {
body, ..
}) => body.iter().any(is_nested_block),
})
|| orelse.iter().any(is_nested_block)
|| finalbody.iter().any(is_nested_block)
}
Stmt::With(ast::StmtWith { body, .. }) => body.iter().any(is_nested_block),
_ => false,
}
}

View File

@@ -1,10 +1,16 @@
use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt};
use anyhow::Result;
use ruff_diagnostics::{Diagnostic, Violation};
use ast::whitespace::indentation;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::identifier;
use ruff_python_ast::{self as ast, ExceptHandler, MatchCase, Stmt};
use ruff_python_codegen::Stylist;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::rules::pyupgrade::fixes::adjust_indentation;
/// ## What it does
/// Checks for `else` clauses on loops without a `break` statement.
@@ -42,15 +48,50 @@ use crate::checkers::ast::Checker;
pub struct UselessElseOnLoop;
impl Violation for UselessElseOnLoop {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!(
"`else` clause on loop without a `break` statement; remove the `else` and de-indent all the \
code inside it"
"`else` clause on loop without a `break` statement; remove the `else` and dedent its contents"
)
}
fn fix_title(&self) -> Option<String> {
Some("Remove `else`".to_string())
}
}
/// PLW0120
pub(crate) fn useless_else_on_loop(
checker: &mut Checker,
stmt: &Stmt,
body: &[Stmt],
orelse: &[Stmt],
) {
if orelse.is_empty() || loop_exits_early(body) {
return;
}
let else_range = identifier::else_(stmt, checker.locator().contents()).expect("else clause");
let mut diagnostic = Diagnostic::new(UselessElseOnLoop, else_range);
if checker.settings.preview.is_enabled() {
diagnostic.try_set_fix(|| {
remove_else(
stmt,
orelse,
else_range,
checker.locator(),
checker.stylist(),
)
});
}
checker.diagnostics.push(diagnostic);
}
/// Returns `true` if the given body contains a `break` statement.
fn loop_exits_early(body: &[Stmt]) -> bool {
body.iter().any(|stmt| match stmt {
Stmt::If(ast::StmtIf {
@@ -91,17 +132,50 @@ fn loop_exits_early(body: &[Stmt]) -> bool {
})
}
/// PLW0120
pub(crate) fn useless_else_on_loop(
checker: &mut Checker,
/// Generate a [`Fix`] to remove the `else` clause from the given statement.
fn remove_else(
stmt: &Stmt,
body: &[Stmt],
orelse: &[Stmt],
) {
if !orelse.is_empty() && !loop_exits_early(body) {
checker.diagnostics.push(Diagnostic::new(
UselessElseOnLoop,
identifier::else_(stmt, checker.locator().contents()).unwrap(),
));
else_range: TextRange,
locator: &Locator,
stylist: &Stylist,
) -> Result<Fix> {
let Some(start) = orelse.first() else {
return Err(anyhow::anyhow!("Empty `else` clause"));
};
let Some(end) = orelse.last() else {
return Err(anyhow::anyhow!("Empty `else` clause"));
};
let start_indentation = indentation(locator, start);
if start_indentation.is_none() {
// Inline `else` block (e.g., `else: x = 1`).
Ok(Fix::safe_edit(Edit::deletion(
else_range.start(),
start.start(),
)))
} else {
// Identify the indentation of the loop itself (e.g., the `while` or `for`).
let Some(desired_indentation) = indentation(locator, stmt) else {
return Err(anyhow::anyhow!("Compound statement cannot be inlined"));
};
// Dedent the content from the end of the `else` to the end of the loop.
let indented = adjust_indentation(
TextRange::new(
locator.full_line_end(else_range.start()),
locator.full_line_end(end.end()),
),
desired_indentation,
locator,
stylist,
)?;
// Replace the content from the start of the `else` to the end of the loop.
Ok(Fix::safe_edit(Edit::replacement(
indented,
locator.line_start(else_range.start()),
locator.full_line_end(end.end()),
)))
}
}

View File

@@ -60,6 +60,7 @@ pub struct Settings {
pub max_statements: usize,
pub max_public_methods: usize,
pub max_locals: usize,
pub max_nested_blocks: usize,
}
impl Default for Settings {
@@ -75,6 +76,7 @@ impl Default for Settings {
max_statements: 50,
max_public_methods: 20,
max_locals: 15,
max_nested_blocks: 5,
}
}
}

View File

@@ -0,0 +1,31 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
non_slot_assignment.py:6:9: PLE0237 Attribute `surname` is not defined in class's `__slots__`
|
4 | def __init__(self, name, surname):
5 | self.name = name
6 | self.surname = surname # [assigning-non-slot]
| ^^^^^^^^^^^^ PLE0237
7 | self.setup()
|
non_slot_assignment.py:18:9: PLE0237 Attribute `middle_name` is not defined in class's `__slots__`
|
16 | def __init__(self, name, middle_name):
17 | self.name = name
18 | self.middle_name = middle_name # [assigning-non-slot]
| ^^^^^^^^^^^^^^^^ PLE0237
19 | self.setup()
|
non_slot_assignment.py:42:9: PLE0237 Attribute `middle_name` is not defined in class's `__slots__`
|
40 | def __init__(self, name, middle_name):
41 | self.name = name
42 | self.middle_name = middle_name # [assigning-non-slot]
| ^^^^^^^^^^^^^^^^ PLE0237
43 | self.setup()
|

View File

@@ -0,0 +1,40 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
potential_index_error.py:1:17: PLE0643 Potential IndexError
|
1 | print([1, 2, 3][3]) # PLE0643
| ^ PLE0643
2 | print([1, 2, 3][-4]) # PLE0643
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
|
potential_index_error.py:2:17: PLE0643 Potential IndexError
|
1 | print([1, 2, 3][3]) # PLE0643
2 | print([1, 2, 3][-4]) # PLE0643
| ^^ PLE0643
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
|
potential_index_error.py:3:17: PLE0643 Potential IndexError
|
1 | print([1, 2, 3][3]) # PLE0643
2 | print([1, 2, 3][-4]) # PLE0643
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE0643
4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
|
potential_index_error.py:4:17: PLE0643 Potential IndexError
|
2 | print([1, 2, 3][-4]) # PLE0643
3 | print([1, 2, 3][999999999999999999999999999999999999999999]) # PLE0643
4 | print([1, 2, 3][-999999999999999999999999999999999999999999]) # PLE0643
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PLE0643
5 |
6 | print([1, 2, 3][2]) # OK
|

View File

@@ -0,0 +1,20 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
too_many_nested_blocks.py:2:5: PLR1702 Too many nested blocks (6 > 5)
|
1 | def correct_fruits(fruits) -> bool:
2 | if len(fruits) > 1: # PLR1702
| _____^
3 | | if "apple" in fruits:
4 | | if "orange" in fruits:
5 | | count = fruits["orange"]
6 | | if count % 2:
7 | | if "kiwi" in fruits:
8 | | if count == 2:
9 | | return True
| |_______________________________________^ PLR1702
10 | return False
|

View File

@@ -11,6 +11,7 @@ collapsible_else_if.py:37:5: PLR5501 Use `elif` instead of `else` then `if`, to
| |________^ PLR5501
39 | pass
|
= help: Convert to `elif`
collapsible_else_if.py:45:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
@@ -23,17 +24,71 @@ collapsible_else_if.py:45:5: PLR5501 Use `elif` instead of `else` then `if`, to
47 | pass
48 | else:
|
= help: Convert to `elif`
collapsible_else_if.py:58:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
collapsible_else_if.py:55:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
56 | elif True:
57 | print(2)
58 | else:
53 | if 1:
54 | pass
55 | else:
| _____^
59 | | if True:
56 | | # inner comment
57 | | if 2:
| |________^ PLR5501
60 | print(3)
61 | else:
58 | pass
59 | else:
|
= help: Convert to `elif`
collapsible_else_if.py:69:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
67 | elif True:
68 | print(2)
69 | else:
| _____^
70 | | if True:
| |________^ PLR5501
71 | print(3)
72 | else:
|
= help: Convert to `elif`
collapsible_else_if.py:79:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
77 | if 1:
78 | pass
79 | else:
| _____^
80 | | if 2: pass
| |________^ PLR5501
81 | else: pass
|
= help: Convert to `elif`
collapsible_else_if.py:87:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
85 | if 1:
86 | pass
87 | else:
| _____^
88 | | if 2: pass
| |________^ PLR5501
89 | else:
90 | pass
|
= help: Convert to `elif`
collapsible_else_if.py:96:5: PLR5501 Use `elif` instead of `else` then `if`, to reduce indentation
|
94 | if 1:
95 | pass
96 | else:
| _____^
97 | | if 2:
| |________^ PLR5501
98 | pass
99 | else: pass
|
= help: Convert to `elif`

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
useless_else_on_loop.py:9:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:9:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
7 | if i % 2:
8 | return i
@@ -10,8 +10,9 @@ useless_else_on_loop.py:9:5: PLW0120 `else` clause on loop without a `break` sta
10 | print("math is broken")
11 | return None
|
= help: Remove `else`
useless_else_on_loop.py:18:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:18:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
16 | while True:
17 | return 1
@@ -20,8 +21,9 @@ useless_else_on_loop.py:18:5: PLW0120 `else` clause on loop without a `break` st
19 | print("math is broken")
20 | return None
|
= help: Remove `else`
useless_else_on_loop.py:30:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:30:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
28 | break
29 |
@@ -29,8 +31,9 @@ useless_else_on_loop.py:30:1: PLW0120 `else` clause on loop without a `break` st
| ^^^^ PLW0120
31 | print("or else!")
|
= help: Remove `else`
useless_else_on_loop.py:37:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:37:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
35 | while False:
36 | break
@@ -38,8 +41,9 @@ useless_else_on_loop.py:37:1: PLW0120 `else` clause on loop without a `break` st
| ^^^^ PLW0120
38 | print("or else!")
|
= help: Remove `else`
useless_else_on_loop.py:42:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:42:1: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
40 | for j in range(10):
41 | pass
@@ -48,8 +52,9 @@ useless_else_on_loop.py:42:1: PLW0120 `else` clause on loop without a `break` st
43 | print("fat chance")
44 | for j in range(10):
|
= help: Remove `else`
useless_else_on_loop.py:88:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:88:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
86 | else:
87 | print("all right")
@@ -58,8 +63,9 @@ useless_else_on_loop.py:88:5: PLW0120 `else` clause on loop without a `break` st
89 | return True
90 | return False
|
= help: Remove `else`
useless_else_on_loop.py:98:9: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and de-indent all the code inside it
useless_else_on_loop.py:98:9: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
96 | for _ in range(3):
97 | pass
@@ -68,5 +74,17 @@ useless_else_on_loop.py:98:9: PLW0120 `else` clause on loop without a `break` st
99 | if 1 < 2: # pylint: disable=comparison-of-constants
100 | break
|
= help: Remove `else`
useless_else_on_loop.py:144:5: PLW0120 `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
142 | for j in range(10):
143 | pass
144 | else:
| ^^^^ PLW0120
145 | # [useless-else-on-loop]
146 | print("fat chance")
|
= help: Remove `else`

View File

@@ -0,0 +1,195 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
collapsible_else_if.py:37:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
35 | if 1:
36 | pass
37 | else:
| _____^
38 | | if 2:
| |________^ PLR5501
39 | pass
|
= help: Convert to `elif`
Safe fix
34 34 | def not_ok0():
35 35 | if 1:
36 36 | pass
37 |- else:
38 |- if 2:
39 |- pass
37 |+ elif 2:
38 |+ pass
40 39 |
41 40 |
42 41 | def not_ok1():
collapsible_else_if.py:45:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
43 | if 1:
44 | pass
45 | else:
| _____^
46 | | if 2:
| |________^ PLR5501
47 | pass
48 | else:
|
= help: Convert to `elif`
Safe fix
42 42 | def not_ok1():
43 43 | if 1:
44 44 | pass
45 |+ elif 2:
46 |+ pass
45 47 | else:
46 |- if 2:
47 |- pass
48 |- else:
49 |- pass
48 |+ pass
50 49 |
51 50 |
52 51 | def not_ok1_with_comments():
collapsible_else_if.py:55:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
53 | if 1:
54 | pass
55 | else:
| _____^
56 | | # inner comment
57 | | if 2:
| |________^ PLR5501
58 | pass
59 | else:
|
= help: Convert to `elif`
Safe fix
52 52 | def not_ok1_with_comments():
53 53 | if 1:
54 54 | pass
55 |+ elif 2:
56 |+ pass
55 57 | else:
56 |- # inner comment
57 |- if 2:
58 |- pass
59 |- else:
60 |- pass # final pass comment
58 |+ pass # final pass comment
61 59 |
62 60 |
63 61 | # Regression test for https://github.com/apache/airflow/blob/f1e1cdcc3b2826e68ba133f350300b5065bbca33/airflow/models/dag.py#L1737
collapsible_else_if.py:69:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
67 | elif True:
68 | print(2)
69 | else:
| _____^
70 | | if True:
| |________^ PLR5501
71 | print(3)
72 | else:
|
= help: Convert to `elif`
Safe fix
66 66 | print(1)
67 67 | elif True:
68 68 | print(2)
69 |+ elif True:
70 |+ print(3)
69 71 | else:
70 |- if True:
71 |- print(3)
72 |- else:
73 |- print(4)
72 |+ print(4)
74 73 |
75 74 |
76 75 | def not_ok3():
collapsible_else_if.py:79:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
77 | if 1:
78 | pass
79 | else:
| _____^
80 | | if 2: pass
| |________^ PLR5501
81 | else: pass
|
= help: Convert to `elif`
Safe fix
76 76 | def not_ok3():
77 77 | if 1:
78 78 | pass
79 |- else:
80 |- if 2: pass
81 |- else: pass
79 |+ elif 2: pass
80 |+ else: pass
82 81 |
83 82 |
84 83 | def not_ok4():
collapsible_else_if.py:87:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
85 | if 1:
86 | pass
87 | else:
| _____^
88 | | if 2: pass
| |________^ PLR5501
89 | else:
90 | pass
|
= help: Convert to `elif`
Safe fix
84 84 | def not_ok4():
85 85 | if 1:
86 86 | pass
87 |+ elif 2: pass
87 88 | else:
88 |- if 2: pass
89 |- else:
90 |- pass
89 |+ pass
91 90 |
92 91 |
93 92 | def not_ok5():
collapsible_else_if.py:96:5: PLR5501 [*] Use `elif` instead of `else` then `if`, to reduce indentation
|
94 | if 1:
95 | pass
96 | else:
| _____^
97 | | if 2:
| |________^ PLR5501
98 | pass
99 | else: pass
|
= help: Convert to `elif`
Safe fix
93 93 | def not_ok5():
94 94 | if 1:
95 95 | pass
96 |- else:
97 |- if 2:
98 |- pass
99 |- else: pass
96 |+ elif 2:
97 |+ pass
98 |+ else: pass

View File

@@ -0,0 +1,187 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
useless_else_on_loop.py:9:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
7 | if i % 2:
8 | return i
9 | else: # [useless-else-on-loop]
| ^^^^ PLW0120
10 | print("math is broken")
11 | return None
|
= help: Remove `else`
Safe fix
6 6 | for i in range(10):
7 7 | if i % 2:
8 8 | return i
9 |- else: # [useless-else-on-loop]
10 |- print("math is broken")
9 |+ print("math is broken")
11 10 | return None
12 11 |
13 12 |
useless_else_on_loop.py:18:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
16 | while True:
17 | return 1
18 | else: # [useless-else-on-loop]
| ^^^^ PLW0120
19 | print("math is broken")
20 | return None
|
= help: Remove `else`
Safe fix
15 15 | """else + return is not acceptable."""
16 16 | while True:
17 17 | return 1
18 |- else: # [useless-else-on-loop]
19 |- print("math is broken")
18 |+ print("math is broken")
20 19 | return None
21 20 |
22 21 |
useless_else_on_loop.py:30:1: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
28 | break
29 |
30 | else: # [useless-else-on-loop]
| ^^^^ PLW0120
31 | print("or else!")
|
= help: Remove `else`
Safe fix
27 27 | for _ in range(10):
28 28 | break
29 29 |
30 |-else: # [useless-else-on-loop]
31 |- print("or else!")
30 |+print("or else!")
32 31 |
33 32 |
34 33 | while True:
useless_else_on_loop.py:37:1: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
35 | while False:
36 | break
37 | else: # [useless-else-on-loop]
| ^^^^ PLW0120
38 | print("or else!")
|
= help: Remove `else`
Safe fix
34 34 | while True:
35 35 | while False:
36 36 | break
37 |-else: # [useless-else-on-loop]
38 |- print("or else!")
37 |+print("or else!")
39 38 |
40 39 | for j in range(10):
41 40 | pass
useless_else_on_loop.py:42:1: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
40 | for j in range(10):
41 | pass
42 | else: # [useless-else-on-loop]
| ^^^^ PLW0120
43 | print("fat chance")
44 | for j in range(10):
|
= help: Remove `else`
Safe fix
39 39 |
40 40 | for j in range(10):
41 41 | pass
42 |-else: # [useless-else-on-loop]
43 |- print("fat chance")
44 |- for j in range(10):
45 |- break
42 |+print("fat chance")
43 |+for j in range(10):
44 |+ break
46 45 |
47 46 |
48 47 | def test_return_for2():
useless_else_on_loop.py:88:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
86 | else:
87 | print("all right")
88 | else: # [useless-else-on-loop]
| ^^^^ PLW0120
89 | return True
90 | return False
|
= help: Remove `else`
Safe fix
85 85 | break
86 86 | else:
87 87 | print("all right")
88 |- else: # [useless-else-on-loop]
89 |- return True
88 |+ return True
90 89 | return False
91 90 |
92 91 |
useless_else_on_loop.py:98:9: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
96 | for _ in range(3):
97 | pass
98 | else:
| ^^^^ PLW0120
99 | if 1 < 2: # pylint: disable=comparison-of-constants
100 | break
|
= help: Remove `else`
Safe fix
95 95 | for _ in range(10):
96 96 | for _ in range(3):
97 97 | pass
98 |- else:
99 |- if 1 < 2: # pylint: disable=comparison-of-constants
100 |- break
98 |+ if 1 < 2: # pylint: disable=comparison-of-constants
99 |+ break
101 100 | else:
102 101 | return True
103 102 | return False
useless_else_on_loop.py:144:5: PLW0120 [*] `else` clause on loop without a `break` statement; remove the `else` and dedent its contents
|
142 | for j in range(10):
143 | pass
144 | else:
| ^^^^ PLW0120
145 | # [useless-else-on-loop]
146 | print("fat chance")
|
= help: Remove `else`
Safe fix
141 141 | """Retain the comment within the `else` block"""
142 142 | for j in range(10):
143 143 | pass
144 |- else:
145 |- # [useless-else-on-loop]
146 |- print("fat chance")
147 |- for j in range(10):
148 |- break
144 |+ # [useless-else-on-loop]
145 |+ print("fat chance")
146 |+ for j in range(10):
147 |+ break

View File

@@ -1,5 +1,5 @@
//! Rules from [pyupgrade](https://pypi.org/project/pyupgrade/).
mod fixes;
pub(crate) mod fixes;
mod helpers;
pub(crate) mod rules;
pub mod settings;

View File

@@ -43,6 +43,10 @@ mod tests {
#[test_case(Rule::NeverUnion, Path::new("RUF020.py"))]
#[test_case(Rule::ParenthesizeChainedOperators, Path::new("RUF021.py"))]
#[test_case(Rule::UnsortedDunderAll, Path::new("RUF022.py"))]
#[test_case(Rule::UnsortedDunderSlots, Path::new("RUF023.py"))]
#[test_case(Rule::MutableFromkeysValue, Path::new("RUF024.py"))]
#[test_case(Rule::UnnecessaryDictComprehensionForIterable, Path::new("RUF025.py"))]
#[test_case(Rule::DefaultFactoryKwarg, Path::new("RUF026.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -0,0 +1,163 @@
use anyhow::Result;
use ast::Keyword;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_constant;
use ruff_python_ast::{self as ast, Expr};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::{remove_argument, Parentheses};
use crate::fix::snippet::SourceCodeSnippet;
/// ## What it does
/// Checks for incorrect usages of `default_factory` as a keyword argument when
/// initializing a `defaultdict`.
///
/// ## Why is this bad?
/// The `defaultdict` constructor accepts a callable as its first argument.
/// For example, it's common to initialize a `defaultdict` with `int` or `list`
/// via `defaultdict(int)` or `defaultdict(list)`, to create a dictionary that
/// returns `0` or `[]` respectively when a key is missing.
///
/// The default factory _must_ be provided as a positional argument, as all
/// keyword arguments to `defaultdict` are interpreted as initial entries in
/// the dictionary. For example, `defaultdict(foo=1, bar=2)` will create a
/// dictionary with `{"foo": 1, "bar": 2}` as its initial entries.
///
/// As such, `defaultdict(default_factory=list)` will create a dictionary with
/// `{"default_factory": list}` as its initial entry, instead of a dictionary
/// that returns `[]` when a key is missing. Specifying a `default_factory`
/// keyword argument is almost always a mistake, and one that type checkers
/// can't reliably detect.
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as converting `default_factory` from a
/// keyword to a positional argument will change the behavior of the code, even
/// if the keyword argument was used erroneously.
///
/// ## Examples
/// ```python
/// defaultdict(default_factory=int)
/// defaultdict(default_factory=list)
/// ```
///
/// Use instead:
/// ```python
/// defaultdict(int)
/// defaultdict(list)
/// ```
#[violation]
pub struct DefaultFactoryKwarg {
default_factory: SourceCodeSnippet,
}
impl Violation for DefaultFactoryKwarg {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("`default_factory` is a positional-only argument to `defaultdict`")
}
fn fix_title(&self) -> Option<String> {
let DefaultFactoryKwarg { default_factory } = self;
if let Some(default_factory) = default_factory.full_display() {
Some(format!("Replace with `defaultdict({default_factory})`"))
} else {
Some("Use positional argument".to_string())
}
}
}
/// RUF026
pub(crate) fn default_factory_kwarg(checker: &mut Checker, call: &ast::ExprCall) {
// If the call isn't a `defaultdict` constructor, return.
if !checker
.semantic()
.resolve_call_path(call.func.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["collections", "defaultdict"]))
{
return;
}
// If the user provided a positional argument for `default_factory`, return.
if !call.arguments.args.is_empty() {
return;
}
// If the user didn't provide a `default_factory` keyword argument, return.
let Some(keyword) = call.arguments.find_keyword("default_factory") else {
return;
};
// If the value is definitively not callable, return.
if is_non_callable_value(&keyword.value) {
return;
}
let mut diagnostic = Diagnostic::new(
DefaultFactoryKwarg {
default_factory: SourceCodeSnippet::from_str(checker.locator().slice(keyword)),
},
call.range(),
);
diagnostic.try_set_fix(|| convert_to_positional(call, keyword, checker.locator()));
checker.diagnostics.push(diagnostic);
}
/// Returns `true` if a value is definitively not callable (e.g., `1` or `[]`).
fn is_non_callable_value(value: &Expr) -> bool {
is_constant(value)
|| matches!(value, |Expr::List(_)| Expr::Dict(_)
| Expr::Set(_)
| Expr::Tuple(_)
| Expr::Slice(_)
| Expr::ListComp(_)
| Expr::SetComp(_)
| Expr::DictComp(_)
| Expr::GeneratorExp(_)
| Expr::FString(_))
}
/// Generate an [`Expr`] to replace `defaultdict(default_factory=callable)` with
/// `defaultdict(callable)`.
///
/// For example, given `defaultdict(default_factory=list)`, generate `defaultdict(list)`.
fn convert_to_positional(
call: &ast::ExprCall,
default_factory: &Keyword,
locator: &Locator,
) -> Result<Fix> {
if call.arguments.len() == 1 {
// Ex) `defaultdict(default_factory=list)`
Ok(Fix::unsafe_edit(Edit::range_replacement(
locator.slice(&default_factory.value).to_string(),
default_factory.range(),
)))
} else {
// Ex) `defaultdict(member=1, default_factory=list)`
// First, remove the `default_factory` keyword argument.
let removal_edit = remove_argument(
default_factory,
&call.arguments,
Parentheses::Preserve,
locator.contents(),
)?;
// Second, insert the value as the first positional argument.
let insertion_edit = Edit::insertion(
format!("{}, ", locator.slice(&default_factory.value)),
call.arguments
.arguments_source_order()
.next()
.ok_or_else(|| anyhow::anyhow!("`default_factory` keyword argument not found"))?
.start(),
);
Ok(Fix::unsafe_edits(insertion_edit, [removal_edit]))
}
}

View File

@@ -59,7 +59,7 @@ pub(super) fn has_default_copy_semantics(
analyze::class::any_call_path(class_def, semantic, &|call_path| {
matches!(
call_path.as_slice(),
["pydantic", "BaseModel" | "BaseSettings"]
["pydantic", "BaseModel" | "BaseSettings" | "BaseConfig"]
| ["pydantic_settings", "BaseSettings"]
| ["msgspec", "Struct"]
)

View File

@@ -2,6 +2,7 @@ pub(crate) use ambiguous_unicode_character::*;
pub(crate) use assignment_in_assert::*;
pub(crate) use asyncio_dangling_task::*;
pub(crate) use collection_literal_concatenation::*;
pub(crate) use default_factory_kwarg::*;
pub(crate) use explicit_f_string_type_conversion::*;
pub(crate) use function_call_in_dataclass_default::*;
pub(crate) use implicit_optional::*;
@@ -9,12 +10,15 @@ pub(crate) use invalid_index_type::*;
pub(crate) use invalid_pyproject_toml::*;
pub(crate) use mutable_class_default::*;
pub(crate) use mutable_dataclass_default::*;
pub(crate) use mutable_fromkeys_value::*;
pub(crate) use never_union::*;
pub(crate) use pairwise_over_zipped::*;
pub(crate) use parenthesize_logical_operators::*;
pub(crate) use quadratic_list_summation::*;
pub(crate) use sort_dunder_all::*;
pub(crate) use sort_dunder_slots::*;
pub(crate) use static_key_dict_comprehension::*;
pub(crate) use unnecessary_dict_comprehension_for_iterable::*;
pub(crate) use unnecessary_iterable_allocation_for_first_element::*;
pub(crate) use unnecessary_key_check::*;
pub(crate) use unused_noqa::*;
@@ -24,6 +28,7 @@ mod assignment_in_assert;
mod asyncio_dangling_task;
mod collection_literal_concatenation;
mod confusables;
mod default_factory_kwarg;
mod explicit_f_string_type_conversion;
mod function_call_in_dataclass_default;
mod helpers;
@@ -32,11 +37,15 @@ mod invalid_index_type;
mod invalid_pyproject_toml;
mod mutable_class_default;
mod mutable_dataclass_default;
mod mutable_fromkeys_value;
mod never_union;
mod pairwise_over_zipped;
mod parenthesize_logical_operators;
mod sequence_sorting;
mod sort_dunder_all;
mod sort_dunder_slots;
mod static_key_dict_comprehension;
mod unnecessary_dict_comprehension_for_iterable;
mod unnecessary_iterable_allocation_for_first_element;
mod unnecessary_key_check;
mod unused_noqa;

View File

@@ -0,0 +1,127 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::is_mutable_expr;
use ruff_python_codegen::Generator;
use ruff_text_size::Ranged;
use ruff_text_size::TextRange;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for mutable objects passed as a value argument to `dict.fromkeys`.
///
/// ## Why is this bad?
/// All values in the dictionary created by the `dict.fromkeys` method
/// refer to the same instance of the provided object. If that object is
/// modified, all values are modified, which can lead to unexpected behavior.
/// For example, if the empty list (`[]`) is provided as the default value,
/// all values in the dictionary will use the same list; as such, appending to
/// any one entry will append to all entries.
///
/// Instead, use a comprehension to generate a dictionary with distinct
/// instances of the default value.
///
/// ## Example
/// ```python
/// cities = dict.fromkeys(["UK", "Poland"], [])
/// cities["UK"].append("London")
/// cities["Poland"].append("Poznan")
/// print(cities) # {'UK': ['London', 'Poznan'], 'Poland': ['London', 'Poznan']}
/// ```
///
/// Use instead:
/// ```python
/// cities = {country: [] for country in ["UK", "Poland"]}
/// cities["UK"].append("London")
/// cities["Poland"].append("Poznan")
/// print(cities) # {'UK': ['London'], 'Poland': ['Poznan']}
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as the edit will change the behavior of
/// the program by using a distinct object for every value in the dictionary,
/// rather than a shared mutable instance. In some cases, programs may rely on
/// the previous behavior.
///
/// ## References
/// - [Python documentation: `dict.fromkeys`](https://docs.python.org/3/library/stdtypes.html#dict.fromkeys)
#[violation]
pub struct MutableFromkeysValue;
impl Violation for MutableFromkeysValue {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not pass mutable objects as values to `dict.fromkeys`")
}
fn fix_title(&self) -> Option<String> {
Some("Replace with comprehension".to_string())
}
}
/// RUF024
pub(crate) fn mutable_fromkeys_value(checker: &mut Checker, call: &ast::ExprCall) {
let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = call.func.as_ref() else {
return;
};
// Check that the call is to `dict.fromkeys`.
if attr != "fromkeys" {
return;
}
let Some(name_expr) = value.as_name_expr() else {
return;
};
if name_expr.id != "dict" {
return;
}
if !checker.semantic().is_builtin("dict") {
return;
}
// Check that the value parameter is a mutable object.
let [keys, value] = call.arguments.args.as_slice() else {
return;
};
if !is_mutable_expr(value, checker.semantic()) {
return;
}
let mut diagnostic = Diagnostic::new(MutableFromkeysValue, call.range());
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
generate_dict_comprehension(keys, value, checker.generator()),
call.range(),
)));
checker.diagnostics.push(diagnostic);
}
/// Format a code snippet to expression `{key: value for key in keys}`, where
/// `keys` and `value` are the parameters of `dict.fromkeys`.
fn generate_dict_comprehension(keys: &Expr, value: &Expr, generator: Generator) -> String {
// Construct `key`.
let key = ast::ExprName {
id: "key".to_string(),
ctx: ast::ExprContext::Load,
range: TextRange::default(),
};
// Construct `key in keys`.
let comp = ast::Comprehension {
target: key.clone().into(),
iter: keys.clone(),
ifs: vec![],
range: TextRange::default(),
is_async: false,
};
// Construct the dict comprehension.
let dict_comp = ast::ExprDictComp {
key: Box::new(key.into()),
value: Box::new(value.clone()),
generators: vec![comp],
range: TextRange::default(),
};
generator.expr(&dict_comp.into())
}

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