Compare commits

..

83 Commits

Author SHA1 Message Date
Zanie
b3980c1113 Add must_use 2023-12-26 12:56:06 -06:00
Zanie
4f6e03ced8 Do not take explicit-preview-rules into account when selecting fixable rules 2023-12-26 12:43:35 -06:00
dependabot[bot]
29513398d2 Bump env_logger from 0.10.0 to 0.10.1 (#9275) 2023-12-25 09:03:35 -05:00
Charlie Marsh
fa78d2d97c Avoid adding return types to stub methods (#9277)
We should avoid adding `-> None` to stubs in `.pyi` files, along with a
few other cases. (We already ignore abstract methods.)

Closes https://github.com/astral-sh/ruff/issues/9270.
2023-12-25 09:03:24 -05:00
dependabot[bot]
8b70240fa2 Bump anyhow from 1.0.75 to 1.0.76 (#9274) 2023-12-25 08:12:40 -05:00
dependabot[bot]
e85e0d45f3 Bump ignore from 0.4.20 to 0.4.21 (#9273) 2023-12-25 08:12:36 -05:00
dependabot[bot]
9b43203575 Bump colored from 2.0.4 to 2.1.0 (#9271) 2023-12-25 08:12:28 -05:00
dependabot[bot]
bae3fa435d Bump pep440_rs from 0.3.12 to 0.4.0 (#9272) 2023-12-25 08:12:20 -05:00
Zanie Blue
6e65601055 Update some references to the old repo org (#9233)
Need https://github.com/pkgxdev/pantry/issues/4531 before we can update
at
af88ffc57e/docs/installation.md (L30-L31)
2023-12-24 20:02:49 +00:00
Charlie Marsh
9d6444138b Remove lexing and parsing from the linter benchmark (#9264)
## Summary

This PR adds some helper structs to the linter paths to enable passing
in the pre-computed tokens and parsed source code during benchmarking,
to remove lexing and parsing from the overall linter benchmark
measurement. We already remove parsing for the formatter, and we have
separate benchmarks for the lexer and the parser, so this should make it
much easier to measure linter performance changes.
2023-12-23 16:43:11 -05:00
Charlie Marsh
6d0c9c4e95 Avoid asyncio-dangling-task for nonlocal and global bindings (#9263)
Closes https://github.com/astral-sh/ruff/issues/9262.
2023-12-23 21:33:50 +00:00
Charlie Marsh
20def33fb7 Remove special pre-visit for module docstrings (#9261)
This ensures that we visit the module docstring like any other string.

Closes https://github.com/astral-sh/ruff/issues/9260.
2023-12-23 10:03:12 -05:00
Charlie Marsh
506ffade6c Remove unnecessary rule enabled check (#9259) 2023-12-23 12:45:22 +00:00
dependabot[bot]
5040fb8cec Bump thiserror from 1.0.50 to 1.0.51 (#9255) 2023-12-23 12:44:44 +00:00
Charlie Marsh
09ac0f9e72 Remove separate push method (#9258) 2023-12-23 12:36:40 +00:00
dependabot[bot]
34d7584ca3 Bump toml from 0.8.2 to 0.8.8 (#9253) 2023-12-23 07:35:24 -05:00
dependabot[bot]
097d0a4322 Bump proc-macro2 from 1.0.70 to 1.0.71 (#9250) 2023-12-23 07:35:18 -05:00
dependabot[bot]
9a672ec112 Bump cachedir from 0.3.0 to 0.3.1 (#9254) 2023-12-23 07:35:11 -05:00
dependabot[bot]
7a109164a6 Bump test-case from 3.2.1 to 3.3.1 (#9252) 2023-12-23 07:35:03 -05:00
Henry Schreiner
74dba3ee59 ci: group dependabot updates (#9249)
See https://github.com/scientific-python/cookie/pull/348.

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
2023-12-22 23:39:52 -06:00
Charlie Marsh
cdea7d71a3 Fix scoping for generators in named expressions in classes (#9248)
Closes https://github.com/astral-sh/ruff/issues/9230.
2023-12-22 18:06:40 +00:00
Charlie Marsh
bb86d359d4 Run cargo with --locked in CI (#9247)
This should ensure that CI fails if the lockfile is not up-to-date (see:
https://github.com/astral-sh/ruff/pull/9235).
2023-12-22 15:53:42 +00:00
Micha Reiser
9cc257ee7d Improve dummy_implementations preview style formatting (#9240) 2023-12-22 03:44:14 +00:00
Micha Reiser
a06723da2b Parenthesize multi-context managers (#9222) 2023-12-22 03:41:03 +00:00
Micha Reiser
fa2c37b411 Parenthesize long type annotations in annotated assignments (#9210) 2023-12-22 03:33:47 +00:00
Micha Reiser
3cc719bd74 Use named preview test functions (#9239) 2023-12-22 00:23:04 +00:00
Micha Reiser
d835b28d01 Show preview changes for tests with options (#9223) 2023-12-21 23:36:19 +00:00
Charlie Marsh
1e7bc1dffe Wrap subscripted dicts in parens for f-string conversion (#9238)
Closes https://github.com/astral-sh/ruff/issues/9227.
2023-12-21 21:51:50 +00:00
Charlie Marsh
e241c1c5df Make parent non-Optional in traverse_union (#9219)
## Summary

This protects callers from having to pass in `None`, and allows the
callback to operate as if it's always a union member.
2023-12-21 21:10:08 +00:00
Charlie Marsh
b0ae1199e8 Add a fix for never-union (#9218)
## Summary

Enables us to rewrite `Never | int` as `int`.
2023-12-21 21:01:09 +00:00
Charlie Marsh
a9ceef5b5d [ruff] Add never-union rule to detect redundant typing.NoReturn and typing.Never (#9217)
## Summary

Adds a rule to detect unions that include `typing.NoReturn` or
`typing.Never`. In such cases, the use of the bottom type is redundant.

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

## Test Plan

`cargo test`
2023-12-21 20:53:31 +00:00
Andrew Gallant
a3e06e5a9d update lock file from v0.1.9 release (#9235)
This should have been done before the actual release, so we add another
step to `CONTRIBUTING.md` to make sure it gets done in the future.

This doesn't fix https://github.com/astral-sh/ruff/issues/9234
completely, but it's a step in the right direction.
2023-12-21 15:47:30 -05:00
Zanie Blue
3c2b800d26 Clarify release workflow steps in CONTRIB guide (#9232)
We always recommend providing the SHA and since
https://github.com/astral-sh/ruff/pull/7279 it does not need to be the
latest commit on `main`.
2023-12-21 12:25:18 -06:00
Andrew Gallant
0263f2715e Bump version to v0.1.9 (#9231) 2023-12-21 13:19:50 -05:00
Micha Reiser
c6d8076034 Set target versions in Black tests (#9221) 2023-12-21 04:20:17 +00:00
Micha Reiser
8cb7950102 Add target_version to formatter options (#9220) 2023-12-21 04:05:58 +00:00
Micha Reiser
ef4bd8d5ff Fix: Avoid parenthesizing subscript targets and values (#9209) 2023-12-20 23:42:25 +00:00
asafamr-mm
5d41c84ef7 SIM300: CONSTANT_CASE variables are improperly flagged for yoda violation (#9164)
## Summary

fixes #6956 
details in issue

Following an advice in
https://github.com/astral-sh/ruff/issues/6956#issuecomment-1817672585,
this change separates expressions to 3 levels of "constant likelihood":
*  literals, empty dict and tuples... (definitely constant, level 2)
*  CONSTANT_CASE vars (probably constant, 1)
* all other expressions (0)

a comparison is marked yoda if the level is strictly higher on its left
hand side

following
https://github.com/astral-sh/ruff/issues/6956#issuecomment-1697107822
marking compound expressions of literals (e.g. `60 * 60` ) as constants
this change current behaviour on
`SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60)` in the fixture
from error to ok
2023-12-20 20:26:07 +00:00
Charlie Marsh
cbe3bf9bde Avoid asyncio-dangling-task violations on shadowed bindings (#9215)
## Summary

Ensures that we avoid flagging cases like:

```python
async def f(x: int):
    if x > 0:
        task = asyncio.create_task(make_request())
    else:
        task = asyncio.create_task(make_request())
    await task
```

Closes https://github.com/astral-sh/ruff/issues/9133.
2023-12-20 12:07:57 -05:00
Charlie Marsh
4b4160eb48 Allow removal of typing from exempt-modules (#9214)
## Summary

If you remove `typing` from `exempt-modules`, we tend to panic, since we
try to add `TYPE_CHECKING` to `from typing import ...` statements while
concurrently attempting to remove other members from that import. This
PR adds special-casing for typing imports to avoid such panics.

Closes https://github.com/astral-sh/ruff/issues/5331
Closes https://github.com/astral-sh/ruff/issues/9196.
Closes https://github.com/astral-sh/ruff/issues/9197.
2023-12-20 11:03:02 -05:00
Charlie Marsh
29846f5b09 Prefer Never to NoReturn in auto-typing (#9213)
Closes https://github.com/astral-sh/ruff/issues/9212.
2023-12-20 09:36:01 -05:00
Charlie Marsh
07b293d949 Add fix to automatically remove print and pprint statements (#9208)
Closes https://github.com/astral-sh/ruff/issues/9207.
2023-12-20 05:35:30 +00:00
Charlie Marsh
5ccc21aea2 Add support for NoReturn in auto-return-typing (#9206)
## Summary

Given a function like:

```python
def func(x: int):
    if not x:
        raise ValueError
    else:
        raise TypeError
```

We now correctly use `NoReturn` as the return type, rather than `None`.

Closes https://github.com/astral-sh/ruff/issues/9201.
2023-12-20 00:06:31 -05:00
Charlie Marsh
f5d4019c2b Add error suppression hint for multi-line strings (#9205)
Closes https://github.com/astral-sh/ruff/issues/9200.
2023-12-20 04:04:30 +00:00
Alex Waygood
bc0bf6f41c [flake8-pyi] Expand PYI018 to cover ParamSpecs and TypeVarTuples (#9198)
## Summary

Part of #8771. flake8-pyi will emit a Y018 error for unused TypeVars,
ParamSpecs or TypeVarTuples; Ruff currently only emits PYI018 for unused
TypeVars.

This is my first "proper" Ruff PR -- let me know if there's a better way
of doing this! Not sure if the repeated calls to `match_typing_expr()`
are ideal.

## Test Plan

I manually updated the fixtures to add some unused ParamSpecs and
TypeVarTuples, and then updated the snapshots using `cargo insta
review`. All tests then passed when run using `cargo test`.
2023-12-20 03:10:07 +00:00
konsti
a2bc635584 Add a non-latin project to the ecosystem checks (#9199)
We've had bugs related to non-latin scripts, most recently #9145, where
just starting a docstring with multi-byte characters would panic. I've
added https://github.com/binary-husky/gpt_academic to catch those in the
ecosystem checks, it's a popular repo with mixed english and chinese
comments and symbols.
2023-12-19 16:14:01 -05:00
Dhruv Manilawala
09296e3e3c Implement no_blank_line_before_class_docstring preview style (#9154)
## Summary

This PR implements the `no_blank_line_before_class_docstring` preview
style.

## Test Plan

Update existing snapshots.

### Formatter ecosystem

`main`

| project | similarity index | total files | changed files |
|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 213 |
| poetry | 0.99905 | 321 | 15 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99958 | 1459 | 36 |

`dhruv/no-blank-line-docstring`

| project | similarity index | total files | changed files |
|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 213 |
| poetry | 0.99905 | 321 | 15 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99958 | 1459 | 36 |

fixes: #8888
2023-12-19 00:43:20 -06:00
Steve C
7c894921df [pylint] Implement too-many-locals (PLR0914) (#9163)
## Summary

Implements [`PLR0914` -
`too-many-locals`](https://pylint.readthedocs.io/en/latest/user_guide/messages/refactor/too-many-locals.html)

See #970 

## Test Plan

`cargo test`
2023-12-18 20:00:04 +00:00
Charlie Marsh
be8f8e62b5 Reverse order of arguments for operator.contains (#9192)
Closes https://github.com/astral-sh/ruff/issues/9191.
2023-12-18 14:39:52 -05:00
Charlie Marsh
c97d3ddafb Add site-packages to default exclusions (#9188)
Suggested in
https://github.com/astral-sh/ruff-vscode/issues/232#issuecomment-1860788600.
This is technically a non-backwards-compatible change, but I would be
very surprised if it affected anyone in practice given that
`site-packages` is always ignored already in virtual environments.
2023-12-18 11:37:25 -05:00
Shantanu
a7514295c1 [flake8-bugbear] Add fix for zip-without-explicit-strict (B905) (#9176) 2023-12-18 16:34:53 +00:00
Charlie Marsh
0bf7683a3f Avoid mutable-class-default violations for Pydantic subclasses (#9187)
Only applies to subclasses defined within the same file, as elsewhere.

See:
https://github.com/astral-sh/ruff/issues/5243#issuecomment-1860776975.
2023-12-18 11:19:07 -05:00
Tuomas Siipola
c532089fb3 Implement reimplemented_operator (FURB118) (#9171)
## Summary

Implement
[FURB118](https://github.com/dosisod/refurb/blob/master/docs/checks.md#furb118-use-operator)
that recommends, for example, that `lambda x, y: x + y` is replaced with
`operator.add`. Part of #1348.

## Test Plan

Added test cases.
2023-12-18 14:59:16 +00:00
dependabot[bot]
0977fa987b Bump dawidd6/action-download-artifact from 2 to 3 (#9178)
Bumps
[dawidd6/action-download-artifact](https://github.com/dawidd6/action-download-artifact)
from 2 to 3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dawidd6/action-download-artifact/releases">dawidd6/action-download-artifact's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.0</h2>
<p>Node was updated from 16 to 20.
Node 20 requires <code>glibc&gt;=2.28</code>.</p>
<h2>v2.28.0</h2>
<p>No release notes provided.</p>
<h2>v2.27.0</h2>
<p>No release notes provided.</p>
<h2>v2.26.1</h2>
<p>No release notes provided.</p>
<h2>v2.26.0</h2>
<p>No release notes provided.</p>
<h2>v2.25.0</h2>
<p>No release notes provided.</p>
<h2>v2.24.4</h2>
<p>No release notes provided.</p>
<h2>v2.24.3</h2>
<p>No release notes provided.</p>
<h2>v2.24.2</h2>
<p>No release notes provided.</p>
<h2>v2.24.0</h2>
<p>No release notes provided.</p>
<h2>v2.23.0</h2>
<p>No release notes provided.</p>
<h2>v2.22.0</h2>
<p>No release notes provided.</p>
<h2>v2.21.1</h2>
<p>No release notes provided.</p>
<h2>v2.21.0</h2>
<p>No release notes provided.</p>
<h2>v2.20.0</h2>
<p>No release notes provided.</p>
<h2>v2.19.0</h2>
<p>No release notes provided.</p>
<h2>v2.18.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e7466d1a75"><code>e7466d1</code></a>
build(deps): bump <code>@​actions/artifact</code> from 1.1.2 to 2.0.0
(<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/260">#260</a>)</li>
<li><a
href="f29d1b6a89"><code>f29d1b6</code></a>
node_modules: upgrade</li>
<li><a
href="587cee61f5"><code>587cee6</code></a>
action: node16 -&gt; node20 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/259">#259</a>)</li>
<li><a
href="1cf761fba6"><code>1cf761f</code></a>
build(deps): bump undici from 5.25.4 to 5.28.2 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/258">#258</a>)</li>
<li><a
href="d44631c448"><code>d44631c</code></a>
build(deps): bump <code>@​actions/github</code> from 5.1.1 to 6.0.0 (<a
href="https://redirect.github.com/dawidd6/action-download-artifact/issues/252">#252</a>)</li>
<li>See full diff in <a
href="https://github.com/dawidd6/action-download-artifact/compare/v2...v3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dawidd6/action-download-artifact&package-manager=github_actions&previous-version=2&new-version=3)](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>
2023-12-18 14:18:14 +01:00
dependabot[bot]
b4e101e157 Bump unicode_names2 from 1.2.0 to 1.2.1 (#9184)
Bumps [unicode_names2](https://github.com/progval/unicode_names2) from
1.2.0 to 1.2.1.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/progval/unicode_names2/blob/master/CHANGELOG.md">unicode_names2's
changelog</a>.</em></p>
<blockquote>
<h1>v1.2.1</h1>
<p><em>2023-12-14</em></p>
<p>Internal:</p>
<ul>
<li>include required license texts in all published crates (<a
href="https://redirect.github.com/progval/unicode_names2/pull/35">#35</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="a18dc1f0f3"><code>a18dc1f</code></a>
Release v1.2.1</li>
<li><a
href="b6be64fecd"><code>b6be64f</code></a>
include required license texts in all published crates (<a
href="https://redirect.github.com/progval/unicode_names2/issues/35">#35</a>)</li>
<li>See full diff in <a
href="https://github.com/progval/unicode_names2/compare/v1.2.0...v1.2.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=unicode_names2&package-manager=cargo&previous-version=1.2.0&new-version=1.2.1)](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>
2023-12-18 09:18:47 +00:00
dependabot[bot]
2345cf1ec9 Bump once_cell from 1.18.0 to 1.19.0 (#9183)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.18.0 to
1.19.0.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/matklad/once_cell/blob/master/CHANGELOG.md">once_cell's
changelog</a>.</em></p>
<blockquote>
<h2>1.19.0</h2>
<ul>
<li>Use <code>portable-atomic</code> instead of
<code>atomic-polyfill</code>, <a
href="https://redirect.github.com/matklad/once_cell/pull/251">#251</a>.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c48d3c2c01"><code>c48d3c2</code></a>
Merge pull request <a
href="https://redirect.github.com/matklad/once_cell/issues/251">#251</a>
from taks/portable-atomic</li>
<li><a
href="8211d80789"><code>8211d80</code></a>
Fix CI</li>
<li><a
href="2715aa9896"><code>2715aa9</code></a>
v1.19.0</li>
<li><a
href="dffcae4440"><code>dffcae4</code></a>
Fix CI</li>
<li><a
href="de4cd9db53"><code>de4cd9d</code></a>
Revert atomic-polyfill feature</li>
<li><a
href="e26736f1f7"><code>e26736f</code></a>
Fix CI</li>
<li><a
href="5f88676dd0"><code>5f88676</code></a>
Use portable_atomic instead of atomic-polyfill</li>
<li><a
href="874f9373ab"><code>874f937</code></a>
clarify that MSRV does bump the minor version</li>
<li><a
href="3cd6549466"><code>3cd6549</code></a>
Merge <a
href="https://redirect.github.com/matklad/once_cell/issues/245">#245</a></li>
<li><a
href="a2eabc917b"><code>a2eabc9</code></a>
Add <code>--generate-link-to-definition</code> option when building on
docs.rs</li>
<li>Additional commits viewable in <a
href="https://github.com/matklad/once_cell/compare/v1.18.0...v1.19.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=once_cell&package-manager=cargo&previous-version=1.18.0&new-version=1.19.0)](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>
2023-12-18 09:05:25 +00:00
dependabot[bot]
7489e6b881 Bump toml from 0.7.8 to 0.8.2 (#9182)
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.8 to 0.8.2.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="fe65b2bfa2"><code>fe65b2b</code></a>
chore: Release</li>
<li><a
href="ed597ebad1"><code>ed597eb</code></a>
chore: Release</li>
<li><a
href="257a0fdc59"><code>257a0fd</code></a>
docs: Update changelog</li>
<li><a
href="4b44f53a31"><code>4b44f53</code></a>
Merge pull request <a
href="https://redirect.github.com/toml-rs/toml/issues/617">#617</a> from
epage/update</li>
<li><a
href="7eaf286110"><code>7eaf286</code></a>
fix(parser): Failed on mixed inline tables</li>
<li><a
href="e1f20378a2"><code>e1f2037</code></a>
test: Verify with latest data</li>
<li><a
href="2f9253c9eb"><code>2f9253c</code></a>
chore: Update toml-test</li>
<li><a
href="c9b481cab5"><code>c9b481c</code></a>
test(toml): Ensure tables are used for validation</li>
<li><a
href="43d7f29cfd"><code>43d7f29</code></a>
Merge pull request <a
href="https://redirect.github.com/toml-rs/toml/issues/615">#615</a> from
toml-rs/renovate/actions-checkout-4.x</li>
<li><a
href="ef9b8372c8"><code>ef9b837</code></a>
chore(deps): update actions/checkout action to v4</li>
<li>Additional commits viewable in <a
href="https://github.com/toml-rs/toml/compare/toml-v0.7.8...toml-v0.8.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=toml&package-manager=cargo&previous-version=0.7.8&new-version=0.8.2)](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>
2023-12-18 09:05:08 +00:00
dependabot[bot]
02f8cc9ad3 Bump tracing-indicatif from 0.3.5 to 0.3.6 (#9180)
Bumps
[tracing-indicatif](https://github.com/emersonford/tracing-indicatif)
from 0.3.5 to 0.3.6.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/emersonford/tracing-indicatif/blob/main/CHANGELOG.md">tracing-indicatif's
changelog</a>.</em></p>
<blockquote>
<h2>0.3.6 - 2023-12-11</h2>
<ul>
<li>update dev dependencies (<a
href="https://redirect.github.com/emersonford/tracing-indicatif/issues/8">#8</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="224e512c21"><code>224e512</code></a>
release 0.3.6</li>
<li><a
href="9bb470df4a"><code>9bb470d</code></a>
Merge pull request <a
href="https://redirect.github.com/emersonford/tracing-indicatif/issues/8">#8</a>
from decathorpe/main</li>
<li><a
href="950bd67450"><code>950bd67</code></a>
deps: update dialoguer to v0.11</li>
<li>See full diff in <a
href="https://github.com/emersonford/tracing-indicatif/compare/0.3.5...0.3.6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tracing-indicatif&package-manager=cargo&previous-version=0.3.5&new-version=0.3.6)](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>
2023-12-18 09:04:53 +00:00
dependabot[bot]
0854f8cfa4 Bump wasm-bindgen-test from 0.3.38 to 0.3.39 (#9181)
Bumps [wasm-bindgen-test](https://github.com/rustwasm/wasm-bindgen) from
0.3.38 to 0.3.39.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/rustwasm/wasm-bindgen/commits">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=wasm-bindgen-test&package-manager=cargo&previous-version=0.3.38&new-version=0.3.39)](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>
2023-12-18 09:04:31 +00:00
Víctor
6feea863a6 Update format.rs to display correct message for already formatted files (#9153)
## Summary

<!-- What's the purpose of the change? What does it do, and why? -->

New messages for "format" mode. 
Fixes #9132 

## Test Plan

<!-- How was it tested? -->

I ran the tests specified in `CONTRIBUTING.md`
```bash
cargo run -p ruff_cli -- check /path/to/some_files.py --no-cache
cargo run -p ruff_cli -- format --check /path/to/some_files.py --no-cache

cargo clippy --workspace --all-targets --all-features -- -D warnings
RUFF_UPDATE_SCHEMA=1 cargo test
pre-commit run --all-files --show-diff-on-failure
```

**Note:** In case no files are detected, either correctly formatted,
changed, or unchanged, it does not display a message. Wouldn't it be
better to show some message in this case?
2023-12-18 00:07:21 -05:00
Charlie Marsh
2643f74a5d Iterate over lambdas in deferred type annotations (#9175)
Closes https://github.com/astral-sh/ruff/issues/9159.
2023-12-18 04:51:23 +00:00
Charlie Marsh
c944d23053 Avoid nested quotations in auto-quoting fix (#9168)
## Summary

Given `Callable[[Callable[_P, _R]], Callable[_P, _R]]` from the
originating issue, when quoting `Callable`, we quoted the inner
`[Callable[_P, _R]]`, and then created a separate edit for the outer
`Callable`. Since there's an extra level of nesting in the subscript,
the edit for `[Callable[_P, _R]]` correctly did _not_ expand to the
entire expression. However, in this case, we should discard the inner
edit, since the expression is getting quoted by the outer edit anyway.

Closes https://github.com/astral-sh/ruff/issues/9162.
2023-12-17 12:53:58 +00:00
Steve C
93d8c56d41 Fix typo in SemanticModel.parent_expression docstring (#9167)
Self-explanatory and self-contained! :)
2023-12-16 21:12:50 -05:00
Charlie Marsh
a336c1bc95 Add a rule to detect string members in runtime-evaluated unions (#9143)
## Summary

A common mistake is to add quotes around one member in an `X | Y`-style
type union, as in:

```python
contract_versions_list: list[ContractVersion] | 'QuerySet[ContractVersion]' | None = None
```

However, doing so will lead to a runtime error if the annotation is
runtime-evaluated. This PR lints against such patterns.

Closes https://github.com/astral-sh/ruff/issues/9139.
2023-12-16 21:22:06 +00:00
Steve C
85b27a994f Fix dropped union expressions for piped non-types in PYI055 autofix (#9161)
## Summary

Fix dropped union expressions for piped non-types in `PYI055` autofix

If you had `type[int] | type[str] | str`, it would have dropped the
`str`, which breaks the type!

Closes #9156 

## Test Plan

`cargo test`
2023-12-16 15:58:28 -05:00
Zanie Blue
0029b4fd07 Fix ecosystem format line changed counts (#9158)
We were erroneously including patch headers so for each _file_ changed
we could include an extra added and removed line e.g. we counted the
diff in the following as +4 -4 instead of +3 -3.

```diff
diff --git a/tests/test_param_include_in_schema.py b/tests/test_param_include_in_schema.py
index 26201e9..f461947 100644
--- a/tests/test_param_include_in_schema.py
+++ b/tests/test_param_include_in_schema.py
@@ -9,14 +9,14 @@ app = FastAPI()
 
 @app.get("/hidden_cookie")
 async def hidden_cookie(
-    hidden_cookie: Optional[str] = Cookie(default=None, include_in_schema=False)
+    hidden_cookie: Optional[str] = Cookie(default=None, include_in_schema=False),
 ):
     return {"hidden_cookie": hidden_cookie}
 
 
 @app.get("/hidden_header")
 async def hidden_header(
-    hidden_header: Optional[str] = Header(default=None, include_in_schema=False)
+    hidden_header: Optional[str] = Header(default=None, include_in_schema=False),
 ):
     return {"hidden_header": hidden_header}
 
@@ -28,7 +28,7 @@ async def hidden_path(hidden_path: str = Path(include_in_schema=False)):
 
 @app.get("/hidden_query")
 async def hidden_query(
-    hidden_query: Optional[str] = Query(default=None, include_in_schema=False)
+    hidden_query: Optional[str] = Query(default=None, include_in_schema=False),
 ):
     return {"hidden_query": hidden_query}
 
```

Tested with a single project locally e.g.

> ℹ️ ecosystem check **detected format changes**. (+65 -65 lines in 39
files in 1 projects)
> 
> <details><summary><a
href="https://github.com/tiangolo/fastapi">tiangolo/fastapi</a> (+65 -65
lines across 39 files)

instead of

> ℹ️ ecosystem check **detected format changes**. (+104 -104 lines in 39
files in 1 projects)
>
> <details><summary><a
href="https://github.com/tiangolo/fastapi">tiangolo/fastapi</a> (+104
-104 lines across 39 files)
2023-12-16 00:14:17 -06:00
Zanie Blue
2c6b534e1f Update ecosystem check headers to show unchanged project count (#9157)
Instead of displaying the total completed project count in the "changed"
section of a header, we now separately calculated the changed and
unchanged count to make the header message nice and clear.

e.g.

> ℹ️ ecosystem check **detected format changes**. (+1772 -1859 lines in
239 files in 26 projects; 6 project errors; 9 projects unchanged)

and

> ℹ️ ecosystem check **detected linter changes**. (+4598 -5023
violations, +0 -40 fixes in 13 projects; 4 project errors; 24 projects
unchanged)


Previously, it would have included the unchanged count in the first
project count.
2023-12-16 00:05:38 -06:00
Charlie Marsh
6ecf844214 Add base-class inheritance detection to flake8-django rules (#9151)
## Summary

As elsewhere, this only applies to classes defined within the same file.

Closes https://github.com/astral-sh/ruff/issues/9150.
2023-12-15 18:01:32 +00:00
konsti
82731b8194 Fix panic in D208 with multibyte indent (#9147)
Fix #9080

Example, where `[]` is a 2 byte non-breaking space:
```
def f():
    """ Docstring header
^^^^ Real indentation is 4 chars
      docstring body, over-indented
^^^^^^ Over-indentation is 6 - 4 = 2 chars due to this line
   [] []  docstring body 2, further indented
^^^^^ We take these 4 chars/5 bytes to match the docstring ...
     ^^^ ... and these 2 chars/3 bytes to remove the `over_indented_size` ...
        ^^ ... but preserve this real indent
```
2023-12-15 12:02:15 -05:00
konsti
cd3c2f773f Prevent invalid utf8 indexing in cell magic detection (#9146)
The example below used to panic because we tried to split at 2 bytes in
the 4-bytes character `转`.
```python
def sample_func(xx):
    """
    转置 (transpose)
    """
    return xx.T
```

Fixes #9145
Fixes https://github.com/astral-sh/ruff-vscode/issues/362

The second commit is a small test refactoring.
2023-12-15 08:15:46 -06:00
Andrew Gallant
3ce145c476 release: switch to Cargo's default (#9031)
This sets `lto = "thin"` instead of using "fat" LTO, and sets
`codegen-units = 16`. These are the defaults for Cargo's `release`
profile, and I think it may give us faster iteration times, especially
when benchmarking. The point of this PR is to see what kind of impact
this has on benchmarks. It is expected that benchmarks may regress to
some extent.

I did some quick ad hoc experiments to quantify this change in compile
times. Namely, I ran:

    cargo build --profile release -p ruff_cli

Then I ran

touch crates/ruff_python_formatter/src/expression/string/docstring.rs

(because that's where i've been working lately) and re-ran

    cargo build --profile release -p ruff_cli

This last command is what I timed, since it reflects how much time one
has to wait between making a change and getting a compiled artifact.

Here are my results:

* With status quo `release` profile, build takes 77s
* with `release` but `lto = "thin"`, build takes 41s
* with `release`, but `lto = false`, build takes 19s
* with `release`, but `lto = false` **and** `codegen-units = 16`, build
takes 7s
* with `release`, but `lto = "thin"` **and** `codegen-units = 16`, build
takes 16s (i believe this is the default `release` configuration)

This PR represents the last option. It's not the fastest to compile, but
it's nearly a whole minute faster! The idea is that with `codegen-units
= 16`, we still make use of parallelism, but keep _some_ level of LTO on
to try and re-gain what we lose by increasing the number of codegen
units.
2023-12-15 08:19:35 -05:00
Joffrey Bluthé
db38078ca3 Document link between import sorting and formatter (#9117) 2023-12-15 10:47:19 +00:00
Micha Reiser
c8d6958d15 Add new with and match sequence test cases (#9128)
## Summary

Add new test cases for `with_item` and `match` sequence that demonstrate how long headers break. 

Removes one use of `optional_parentheses` in a position where it is know that the parentheses always need to be added.

## Test Plan

cargo test
2023-12-15 11:45:13 +09:00
Micha Reiser
25b2361411 Extend can_omit_optional_parentheses documentation (#9127)
## Summary

Add some more documentation to `can_omit_optional_parentheses` because it is realy hard to understand.
Restrict the `Attribute` and `None` `OperatorPrecedence` branches to ensure they only get applyied to the intended nodes.

## Test Plan

Ecosystem check reports no differences. The compatibility index remains unchanged.
2023-12-15 11:18:40 +09:00
Charlie Marsh
d1a7bc38ff Enable annotation quoting for multi-line expressions (#9142)
Given:

```python
x: DataFrame[
    int
] = 1
```

We currently wrap the annotation in single quotes, which leads to a
syntax error:

```python
x: "DataFrame[
    int
]" = 1
```

There are a few options for what to suggest for users here... Use triple
quotes:

```python
x: """DataFrame[
    int
]""" = 1
```

Or, use an implicit string concatenation (which may require
parentheses):

```python
x: ("DataFrame["
    "int"
"]") = 1
```

The solution I settled on here is to use the `Generator`, which
effectively means we write it out on a single line, like:

```python
x: "DataFrame[int]" = 1
```

It's kind of the "least opinionated" solution, but it does mean we'll
expand to a very long line in some cases.

Closes https://github.com/astral-sh/ruff/issues/9136.
2023-12-15 01:03:09 +00:00
Charlie Marsh
6c224cec52 Deduplicate edits when quoting annotations (#9140)
If you have multiple sub-expressions that need to be quoted, we'll
generate the same edit twice.

Closes https://github.com/astral-sh/ruff/issues/9135.
2023-12-14 19:46:35 +00:00
Dhruv Manilawala
189e947808 Split string formatting to individual nodes (#9058)
This PR splits the string formatting code in the formatter to be handled
by the respective nodes.

Previously, the string formatting was done through a single
`FormatString` interface. Now, the nodes themselves are responsible for
formatting.

The following changes were made:
1. Remove `StringLayout::ImplicitStringConcatenationInBinaryLike` and
inline the call to `FormatStringContinuation`. After the refactor, the
binary like formatting would delegate to `FormatString` which would then
delegate to `FormatStringContinuation`. This removes the intermediary
steps.
2. Add formatter implementation for `FStringPart` which delegates it to
the respective string literal or f-string node.
3. Add `ExprStringLiteralKind` which is either `String` or `Docstring`.
If it's a docstring variant, then the string expression would not be
implicitly concatenated. This is guaranteed by the
`DocstringStmt::try_from_expression` constructor.
4. Add `StringLiteralKind` which is either a `String`, `Docstring` or
`InImplicitlyConcatenatedFString`. The last variant is for when the
string literal is implicitly concatenated with an f-string (`"foo" f"bar
{x}"`).
5. Remove `FormatString`.
6. Extract the f-string quote detection as a standalone function which
is public to the crate. This is used to detect the quote to be used for
an f-string at the expression level (`ExprFString` or
`FormatStringContinuation`).


### Formatter ecosystem result

**This PR**

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 214 |
| poetry | 0.99905 | 321 | 15 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99958 | 1459 | 36 |

**main**

| project | similarity index | total files | changed files |

|----------------|------------------:|------------------:|------------------:|
| cpython | 0.75804 | 1799 | 1648 |
| django | 0.99984 | 2772 | 34 |
| home-assistant | 0.99955 | 10596 | 214 |
| poetry | 0.99905 | 321 | 15 |
| transformers | 0.99967 | 2657 | 324 |
| twine | 1.00000 | 33 | 0 |
| typeshed | 0.99980 | 3669 | 18 |
| warehouse | 0.99976 | 654 | 14 |
| zulip | 0.99958 | 1459 | 36 |
2023-12-14 12:55:10 -06:00
Andrew Gallant
28b1aa201b ruff_python_formatter: fix 'dynamic' mode with doctests (#9129)
This fixes a bug where the current indent level was not calculated
correctly for doctests. Namely, it didn't account for the extra indent
level (in terms of ASCII spaces) used by by the PS1 (`>>> `) and PS2
(`... `) prompts. As a result, lines could extend up to 4 spaces beyond
the configured line length limit.

We fix that by passing the `CodeExampleKind` to the `format` routine
instead of just the code itself. In this way, `format` can query whether
there will be any extra indent added _after_ formatting the code and
take that into account for its line length setting.

We add a few regression tests, taken directly from @stinodego's
examples.

Fixes #9126
2023-12-14 09:53:43 -05:00
Micha Reiser
c99eae2c08 can_omit_optional_parentheses: Exit early for unparenthesized expressions (#9125) 2023-12-14 06:02:53 +00:00
Micha Reiser
7256b882b9 Fix can_omit_optional_parentheses for expressions with a right most fstring (#9124) 2023-12-14 04:58:17 +00:00
Charlie Marsh
b8fc006e52 Fix blog post URL in changelog (#9119)
Closes https://github.com/astral-sh/ruff/issues/9118.
2023-12-13 19:39:57 +00:00
Charlie Marsh
c014622003 Bump version to v0.1.8 (#9116) 2023-12-13 13:19:51 -05:00
Andrew Gallant
b6fb972e6f config: add new docstring-code-format knob (#8854)
This PR does the plumbing to make a new formatting option,
`docstring-code-format`, available in the configuration for end users.
It is disabled by default (opt-in). It is opt-in at least initially to
reflect a conservative posture. The intent is to make it opt-out at some
point in the future.

This was split out from #8811 in order to make #8811 easier to merge.
Namely, once this is merged, docstring code snippet formatting will
become available to end users. (See comments below for how we arrived at
the name.)

Closes #7146

## Test Plan

Other than the standard test suite, I ran the formatter over the CPython
and polars projects to ensure both that the result looked sensible and
that tests still passed. At time of writing, one issue that currently
appears is that reformatting code snippets trips the long line lint:
https://github.com/BurntSushi/polars/actions/runs/7006619426/job/19058868021
2023-12-13 11:02:11 -05:00
287 changed files with 10397 additions and 3467 deletions

View File

@@ -5,6 +5,10 @@ updates:
schedule:
interval: "weekly"
labels: ["internal"]
groups:
actions:
patterns:
- "*"
- package-ecosystem: "cargo"
directory: "/"

View File

@@ -95,9 +95,9 @@ jobs:
rustup target add wasm32-unknown-unknown
- uses: Swatinem/rust-cache@v2
- name: "Clippy"
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
- name: "Clippy (wasm)"
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features -- -D warnings
run: cargo clippy -p ruff_wasm --target wasm32-unknown-unknown --all-features --locked -- -D warnings
cargo-test-linux:
runs-on: ubuntu-latest
@@ -226,7 +226,7 @@ jobs:
name: ruff
path: target/debug
- uses: dawidd6/action-download-artifact@v2
- uses: dawidd6/action-download-artifact@v3
name: Download baseline Ruff binary
with:
name: ruff

View File

@@ -17,7 +17,7 @@ jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: dawidd6/action-download-artifact@v2
- uses: dawidd6/action-download-artifact@v3
name: Download pull request number
with:
name: pr-number
@@ -32,7 +32,7 @@ jobs:
echo "pr-number=$(<pr-number)" >> $GITHUB_OUTPUT
fi
- uses: dawidd6/action-download-artifact@v2
- uses: dawidd6/action-download-artifact@v3
name: "Download ecosystem results"
id: download-ecosystem-result
if: steps.pr-number.outputs.pr-number

View File

@@ -73,7 +73,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: x86_64
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel - x86_64"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*.whl --force-reinstall
@@ -112,7 +112,7 @@ jobs:
- name: "Build wheels - universal2"
uses: PyO3/maturin-action@v1
with:
args: --release --target universal2-apple-darwin --out dist
args: --release --locked --target universal2-apple-darwin --out dist
- name: "Test wheel - universal2"
run: |
pip install dist/${{ env.PACKAGE_NAME }}-*universal2.whl --force-reinstall
@@ -161,7 +161,7 @@ jobs:
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.platform.target }}
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel"
if: ${{ !startsWith(matrix.platform.target, 'aarch64') }}
shell: bash
@@ -210,7 +210,7 @@ jobs:
with:
target: ${{ matrix.target }}
manylinux: auto
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel"
if: ${{ startsWith(matrix.target, 'x86_64') }}
run: |
@@ -269,7 +269,7 @@ jobs:
target: ${{ matrix.platform.target }}
manylinux: auto
docker-options: ${{ matrix.platform.maturin_docker_options }}
args: --release --out dist
args: --release --locked --out dist
- uses: uraimo/run-on-arch-action@v2
if: matrix.platform.arch != 'ppc64'
name: Test wheel
@@ -324,7 +324,7 @@ jobs:
with:
target: ${{ matrix.target }}
manylinux: musllinux_1_2
args: --release --out dist
args: --release --locked --out dist
- name: "Test wheel"
if: matrix.target == 'x86_64-unknown-linux-musl'
uses: addnab/docker-run-action@v3
@@ -379,7 +379,7 @@ jobs:
with:
target: ${{ matrix.platform.target }}
manylinux: musllinux_1_2
args: --release --out dist
args: --release --locked --out dist
docker-options: ${{ matrix.platform.maturin_docker_options }}
- uses: uraimo/run-on-arch-action@v2
name: Test wheel

View File

@@ -1,5 +1,42 @@
# Breaking Changes
## 0.1.9
### `site-packages` is now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))
Ruff maintains a list of default exclusions, which now consists of the following patterns:
- `.bzr`
- `.direnv`
- `.eggs`
- `.git-rewrite`
- `.git`
- `.hg`
- `.ipynb_checkpoints`
- `.mypy_cache`
- `.nox`
- `.pants.d`
- `.pyenv`
- `.pytest_cache`
- `.pytype`
- `.ruff_cache`
- `.svn`
- `.tox`
- `.venv`
- `.vscode`
- `__pypackages__`
- `_build`
- `buck-out`
- `build`
- `dist`
- `node_modules`
- `site-packages`
- `venv`
Previously, the `site-packages` directory was not excluded by default. While `site-packages` tends
to be excluded anyway by virtue of the `.venv` exclusion, this may not be the case when using Ruff
from VS Code outside a virtual environment.
## 0.1.0
### The deprecated `format` setting has been removed

View File

@@ -1,5 +1,118 @@
# Changelog
## 0.1.9
### Breaking changes
- Add site-packages to default exclusions ([#9188](https://github.com/astral-sh/ruff/pull/9188))
### Preview features
- Fix: Avoid parenthesizing subscript targets and values ([#9209](https://github.com/astral-sh/ruff/pull/9209))
- \[`pylint`\] Implement `too-many-locals` (`PLR0914`) ([#9163](https://github.com/astral-sh/ruff/pull/9163))
- Implement `reimplemented_operator` (FURB118) ([#9171](https://github.com/astral-sh/ruff/pull/9171))
- Add a rule to detect string members in runtime-evaluated unions ([#9143](https://github.com/astral-sh/ruff/pull/9143))
- Implement `no_blank_line_before_class_docstring` preview style ([#9154](https://github.com/astral-sh/ruff/pull/9154))
### Rule changes
- `CONSTANT_CASE` variables are improperly flagged for yoda violation (`SIM300`) ([#9164](https://github.com/astral-sh/ruff/pull/9164))
- \[`flake8-pyi`\] Cover ParamSpecs and TypeVarTuples (`PYI018`) ([#9198](https://github.com/astral-sh/ruff/pull/9198))
- \[`flake8-bugbear`\] Add fix for `zip-without-explicit-strict` (`B905`) ([#9176](https://github.com/astral-sh/ruff/pull/9176))
- Add fix to automatically remove `print` and `pprint` statements (`T201`, `T203`) ([#9208](https://github.com/astral-sh/ruff/pull/9208))
- Prefer `Never` to `NoReturn` in auto-typing in Python >= 3.11 (`ANN201`) ([#9213](https://github.com/astral-sh/ruff/pull/9213))
### Formatter
- `can_omit_optional_parentheses`: Exit early for unparenthesized expressions ([#9125](https://github.com/astral-sh/ruff/pull/9125))
- Fix `dynamic` mode with doctests so that it doesn't exceed configured line width ([#9129](https://github.com/astral-sh/ruff/pull/9129))
- Fix `can_omit_optional_parentheses` for expressions with a right most fstring ([#9124](https://github.com/astral-sh/ruff/pull/9124))
- Add `target_version` to formatter options ([#9220](https://github.com/astral-sh/ruff/pull/9220))
### CLI
- Update `ruff format --check` to display message for already formatted files ([#9153](https://github.com/astral-sh/ruff/pull/9153))
### Bug fixes
- Reverse order of arguments for `operator.contains` ([#9192](https://github.com/astral-sh/ruff/pull/9192))
- Iterate over lambdas in deferred type annotations ([#9175](https://github.com/astral-sh/ruff/pull/9175))
- Fix panic in `D208` with multibyte indent ([#9147](https://github.com/astral-sh/ruff/pull/9147))
- Add support for `NoReturn` in auto-return-typing ([#9206](https://github.com/astral-sh/ruff/pull/9206))
- Allow removal of `typing` from `exempt-modules` ([#9214](https://github.com/astral-sh/ruff/pull/9214))
- Avoid `mutable-class-default` violations for Pydantic subclasses ([#9187](https://github.com/astral-sh/ruff/pull/9187))
- Fix dropped union expressions for piped non-types in `PYI055` autofix ([#9161](https://github.com/astral-sh/ruff/pull/9161))
- Enable annotation quoting for multi-line expressions ([#9142](https://github.com/astral-sh/ruff/pull/9142))
- Deduplicate edits when quoting annotations ([#9140](https://github.com/astral-sh/ruff/pull/9140))
- Prevent invalid utf8 indexing in cell magic detection ([#9146](https://github.com/astral-sh/ruff/pull/9146))
- Avoid nested quotations in auto-quoting fix ([#9168](https://github.com/astral-sh/ruff/pull/9168))
- Add base-class inheritance detection to flake8-django rules ([#9151](https://github.com/astral-sh/ruff/pull/9151))
- Avoid `asyncio-dangling-task` violations on shadowed bindings ([#9215](https://github.com/astral-sh/ruff/pull/9215))
### Documentation
- Fix blog post URL in changelog ([#9119](https://github.com/astral-sh/ruff/pull/9119))
- Add error suppression hint for multi-line strings ([#9205](https://github.com/astral-sh/ruff/pull/9205))
- Fix typo in SemanticModel.parent_expression docstring ([#9167](https://github.com/astral-sh/ruff/pull/9167))
- Document link between import sorting and formatter ([#9117](https://github.com/astral-sh/ruff/pull/9117))
## 0.1.8
This release includes opt-in support for formatting Python snippets within
docstrings via the `docstring-code-format` setting.
[Check out the blog post](https://astral.sh/blog/ruff-v0.1.8) for more details!
### Preview features
- Add `"preserve"` quote-style to mimic Black's skip-string-normalization ([#8822](https://github.com/astral-sh/ruff/pull/8822))
- Implement `prefer_splitting_right_hand_side_of_assignments` preview style ([#8943](https://github.com/astral-sh/ruff/pull/8943))
- \[`pycodestyle`\] Add fix for `unexpected-spaces-around-keyword-parameter-equals` ([#9072](https://github.com/astral-sh/ruff/pull/9072))
- \[`pycodestyle`\] Add fix for comment-related whitespace rules ([#9075](https://github.com/astral-sh/ruff/pull/9075))
- \[`pycodestyle`\] Allow `sys.path` modifications between imports ([#9047](https://github.com/astral-sh/ruff/pull/9047))
- \[`refurb`\] Implement `hashlib-digest-hex` (`FURB181`) ([#9077](https://github.com/astral-sh/ruff/pull/9077))
### Rule changes
- Allow `flake8-type-checking` rules to automatically quote runtime-evaluated references ([#6001](https://github.com/astral-sh/ruff/pull/6001))
- Allow transparent cell magics in Jupyter Notebooks ([#8911](https://github.com/astral-sh/ruff/pull/8911))
- \[`flake8-annotations`\] Avoid `ANN2xx` fixes for abstract methods with empty bodies ([#9034](https://github.com/astral-sh/ruff/pull/9034))
- \[`flake8-self`\] Ignore underscore references in type annotations ([#9036](https://github.com/astral-sh/ruff/pull/9036))
- \[`pep8-naming`\] Allow class names when `apps.get_model` is a non-string ([#9065](https://github.com/astral-sh/ruff/pull/9065))
- \[`pycodestyle`\] Allow `matplotlib.use` calls to intersperse imports ([#9094](https://github.com/astral-sh/ruff/pull/9094))
- \[`pyflakes`\] Support fixing unused assignments in tuples by renaming variables (`F841`) ([#9107](https://github.com/astral-sh/ruff/pull/9107))
- \[`pylint`\] Add fix for `subprocess-run-without-check` (`PLW1510`) ([#6708](https://github.com/astral-sh/ruff/pull/6708))
### Formatter
- Add `docstring-code-format` knob to enable docstring snippet formatting ([#8854](https://github.com/astral-sh/ruff/pull/8854))
- Use double quotes for all docstrings, including single-quoted docstrings ([#9020](https://github.com/astral-sh/ruff/pull/9020))
- Implement "dynamic" line width mode for docstring code formatting ([#9098](https://github.com/astral-sh/ruff/pull/9098))
- Support reformatting Markdown code blocks ([#9030](https://github.com/astral-sh/ruff/pull/9030))
- add support for formatting reStructuredText code snippets ([#9003](https://github.com/astral-sh/ruff/pull/9003))
- Avoid trailing comma for single-argument with positional separator ([#9076](https://github.com/astral-sh/ruff/pull/9076))
- Fix handling of trailing target comment ([#9051](https://github.com/astral-sh/ruff/pull/9051))
### CLI
- Hide unsafe fix suggestions when explicitly disabled ([#9095](https://github.com/astral-sh/ruff/pull/9095))
- Add SARIF support to `--output-format` ([#9078](https://github.com/astral-sh/ruff/pull/9078))
### Bug fixes
- Apply unnecessary index rule prior to enumerate rewrite ([#9012](https://github.com/astral-sh/ruff/pull/9012))
- \[`flake8-err-msg`\] Allow `EM` fixes even if `msg` variable is defined ([#9059](https://github.com/astral-sh/ruff/pull/9059))
- \[`flake8-pie`\] Prevent keyword arguments duplication ([#8450](https://github.com/astral-sh/ruff/pull/8450))
- \[`flake8-pie`\] Respect trailing comma in `unnecessary-dict-kwargs` (`PIE804`) ([#9015](https://github.com/astral-sh/ruff/pull/9015))
- \[`flake8-raise`\] Avoid removing parentheses on ctypes.WinError ([#9027](https://github.com/astral-sh/ruff/pull/9027))
- \[`isort`\] Avoid invalid combination of `force-sort-within-types` and `lines-between-types` ([#9041](https://github.com/astral-sh/ruff/pull/9041))
- \[`isort`\] Ensure that from-style imports are always ordered first in `__future__` ([#9039](https://github.com/astral-sh/ruff/pull/9039))
- \[`pycodestyle`\] Allow tab indentation before keyword ([#9099](https://github.com/astral-sh/ruff/pull/9099))
- \[`pylint`\] Ignore `@overrides` and `@overloads` for `too-many-positional` ([#9000](https://github.com/astral-sh/ruff/pull/9000))
- \[`pyupgrade`\] Enable `printf-string-formatting` fix with comments on right-hand side ([#9037](https://github.com/astral-sh/ruff/pull/9037))
- \[`refurb`\] Make `math-constant` (`FURB152`) rule more targeted ([#9054](https://github.com/astral-sh/ruff/pull/9054))
- \[`refurb`\] Support floating-point base in `redundant-log-base` (`FURB163`) ([#9100](https://github.com/astral-sh/ruff/pull/9100))
- \[`ruff`\] Detect `unused-asyncio-dangling-task` (`RUF006`) on unused assignments ([#9060](https://github.com/astral-sh/ruff/pull/9060))
## 0.1.7
### Preview features

View File

@@ -326,16 +326,18 @@ We use an experimental in-house tool for managing releases.
- Often labels will be missing from pull requests they will need to be manually organized into the proper section
- Changes should be edited to be user-facing descriptions, avoiding internal details
1. Highlight any breaking changes in `BREAKING_CHANGES.md`
1. Run `cargo check`. This should update the lock file with new versions.
1. Create a pull request with the changelog and version updates
1. Merge the PR
1. Run the release workflow with the version number (without starting `v`) as input. Make sure
main has your merged PR as last commit
1. Run the [release workflow](https://github.com/astral-sh/ruff/actions/workflows/release.yaml) with:
- The new version number (without starting `v`)
- The commit hash of the merged release pull request on `main`
1. The release workflow will do the following:
1. Build all the assets. If this fails (even though we tested in step 4), we haven't tagged or
uploaded anything, you can restart after pushing a fix.
1. Upload to PyPI.
1. Create and push the Git tag (as extracted from `pyproject.toml`). We create the Git tag only
after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/charliermarsh/ruff/issues/4468)).
after building the wheels and uploading to PyPI, since we can't delete or modify the tag ([#4468](https://github.com/astral-sh/ruff/issues/4468)).
1. Attach artifacts to draft GitHub release
1. Trigger downstream repositories. This can fail non-catastrophically, as we can run any
downstream jobs manually if needed.
@@ -344,7 +346,10 @@ We use an experimental in-house tool for managing releases.
1. Copy the changelog for the release into the GitHub release
- See previous releases for formatting of section headers
1. Generate the contributor list with `rooster contributors` and add to the release notes
1. If needed, [update the schemastore](https://github.com/charliermarsh/ruff/blob/main/scripts/update_schemastore.py)
1. If needed, [update the schemastore](https://github.com/astral-sh/ruff/blob/main/scripts/update_schemastore.py).
1. One can determine if an update is needed when
`git diff old-version-tag new-version-tag -- ruff.schema.json` returns a non-empty diff.
1. Once run successfully, you should follow the link in the output to create a PR.
1. If needed, update the `ruff-lsp` and `ruff-vscode` repositories.
## Ecosystem CI
@@ -556,10 +561,10 @@ examples.
#### Linux
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
Install `perf` and build `ruff_benchmark` with the `profiling` profile and then run it with perf
```shell
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
cargo bench -p ruff_benchmark --no-run --profile=profiling && perf record --call-graph dwarf -F 9999 cargo bench -p ruff_benchmark --profile=profiling -- --profile-time=1
```
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to
@@ -567,8 +572,8 @@ gather enough samples for a good flamegraph (change the 999, the sample rate, an
of checks, to your liking)
```shell
cargo build --bin ruff_dev --profile=release-debug
perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
cargo build --bin ruff_dev --profile=profiling
perf record -g -F 999 target/profiling/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
```
Then convert the recorded profile
@@ -598,7 +603,7 @@ cargo install cargo-instruments
Then run the profiler with
```shell
cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
cargo instruments -t time --bench linter --profile profiling -p ruff_benchmark -- --profile-time=1
```
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`

153
Cargo.lock generated
View File

@@ -123,9 +123,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.75"
version = "1.0.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6"
checksum = "59d2a3357dde987206219e78ecfbbb6e8dad06cbb65292758d3270e6254f7355"
[[package]]
name = "argfile"
@@ -234,9 +234,9 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "cachedir"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e236bf5873ea57ec2877445297f4da008916bfae51567131acfc54a073d694f3"
checksum = "4703f3937077db8fa35bee3c8789343c1aec2585f0146f09d658d4ccc0e8d873"
dependencies = [
"tempfile",
]
@@ -434,11 +434,10 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "colored"
version = "2.0.4"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"is-terminal",
"lazy_static",
"windows-sys 0.48.0",
]
@@ -736,9 +735,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]]
name = "env_logger"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece"
dependencies = [
"humantime",
"is-terminal",
@@ -809,7 +808,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.1.7"
version = "0.1.9"
dependencies = [
"anyhow",
"clap",
@@ -818,7 +817,7 @@ dependencies = [
"itertools 0.11.0",
"log",
"once_cell",
"pep440_rs",
"pep440_rs 0.4.0",
"pretty_assertions",
"regex",
"ruff_linter",
@@ -828,7 +827,7 @@ dependencies = [
"serde_json",
"strum",
"strum_macros",
"toml 0.7.8",
"toml",
]
[[package]]
@@ -998,17 +997,16 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.20"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
checksum = "747ad1b4ae841a78e8aba0d63adbfbeaea26b517b63705d47856b73015d27060"
dependencies = [
"crossbeam-deque",
"globset",
"lazy_static",
"log",
"memchr",
"regex",
"regex-automata 0.4.3",
"same-file",
"thread_local",
"walkdir",
"winapi-util",
]
@@ -1482,9 +1480,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "once_cell"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "oorandom"
@@ -1605,6 +1603,18 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "pep440_rs"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c29f9c43de378b4e4e0cd7dbcce0e5cfb80443de8c05620368b2948bc936a1"
dependencies = [
"once_cell",
"regex",
"serde",
"unicode-width",
]
[[package]]
name = "pep508_rs"
version = "0.2.1"
@@ -1612,7 +1622,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0713d7bb861ca2b7d4c50a38e1f31a4b63a2e2df35ef1e5855cc29e108453e2"
dependencies = [
"once_cell",
"pep440_rs",
"pep440_rs 0.3.12",
"regex",
"serde",
"thiserror",
@@ -1794,9 +1804,9 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.70"
version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b"
checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
dependencies = [
"unicode-ident",
]
@@ -1808,10 +1818,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46d4a5e69187f23a29f8aa0ea57491d104ba541bc55f76552c2a74962aa20e04"
dependencies = [
"indexmap",
"pep440_rs",
"pep440_rs 0.3.12",
"pep508_rs",
"serde",
"toml 0.8.2",
"toml",
]
[[package]]
@@ -2063,7 +2073,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.1.7"
version = "0.1.9"
dependencies = [
"annotate-snippets 0.9.2",
"anyhow",
@@ -2155,7 +2165,7 @@ dependencies = [
"strum",
"strum_macros",
"tempfile",
"toml 0.7.8",
"toml",
"tracing",
"tracing-indicatif",
"tracing-subscriber",
@@ -2199,7 +2209,7 @@ dependencies = [
[[package]]
name = "ruff_linter"
version = "0.1.7"
version = "0.1.9"
dependencies = [
"aho-corasick",
"annotate-snippets 0.9.2",
@@ -2222,7 +2232,7 @@ dependencies = [
"once_cell",
"path-absolutize",
"pathdiff",
"pep440_rs",
"pep440_rs 0.4.0",
"pretty_assertions",
"pyproject-toml",
"quick-junit",
@@ -2255,7 +2265,7 @@ dependencies = [
"tempfile",
"test-case",
"thiserror",
"toml 0.7.8",
"toml",
"typed-arena",
"unicode-width",
"unicode_names2",
@@ -2452,7 +2462,7 @@ dependencies = [
[[package]]
name = "ruff_shrinking"
version = "0.1.7"
version = "0.1.9"
dependencies = [
"anyhow",
"clap",
@@ -2528,7 +2538,7 @@ dependencies = [
"log",
"once_cell",
"path-absolutize",
"pep440_rs",
"pep440_rs 0.4.0",
"regex",
"ruff_cache",
"ruff_formatter",
@@ -2543,7 +2553,7 @@ dependencies = [
"shellexpand",
"strum",
"tempfile",
"toml 0.7.8",
"toml",
]
[[package]]
@@ -2731,9 +2741,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
dependencies = [
"serde",
]
@@ -2958,9 +2968,9 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "test-case"
version = "3.2.1"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8f1e820b7f1d95a0cdbf97a5df9de10e1be731983ab943e56703ac1b8e9d425"
checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8"
dependencies = [
"test-case-macros",
]
@@ -2993,18 +3003,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.50"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.50"
version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df"
dependencies = [
"proc-macro2",
"quote",
@@ -3093,55 +3103,30 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "toml"
version = "0.7.8"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.19.15",
]
[[package]]
name = "toml"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.20.2",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.3"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
version = "0.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap",
"serde",
"serde_spanned",
"toml_datetime",
"winnow",
]
[[package]]
name = "toml_edit"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338"
checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
dependencies = [
"indexmap",
"serde",
@@ -3185,9 +3170,9 @@ dependencies = [
[[package]]
name = "tracing-indicatif"
version = "0.3.5"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e05fe4a1c906d94b275d8aeb8ff8b9deaca502aeb59ae8ab500a92b8032ac8"
checksum = "069580424efe11d97c3fef4197fa98c004fa26672cc71ad8770d224e23b1951d"
dependencies = [
"indicatif",
"tracing",
@@ -3307,9 +3292,9 @@ checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "unicode_names2"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d5506ae2c3c1ccbdf468e52fc5ef536c2ccd981f01273a4cb81aa61021f3a5f"
checksum = "ac64ef2f016dc69dfa8283394a70b057066eb054d5fcb6b9eb17bd2ec5097211"
dependencies = [
"phf",
"unicode_names2_generator",
@@ -3317,9 +3302,9 @@ dependencies = [
[[package]]
name = "unicode_names2_generator"
version = "1.2.0"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6dfc680313e95bc6637fa278cd7a22390c3c2cd7b8b2bd28755bc6c0fc811e7"
checksum = "013f6a731e80f3930de580e55ba41dfa846de4e0fdee4a701f97989cb1597d6a"
dependencies = [
"getopts",
"log",
@@ -3488,9 +3473,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.38"
version = "0.4.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02"
checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12"
dependencies = [
"cfg-if",
"js-sys",
@@ -3529,9 +3514,9 @@ checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f"
[[package]]
name = "wasm-bindgen-test"
version = "0.3.38"
version = "0.3.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6433b7c56db97397842c46b67e11873eda263170afeb3a2dc74a7cb370fee0d"
checksum = "2cf9242c0d27999b831eae4767b2a146feb0b27d332d553e605864acd2afd403"
dependencies = [
"console_error_panic_hook",
"js-sys",
@@ -3543,9 +3528,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.38"
version = "0.3.39"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735"
checksum = "794645f5408c9a039fd09f4d113cdfb2e7eba5ff1956b07bcf701cf4b394fe89"
dependencies = [
"proc-macro2",
"quote",

View File

@@ -12,24 +12,24 @@ authors = ["Charlie Marsh <charlie.r.marsh@gmail.com>"]
license = "MIT"
[workspace.dependencies]
anyhow = { version = "1.0.69" }
anyhow = { version = "1.0.76" }
bitflags = { version = "2.4.1" }
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
clap = { version = "4.4.7", features = ["derive"] }
colored = { version = "2.0.0" }
colored = { version = "2.1.0" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
ignore = { version = "0.4.20" }
ignore = { version = "0.4.21" }
insta = { version = "1.34.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.1" }
itertools = { version = "0.11.0" }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
memchr = { version = "2.6.4" }
once_cell = { version = "1.17.1" }
once_cell = { version = "1.19.0" }
path-absolutize = { version = "3.1.1" }
proc-macro2 = { version = "1.0.70" }
proc-macro2 = { version = "1.0.71" }
quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
@@ -43,14 +43,14 @@ static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.40" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.50" }
toml = { version = "0.7.8" }
test-case = { version = "3.3.1" }
thiserror = { version = "1.0.51" }
toml = { version = "0.8.8" }
tracing = { version = "0.1.40" }
tracing-indicatif = { version = "0.3.4" }
tracing-indicatif = { version = "0.3.6" }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
unicode-ident = { version = "1.0.12" }
unicode_names2 = { version = "1.2.0" }
unicode_names2 = { version = "1.2.1" }
unicode-width = { version = "0.1.11" }
uuid = { version = "1.6.1", features = ["v4", "fast-rng", "macro-diagnostics", "js"] }
wsl = { version = "0.1.0" }
@@ -88,7 +88,20 @@ rc_mutex = "warn"
rest_pat_in_fully_bound_structs = "warn"
[profile.release]
lto = "fat"
# Note that we set these explicitly, and these values
# were chosen based on a trade-off between compile times
# and runtime performance[1].
#
# [1]: https://github.com/astral-sh/ruff/pull/9031
lto = "thin"
codegen-units = 16
# Some crates don't change as much but benefit more from
# more expensive optimization passes, so we selectively
# decrease codegen-units in some cases.
[profile.release.package.ruff_python_parser]
codegen-units = 1
[profile.release.package.ruff_python_ast]
codegen-units = 1
[profile.dev.package.insta]
@@ -102,8 +115,8 @@ opt-level = 3
[profile.dev.package.ruff_python_parser]
opt-level = 1
# Use the `--profile release-debug` flag to show symbols in release mode.
# e.g. `cargo build --profile release-debug`
[profile.release-debug]
# Use the `--profile profiling` flag to show symbols in release mode.
# e.g. `cargo build --profile profiling`
[profile.profiling]
inherits = "release"
debug = 1

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.7
rev: v0.1.9
hooks:
# Run the linter.
- id: ruff
@@ -194,20 +194,25 @@ exclude = [
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
]

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.1.7"
version = "0.1.9"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""
@@ -23,7 +23,7 @@ configparser = { version = "3.0.3" }
itertools = { workspace = true }
log = { workspace = true }
once_cell = { workspace = true }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.4.0", features = ["serde"] }
regex = { workspace = true }
rustc-hash = { workspace = true }
serde = { workspace = true }

View File

@@ -2,7 +2,7 @@ use ruff_benchmark::criterion::{
criterion_group, criterion_main, BenchmarkGroup, BenchmarkId, Criterion, Throughput,
};
use ruff_benchmark::{TestCase, TestFile, TestFileDownloadError};
use ruff_linter::linter::lint_only;
use ruff_linter::linter::{lint_only, ParseSource};
use ruff_linter::rule_selector::PreviewOptions;
use ruff_linter::settings::rule_table::RuleTable;
use ruff_linter::settings::types::PreviewMode;
@@ -10,6 +10,7 @@ use ruff_linter::settings::{flags, LinterSettings};
use ruff_linter::source_kind::SourceKind;
use ruff_linter::{registry::Rule, RuleSelector};
use ruff_python_ast::PySourceType;
use ruff_python_parser::{lexer, parse_program_tokens, Mode};
#[cfg(target_os = "windows")]
#[global_allocator]
@@ -53,7 +54,13 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
BenchmarkId::from_parameter(case.name()),
&case,
|b, case| {
let kind = SourceKind::Python(case.code().to_string());
// Tokenize the source.
let tokens = lexer::lex(case.code(), Mode::Module).collect::<Vec<_>>();
// Parse the source.
let ast =
parse_program_tokens(tokens.clone(), case.code(), case.name(), false).unwrap();
b.iter(|| {
let path = case.path();
let result = lint_only(
@@ -61,8 +68,12 @@ fn benchmark_linter(mut group: BenchmarkGroup, settings: &LinterSettings) {
None,
settings,
flags::Noqa::Enabled,
&kind,
&SourceKind::Python(case.code().to_string()),
PySourceType::from(path.as_path()),
ParseSource::Precomputed {
tokens: &tokens,
ast: &ast,
},
);
// Assert that file contains no parse errors

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_cli"
version = "0.1.7"
version = "0.1.9"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -33,7 +33,7 @@ anyhow = { workspace = true }
argfile = { version = "0.1.6" }
bincode = { version = "1.3.3" }
bitflags = { workspace = true }
cachedir = { version = "0.3.0" }
cachedir = { version = "0.3.1" }
chrono = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
clap_complete_command = { version = "0.5.1" }

View File

@@ -515,7 +515,7 @@ impl<'a> FormatResults<'a> {
if changed > 0 && unchanged > 0 {
writeln!(
f,
"{} file{} {}, {} file{} left unchanged",
"{} file{} {}, {} file{} {}",
changed,
if changed == 1 { "" } else { "s" },
match self.mode {
@@ -524,6 +524,10 @@ impl<'a> FormatResults<'a> {
},
unchanged,
if unchanged == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "left unchanged",
FormatMode::Check | FormatMode::Diff => "already formatted",
},
)
} else if changed > 0 {
writeln!(
@@ -539,9 +543,13 @@ impl<'a> FormatResults<'a> {
} else if unchanged > 0 {
writeln!(
f,
"{} file{} left unchanged",
"{} file{} {}",
unchanged,
if unchanged == 1 { "" } else { "s" },
match self.mode {
FormatMode::Write => "left unchanged",
FormatMode::Check | FormatMode::Diff => "already formatted",
},
)
} else {
Ok(())

View File

@@ -12,7 +12,7 @@ use rustc_hash::FxHashMap;
use crate::cache::{Cache, FileCacheKey, LintCacheData};
use ruff_diagnostics::Diagnostic;
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult};
use ruff_linter::linter::{lint_fix, lint_only, FixTable, FixerResult, LinterResult, ParseSource};
use ruff_linter::logging::DisplayParseError;
use ruff_linter::message::Message;
use ruff_linter::pyproject_toml::lint_pyproject_toml;
@@ -303,12 +303,28 @@ pub(crate) fn lint_path(
(result, fixed)
} else {
// If we fail to fix, lint the original source code.
let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
let result = lint_only(
path,
package,
settings,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
(result, fixed)
}
} else {
let result = lint_only(path, package, settings, noqa, &source_kind, source_type);
let result = lint_only(
path,
package,
settings,
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
(result, fixed)
};
@@ -444,6 +460,7 @@ pub(crate) fn lint_stdin(
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
@@ -462,6 +479,7 @@ pub(crate) fn lint_stdin(
noqa,
&source_kind,
source_type,
ParseSource::None,
);
let fixed = FxHashMap::default();
(result, fixed)

View File

@@ -139,6 +139,99 @@ if condition:
Ok(())
}
#[test]
fn docstring_options() -> Result<()> {
let tempdir = TempDir::new()?;
let ruff_toml = tempdir.path().join("ruff.toml");
fs::write(
&ruff_toml,
r#"
[format]
docstring-code-format = true
docstring-code-line-length = 20
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.args(["format", "--config"])
.arg(&ruff_toml)
.arg("-")
.pass_stdin(r#"
def f(x):
'''
Something about `f`. And an example:
.. code-block:: python
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
Another example:
```py
foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
```
And another:
>>> foo, bar, quux = this_is_a_long_line(lion, hippo, lemur, bear)
'''
pass
"#), @r###"
success: true
exit_code: 0
----- stdout -----
def f(x):
"""
Something about `f`. And an example:
.. code-block:: python
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
Another example:
```py
(
foo,
bar,
quux,
) = this_is_a_long_line(
lion,
hippo,
lemur,
bear,
)
```
And another:
>>> (
... foo,
... bar,
... quux,
... ) = this_is_a_long_line(
... lion,
... hippo,
... lemur,
... bear,
... )
"""
pass
----- stderr -----
"###);
Ok(())
}
#[test]
fn mixed_line_endings() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -162,7 +255,7 @@ fn mixed_line_endings() -> Result<()> {
----- stdout -----
----- stderr -----
2 files left unchanged
2 files already formatted
"###);
Ok(())
}
@@ -235,6 +328,60 @@ OTHER = "OTHER"
Ok(())
}
#[test]
fn messages() -> Result<()> {
let tempdir = TempDir::new()?;
fs::write(
tempdir.path().join("main.py"),
r#"
from test import say_hy
if __name__ == "__main__":
say_hy("dear Ruff contributor")
"#,
)?;
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--no-cache", "--isolated", "--check"])
.arg("main.py"), @r###"
success: false
exit_code: 1
----- stdout -----
Would reformat: main.py
1 file would be reformatted
----- stderr -----
"###);
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--no-cache", "--isolated"])
.arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
1 file reformatted
----- stderr -----
"###);
assert_cmd_snapshot!(Command::new(get_cargo_bin(BIN_NAME))
.current_dir(tempdir.path())
.args(["format", "--no-cache", "--isolated"])
.arg("main.py"), @r###"
success: true
exit_code: 0
----- stdout -----
1 file left unchanged
----- stderr -----
"###);
Ok(())
}
#[test]
fn force_exclude() -> Result<()> {
let tempdir = TempDir::new()?;
@@ -783,7 +930,7 @@ fn test_diff() {
----- stderr -----
2 files would be reformatted, 1 file left unchanged
2 files would be reformatted, 1 file already formatted
"###);
});
}

View File

@@ -7,7 +7,7 @@ use ruff_text_size::{Ranged, TextRange, TextSize};
/// A text edit to be applied to a source file. Inserts, deletes, or replaces
/// content at a given location.
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Edit {
/// The start location of the edit.

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff_linter"
version = "0.1.7"
version = "0.1.9"
publish = false
authors = { workspace = true }
edition = { workspace = true }
@@ -52,7 +52,7 @@ path-absolutize = { workspace = true, features = [
"use_unix_paths_on_wasm",
] }
pathdiff = { version = "0.2.1" }
pep440_rs = { version = "0.3.12", features = ["serde"] }
pep440_rs = { version = "0.4.0", features = ["serde"] }
pyproject-toml = { version = "0.8.1" }
quick-junit = { version = "0.3.5" }
regex = { workspace = true }

View File

@@ -182,3 +182,50 @@ class Foo(abc.ABC):
return 1
else:
return 1.5
def func(x: int):
try:
pass
except:
return 2
def func(x: int):
try:
pass
except:
return 2
else:
return 3
def func(x: int):
if not x:
raise ValueError
else:
raise TypeError
def func(x: int):
if not x:
raise ValueError
else:
return 1
from typing import overload
@overload
def overloaded(i: int) -> "int":
...
@overload
def overloaded(i: "str") -> "str":
...
def overloaded(i):
return i

View File

@@ -127,3 +127,21 @@ class MultipleConsecutiveFields(models.Model):
pass
middle_name = models.CharField(max_length=32)
class BaseModel(models.Model):
pass
class StrBeforeFieldInheritedModel(BaseModel):
"""Model with `__str__` before fields."""
class Meta:
verbose_name = "test"
verbose_name_plural = "tests"
def __str__(self):
return "foobar"
first_name = models.CharField(max_length=32)

View File

@@ -1,8 +1,13 @@
import typing
import typing_extensions
from typing import TypeVar
from typing_extensions import ParamSpec, TypeVarTuple
_T = typing.TypeVar("_T")
_P = TypeVar("_P")
_Ts = typing_extensions.TypeVarTuple("_Ts")
_P = ParamSpec("_P")
_P2 = typing.ParamSpec("_P2")
_Ts2 = TypeVarTuple("_Ts2")
# OK
_UsedTypeVar = TypeVar("_UsedTypeVar")

View File

@@ -1,8 +1,13 @@
import typing
import typing_extensions
from typing import TypeVar
from typing_extensions import ParamSpec, TypeVarTuple
_T = typing.TypeVar("_T")
_P = TypeVar("_P")
_Ts = typing_extensions.TypeVarTuple("_Ts")
_P = ParamSpec("_P")
_P2 = typing.ParamSpec("_P2")
_Ts2 = TypeVarTuple("_Ts2")
# OK
_UsedTypeVar = TypeVar("_UsedTypeVar")

View File

@@ -37,3 +37,28 @@ def func():
# PYI055
x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker
def convert_union(union: UnionType) -> _T | None:
converters: tuple[
type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
] = union.__args__
...
def convert_union(union: UnionType) -> _T | None:
converters: tuple[
Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
] = union.__args__
...
def convert_union(union: UnionType) -> _T | None:
converters: tuple[
Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
] = union.__args__
...
def convert_union(union: UnionType) -> _T | None:
converters: tuple[
Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
] = union.__args__
...

View File

@@ -1,6 +1,5 @@
# Errors
"yoda" == compare # SIM300
"yoda" == compare # SIM300
42 == age # SIM300
("a", "b") == compare # SIM300
"yoda" <= compare # SIM300
@@ -13,10 +12,17 @@ YODA > age # SIM300
YODA >= age # SIM300
JediOrder.YODA == age # SIM300
0 < (number - 100) # SIM300
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
B<A[0][0]or B
B or(B)<A[0][0]
# Errors in preview
['upper'] == UPPER_LIST
{} == DummyHandler.CONFIG
# Errors in stable
UPPER_LIST == ['upper']
DummyHandler.CONFIG == {}
# OK
compare == "yoda"
age == 42
@@ -31,3 +37,6 @@ age <= YODA
YODA == YODA
age == JediOrder.YODA
(number - 100) > 0
SECONDS_IN_DAY == 60 * 60 * 24 # Error in 0.1.8
SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # Error in 0.1.8
{"non-empty-dict": "is-ok"} == DummyHandler.CONFIG

View File

@@ -0,0 +1,18 @@
from __future__ import annotations
from typing import TypeVar
x: "int" | str # TCH006
x: ("int" | str) | "bool" # TCH006
def func():
x: "int" | str # OK
z: list[str, str | "int"] = [] # TCH006
type A = Value["int" | str] # OK
OldS = TypeVar('OldS', int | 'str', str) # TCH006

View File

@@ -0,0 +1,16 @@
from typing import TypeVar
x: "int" | str # TCH006
x: ("int" | str) | "bool" # TCH006
def func():
x: "int" | str # OK
z: list[str, str | "int"] = [] # TCH006
type A = Value["int" | str] # OK
OldS = TypeVar('OldS', int | 'str', str) # TCH006

View File

@@ -0,0 +1,7 @@
"""Add `TYPE_CHECKING` to an existing `typing` import. Another member is moved."""
from __future__ import annotations
from typing import Final
Const: Final[dict] = {}

View File

@@ -0,0 +1,7 @@
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
from __future__ import annotations
from typing import Final, TYPE_CHECKING
Const: Final[dict] = {}

View File

@@ -0,0 +1,7 @@
"""Using `TYPE_CHECKING` from an existing `typing` import. Another member is moved."""
from __future__ import annotations
from typing import Final, Mapping
Const: Final[dict] = {}

View File

@@ -65,3 +65,28 @@ def f():
def func(value: DataFrame):
...
def f():
from pandas import DataFrame, Series
def baz() -> DataFrame | Series:
...
def f():
from pandas import DataFrame, Series
def baz() -> (
DataFrame |
Series
):
...
class C:
x: DataFrame[
int
] = 1
def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
...

View File

@@ -0,0 +1,9 @@
import a
"""Some other docstring."""
import b
"""Some other docstring."""
import c

View File

@@ -713,5 +713,12 @@ def retain_extra_whitespace_not_overindented():
This is not overindented
This is overindented, but since one line is not overindented this should not raise
And so is this, but it we should preserve the extra space on this line relative
And so is this, but it we should preserve the extra space on this line relative
"""
def inconsistent_indent_byte_size():
"""There's a non-breaking space (2-bytes) after 3 spaces (https://github.com/astral-sh/ruff/issues/9080).
    Returns:
"""

View File

@@ -0,0 +1,4 @@
import re
from typing import Annotated
type X = Annotated[int, lambda: re.compile("x")]

View File

@@ -0,0 +1,5 @@
"""Docstring"""
"""Non-docstring"""
from __future__ import absolute_import

View File

@@ -0,0 +1,8 @@
"""Test for accessing class members within a generator."""
class Class:
items = []
if len(replacements := {item[1] for item in items}) > 1:
pass

View File

@@ -0,0 +1,36 @@
def func() -> None: # OK
# 15 is max default
first = 1
second = 2
third = 3
fourth = 4
fifth = 5
sixth = 6
seventh = 7
eighth = 8
ninth = 9
tenth = 10
eleventh = 11
twelveth = 12
thirteenth = 13
fourteenth = 14
fifteenth = 15
def func() -> None: # PLR0914
first = 1
second = 2
third = 3
fourth = 4
fifth = 5
sixth = 6
seventh = 7
eighth = 8
ninth = 9
tenth = 10
eleventh = 11
twelfth = 12
thirteenth = 13
fourteenth = 14
fifteenth = 15
sixteenth = 16

View File

@@ -1,30 +1,28 @@
# These should change
x = u"Hello"
u"Hello"
u'world'
x = u"Hello" # UP025
print(u"Hello")
u'world' # UP025
print(u'world')
print(u"Hello") # UP025
print(u'world') # UP025
import foo
foo(u"Hello", U"world", a=u"Hello", b=u"world")
foo(u"Hello", U"world", a=u"Hello", b=u"world") # UP025
# These should stay quoted they way they are
# Retain quotes when fixing.
x = u'hello' # UP025
x = u"""hello""" # UP025
x = u'''hello''' # UP025
x = u'Hello "World"' # UP025
x = u'hello'
x = u"""hello"""
x = u'''hello'''
x = u'Hello "World"'
# These should not change
u = "Hello"
u = u
u = "Hello" # OK
u = u # OK
def hello():
return"Hello"
return"Hello" # OK
f"foo"u"bar"
f"foo" u"bar"
f"foo"u"bar" # OK
f"foo" u"bar" # OK

View File

@@ -243,3 +243,12 @@ raise ValueError(
).format(a, b)
("{}" "{{{}}}").format(a, b)
# The dictionary should be parenthesized.
"{}".format({0: 1}[0])
# The dictionary should be parenthesized.
"{}".format({0: 1}.bar)
# The dictionary should be parenthesized.
"{}".format({0: 1}())

View File

@@ -0,0 +1,61 @@
# Errors.
op_bitnot = lambda x: ~x
op_not = lambda x: not x
op_pos = lambda x: +x
op_neg = lambda x: -x
op_add = lambda x, y: x + y
op_sub = lambda x, y: x - y
op_mult = lambda x, y: x * y
op_matmutl = lambda x, y: x @ y
op_truediv = lambda x, y: x / y
op_mod = lambda x, y: x % y
op_pow = lambda x, y: x ** y
op_lshift = lambda x, y: x << y
op_rshift = lambda x, y: x >> y
op_bitor = lambda x, y: x | y
op_xor = lambda x, y: x ^ y
op_bitand = lambda x, y: x & y
op_floordiv = lambda x, y: x // y
op_eq = lambda x, y: x == y
op_ne = lambda x, y: x != y
op_lt = lambda x, y: x < y
op_lte = lambda x, y: x <= y
op_gt = lambda x, y: x > y
op_gte = lambda x, y: x >= y
op_is = lambda x, y: x is y
op_isnot = lambda x, y: x is not y
op_in = lambda x, y: y in x
def op_not2(x):
return not x
def op_add2(x, y):
return x + y
class Adder:
def add(x, y):
return x + y
# OK.
op_add3 = lambda x, y = 1: x + y
op_neg2 = lambda x, y: y - x
op_notin = lambda x, y: y not in x
op_and = lambda x, y: y and x
op_or = lambda x, y: y or x
op_in = lambda x, y: x in y
def op_neg3(x, y):
return y - x
def op_add4(x, y = 1):
return x + y
def op_add5(x, y):
print("op_add5")
return x + y

View File

@@ -122,3 +122,43 @@ async def f():
# OK
async def f():
task[i] = asyncio.create_task(coordinator.ws_connect())
# OK
async def f(x: int):
if x > 0:
task = asyncio.create_task(make_request())
else:
task = asyncio.create_task(make_request())
await task
# OK
async def f(x: bool):
if x:
t = asyncio.create_task(asyncio.sleep(1))
else:
t = None
try:
await asyncio.sleep(1)
finally:
if t:
await t
# Error
async def f(x: bool):
if x:
t = asyncio.create_task(asyncio.sleep(1))
else:
t = None
# OK
async def f(x: bool):
global T
if x:
T = asyncio.create_task(asyncio.sleep(1))
else:
T = None

View File

@@ -59,3 +59,11 @@ class F(BaseSettings):
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
class G(F):
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []

View File

@@ -0,0 +1,8 @@
from typing import Never, NoReturn, Union
Union[Never, int]
Union[NoReturn, int]
Never | int
NoReturn | int
Union[Union[Never, int], Union[NoReturn, int]]
Union[NoReturn, int, float]

View File

@@ -3,12 +3,11 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff};
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint};
/// Run lint rules over the [`Binding`]s.
pub(crate) fn bindings(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::AsyncioDanglingTask,
Rule::InvalidAllFormat,
Rule::InvalidAllObject,
Rule::NonAsciiName,
@@ -72,12 +71,5 @@ pub(crate) fn bindings(checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::AsyncioDanglingTask) {
if let Some(diagnostic) =
ruff::rules::asyncio_dangling_binding(binding, &checker.semantic)
{
checker.diagnostics.push(diagnostic);
}
}
}
}

View File

@@ -2,7 +2,7 @@ use ruff_python_ast::Expr;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_pie, pylint};
use crate::rules::{flake8_pie, pylint, refurb};
/// Run lint rules over all deferred lambdas in the [`SemanticModel`].
pub(crate) fn deferred_lambdas(checker: &mut Checker) {
@@ -21,6 +21,9 @@ pub(crate) fn deferred_lambdas(checker: &mut Checker) {
if checker.enabled(Rule::ReimplementedContainerBuiltin) {
flake8_pie::rules::reimplemented_container_builtin(checker, lambda);
}
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &lambda.into());
}
}
}
}

View File

@@ -5,16 +5,21 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint};
use crate::rules::{
flake8_pyi, flake8_type_checking, flake8_unused_arguments, pyflakes, pylint, ruff,
};
/// Run lint rules over all deferred scopes in the [`SemanticModel`].
pub(crate) fn deferred_scopes(checker: &mut Checker) {
if !checker.any_enabled(&[
Rule::AsyncioDanglingTask,
Rule::GlobalVariableNotAssigned,
Rule::ImportShadowedByLoopVar,
Rule::NoSelfUse,
Rule::RedefinedArgumentFromLocal,
Rule::RedefinedWhileUnused,
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TooManyLocals,
Rule::TypingOnlyFirstPartyImport,
Rule::TypingOnlyStandardLibraryImport,
Rule::TypingOnlyThirdPartyImport,
@@ -31,7 +36,6 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -269,6 +273,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::AsyncioDanglingTask) {
ruff::rules::asyncio_dangling_binding(scope, &checker.semantic, &mut diagnostics);
}
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
@@ -336,6 +344,10 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope_id, scope, &mut diagnostics);
}
if checker.enabled(Rule::TooManyLocals) {
pylint::rules::too_many_locals(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);

View File

@@ -15,8 +15,9 @@ use crate::rules::{
flake8_comprehensions, flake8_datetimez, flake8_debugger, flake8_django,
flake8_future_annotations, flake8_gettext, flake8_implicit_str_concat, flake8_logging,
flake8_logging_format, flake8_pie, flake8_print, flake8_pyi, flake8_pytest_style, flake8_self,
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_use_pathlib, flynt, numpy,
pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade, refurb, ruff,
flake8_simplify, flake8_tidy_imports, flake8_trio, flake8_type_checking, flake8_use_pathlib,
flynt, numpy, pandas_vet, pep8_naming, pycodestyle, pyflakes, pygrep_hooks, pylint, pyupgrade,
refurb, ruff,
};
use crate::settings::types::PythonVersion;
@@ -80,6 +81,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::DuplicateUnionMember,
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
Rule::NeverUnion,
]) {
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
@@ -99,6 +101,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NeverUnion) {
ruff::rules::never_union(checker, expr);
}
if checker.any_enabled(&[
Rule::SysVersionSlice3,
Rule::SysVersion2,
@@ -1153,6 +1159,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NeverUnion) {
ruff::rules::never_union(checker, expr);
}
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
if !checker.semantic.in_nested_union() {
@@ -1170,6 +1180,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryTypeUnion) {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
if checker.enabled(Rule::RuntimeStringUnion) {
flake8_type_checking::rules::runtime_string_union(checker, expr);
}
}
}
Expr::UnaryOp(

View File

@@ -368,6 +368,9 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
.diagnostics
.extend(ruff::rules::unreachable::in_function(name, body));
}
if checker.enabled(Rule::ReimplementedOperator) {
refurb::rules::reimplemented_operator(checker, &function_def.into());
}
}
Stmt::Return(_) => {
if checker.enabled(Rule::ReturnOutsideFunction) {
@@ -397,27 +400,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_django::rules::nullable_model_string_field(checker, body);
}
if checker.enabled(Rule::DjangoExcludeWithModelForm) {
if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(
checker,
arguments.as_deref(),
body,
) {
checker.diagnostics.push(diagnostic);
}
flake8_django::rules::exclude_with_model_form(checker, class_def);
}
if checker.enabled(Rule::DjangoAllWithModelForm) {
if let Some(diagnostic) =
flake8_django::rules::all_with_model_form(checker, arguments.as_deref(), body)
{
checker.diagnostics.push(diagnostic);
}
flake8_django::rules::all_with_model_form(checker, class_def);
}
if checker.enabled(Rule::DjangoUnorderedBodyContentInModel) {
flake8_django::rules::unordered_body_content_in_model(
checker,
arguments.as_deref(),
body,
);
flake8_django::rules::unordered_body_content_in_model(checker, class_def);
}
if !checker.source_type.is_stub() {
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {

View File

@@ -287,7 +287,18 @@ where
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
Stmt::Expr(ast::StmtExpr { value, .. })
if !self
.semantic
.flags
.intersects(SemanticModelFlags::MODULE_DOCSTRING)
&& value.is_string_literal_expr() =>
{
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
}
Stmt::ImportFrom(ast::StmtImportFrom { module, names, .. }) => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
// Allow __future__ imports until we see a non-__future__ import.
if let Some("__future__") = module.as_deref() {
if names
@@ -301,9 +312,11 @@ where
}
}
Stmt::Import(_) => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
}
_ => {
self.semantic.flags |= SemanticModelFlags::MODULE_DOCSTRING;
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !(self.semantic.seen_import_boundary()
|| helpers::is_assignment_to_a_dunder(stmt)
@@ -1435,11 +1448,8 @@ where
impl<'a> Checker<'a> {
/// Visit a [`Module`]. Returns `true` if the module contains a module-level docstring.
fn visit_module(&mut self, python_ast: &'a Suite) -> bool {
fn visit_module(&mut self, python_ast: &'a Suite) {
analyze::module(python_ast, self);
let docstring = docstrings::extraction::docstring_from(python_ast);
docstring.is_some()
}
/// Visit a list of [`Comprehension`] nodes, assumed to be the comprehensions that compose a
@@ -1745,10 +1755,13 @@ impl<'a> Checker<'a> {
return;
}
// If the expression is the left-hand side of a walrus operator, then it's a named
// expression assignment.
if self
.semantic
.current_expressions()
.any(Expr::is_named_expr_expr)
.filter_map(Expr::as_named_expr_expr)
.any(|parent| parent.target.as_ref() == expr)
{
self.add_binding(id, expr.range(), BindingKind::NamedExprAssignment, flags);
return;
@@ -2003,23 +2016,19 @@ pub(crate) fn check_ast(
);
checker.bind_builtins();
// Check for module docstring.
let python_ast = if checker.visit_module(python_ast) {
&python_ast[1..]
} else {
python_ast
};
// Iterate over the AST.
checker.visit_module(python_ast);
checker.visit_body(python_ast);
// Visit any deferred syntax nodes.
// Visit any deferred syntax nodes. Take care to visit in order, such that we avoid adding
// new deferred nodes after visiting nodes of that kind. For example, visiting a deferred
// function can add a deferred lambda, but the opposite is not true.
checker.visit_deferred_functions();
checker.visit_deferred_lambdas();
checker.visit_deferred_future_type_definitions();
checker.visit_deferred_type_param_definitions();
checker.visit_deferred_future_type_definitions();
let allocator = typed_arena::Arena::new();
checker.visit_deferred_string_type_definitions(&allocator);
checker.visit_deferred_lambdas();
checker.visit_exports();
// Check docstrings, bindings, and unresolved references.

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_diagnostics::Diagnostic;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::TokenKind;
@@ -97,7 +97,7 @@ pub(crate) fn check_logical_lines(
indent_size,
) {
if settings.rules.enabled(kind.rule()) {
context.push(kind, range);
context.push_diagnostic(Diagnostic::new(kind, range));
}
}
@@ -123,18 +123,6 @@ impl<'a> LogicalLinesContext<'a> {
}
}
pub(crate) fn push<K: Into<DiagnosticKind>>(&mut self, kind: K, range: TextRange) {
let kind = kind.into();
if self.settings.rules.enabled(kind.rule()) {
self.diagnostics.push(Diagnostic {
kind,
range,
fix: None,
parent: None,
});
}
}
pub(crate) fn push_diagnostic(&mut self, diagnostic: Diagnostic) {
if self.settings.rules.enabled(diagnostic.kind.rule()) {
self.diagnostics.push(diagnostic);

View File

@@ -252,6 +252,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R0911") => (RuleGroup::Stable, rules::pylint::rules::TooManyReturnStatements),
(Pylint, "R0912") => (RuleGroup::Stable, rules::pylint::rules::TooManyBranches),
(Pylint, "R0913") => (RuleGroup::Stable, rules::pylint::rules::TooManyArguments),
(Pylint, "R0914") => (RuleGroup::Preview, rules::pylint::rules::TooManyLocals),
(Pylint, "R0915") => (RuleGroup::Stable, rules::pylint::rules::TooManyStatements),
(Pylint, "R0916") => (RuleGroup::Preview, rules::pylint::rules::TooManyBooleanExpressions),
(Pylint, "R0917") => (RuleGroup::Preview, rules::pylint::rules::TooManyPositional),
@@ -807,6 +808,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8TypeChecking, "003") => (RuleGroup::Stable, rules::flake8_type_checking::rules::TypingOnlyStandardLibraryImport),
(Flake8TypeChecking, "004") => (RuleGroup::Stable, rules::flake8_type_checking::rules::RuntimeImportInTypeCheckingBlock),
(Flake8TypeChecking, "005") => (RuleGroup::Stable, rules::flake8_type_checking::rules::EmptyTypeCheckingBlock),
(Flake8TypeChecking, "006") => (RuleGroup::Preview, rules::flake8_type_checking::rules::RuntimeStringUnion),
// tryceratops
(Tryceratops, "002") => (RuleGroup::Stable, rules::tryceratops::rules::RaiseVanillaClass),
@@ -899,6 +901,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "018") => (RuleGroup::Preview, rules::ruff::rules::AssignmentInAssert),
(Ruff, "019") => (RuleGroup::Preview, rules::ruff::rules::UnnecessaryKeyCheck),
(Ruff, "020") => (RuleGroup::Preview, rules::ruff::rules::NeverUnion),
(Ruff, "100") => (RuleGroup::Stable, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Stable, rules::ruff::rules::InvalidPyprojectToml),
@@ -951,6 +954,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "105") => (RuleGroup::Preview, rules::refurb::rules::PrintEmptyString),
#[allow(deprecated)]
(Refurb, "113") => (RuleGroup::Nursery, rules::refurb::rules::RepeatedAppend),
(Refurb, "118") => (RuleGroup::Preview, rules::refurb::rules::ReimplementedOperator),
#[allow(deprecated)]
(Refurb, "131") => (RuleGroup::Nursery, rules::refurb::rules::DeleteFullSlice),
#[allow(deprecated)]

View File

@@ -13,7 +13,7 @@ use ruff_text_size::{Ranged, TextSize};
use ruff_diagnostics::Edit;
use ruff_python_ast::imports::{AnyImport, Import, ImportFrom};
use ruff_python_codegen::Stylist;
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{ImportedName, SemanticModel};
use ruff_python_trivia::textwrap::indent;
use ruff_source_file::Locator;
@@ -132,7 +132,48 @@ impl<'a> Importer<'a> {
)?;
// Import the `TYPE_CHECKING` symbol from the typing module.
let (type_checking_edit, type_checking) = self.get_or_import_type_checking(at, semantic)?;
let (type_checking_edit, type_checking) =
if let Some(type_checking) = Self::find_type_checking(at, semantic)? {
// Special-case: if the `TYPE_CHECKING` symbol is imported as part of the same
// statement that we're modifying, avoid adding a no-op edit. For example, here,
// the `TYPE_CHECKING` no-op edit would overlap with the edit to remove `Final`
// from the import:
// ```python
// from __future__ import annotations
//
// from typing import Final, TYPE_CHECKING
//
// Const: Final[dict] = {}
// ```
let edit = if type_checking.statement(semantic) == import.statement {
None
} else {
Some(Edit::range_replacement(
self.locator.slice(type_checking.range()).to_string(),
type_checking.range(),
))
};
(edit, type_checking.into_name())
} else {
// Special-case: if the `TYPE_CHECKING` symbol would be added to the same import
// we're modifying, import it as a separate import statement. For example, here,
// we're concurrently removing `Final` and adding `TYPE_CHECKING`, so it's easier to
// use a separate import statement:
// ```python
// from __future__ import annotations
//
// from typing import Final
//
// Const: Final[dict] = {}
// ```
let (edit, name) = self.import_symbol(
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
at,
Some(import.statement),
semantic,
)?;
(Some(edit), name)
};
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
@@ -157,28 +198,21 @@ impl<'a> Importer<'a> {
})
}
/// Generate an [`Edit`] to reference `typing.TYPE_CHECKING`. Returns the [`Edit`] necessary to
/// make the symbol available in the current scope along with the bound name of the symbol.
fn get_or_import_type_checking(
&self,
/// Find a reference to `typing.TYPE_CHECKING`.
fn find_type_checking(
at: TextSize,
semantic: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
) -> Result<Option<ImportedName>, ResolutionError> {
for module in semantic.typing_modules() {
if let Some((edit, name)) = self.get_symbol(
if let Some(imported_name) = Self::find_symbol(
&ImportRequest::import_from(module, "TYPE_CHECKING"),
at,
semantic,
)? {
return Ok((edit, name));
return Ok(Some(imported_name));
}
}
self.import_symbol(
&ImportRequest::import_from("typing", "TYPE_CHECKING"),
at,
semantic,
)
Ok(None)
}
/// Generate an [`Edit`] to reference the given symbol. Returns the [`Edit`] necessary to make
@@ -192,16 +226,15 @@ impl<'a> Importer<'a> {
semantic: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
self.get_symbol(symbol, at, semantic)?
.map_or_else(|| self.import_symbol(symbol, at, semantic), Ok)
.map_or_else(|| self.import_symbol(symbol, at, None, semantic), Ok)
}
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
fn get_symbol(
&self,
/// Return the [`ImportedName`] to for existing symbol, if it's present in the given [`SemanticModel`].
fn find_symbol(
symbol: &ImportRequest,
at: TextSize,
semantic: &SemanticModel,
) -> Result<Option<(Edit, String)>, ResolutionError> {
) -> Result<Option<ImportedName>, ResolutionError> {
// If the symbol is already available in the current scope, use it.
let Some(imported_name) =
semantic.resolve_qualified_import_name(symbol.module, symbol.member)
@@ -226,6 +259,21 @@ impl<'a> Importer<'a> {
return Err(ResolutionError::IncompatibleContext);
}
Ok(Some(imported_name))
}
/// Return an [`Edit`] to reference an existing symbol, if it's present in the given [`SemanticModel`].
fn get_symbol(
&self,
symbol: &ImportRequest,
at: TextSize,
semantic: &SemanticModel,
) -> Result<Option<(Edit, String)>, ResolutionError> {
// Find the symbol in the current scope.
let Some(imported_name) = Self::find_symbol(symbol, at, semantic)? else {
return Ok(None);
};
// We also add a no-op edit to force conflicts with any other fixes that might try to
// remove the import. Consider:
//
@@ -259,9 +307,13 @@ impl<'a> Importer<'a> {
&self,
symbol: &ImportRequest,
at: TextSize,
except: Option<&Stmt>,
semantic: &SemanticModel,
) -> Result<(Edit, String), ResolutionError> {
if let Some(stmt) = self.find_import_from(symbol.module, at) {
if let Some(stmt) = self
.find_import_from(symbol.module, at)
.filter(|stmt| except != Some(stmt))
{
// Case 1: `from functools import lru_cache` is in scope, and we're trying to reference
// `functools.cache`; thus, we add `cache` to the import, and return `"cache"` as the
// bound name.
@@ -423,14 +475,18 @@ impl RuntimeImportEdit {
#[derive(Debug)]
pub(crate) struct TypingImportEdit {
/// The edit to add the `TYPE_CHECKING` symbol to the module.
type_checking_edit: Edit,
type_checking_edit: Option<Edit>,
/// The edit to add the import to a `TYPE_CHECKING` block.
add_import_edit: Edit,
}
impl TypingImportEdit {
pub(crate) fn into_edits(self) -> Vec<Edit> {
vec![self.type_checking_edit, self.add_import_edit]
pub(crate) fn into_edits(self) -> (Edit, Option<Edit>) {
if let Some(type_checking_edit) = self.type_checking_edit {
(type_checking_edit, Some(self.add_import_edit))
} else {
(self.add_import_edit, None)
}
}
}

View File

@@ -11,7 +11,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_notebook::Notebook;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_ast::{PySourceType, Suite};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::lexer::LexResult;
@@ -73,7 +73,6 @@ pub struct FixerResult<'a> {
pub fn check_path(
path: &Path,
package: Option<&Path>,
tokens: Vec<LexResult>,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
@@ -82,6 +81,7 @@ pub fn check_path(
noqa: flags::Noqa,
source_kind: &SourceKind,
source_type: PySourceType,
tokens: TokenSource,
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
@@ -144,12 +144,8 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
match ruff_python_parser::parse_program_tokens(
tokens,
source_kind.source_code(),
&path.to_string_lossy(),
source_type.is_ipynb(),
) {
// Parse, if the AST wasn't pre-provided provided.
match tokens.into_ast_source(source_kind, source_type, path) {
Ok(python_ast) => {
let cell_offsets = source_kind.as_ipy_notebook().map(Notebook::cell_offsets);
if use_ast {
@@ -325,7 +321,6 @@ pub fn add_noqa_to_path(
} = check_path(
path,
package,
tokens,
&locator,
&stylist,
&indexer,
@@ -334,6 +329,7 @@ pub fn add_noqa_to_path(
flags::Noqa::Disabled,
source_kind,
source_type,
TokenSource::Tokens(tokens),
);
// Log any parse errors.
@@ -365,10 +361,10 @@ pub fn lint_only(
noqa: flags::Noqa,
source_kind: &SourceKind,
source_type: PySourceType,
data: ParseSource,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
let tokens = data.into_token_source(source_kind, source_type);
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(source_kind.source_code());
@@ -391,7 +387,6 @@ pub fn lint_only(
let result = check_path(
path,
package,
tokens,
&locator,
&stylist,
&indexer,
@@ -400,6 +395,7 @@ pub fn lint_only(
noqa,
source_kind,
source_type,
tokens,
);
result.map(|(diagnostics, imports)| {
@@ -487,7 +483,6 @@ pub fn lint_fix<'a>(
let result = check_path(
path,
package,
tokens,
&locator,
&stylist,
&indexer,
@@ -496,6 +491,7 @@ pub fn lint_fix<'a>(
noqa,
&transformed,
source_type,
TokenSource::Tokens(tokens),
);
if iterations == 0 {
@@ -632,6 +628,95 @@ This indicates a bug in Ruff. If you could open an issue at:
}
}
#[derive(Debug, Clone)]
pub enum ParseSource<'a> {
/// Extract the tokens and AST from the given source code.
None,
/// Use the precomputed tokens and AST.
Precomputed {
tokens: &'a [LexResult],
ast: &'a Suite,
},
}
impl<'a> ParseSource<'a> {
/// Convert to a [`TokenSource`], tokenizing if necessary.
fn into_token_source(
self,
source_kind: &SourceKind,
source_type: PySourceType,
) -> TokenSource<'a> {
match self {
Self::None => TokenSource::Tokens(ruff_python_parser::tokenize(
source_kind.source_code(),
source_type.as_mode(),
)),
Self::Precomputed { tokens, ast } => TokenSource::Precomputed { tokens, ast },
}
}
}
#[derive(Debug, Clone)]
pub enum TokenSource<'a> {
/// Use the precomputed tokens to generate the AST.
Tokens(Vec<LexResult>),
/// Use the precomputed tokens and AST.
Precomputed {
tokens: &'a [LexResult],
ast: &'a Suite,
},
}
impl Deref for TokenSource<'_> {
type Target = [LexResult];
fn deref(&self) -> &Self::Target {
match self {
Self::Tokens(tokens) => tokens,
Self::Precomputed { tokens, .. } => tokens,
}
}
}
impl<'a> TokenSource<'a> {
/// Convert to an [`AstSource`], parsing if necessary.
fn into_ast_source(
self,
source_kind: &SourceKind,
source_type: PySourceType,
path: &Path,
) -> Result<AstSource<'a>, ParseError> {
match self {
Self::Tokens(tokens) => Ok(AstSource::Ast(ruff_python_parser::parse_program_tokens(
tokens,
source_kind.source_code(),
&path.to_string_lossy(),
source_type.is_ipynb(),
)?)),
Self::Precomputed { ast, .. } => Ok(AstSource::Precomputed(ast)),
}
}
}
#[derive(Debug, Clone)]
pub enum AstSource<'a> {
/// Extract the AST from the given source code.
Ast(Suite),
/// Use the precomputed AST.
Precomputed(&'a Suite),
}
impl Deref for AstSource<'_> {
type Target = Suite;
fn deref(&self) -> &Self::Target {
match self {
Self::Ast(ast) => ast,
Self::Precomputed(ast) => ast,
}
}
}
#[cfg(test)]
mod tests {
use std::path::Path;

View File

@@ -243,6 +243,17 @@ pub struct PreviewOptions {
pub require_explicit: bool,
}
impl PreviewOptions {
/// Return a copy with the same preview mode setting but require explicit disabled.
#[must_use]
pub fn without_require_explicit(&self) -> Self {
Self {
mode: self.mode,
require_explicit: false,
}
}
}
#[cfg(feature = "schemars")]
mod schema {
use itertools::Itertools;

View File

@@ -3,7 +3,7 @@ use rustc_hash::FxHashSet;
use ruff_diagnostics::Edit;
use ruff_python_ast::helpers::{
implicit_return, pep_604_union, typing_optional, typing_union, ReturnStatementVisitor,
pep_604_union, typing_optional, typing_union, ReturnStatementVisitor, Terminal,
};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, ExprContext};
@@ -57,6 +57,14 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
visitor.returns
};
// Determine the terminal behavior (i.e., implicit return, no return, etc.).
let terminal = Terminal::from_function(function);
// If every control flow path raises an exception, return `NoReturn`.
if terminal == Some(Terminal::Raise) {
return Some(AutoPythonType::Never);
}
// Determine the return type of the first `return` statement.
let Some((return_statement, returns)) = returns.split_first() else {
return Some(AutoPythonType::Atom(PythonType::None));
@@ -80,7 +88,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
// if x > 0:
// return 1
// ```
if implicit_return(function) {
if terminal.is_none() {
return_type = return_type.union(ResolvedPythonType::Atom(PythonType::None));
}
@@ -94,6 +102,7 @@ pub(crate) fn auto_return_type(function: &ast::StmtFunctionDef) -> Option<AutoPy
#[derive(Debug)]
pub(crate) enum AutoPythonType {
Never,
Atom(PythonType),
Union(FxHashSet<PythonType>),
}
@@ -111,6 +120,28 @@ impl AutoPythonType {
target_version: PythonVersion,
) -> Option<(Expr, Vec<Edit>)> {
match self {
AutoPythonType::Never => {
let (no_return_edit, binding) = importer
.get_or_import_symbol(
&ImportRequest::import_from(
"typing",
if target_version >= PythonVersion::Py311 {
"Never"
} else {
"NoReturn"
},
),
at,
semantic,
)
.ok()?;
let expr = Expr::Name(ast::ExprName {
id: binding,
range: TextRange::default(),
ctx: ExprContext::Load,
});
Some((expr, vec![no_return_edit]))
}
AutoPythonType::Atom(python_type) => {
let expr = type_expr(python_type)?;
Some((expr, vec![]))

View File

@@ -482,7 +482,6 @@ impl Violation for AnyType {
format!("Dynamically typed expressions (typing.Any) are disallowed in `{name}`")
}
}
fn is_none_returning(body: &[Stmt]) -> bool {
let mut visitor = ReturnStatementVisitor::default();
visitor.visit_body(body);
@@ -537,17 +536,41 @@ fn check_dynamically_typed<F>(
}
}
fn is_empty_body(body: &[Stmt]) -> bool {
body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
/// Return `true` if a function appears to be a stub.
fn is_stub_function(function_def: &ast::StmtFunctionDef, checker: &Checker) -> bool {
/// Returns `true` if a function has an empty body.
fn is_empty_body(function_def: &ast::StmtFunctionDef) -> bool {
function_def.body.iter().all(|stmt| match stmt {
Stmt::Pass(_) => true,
Stmt::Expr(ast::StmtExpr { value, range: _ }) => {
matches!(
value.as_ref(),
Expr::StringLiteral(_) | Expr::EllipsisLiteral(_)
)
}
_ => false,
})
}
// Ignore functions with empty bodies in...
if is_empty_body(function_def) {
// Stub definitions (.pyi files)...
if checker.source_type.is_stub() {
return true;
}
_ => false,
})
// Abstract methods...
if visibility::is_abstract(&function_def.decorator_list, checker.semantic()) {
return true;
}
// Overload definitions...
if visibility::is_overload(&function_def.decorator_list, checker.semantic()) {
return true;
}
}
false
}
/// Generate flake8-annotation checks for a given `Definition`.
@@ -738,9 +761,7 @@ pub(crate) fn definition(
) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
@@ -771,9 +792,7 @@ pub(crate) fn definition(
}
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
let return_type = if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
@@ -843,25 +862,22 @@ pub(crate) fn definition(
match visibility {
visibility::Visibility::Public => {
if checker.enabled(Rule::MissingReturnTypeUndocumentedPublicFunction) {
let return_type =
if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
@@ -885,25 +901,22 @@ pub(crate) fn definition(
}
visibility::Visibility::Private => {
if checker.enabled(Rule::MissingReturnTypePrivateFunction) {
let return_type =
if visibility::is_abstract(decorator_list, checker.semantic())
&& is_empty_body(body)
{
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let return_type = if is_stub_function(function, checker) {
None
} else {
auto_return_type(function)
.and_then(|return_type| {
return_type.into_expression(
checker.importer(),
function.parameters.start(),
checker.semantic(),
checker.settings.target_version,
)
})
.map(|(return_type, edits)| {
(checker.generator().expr(&return_type), edits)
})
};
let mut diagnostic = Diagnostic::new(
MissingReturnTypePrivateFunction {
name: name.to_string(),

View File

@@ -495,4 +495,88 @@ auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public
182 182 | return 1
183 183 | else:
auto_return_type.py:187:5: ANN201 [*] Missing return type annotation for public function `func`
|
187 | def func(x: int):
| ^^^^ ANN201
188 | try:
189 | pass
|
= help: Add return type annotation: `int | None`
Unsafe fix
184 184 | return 1.5
185 185 |
186 186 |
187 |-def func(x: int):
187 |+def func(x: int) -> int | None:
188 188 | try:
189 189 | pass
190 190 | except:
auto_return_type.py:194:5: ANN201 [*] Missing return type annotation for public function `func`
|
194 | def func(x: int):
| ^^^^ ANN201
195 | try:
196 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
191 191 | return 2
192 192 |
193 193 |
194 |-def func(x: int):
194 |+def func(x: int) -> int:
195 195 | try:
196 196 | pass
197 197 | except:
auto_return_type.py:203:5: ANN201 [*] Missing return type annotation for public function `func`
|
203 | def func(x: int):
| ^^^^ ANN201
204 | if not x:
205 | raise ValueError
|
= help: Add return type annotation: `Never`
Unsafe fix
151 151 |
152 152 | import abc
153 153 | from abc import abstractmethod
154 |+from typing import Never
154 155 |
155 156 |
156 157 | class Foo(abc.ABC):
--------------------------------------------------------------------------------
200 201 | return 3
201 202 |
202 203 |
203 |-def func(x: int):
204 |+def func(x: int) -> Never:
204 205 | if not x:
205 206 | raise ValueError
206 207 | else:
auto_return_type.py:210:5: ANN201 [*] Missing return type annotation for public function `func`
|
210 | def func(x: int):
| ^^^^ ANN201
211 | if not x:
212 | raise ValueError
|
= help: Add return type annotation: `int`
Unsafe fix
207 207 | raise TypeError
208 208 |
209 209 |
210 |-def func(x: int):
210 |+def func(x: int) -> int:
211 211 | if not x:
212 212 | raise ValueError
213 213 | else:

View File

@@ -550,4 +550,96 @@ auto_return_type.py:180:9: ANN201 [*] Missing return type annotation for public
182 182 | return 1
183 183 | else:
auto_return_type.py:187:5: ANN201 [*] Missing return type annotation for public function `func`
|
187 | def func(x: int):
| ^^^^ ANN201
188 | try:
189 | pass
|
= help: Add return type annotation: `Optional[int]`
Unsafe fix
151 151 |
152 152 | import abc
153 153 | from abc import abstractmethod
154 |+from typing import Optional
154 155 |
155 156 |
156 157 | class Foo(abc.ABC):
--------------------------------------------------------------------------------
184 185 | return 1.5
185 186 |
186 187 |
187 |-def func(x: int):
188 |+def func(x: int) -> Optional[int]:
188 189 | try:
189 190 | pass
190 191 | except:
auto_return_type.py:194:5: ANN201 [*] Missing return type annotation for public function `func`
|
194 | def func(x: int):
| ^^^^ ANN201
195 | try:
196 | pass
|
= help: Add return type annotation: `int`
Unsafe fix
191 191 | return 2
192 192 |
193 193 |
194 |-def func(x: int):
194 |+def func(x: int) -> int:
195 195 | try:
196 196 | pass
197 197 | except:
auto_return_type.py:203:5: ANN201 [*] Missing return type annotation for public function `func`
|
203 | def func(x: int):
| ^^^^ ANN201
204 | if not x:
205 | raise ValueError
|
= help: Add return type annotation: `NoReturn`
Unsafe fix
151 151 |
152 152 | import abc
153 153 | from abc import abstractmethod
154 |+from typing import NoReturn
154 155 |
155 156 |
156 157 | class Foo(abc.ABC):
--------------------------------------------------------------------------------
200 201 | return 3
201 202 |
202 203 |
203 |-def func(x: int):
204 |+def func(x: int) -> NoReturn:
204 205 | if not x:
205 206 | raise ValueError
206 207 | else:
auto_return_type.py:210:5: ANN201 [*] Missing return type annotation for public function `func`
|
210 | def func(x: int):
| ^^^^ ANN201
211 | if not x:
212 | raise ValueError
|
= help: Add return type annotation: `int`
Unsafe fix
207 207 | raise TypeError
208 208 |
209 209 |
210 |-def func(x: int):
210 |+def func(x: int) -> int:
211 211 | if not x:
212 212 | raise ValueError
213 213 | else:

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Arguments, Expr};
@@ -6,6 +6,7 @@ use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
/// ## What it does
/// Checks for `zip` calls without an explicit `strict` parameter.
@@ -28,16 +29,25 @@ use crate::checkers::ast::Checker;
/// zip(a, b, strict=True)
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for `zip` calls that contain
/// `**kwargs`, as adding a `check` keyword argument to such a call may lead
/// to a duplicate keyword argument error.
///
/// ## References
/// - [Python documentation: `zip`](https://docs.python.org/3/library/functions.html#zip)
#[violation]
pub struct ZipWithoutExplicitStrict;
impl Violation for ZipWithoutExplicitStrict {
impl AlwaysFixableViolation for ZipWithoutExplicitStrict {
#[derive_message_formats]
fn message(&self) -> String {
format!("`zip()` without an explicit `strict=` parameter")
}
fn fix_title(&self) -> String {
"Add explicit `strict=False`".to_string()
}
}
/// B905
@@ -52,9 +62,27 @@ pub(crate) fn zip_without_explicit_strict(checker: &mut Checker, call: &ast::Exp
.iter()
.any(|arg| is_infinite_iterator(arg, checker.semantic()))
{
checker
.diagnostics
.push(Diagnostic::new(ZipWithoutExplicitStrict, call.range()));
let mut diagnostic = Diagnostic::new(ZipWithoutExplicitStrict, call.range());
diagnostic.set_fix(Fix::applicable_edit(
add_argument(
"strict=False",
&call.arguments,
checker.indexer().comment_ranges(),
checker.locator().contents(),
),
// If the function call contains `**kwargs`, mark the fix as unsafe.
if call
.arguments
.keywords
.iter()
.any(|keyword| keyword.arg.is_none())
{
Applicability::Unsafe
} else {
Applicability::Safe
},
));
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_bugbear/mod.rs
---
B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter
B905.py:4:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
3 | # Errors
4 | zip()
@@ -9,8 +9,19 @@ B905.py:4:1: B905 `zip()` without an explicit `strict=` parameter
5 | zip(range(3))
6 | zip("a", "b")
|
= help: Add explicit `strict=False`
B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
Safe fix
1 1 | from itertools import count, cycle, repeat
2 2 |
3 3 | # Errors
4 |-zip()
4 |+zip(strict=False)
5 5 | zip(range(3))
6 6 | zip("a", "b")
7 7 | zip("a", "b", *zip("c"))
B905.py:5:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
3 | # Errors
4 | zip()
@@ -19,8 +30,19 @@ B905.py:5:1: B905 `zip()` without an explicit `strict=` parameter
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
|
= help: Add explicit `strict=False`
B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
Safe fix
2 2 |
3 3 | # Errors
4 4 | zip()
5 |-zip(range(3))
5 |+zip(range(3), strict=False)
6 6 | zip("a", "b")
7 7 | zip("a", "b", *zip("c"))
8 8 | zip(zip("a"), strict=False)
B905.py:6:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
4 | zip()
5 | zip(range(3))
@@ -29,8 +51,19 @@ B905.py:6:1: B905 `zip()` without an explicit `strict=` parameter
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
|
= help: Add explicit `strict=False`
B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
Safe fix
3 3 | # Errors
4 4 | zip()
5 5 | zip(range(3))
6 |-zip("a", "b")
6 |+zip("a", "b", strict=False)
7 7 | zip("a", "b", *zip("c"))
8 8 | zip(zip("a"), strict=False)
9 9 | zip(zip("a", strict=True))
B905.py:7:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
5 | zip(range(3))
6 | zip("a", "b")
@@ -39,8 +72,19 @@ B905.py:7:1: B905 `zip()` without an explicit `strict=` parameter
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
|
= help: Add explicit `strict=False`
B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
Safe fix
4 4 | zip()
5 5 | zip(range(3))
6 6 | zip("a", "b")
7 |-zip("a", "b", *zip("c"))
7 |+zip("a", "b", *zip("c"), strict=False)
8 8 | zip(zip("a"), strict=False)
9 9 | zip(zip("a", strict=True))
10 10 |
B905.py:7:16: B905 [*] `zip()` without an explicit `strict=` parameter
|
5 | zip(range(3))
6 | zip("a", "b")
@@ -49,8 +93,19 @@ B905.py:7:16: B905 `zip()` without an explicit `strict=` parameter
8 | zip(zip("a"), strict=False)
9 | zip(zip("a", strict=True))
|
= help: Add explicit `strict=False`
B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
Safe fix
4 4 | zip()
5 5 | zip(range(3))
6 6 | zip("a", "b")
7 |-zip("a", "b", *zip("c"))
7 |+zip("a", "b", *zip("c", strict=False))
8 8 | zip(zip("a"), strict=False)
9 9 | zip(zip("a", strict=True))
10 10 |
B905.py:8:5: B905 [*] `zip()` without an explicit `strict=` parameter
|
6 | zip("a", "b")
7 | zip("a", "b", *zip("c"))
@@ -58,8 +113,19 @@ B905.py:8:5: B905 `zip()` without an explicit `strict=` parameter
| ^^^^^^^^ B905
9 | zip(zip("a", strict=True))
|
= help: Add explicit `strict=False`
B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
Safe fix
5 5 | zip(range(3))
6 6 | zip("a", "b")
7 7 | zip("a", "b", *zip("c"))
8 |-zip(zip("a"), strict=False)
8 |+zip(zip("a", strict=False), strict=False)
9 9 | zip(zip("a", strict=True))
10 10 |
11 11 | # OK
B905.py:9:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
7 | zip("a", "b", *zip("c"))
8 | zip(zip("a"), strict=False)
@@ -68,21 +134,49 @@ B905.py:9:1: B905 `zip()` without an explicit `strict=` parameter
10 |
11 | # OK
|
= help: Add explicit `strict=False`
B905.py:24:1: B905 `zip()` without an explicit `strict=` parameter
Safe fix
6 6 | zip("a", "b")
7 7 | zip("a", "b", *zip("c"))
8 8 | zip(zip("a"), strict=False)
9 |-zip(zip("a", strict=True))
9 |+zip(zip("a", strict=True), strict=False)
10 10 |
11 11 | # OK
12 12 | zip(range(3), strict=True)
B905.py:24:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
25 | zip([1, 2, 3], repeat(1, times=4))
|
= help: Add explicit `strict=False`
B905.py:25:1: B905 `zip()` without an explicit `strict=` parameter
Safe fix
21 21 | zip([1, 2, 3], repeat(1, times=None))
22 22 |
23 23 | # Errors (limited iterators).
24 |-zip([1, 2, 3], repeat(1, 1))
24 |+zip([1, 2, 3], repeat(1, 1), strict=False)
25 25 | zip([1, 2, 3], repeat(1, times=4))
B905.py:25:1: B905 [*] `zip()` without an explicit `strict=` parameter
|
23 | # Errors (limited iterators).
24 | zip([1, 2, 3], repeat(1, 1))
25 | zip([1, 2, 3], repeat(1, times=4))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B905
|
= help: Add explicit `strict=False`
Safe fix
22 22 |
23 23 | # Errors (limited iterators).
24 24 | zip([1, 2, 3], repeat(1, 1))
25 |-zip([1, 2, 3], repeat(1, times=4))
25 |+zip([1, 2, 3], repeat(1, times=4), strict=False)

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
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;
use crate::checkers::ast::Checker;
@@ -48,21 +47,12 @@ impl Violation for DjangoAllWithModelForm {
}
/// DJ007
pub(crate) fn all_with_model_form(
checker: &Checker,
arguments: Option<&Arguments>,
body: &[Stmt],
) -> Option<Diagnostic> {
if !arguments.is_some_and(|arguments| {
arguments
.args
.iter()
.any(|base| is_model_form(base, checker.semantic()))
}) {
return None;
pub(crate) fn all_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_model_form(class_def, checker.semantic()) {
return;
}
for element in body {
for element in &class_def.body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};
@@ -83,12 +73,18 @@ pub(crate) fn all_with_model_form(
match value.as_ref() {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if value == "__all__" {
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
checker
.diagnostics
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
return;
}
}
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => {
if value == "__all__".as_bytes() {
return Some(Diagnostic::new(DjangoAllWithModelForm, element.range()));
checker
.diagnostics
.push(Diagnostic::new(DjangoAllWithModelForm, element.range()));
return;
}
}
_ => (),
@@ -96,5 +92,4 @@ pub(crate) fn all_with_model_form(
}
}
}
None
}

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
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;
use crate::checkers::ast::Checker;
@@ -46,21 +45,12 @@ impl Violation for DjangoExcludeWithModelForm {
}
/// DJ006
pub(crate) fn exclude_with_model_form(
checker: &Checker,
arguments: Option<&Arguments>,
body: &[Stmt],
) -> Option<Diagnostic> {
if !arguments.is_some_and(|arguments| {
arguments
.args
.iter()
.any(|base| is_model_form(base, checker.semantic()))
}) {
return None;
pub(crate) fn exclude_with_model_form(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_model_form(class_def, checker.semantic()) {
return;
}
for element in body {
for element in &class_def.body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};
@@ -76,10 +66,12 @@ pub(crate) fn exclude_with_model_form(
continue;
};
if id == "exclude" {
return Some(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
checker
.diagnostics
.push(Diagnostic::new(DjangoExcludeWithModelForm, target.range()));
return;
}
}
}
}
None
}

View File

@@ -1,17 +1,17 @@
use ruff_python_ast::Expr;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::SemanticModel;
use ruff_python_semantic::{analyze, SemanticModel};
/// Return `true` if a Python class appears to be a Django model, based on its base classes.
pub(super) fn is_model(base: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(base).is_some_and(|call_path| {
pub(super) fn is_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
analyze::class::any_over_body(class_def, semantic, &|call_path| {
matches!(call_path.as_slice(), ["django", "db", "models", "Model"])
})
}
/// Return `true` if a Python class appears to be a Django model form, based on its base classes.
pub(super) fn is_model_form(base: &Expr, semantic: &SemanticModel) -> bool {
semantic.resolve_call_path(base).is_some_and(|call_path| {
pub(super) fn is_model_form(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
analyze::class::any_over_body(class_def, semantic, &|call_path| {
matches!(
call_path.as_slice(),
["django", "forms", "ModelForm"] | ["django", "forms", "models", "ModelForm"]

View File

@@ -1,10 +1,9 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_const_true;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -52,57 +51,39 @@ impl Violation for DjangoModelWithoutDunderStr {
}
/// DJ008
pub(crate) fn model_without_dunder_str(
checker: &mut Checker,
ast::StmtClassDef {
name,
arguments,
body,
..
}: &ast::StmtClassDef,
) {
if !is_non_abstract_model(arguments.as_deref(), body, checker.semantic()) {
pub(crate) fn model_without_dunder_str(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !is_non_abstract_model(class_def, checker.semantic()) {
return;
}
if has_dunder_method(body) {
if has_dunder_method(class_def) {
return;
}
checker
.diagnostics
.push(Diagnostic::new(DjangoModelWithoutDunderStr, name.range()));
checker.diagnostics.push(Diagnostic::new(
DjangoModelWithoutDunderStr,
class_def.identifier(),
));
}
fn has_dunder_method(body: &[Stmt]) -> bool {
body.iter().any(|val| match val {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
if name == "__str__" {
return true;
}
false
}
/// Returns `true` if the class has `__str__` method.
fn has_dunder_method(class_def: &ast::StmtClassDef) -> bool {
class_def.body.iter().any(|val| match val {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => name == "__str__",
_ => false,
})
}
fn is_non_abstract_model(
arguments: Option<&Arguments>,
body: &[Stmt],
semantic: &SemanticModel,
) -> bool {
let Some(Arguments { args: bases, .. }) = arguments else {
return false;
};
if is_model_abstract(body) {
return false;
/// Returns `true` if the class is a non-abstract Django model.
fn is_non_abstract_model(class_def: &ast::StmtClassDef, semantic: &SemanticModel) -> bool {
if class_def.bases().is_empty() || is_model_abstract(class_def) {
false
} else {
helpers::is_model(class_def, semantic)
}
bases.iter().any(|base| helpers::is_model(base, semantic))
}
/// Check if class is abstract, in terms of Django model inheritance.
fn is_model_abstract(body: &[Stmt]) -> bool {
for element in body {
fn is_model_abstract(class_def: &ast::StmtClassDef) -> bool {
for element in &class_def.body {
let Stmt::ClassDef(ast::StmtClassDef { name, body, .. }) = element else {
continue;
};

View File

@@ -1,9 +1,8 @@
use std::fmt;
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Stmt};
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
@@ -79,6 +78,50 @@ impl Violation for DjangoUnorderedBodyContentInModel {
}
}
/// DJ012
pub(crate) fn unordered_body_content_in_model(
checker: &mut Checker,
class_def: &ast::StmtClassDef,
) {
if !helpers::is_model(class_def, checker.semantic()) {
return;
}
// Track all the element types we've seen so far.
let mut element_types = Vec::new();
let mut prev_element_type = None;
for element in &class_def.body {
let Some(element_type) = get_element_type(element, checker.semantic()) else {
continue;
};
// Skip consecutive elements of the same type. It's less noisy to only report
// violations at type boundaries (e.g., avoid raising a violation for _every_
// field declaration that's out of order).
if prev_element_type == Some(element_type) {
continue;
}
prev_element_type = Some(element_type);
if let Some(&prev_element_type) = element_types
.iter()
.find(|&&prev_element_type| prev_element_type > element_type)
{
let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel {
element_type,
prev_element_type,
},
element.range(),
);
checker.diagnostics.push(diagnostic);
} else {
element_types.push(element_type);
}
}
}
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
enum ContentType {
FieldDeclaration,
@@ -140,53 +183,3 @@ fn get_element_type(element: &Stmt, semantic: &SemanticModel) -> Option<ContentT
_ => None,
}
}
/// DJ012
pub(crate) fn unordered_body_content_in_model(
checker: &mut Checker,
arguments: Option<&Arguments>,
body: &[Stmt],
) {
if !arguments.is_some_and(|arguments| {
arguments
.args
.iter()
.any(|base| helpers::is_model(base, checker.semantic()))
}) {
return;
}
// Track all the element types we've seen so far.
let mut element_types = Vec::new();
let mut prev_element_type = None;
for element in body {
let Some(element_type) = get_element_type(element, checker.semantic()) else {
continue;
};
// Skip consecutive elements of the same type. It's less noisy to only report
// violations at type boundaries (e.g., avoid raising a violation for _every_
// field declaration that's out of order).
if prev_element_type == Some(element_type) {
continue;
}
prev_element_type = Some(element_type);
if let Some(&prev_element_type) = element_types
.iter()
.find(|&&prev_element_type| prev_element_type > element_type)
{
let diagnostic = Diagnostic::new(
DjangoUnorderedBodyContentInModel {
element_type,
prev_element_type,
},
element.range(),
);
checker.diagnostics.push(diagnostic);
} else {
element_types.push(element_type);
}
}
}

View File

@@ -54,4 +54,12 @@ DJ012.py:129:5: DJ012 Order of model's inner classes, methods, and fields does n
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|
DJ012.py:146:5: DJ012 Order of model's inner classes, methods, and fields does not follow the Django Style Guide: field declaration should come before `Meta` class
|
144 | return "foobar"
145 |
146 | first_name = models.CharField(max_length=32)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ DJ012
|

View File

@@ -1,10 +1,10 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{Diagnostic, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::delete_stmt;
use crate::registry::AsRule;
/// ## What it does
@@ -28,14 +28,24 @@ use crate::registry::AsRule;
/// def add_numbers(a, b):
/// return a + b
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may remove `print` statements
/// that are used beyond debugging purposes.
#[violation]
pub struct Print;
impl Violation for Print {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("`print` found")
}
fn fix_title(&self) -> Option<String> {
Some("Remove `print`".to_string())
}
}
/// ## What it does
@@ -65,19 +75,29 @@ impl Violation for Print {
/// dict_c = {**dict_a, **dict_b}
/// return dict_c
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as it may remove `pprint` statements
/// that are used beyond debugging purposes.
#[violation]
pub struct PPrint;
impl Violation for PPrint {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("`pprint` found")
}
fn fix_title(&self) -> Option<String> {
Some("Remove `pprint`".to_string())
}
}
/// T201, T203
pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
let diagnostic = {
let mut diagnostic = {
let call_path = checker.semantic().resolve_call_path(&call.func);
if call_path
.as_ref()
@@ -113,5 +133,15 @@ pub(crate) fn print_call(checker: &mut Checker, call: &ast::ExprCall) {
return;
}
// Remove the `print`, if it's a standalone statement.
if checker.semantic().current_expression_parent().is_none() {
let statement = checker.semantic().current_statement();
let parent = checker.semantic().current_statement_parent();
let edit = delete_stmt(statement, parent, checker.locator(), checker.indexer());
diagnostic.set_fix(Fix::unsafe_edit(edit).isolate(Checker::isolation(
checker.semantic().current_statement_parent_id(),
)));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_print/mod.rs
---
T201.py:4:1: T201 `print` found
T201.py:4:1: T201 [*] `print` found
|
2 | import tempfile
3 |
@@ -10,8 +10,18 @@ T201.py:4:1: T201 `print` found
5 | print("Hello, world!", file=None) # T201
6 | print("Hello, world!", file=sys.stdout) # T201
|
= help: Remove `print`
T201.py:5:1: T201 `print` found
Unsafe fix
1 1 | import sys
2 2 | import tempfile
3 3 |
4 |-print("Hello, world!") # T201
5 4 | print("Hello, world!", file=None) # T201
6 5 | print("Hello, world!", file=sys.stdout) # T201
7 6 | print("Hello, world!", file=sys.stderr) # T201
T201.py:5:1: T201 [*] `print` found
|
4 | print("Hello, world!") # T201
5 | print("Hello, world!", file=None) # T201
@@ -19,8 +29,18 @@ T201.py:5:1: T201 `print` found
6 | print("Hello, world!", file=sys.stdout) # T201
7 | print("Hello, world!", file=sys.stderr) # T201
|
= help: Remove `print`
T201.py:6:1: T201 `print` found
Unsafe fix
2 2 | import tempfile
3 3 |
4 4 | print("Hello, world!") # T201
5 |-print("Hello, world!", file=None) # T201
6 5 | print("Hello, world!", file=sys.stdout) # T201
7 6 | print("Hello, world!", file=sys.stderr) # T201
8 7 |
T201.py:6:1: T201 [*] `print` found
|
4 | print("Hello, world!") # T201
5 | print("Hello, world!", file=None) # T201
@@ -28,8 +48,18 @@ T201.py:6:1: T201 `print` found
| ^^^^^ T201
7 | print("Hello, world!", file=sys.stderr) # T201
|
= help: Remove `print`
T201.py:7:1: T201 `print` found
Unsafe fix
3 3 |
4 4 | print("Hello, world!") # T201
5 5 | print("Hello, world!", file=None) # T201
6 |-print("Hello, world!", file=sys.stdout) # T201
7 6 | print("Hello, world!", file=sys.stderr) # T201
8 7 |
9 8 | with tempfile.NamedTemporaryFile() as fp:
T201.py:7:1: T201 [*] `print` found
|
5 | print("Hello, world!", file=None) # T201
6 | print("Hello, world!", file=sys.stdout) # T201
@@ -38,5 +68,15 @@ T201.py:7:1: T201 `print` found
8 |
9 | with tempfile.NamedTemporaryFile() as fp:
|
= help: Remove `print`
Unsafe fix
4 4 | print("Hello, world!") # T201
5 5 | print("Hello, world!", file=None) # T201
6 6 | print("Hello, world!", file=sys.stdout) # T201
7 |-print("Hello, world!", file=sys.stderr) # T201
8 7 |
9 8 | with tempfile.NamedTemporaryFile() as fp:
10 9 | print("Hello, world!", file=fp) # OK

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/flake8_print/mod.rs
---
T203.py:3:1: T203 `pprint` found
T203.py:3:1: T203 [*] `pprint` found
|
1 | from pprint import pprint
2 |
@@ -10,8 +10,17 @@ T203.py:3:1: T203 `pprint` found
4 |
5 | import pprint
|
= help: Remove `pprint`
T203.py:7:1: T203 `pprint` found
Unsafe fix
1 1 | from pprint import pprint
2 2 |
3 |-pprint("Hello, world!") # T203
4 3 |
5 4 | import pprint
6 5 |
T203.py:7:1: T203 [*] `pprint` found
|
5 | import pprint
6 |
@@ -20,5 +29,14 @@ T203.py:7:1: T203 `pprint` found
8 |
9 | pprint.pformat("Hello, world!")
|
= help: Remove `pprint`
Unsafe fix
4 4 |
5 5 | import pprint
6 6 |
7 |-pprint.pprint("Hello, world!") # T203
8 7 |
9 8 | pprint.pformat("Hello, world!")

View File

@@ -1,54 +0,0 @@
use ruff_python_ast::{self as ast, Expr, Operator};
use ruff_python_semantic::SemanticModel;
/// Traverse a "union" type annotation, applying `func` to each union member.
/// Supports traversal of `Union` and `|` union expressions.
/// The function is called with each expression in the union (excluding declarations of nested unions)
/// and the parent expression (if any).
pub(super) fn traverse_union<'a, F>(
func: &mut F,
semantic: &SemanticModel,
expr: &'a Expr,
parent: Option<&'a Expr>,
) where
F: FnMut(&'a Expr, Option<&'a Expr>),
{
// Ex) x | y
if let Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
left,
right,
range: _,
}) = expr
{
// The union data structure usually looks like this:
// a | b | c -> (a | b) | c
//
// However, parenthesized expressions can coerce it into any structure:
// a | (b | c)
//
// So we have to traverse both branches in order (left, then right), to report members
// in the order they appear in the source code.
// Traverse the left then right arms
traverse_union(func, semantic, left, Some(expr));
traverse_union(func, semantic, right, Some(expr));
return;
}
// Ex) Union[x, y]
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if semantic.match_typing_expr(value, "Union") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
// Traverse each element of the tuple within the union recursively to handle cases
// such as `Union[..., Union[...]]
elts.iter()
.for_each(|elt| traverse_union(func, semantic, elt, Some(expr)));
return;
}
}
}
// Otherwise, call the function on expression
func(expr, parent);
}

View File

@@ -1,5 +1,4 @@
//! Rules from [flake8-pyi](https://pypi.org/project/flake8-pyi/).
mod helpers;
pub(crate) mod rules;
#[cfg(test)]

View File

@@ -1,15 +1,16 @@
use ruff_python_ast::{self as ast, Expr};
use rustc_hash::FxHashSet;
use std::collections::HashSet;
use crate::checkers::ast::Checker;
use rustc_hash::FxHashSet;
use crate::rules::flake8_pyi::helpers::traverse_union;
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableExpr;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for duplicate union members.
///
@@ -55,7 +56,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
let mut diagnostics: Vec<Diagnostic> = Vec::new();
// Adds a member to `literal_exprs` if it is a `Literal` annotation
let mut check_for_duplicate_members = |expr: &'a Expr, parent: Option<&'a Expr>| {
let mut check_for_duplicate_members = |expr: &'a Expr, parent: &'a Expr| {
// If we've already seen this union member, raise a violation.
if !seen_nodes.insert(expr.into()) {
let mut diagnostic = Diagnostic::new(
@@ -68,7 +69,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
// parent without the duplicate.
// If the parent node is not a `BinOp` we will not perform a fix
if let Some(parent @ Expr::BinOp(ast::ExprBinOp { left, right, .. })) = parent {
if let Expr::BinOp(ast::ExprBinOp { left, right, .. }) = parent {
// Replace the parent with its non-duplicate child.
let child = if expr == left.as_ref() { right } else { left };
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
@@ -81,12 +82,7 @@ pub(crate) fn duplicate_union_member<'a>(checker: &mut Checker, expr: &'a Expr)
};
// Traverse the union, collect all diagnostic members
traverse_union(
&mut check_for_duplicate_members,
checker.semantic(),
expr,
None,
);
traverse_union(&mut check_for_duplicate_members, checker.semantic(), expr);
// Add all diagnostics to the checker
checker.diagnostics.append(&mut diagnostics);

View File

@@ -1,14 +1,16 @@
use rustc_hash::FxHashSet;
use std::fmt;
use rustc_hash::FxHashSet;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, LiteralExpressionRef};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_python_semantic::SemanticModel;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::snippet::SourceCodeSnippet;
use crate::{checkers::ast::Checker, rules::flake8_pyi::helpers::traverse_union};
/// ## What it does
/// Checks for the presence of redundant `Literal` types and builtin super
@@ -64,7 +66,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
// Adds a member to `literal_exprs` for each value in a `Literal`, and any builtin types
// to `builtin_types_in_union`.
let mut func = |expr: &'a Expr, _| {
let mut func = |expr: &'a Expr, _parent: &'a Expr| {
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if checker.semantic().match_typing_expr(value, "Literal") {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = slice.as_ref() {
@@ -82,7 +84,7 @@ pub(crate) fn redundant_literal_union<'a>(checker: &mut Checker, union: &'a Expr
builtin_types_in_union.insert(builtin_type);
};
traverse_union(&mut func, checker.semantic(), union, None);
traverse_union(&mut func, checker.semantic(), union);
for typing_literal_expr in typing_literal_exprs {
let Some(literal_type) = match_literal_type(typing_literal_expr) else {

View File

@@ -1,11 +1,10 @@
use ruff_python_ast::{Expr, Parameters};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Parameters};
use ruff_python_semantic::analyze::typing::traverse_union;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::rules::flake8_pyi::helpers::traverse_union;
/// ## What it does
/// Checks for union annotations that contain redundant numeric types (e.g.,
@@ -90,7 +89,7 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) {
let mut has_complex = false;
let mut has_int = false;
let mut func = |expr: &Expr, _parent: Option<&Expr>| {
let mut func = |expr: &Expr, _parent: &Expr| {
let Some(call_path) = checker.semantic().resolve_call_path(expr) else {
return;
};
@@ -103,7 +102,7 @@ fn check_annotation(checker: &mut Checker, annotation: &Expr) {
}
};
traverse_union(&mut func, checker.semantic(), annotation, None);
traverse_union(&mut func, checker.semantic(), annotation);
if has_complex {
if has_float {

View File

@@ -1,13 +1,12 @@
use ast::{ExprSubscript, Operator};
use ast::Operator;
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::traverse_union;
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
use crate::rules::flake8_pyi::helpers::traverse_union;
/// ## What it does
/// Checks for the presence of multiple literal types in a union.
///
@@ -62,7 +61,7 @@ fn concatenate_bin_ors(exprs: Vec<&Expr>) -> Expr {
})
}
fn make_union(subscript: &ExprSubscript, exprs: Vec<&Expr>) -> Expr {
fn make_union(subscript: &ast::ExprSubscript, exprs: Vec<&Expr>) -> Expr {
Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
@@ -108,7 +107,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
let mut total_literals = 0;
// Split members into `literal_exprs` if they are a `Literal` annotation and `other_exprs` otherwise
let mut collect_literal_expr = |expr: &'a Expr, _| {
let mut collect_literal_expr = |expr: &'a Expr, _parent: &'a Expr| {
if let Expr::Subscript(ast::ExprSubscript { value, slice, .. }) = expr {
if checker.semantic().match_typing_expr(value, "Literal") {
total_literals += 1;
@@ -137,7 +136,7 @@ pub(crate) fn unnecessary_literal_union<'a>(checker: &mut Checker, expr: &'a Exp
};
// Traverse the union, collect all members, split out the literals from the rest.
traverse_union(&mut collect_literal_expr, checker.semantic(), expr, None);
traverse_union(&mut collect_literal_expr, checker.semantic(), expr);
let union_subscript = expr.as_subscript_expr();
if union_subscript.is_some_and(|subscript| {

View File

@@ -2,9 +2,10 @@ use ast::{ExprContext, Operator};
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::traverse_union;
use ruff_text_size::{Ranged, TextRange};
use crate::{checkers::ast::Checker, rules::flake8_pyi::helpers::traverse_union};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the presence of multiple `type`s in a union.
@@ -80,21 +81,28 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
}
let mut type_exprs = Vec::new();
let mut other_exprs = Vec::new();
let mut collect_type_exprs = |expr: &'a Expr, _| {
let Some(subscript) = expr.as_subscript_expr() else {
return;
};
if checker
.semantic()
.resolve_call_path(subscript.value.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
{
type_exprs.push(&subscript.slice);
let mut collect_type_exprs = |expr: &'a Expr, _parent: &'a Expr| {
let subscript = expr.as_subscript_expr();
if subscript.is_none() {
other_exprs.push(expr);
} else {
let unwrapped = subscript.unwrap();
if checker
.semantic()
.resolve_call_path(unwrapped.value.as_ref())
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
{
type_exprs.push(&unwrapped.slice);
} else {
other_exprs.push(expr);
}
}
};
traverse_union(&mut collect_type_exprs, checker.semantic(), union, None);
traverse_union(&mut collect_type_exprs, checker.semantic(), union);
if type_exprs.len() > 1 {
let type_members: Vec<String> = type_exprs
@@ -113,55 +121,82 @@ pub(crate) fn unnecessary_type_union<'a>(checker: &mut Checker, union: &'a Expr)
if checker.semantic().is_builtin("type") {
let content = if let Some(subscript) = subscript {
checker
.generator()
.expr(&Expr::Subscript(ast::ExprSubscript {
value: Box::new(Expr::Name(ast::ExprName {
id: "type".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: type_members
.into_iter()
.map(|type_member| {
Expr::Name(ast::ExprName {
id: type_member,
ctx: ExprContext::Load,
range: TextRange::default(),
})
})
.collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
let types = &Expr::Subscript(ast::ExprSubscript {
value: Box::new(Expr::Name(ast::ExprName {
id: "type".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
}))
} else {
checker
.generator()
.expr(&Expr::Subscript(ast::ExprSubscript {
value: Box::new(Expr::Name(ast::ExprName {
id: "type".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
slice: Box::new(concatenate_bin_ors(
type_exprs
.clone()
})),
slice: Box::new(Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: type_members
.into_iter()
.map(std::convert::AsRef::as_ref)
.map(|type_member| {
Expr::Name(ast::ExprName {
id: type_member,
ctx: ExprContext::Load,
range: TextRange::default(),
})
})
.collect(),
)),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
ctx: ExprContext::Load,
range: TextRange::default(),
}))
})),
ctx: ExprContext::Load,
range: TextRange::default(),
});
if other_exprs.is_empty() {
checker.generator().expr(types)
} else {
let mut exprs = Vec::new();
exprs.push(types);
exprs.extend(other_exprs);
let union = Expr::Subscript(ast::ExprSubscript {
value: subscript.value.clone(),
slice: Box::new(Expr::Tuple(ast::ExprTuple {
elts: exprs.into_iter().cloned().collect(),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
ctx: ExprContext::Load,
range: TextRange::default(),
});
checker.generator().expr(&union)
}
} else {
let types = &Expr::Subscript(ast::ExprSubscript {
value: Box::new(Expr::Name(ast::ExprName {
id: "type".into(),
ctx: ExprContext::Load,
range: TextRange::default(),
})),
slice: Box::new(concatenate_bin_ors(
type_exprs
.clone()
.into_iter()
.map(std::convert::AsRef::as_ref)
.collect(),
)),
ctx: ExprContext::Load,
range: TextRange::default(),
});
if other_exprs.is_empty() {
checker.generator().expr(types)
} else {
let mut exprs = Vec::new();
exprs.push(types);
exprs.extend(other_exprs);
checker.generator().expr(&concatenate_bin_ors(exprs))
}
};
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(

View File

@@ -7,28 +7,35 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the presence of unused private `TypeVar` declarations.
/// Checks for the presence of unused private `TypeVar`, `ParamSpec` or
/// `TypeVarTuple` declarations.
///
/// ## Why is this bad?
/// A private `TypeVar` that is defined but not used is likely a mistake, and
/// A private `TypeVar` that is defined but not used is likely a mistake. It
/// should either be used, made public, or removed to avoid confusion.
///
/// ## Example
/// ```python
/// import typing
/// import typing_extensions
///
/// _T = typing.TypeVar("_T")
/// _Ts = typing_extensions.TypeVarTuple("_Ts")
/// ```
#[violation]
pub struct UnusedPrivateTypeVar {
name: String,
type_var_like_name: String,
type_var_like_kind: String,
}
impl Violation for UnusedPrivateTypeVar {
#[derive_message_formats]
fn message(&self) -> String {
let UnusedPrivateTypeVar { name } = self;
format!("Private TypeVar `{name}` is never used")
let UnusedPrivateTypeVar {
type_var_like_name,
type_var_like_kind,
} = self;
format!("Private {type_var_like_kind} `{type_var_like_name}` is never used")
}
}
@@ -185,13 +192,26 @@ pub(crate) fn unused_private_type_var(
let Expr::Call(ast::ExprCall { func, .. }) = value.as_ref() else {
continue;
};
if !checker.semantic().match_typing_expr(func, "TypeVar") {
let semantic = checker.semantic();
let Some(type_var_like_kind) = semantic.resolve_call_path(func).and_then(|call_path| {
if semantic.match_typing_call_path(&call_path, "TypeVar") {
Some("TypeVar")
} else if semantic.match_typing_call_path(&call_path, "ParamSpec") {
Some("ParamSpec")
} else if semantic.match_typing_call_path(&call_path, "TypeVarTuple") {
Some("TypeVarTuple")
} else {
None
}
}) else {
continue;
}
};
diagnostics.push(Diagnostic::new(
UnusedPrivateTypeVar {
name: id.to_string(),
type_var_like_name: id.to_string(),
type_var_like_kind: type_var_like_kind.to_string(),
},
binding.range(),
));

View File

@@ -1,22 +1,52 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI018.py:4:1: PYI018 Private TypeVar `_T` is never used
PYI018.py:6:1: PYI018 Private TypeVar `_T` is never used
|
2 | from typing import TypeVar
3 |
4 | _T = typing.TypeVar("_T")
4 | from typing_extensions import ParamSpec, TypeVarTuple
5 |
6 | _T = typing.TypeVar("_T")
| ^^ PYI018
5 | _P = TypeVar("_P")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
|
PYI018.py:5:1: PYI018 Private TypeVar `_P` is never used
PYI018.py:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
4 | _T = typing.TypeVar("_T")
5 | _P = TypeVar("_P")
| ^^ PYI018
6 |
7 | # OK
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
| ^^^ PYI018
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
|
PYI018.py:8:1: PYI018 Private ParamSpec `_P` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
| ^^ PYI018
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
|
PYI018.py:9:1: PYI018 Private ParamSpec `_P2` is never used
|
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
| ^^^ PYI018
10 | _Ts2 = TypeVarTuple("_Ts2")
|
PYI018.py:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
| ^^^^ PYI018
11 |
12 | # OK
|

View File

@@ -1,22 +1,52 @@
---
source: crates/ruff_linter/src/rules/flake8_pyi/mod.rs
---
PYI018.pyi:4:1: PYI018 Private TypeVar `_T` is never used
PYI018.pyi:6:1: PYI018 Private TypeVar `_T` is never used
|
2 | from typing import TypeVar
3 |
4 | _T = typing.TypeVar("_T")
4 | from typing_extensions import ParamSpec, TypeVarTuple
5 |
6 | _T = typing.TypeVar("_T")
| ^^ PYI018
5 | _P = TypeVar("_P")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
|
PYI018.pyi:5:1: PYI018 Private TypeVar `_P` is never used
PYI018.pyi:7:1: PYI018 Private TypeVarTuple `_Ts` is never used
|
4 | _T = typing.TypeVar("_T")
5 | _P = TypeVar("_P")
| ^^ PYI018
6 |
7 | # OK
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
| ^^^ PYI018
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
|
PYI018.pyi:8:1: PYI018 Private ParamSpec `_P` is never used
|
6 | _T = typing.TypeVar("_T")
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
| ^^ PYI018
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
|
PYI018.pyi:9:1: PYI018 Private ParamSpec `_P2` is never used
|
7 | _Ts = typing_extensions.TypeVarTuple("_Ts")
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
| ^^^ PYI018
10 | _Ts2 = TypeVarTuple("_Ts2")
|
PYI018.pyi:10:1: PYI018 Private TypeVarTuple `_Ts2` is never used
|
8 | _P = ParamSpec("_P")
9 | _P2 = typing.ParamSpec("_P2")
10 | _Ts2 = TypeVarTuple("_Ts2")
| ^^^^ PYI018
11 |
12 | # OK
|

View File

@@ -54,5 +54,91 @@ PYI055.py:39:8: PYI055 [*] Multiple `type` members in a union. Combine them into
38 38 | # PYI055
39 |- x: Union[type[requests_mock.Mocker], type[httpretty], type[str]] = requests_mock.Mocker
39 |+ x: type[Union[requests_mock.Mocker, httpretty, str]] = requests_mock.Mocker
40 40 |
41 41 |
42 42 | def convert_union(union: UnionType) -> _T | None:
PYI055.py:44:9: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
42 | def convert_union(union: UnionType) -> _T | None:
43 | converters: tuple[
44 | type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
45 | ] = union.__args__
46 | ...
|
= help: Combine multiple `type` members
Safe fix
41 41 |
42 42 | def convert_union(union: UnionType) -> _T | None:
43 43 | converters: tuple[
44 |- type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
44 |+ type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T], ... # PYI055
45 45 | ] = union.__args__
46 46 | ...
47 47 |
PYI055.py:50:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
48 | def convert_union(union: UnionType) -> _T | None:
49 | converters: tuple[
50 | Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
51 | ] = union.__args__
52 | ...
|
= help: Combine multiple `type` members
Safe fix
47 47 |
48 48 | def convert_union(union: UnionType) -> _T | None:
49 49 | converters: tuple[
50 |- Union[type[_T] | type[Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
50 |+ Union[type[_T | Converter[_T]] | Converter[_T] | Callable[[str], _T]], ... # PYI055
51 51 | ] = union.__args__
52 52 | ...
53 53 |
PYI055.py:56:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
54 | def convert_union(union: UnionType) -> _T | None:
55 | converters: tuple[
56 | Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
57 | ] = union.__args__
58 | ...
|
= help: Combine multiple `type` members
Safe fix
53 53 |
54 54 | def convert_union(union: UnionType) -> _T | None:
55 55 | converters: tuple[
56 |- Union[type[_T] | type[Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
56 |+ Union[type[_T | Converter[_T]]] | Converter[_T] | Callable[[str], _T], ... # PYI055
57 57 | ] = union.__args__
58 58 | ...
59 59 |
PYI055.py:62:15: PYI055 [*] Multiple `type` members in a union. Combine them into one, e.g., `type[_T | Converter[_T]]`.
|
60 | def convert_union(union: UnionType) -> _T | None:
61 | converters: tuple[
62 | Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI055
63 | ] = union.__args__
64 | ...
|
= help: Combine multiple `type` members
Safe fix
59 59 |
60 60 | def convert_union(union: UnionType) -> _T | None:
61 61 | converters: tuple[
62 |- Union[type[_T] | type[Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
62 |+ Union[type[_T | Converter[_T]] | str] | Converter[_T] | Callable[[str], _T], ... # PYI055
63 63 | ] = union.__args__
64 64 | ...

View File

@@ -120,7 +120,7 @@ PYI055.pyi:10:15: PYI055 [*] Multiple `type` members in a union. Combine them in
8 8 | z: Union[type[float, int], type[complex]]
9 9 |
10 |-def func(arg: type[int] | str | type[float]) -> None: ...
10 |+def func(arg: type[int | float]) -> None: ...
10 |+def func(arg: type[int | float] | str) -> None: ...
11 11 |
12 12 | # OK
13 13 | x: type[int, str, float]

View File

@@ -56,6 +56,7 @@ mod tests {
}
#[test_case(Rule::InDictKeys, Path::new("SIM118.py"))]
#[test_case(Rule::YodaConditions, Path::new("SIM300.py"))]
#[test_case(Rule::IfElseBlockInsteadOfDictGet, Path::new("SIM401.py"))]
#[test_case(Rule::DictGetWithNoneDefault, Path::new("SIM910.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {

View File

@@ -1,3 +1,5 @@
use std::cmp;
use anyhow::Result;
use libcst_native::CompOp;
@@ -14,6 +16,7 @@ use crate::cst::helpers::or_space;
use crate::cst::matchers::{match_comparison, transform_expression};
use crate::fix::edits::pad;
use crate::fix::snippet::SourceCodeSnippet;
use crate::settings::types::PreviewMode;
/// ## What it does
/// Checks for conditions that position a constant on the left-hand side of the
@@ -78,18 +81,65 @@ impl Violation for YodaConditions {
}
}
/// Return `true` if an [`Expr`] is a constant or a constant-like name.
fn is_constant_like(expr: &Expr) -> bool {
match expr {
Expr::Attribute(ast::ExprAttribute { attr, .. }) => str::is_cased_uppercase(attr),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts.iter().all(is_constant_like),
Expr::Name(ast::ExprName { id, .. }) => str::is_cased_uppercase(id),
Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert,
operand,
range: _,
}) => operand.is_literal_expr(),
_ => expr.is_literal_expr(),
/// Comparisons left-hand side must not be more [`ConstantLikelihood`] than the right-hand side.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
enum ConstantLikelihood {
/// The expression is unlikely to be a constant (e.g., `foo` or `foo(bar)`).
Unlikely = 0,
/// The expression is likely to be a constant (e.g., `FOO`).
Probably = 1,
/// The expression is definitely a constant (e.g., `42` or `"foo"`).
Definitely = 2,
}
impl ConstantLikelihood {
/// Determine the [`ConstantLikelihood`] of an expression.
fn from_expression(expr: &Expr, preview: PreviewMode) -> Self {
match expr {
_ if expr.is_literal_expr() => ConstantLikelihood::Definitely,
Expr::Attribute(ast::ExprAttribute { attr, .. }) => {
ConstantLikelihood::from_identifier(attr)
}
Expr::Name(ast::ExprName { id, .. }) => ConstantLikelihood::from_identifier(id),
Expr::Tuple(ast::ExprTuple { elts, .. }) => elts
.iter()
.map(|expr| ConstantLikelihood::from_expression(expr, preview))
.min()
.unwrap_or(ConstantLikelihood::Definitely),
Expr::List(ast::ExprList { elts, .. }) if preview.is_enabled() => elts
.iter()
.map(|expr| ConstantLikelihood::from_expression(expr, preview))
.min()
.unwrap_or(ConstantLikelihood::Definitely),
Expr::Dict(ast::ExprDict { values: vs, .. }) if preview.is_enabled() => {
if vs.is_empty() {
ConstantLikelihood::Definitely
} else {
ConstantLikelihood::Probably
}
}
Expr::BinOp(ast::ExprBinOp { left, right, .. }) => cmp::min(
ConstantLikelihood::from_expression(left, preview),
ConstantLikelihood::from_expression(right, preview),
),
Expr::UnaryOp(ast::ExprUnaryOp {
op: UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert,
operand,
range: _,
}) => ConstantLikelihood::from_expression(operand, preview),
_ => ConstantLikelihood::Unlikely,
}
}
/// Determine the [`ConstantLikelihood`] of an identifier.
fn from_identifier(identifier: &str) -> Self {
if str::is_cased_uppercase(identifier) {
ConstantLikelihood::Probably
} else {
ConstantLikelihood::Unlikely
}
}
}
@@ -180,7 +230,9 @@ pub(crate) fn yoda_conditions(
return;
}
if !is_constant_like(left) || is_constant_like(right) {
if ConstantLikelihood::from_expression(left, checker.settings.preview)
<= ConstantLikelihood::from_expression(right, checker.settings.preview)
{
return;
}

View File

@@ -6,8 +6,8 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda
1 | # Errors
2 | "yoda" == compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
3 | "yoda" == compare # SIM300
4 | 42 == age # SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
|
= help: Replace Yoda condition with `compare == "yoda"`
@@ -15,342 +15,340 @@ SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda
1 1 | # Errors
2 |-"yoda" == compare # SIM300
2 |+compare == "yoda" # SIM300
3 3 | "yoda" == compare # SIM300
4 4 | 42 == age # SIM300
5 5 | ("a", "b") == compare # SIM300
3 3 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda"` instead
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
|
1 | # Errors
2 | "yoda" == compare # SIM300
3 | "yoda" == compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
4 | 42 == age # SIM300
5 | ("a", "b") == compare # SIM300
|
= help: Replace Yoda condition with `compare == "yoda"`
Safe fix
1 1 | # Errors
2 2 | "yoda" == compare # SIM300
3 |-"yoda" == compare # SIM300
3 |+compare == "yoda" # SIM300
4 4 | 42 == age # SIM300
5 5 | ("a", "b") == compare # SIM300
6 6 | "yoda" <= compare # SIM300
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
|
2 | "yoda" == compare # SIM300
3 | "yoda" == compare # SIM300
4 | 42 == age # SIM300
3 | 42 == age # SIM300
| ^^^^^^^^^ SIM300
5 | ("a", "b") == compare # SIM300
6 | "yoda" <= compare # SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
|
= help: Replace Yoda condition with `age == 42`
Safe fix
1 1 | # Errors
2 2 | "yoda" == compare # SIM300
3 3 | "yoda" == compare # SIM300
4 |-42 == age # SIM300
4 |+age == 42 # SIM300
5 5 | ("a", "b") == compare # SIM300
6 6 | "yoda" <= compare # SIM300
7 7 | "yoda" < compare # SIM300
3 |-42 == age # SIM300
3 |+age == 42 # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
|
3 | "yoda" == compare # SIM300
4 | 42 == age # SIM300
5 | ("a", "b") == compare # SIM300
2 | "yoda" == compare # SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
6 | "yoda" <= compare # SIM300
7 | "yoda" < compare # SIM300
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
|
= help: Replace Yoda condition with `compare == ("a", "b")`
Safe fix
1 1 | # Errors
2 2 | "yoda" == compare # SIM300
3 3 | "yoda" == compare # SIM300
4 4 | 42 == age # SIM300
5 |-("a", "b") == compare # SIM300
5 |+compare == ("a", "b") # SIM300
6 6 | "yoda" <= compare # SIM300
7 7 | "yoda" < compare # SIM300
8 8 | 42 > age # SIM300
3 3 | 42 == age # SIM300
4 |-("a", "b") == compare # SIM300
4 |+compare == ("a", "b") # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
|
4 | 42 == age # SIM300
5 | ("a", "b") == compare # SIM300
6 | "yoda" <= compare # SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
7 | "yoda" < compare # SIM300
8 | 42 > age # SIM300
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
|
= help: Replace Yoda condition with `compare >= "yoda"`
Safe fix
3 3 | "yoda" == compare # SIM300
4 4 | 42 == age # SIM300
5 5 | ("a", "b") == compare # SIM300
6 |-"yoda" <= compare # SIM300
6 |+compare >= "yoda" # SIM300
7 7 | "yoda" < compare # SIM300
8 8 | 42 > age # SIM300
9 9 | -42 > age # SIM300
2 2 | "yoda" == compare # SIM300
3 3 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 |-"yoda" <= compare # SIM300
5 |+compare >= "yoda" # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
|
5 | ("a", "b") == compare # SIM300
6 | "yoda" <= compare # SIM300
7 | "yoda" < compare # SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
| ^^^^^^^^^^^^^^^^ SIM300
8 | 42 > age # SIM300
9 | -42 > age # SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
|
= help: Replace Yoda condition with `compare > "yoda"`
Safe fix
4 4 | 42 == age # SIM300
5 5 | ("a", "b") == compare # SIM300
6 6 | "yoda" <= compare # SIM300
7 |-"yoda" < compare # SIM300
7 |+compare > "yoda" # SIM300
8 8 | 42 > age # SIM300
9 9 | -42 > age # SIM300
10 10 | +42 > age # SIM300
3 3 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
6 |-"yoda" < compare # SIM300
6 |+compare > "yoda" # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instead
|
6 | "yoda" <= compare # SIM300
7 | "yoda" < compare # SIM300
8 | 42 > age # SIM300
| ^^^^^^^^ SIM300
9 | -42 > age # SIM300
10 | +42 > age # SIM300
|
= help: Replace Yoda condition with `age < 42`
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instead
|
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
| ^^^^^^^^ SIM300
8 | -42 > age # SIM300
9 | +42 > age # SIM300
|
= help: Replace Yoda condition with `age < 42`
Safe fix
5 5 | ("a", "b") == compare # SIM300
6 6 | "yoda" <= compare # SIM300
7 7 | "yoda" < compare # SIM300
8 |-42 > age # SIM300
8 |+age < 42 # SIM300
9 9 | -42 > age # SIM300
10 10 | +42 > age # SIM300
11 11 | YODA == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
7 |-42 > age # SIM300
7 |+age < 42 # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
|
7 | "yoda" < compare # SIM300
8 | 42 > age # SIM300
9 | -42 > age # SIM300
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
| ^^^^^^^^^ SIM300
10 | +42 > age # SIM300
11 | YODA == age # SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
|
= help: Replace Yoda condition with `age < -42`
Safe fix
6 6 | "yoda" <= compare # SIM300
7 7 | "yoda" < compare # SIM300
8 8 | 42 > age # SIM300
9 |--42 > age # SIM300
9 |+age < -42 # SIM300
10 10 | +42 > age # SIM300
11 11 | YODA == age # SIM300
12 12 | YODA > age # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
8 |--42 > age # SIM300
8 |+age < -42 # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
|
8 | 42 > age # SIM300
9 | -42 > age # SIM300
10 | +42 > age # SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
9 | +42 > age # SIM300
| ^^^^^^^^^ SIM300
11 | YODA == age # SIM300
12 | YODA > age # SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
|
= help: Replace Yoda condition with `age < +42`
Safe fix
7 7 | "yoda" < compare # SIM300
8 8 | 42 > age # SIM300
9 9 | -42 > age # SIM300
10 |-+42 > age # SIM300
10 |+age < +42 # SIM300
11 11 | YODA == age # SIM300
12 12 | YODA > age # SIM300
13 13 | YODA >= age # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
9 |-+42 > age # SIM300
9 |+age < +42 # SIM300
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
|
9 | -42 > age # SIM300
10 | +42 > age # SIM300
11 | YODA == age # SIM300
8 | -42 > age # SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
| ^^^^^^^^^^^ SIM300
12 | YODA > age # SIM300
13 | YODA >= age # SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
|
= help: Replace Yoda condition with `age == YODA`
Safe fix
8 8 | 42 > age # SIM300
9 9 | -42 > age # SIM300
10 10 | +42 > age # SIM300
11 |-YODA == age # SIM300
11 |+age == YODA # SIM300
12 12 | YODA > age # SIM300
13 13 | YODA >= age # SIM300
14 14 | JediOrder.YODA == age # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
10 |-YODA == age # SIM300
10 |+age == YODA # SIM300
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
|
10 | +42 > age # SIM300
11 | YODA == age # SIM300
12 | YODA > age # SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
| ^^^^^^^^^^ SIM300
13 | YODA >= age # SIM300
14 | JediOrder.YODA == age # SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
|
= help: Replace Yoda condition with `age < YODA`
Safe fix
9 9 | -42 > age # SIM300
10 10 | +42 > age # SIM300
11 11 | YODA == age # SIM300
12 |-YODA > age # SIM300
12 |+age < YODA # SIM300
13 13 | YODA >= age # SIM300
14 14 | JediOrder.YODA == age # SIM300
15 15 | 0 < (number - 100) # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
11 |-YODA > age # SIM300
11 |+age < YODA # SIM300
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
|
11 | YODA == age # SIM300
12 | YODA > age # SIM300
13 | YODA >= age # SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
| ^^^^^^^^^^^ SIM300
14 | JediOrder.YODA == age # SIM300
15 | 0 < (number - 100) # SIM300
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
|
= help: Replace Yoda condition with `age <= YODA`
Safe fix
10 10 | +42 > age # SIM300
11 11 | YODA == age # SIM300
12 12 | YODA > age # SIM300
13 |-YODA >= age # SIM300
13 |+age <= YODA # SIM300
14 14 | JediOrder.YODA == age # SIM300
15 15 | 0 < (number - 100) # SIM300
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
12 |-YODA >= age # SIM300
12 |+age <= YODA # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
|
12 | YODA > age # SIM300
13 | YODA >= age # SIM300
14 | JediOrder.YODA == age # SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
15 | 0 < (number - 100) # SIM300
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
|
= help: Replace Yoda condition with `age == JediOrder.YODA`
Safe fix
11 11 | YODA == age # SIM300
12 12 | YODA > age # SIM300
13 13 | YODA >= age # SIM300
14 |-JediOrder.YODA == age # SIM300
14 |+age == JediOrder.YODA # SIM300
15 15 | 0 < (number - 100) # SIM300
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 17 | B<A[0][0]or B
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
13 |-JediOrder.YODA == age # SIM300
13 |+age == JediOrder.YODA # SIM300
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
16 16 | B or(B)<A[0][0]
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
|
13 | YODA >= age # SIM300
14 | JediOrder.YODA == age # SIM300
15 | 0 < (number - 100) # SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
| ^^^^^^^^^^^^^^^^^^ SIM300
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 | B<A[0][0]or B
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
|
= help: Replace Yoda condition with `(number - 100) > 0`
Safe fix
12 12 | YODA > age # SIM300
13 13 | YODA >= age # SIM300
14 14 | JediOrder.YODA == age # SIM300
15 |-0 < (number - 100) # SIM300
15 |+(number - 100) > 0 # SIM300
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 17 | B<A[0][0]or B
18 18 | B or(B)<A[0][0]
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 |-0 < (number - 100) # SIM300
14 |+(number - 100) > 0 # SIM300
15 15 | B<A[0][0]or B
16 16 | B or(B)<A[0][0]
17 17 |
SIM300.py:16:1: SIM300 [*] Yoda conditions are discouraged
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
|
14 | JediOrder.YODA == age # SIM300
15 | 0 < (number - 100) # SIM300
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
17 | B<A[0][0]or B
18 | B or(B)<A[0][0]
|
= help: Replace Yoda condition
Safe fix
13 13 | YODA >= age # SIM300
14 14 | JediOrder.YODA == age # SIM300
15 15 | 0 < (number - 100) # SIM300
16 |-SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
16 |+(60 * 60) < SomeClass().settings.SOME_CONSTANT_VALUE # SIM300
17 17 | B<A[0][0]or B
18 18 | B or(B)<A[0][0]
19 19 |
SIM300.py:17:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
|
15 | 0 < (number - 100) # SIM300
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 | B<A[0][0]or B
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
| ^^^^^^^^^ SIM300
18 | B or(B)<A[0][0]
16 | B or(B)<A[0][0]
|
= help: Replace Yoda condition with `A[0][0] > B`
Safe fix
14 14 | JediOrder.YODA == age # SIM300
15 15 | 0 < (number - 100) # SIM300
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 |-B<A[0][0]or B
17 |+A[0][0] > B or B
18 18 | B or(B)<A[0][0]
19 19 |
20 20 | # OK
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
15 |-B<A[0][0]or B
15 |+A[0][0] > B or B
16 16 | B or(B)<A[0][0]
17 17 |
18 18 | # Errors in preview
SIM300.py:18:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
SIM300.py:16:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
|
16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 | B<A[0][0]or B
18 | B or(B)<A[0][0]
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
| ^^^^^^^^^^^ SIM300
19 |
20 | # OK
17 |
18 | # Errors in preview
|
= help: Replace Yoda condition with `A[0][0] > (B)`
Safe fix
15 15 | 0 < (number - 100) # SIM300
16 16 | SomeClass().settings.SOME_CONSTANT_VALUE > (60 * 60) # SIM300
17 17 | B<A[0][0]or B
18 |-B or(B)<A[0][0]
18 |+B or A[0][0] > (B)
19 19 |
20 20 | # OK
21 21 | compare == "yoda"
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
16 |-B or(B)<A[0][0]
16 |+B or A[0][0] > (B)
17 17 |
18 18 | # Errors in preview
19 19 | ['upper'] == UPPER_LIST
SIM300.py:23:1: SIM300 [*] Yoda conditions are discouraged, use `['upper'] == UPPER_LIST` instead
|
22 | # Errors in stable
23 | UPPER_LIST == ['upper']
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM300
24 | DummyHandler.CONFIG == {}
|
= help: Replace Yoda condition with `['upper'] == UPPER_LIST`
Safe fix
20 20 | {} == DummyHandler.CONFIG
21 21 |
22 22 | # Errors in stable
23 |-UPPER_LIST == ['upper']
23 |+['upper'] == UPPER_LIST
24 24 | DummyHandler.CONFIG == {}
25 25 |
26 26 | # OK
SIM300.py:24:1: SIM300 [*] Yoda conditions are discouraged, use `{} == DummyHandler.CONFIG` instead
|
22 | # Errors in stable
23 | UPPER_LIST == ['upper']
24 | DummyHandler.CONFIG == {}
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
25 |
26 | # OK
|
= help: Replace Yoda condition with `{} == DummyHandler.CONFIG`
Safe fix
21 21 |
22 22 | # Errors in stable
23 23 | UPPER_LIST == ['upper']
24 |-DummyHandler.CONFIG == {}
24 |+{} == DummyHandler.CONFIG
25 25 |
26 26 | # OK
27 27 | compare == "yoda"

View File

@@ -0,0 +1,354 @@
---
source: crates/ruff_linter/src/rules/flake8_simplify/mod.rs
---
SIM300.py:2:1: SIM300 [*] Yoda conditions are discouraged, use `compare == "yoda"` instead
|
1 | # Errors
2 | "yoda" == compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
|
= help: Replace Yoda condition with `compare == "yoda"`
Safe fix
1 1 | # Errors
2 |-"yoda" == compare # SIM300
2 |+compare == "yoda" # SIM300
3 3 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
SIM300.py:3:1: SIM300 [*] Yoda conditions are discouraged, use `age == 42` instead
|
1 | # Errors
2 | "yoda" == compare # SIM300
3 | 42 == age # SIM300
| ^^^^^^^^^ SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
|
= help: Replace Yoda condition with `age == 42`
Safe fix
1 1 | # Errors
2 2 | "yoda" == compare # SIM300
3 |-42 == age # SIM300
3 |+age == 42 # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
SIM300.py:4:1: SIM300 [*] Yoda conditions are discouraged, use `compare == ("a", "b")` instead
|
2 | "yoda" == compare # SIM300
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
|
= help: Replace Yoda condition with `compare == ("a", "b")`
Safe fix
1 1 | # Errors
2 2 | "yoda" == compare # SIM300
3 3 | 42 == age # SIM300
4 |-("a", "b") == compare # SIM300
4 |+compare == ("a", "b") # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
SIM300.py:5:1: SIM300 [*] Yoda conditions are discouraged, use `compare >= "yoda"` instead
|
3 | 42 == age # SIM300
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
| ^^^^^^^^^^^^^^^^^ SIM300
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
|
= help: Replace Yoda condition with `compare >= "yoda"`
Safe fix
2 2 | "yoda" == compare # SIM300
3 3 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 |-"yoda" <= compare # SIM300
5 |+compare >= "yoda" # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
SIM300.py:6:1: SIM300 [*] Yoda conditions are discouraged, use `compare > "yoda"` instead
|
4 | ("a", "b") == compare # SIM300
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
| ^^^^^^^^^^^^^^^^ SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
|
= help: Replace Yoda condition with `compare > "yoda"`
Safe fix
3 3 | 42 == age # SIM300
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
6 |-"yoda" < compare # SIM300
6 |+compare > "yoda" # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
SIM300.py:7:1: SIM300 [*] Yoda conditions are discouraged, use `age < 42` instead
|
5 | "yoda" <= compare # SIM300
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
| ^^^^^^^^ SIM300
8 | -42 > age # SIM300
9 | +42 > age # SIM300
|
= help: Replace Yoda condition with `age < 42`
Safe fix
4 4 | ("a", "b") == compare # SIM300
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
7 |-42 > age # SIM300
7 |+age < 42 # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
SIM300.py:8:1: SIM300 [*] Yoda conditions are discouraged, use `age < -42` instead
|
6 | "yoda" < compare # SIM300
7 | 42 > age # SIM300
8 | -42 > age # SIM300
| ^^^^^^^^^ SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
|
= help: Replace Yoda condition with `age < -42`
Safe fix
5 5 | "yoda" <= compare # SIM300
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
8 |--42 > age # SIM300
8 |+age < -42 # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
SIM300.py:9:1: SIM300 [*] Yoda conditions are discouraged, use `age < +42` instead
|
7 | 42 > age # SIM300
8 | -42 > age # SIM300
9 | +42 > age # SIM300
| ^^^^^^^^^ SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
|
= help: Replace Yoda condition with `age < +42`
Safe fix
6 6 | "yoda" < compare # SIM300
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
9 |-+42 > age # SIM300
9 |+age < +42 # SIM300
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
SIM300.py:10:1: SIM300 [*] Yoda conditions are discouraged, use `age == YODA` instead
|
8 | -42 > age # SIM300
9 | +42 > age # SIM300
10 | YODA == age # SIM300
| ^^^^^^^^^^^ SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
|
= help: Replace Yoda condition with `age == YODA`
Safe fix
7 7 | 42 > age # SIM300
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
10 |-YODA == age # SIM300
10 |+age == YODA # SIM300
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
SIM300.py:11:1: SIM300 [*] Yoda conditions are discouraged, use `age < YODA` instead
|
9 | +42 > age # SIM300
10 | YODA == age # SIM300
11 | YODA > age # SIM300
| ^^^^^^^^^^ SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
|
= help: Replace Yoda condition with `age < YODA`
Safe fix
8 8 | -42 > age # SIM300
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
11 |-YODA > age # SIM300
11 |+age < YODA # SIM300
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
SIM300.py:12:1: SIM300 [*] Yoda conditions are discouraged, use `age <= YODA` instead
|
10 | YODA == age # SIM300
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
| ^^^^^^^^^^^ SIM300
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
|
= help: Replace Yoda condition with `age <= YODA`
Safe fix
9 9 | +42 > age # SIM300
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
12 |-YODA >= age # SIM300
12 |+age <= YODA # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
SIM300.py:13:1: SIM300 [*] Yoda conditions are discouraged, use `age == JediOrder.YODA` instead
|
11 | YODA > age # SIM300
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
| ^^^^^^^^^^^^^^^^^^^^^ SIM300
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
|
= help: Replace Yoda condition with `age == JediOrder.YODA`
Safe fix
10 10 | YODA == age # SIM300
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
13 |-JediOrder.YODA == age # SIM300
13 |+age == JediOrder.YODA # SIM300
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
16 16 | B or(B)<A[0][0]
SIM300.py:14:1: SIM300 [*] Yoda conditions are discouraged, use `(number - 100) > 0` instead
|
12 | YODA >= age # SIM300
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
| ^^^^^^^^^^^^^^^^^^ SIM300
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
|
= help: Replace Yoda condition with `(number - 100) > 0`
Safe fix
11 11 | YODA > age # SIM300
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 |-0 < (number - 100) # SIM300
14 |+(number - 100) > 0 # SIM300
15 15 | B<A[0][0]or B
16 16 | B or(B)<A[0][0]
17 17 |
SIM300.py:15:1: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > B` instead
|
13 | JediOrder.YODA == age # SIM300
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
| ^^^^^^^^^ SIM300
16 | B or(B)<A[0][0]
|
= help: Replace Yoda condition with `A[0][0] > B`
Safe fix
12 12 | YODA >= age # SIM300
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
15 |-B<A[0][0]or B
15 |+A[0][0] > B or B
16 16 | B or(B)<A[0][0]
17 17 |
18 18 | # Errors in preview
SIM300.py:16:5: SIM300 [*] Yoda conditions are discouraged, use `A[0][0] > (B)` instead
|
14 | 0 < (number - 100) # SIM300
15 | B<A[0][0]or B
16 | B or(B)<A[0][0]
| ^^^^^^^^^^^ SIM300
17 |
18 | # Errors in preview
|
= help: Replace Yoda condition with `A[0][0] > (B)`
Safe fix
13 13 | JediOrder.YODA == age # SIM300
14 14 | 0 < (number - 100) # SIM300
15 15 | B<A[0][0]or B
16 |-B or(B)<A[0][0]
16 |+B or A[0][0] > (B)
17 17 |
18 18 | # Errors in preview
19 19 | ['upper'] == UPPER_LIST
SIM300.py:19:1: SIM300 [*] Yoda conditions are discouraged, use `UPPER_LIST == ['upper']` instead
|
18 | # Errors in preview
19 | ['upper'] == UPPER_LIST
| ^^^^^^^^^^^^^^^^^^^^^^^ SIM300
20 | {} == DummyHandler.CONFIG
|
= help: Replace Yoda condition with `UPPER_LIST == ['upper']`
Safe fix
16 16 | B or(B)<A[0][0]
17 17 |
18 18 | # Errors in preview
19 |-['upper'] == UPPER_LIST
19 |+UPPER_LIST == ['upper']
20 20 | {} == DummyHandler.CONFIG
21 21 |
22 22 | # Errors in stable
SIM300.py:20:1: SIM300 [*] Yoda conditions are discouraged, use `DummyHandler.CONFIG == {}` instead
|
18 | # Errors in preview
19 | ['upper'] == UPPER_LIST
20 | {} == DummyHandler.CONFIG
| ^^^^^^^^^^^^^^^^^^^^^^^^^ SIM300
21 |
22 | # Errors in stable
|
= help: Replace Yoda condition with `DummyHandler.CONFIG == {}`
Safe fix
17 17 |
18 18 | # Errors in preview
19 19 | ['upper'] == UPPER_LIST
20 |-{} == DummyHandler.CONFIG
20 |+DummyHandler.CONFIG == {}
21 21 |
22 22 | # Errors in stable
23 23 | UPPER_LIST == ['upper']

View File

@@ -1,13 +1,12 @@
use anyhow::Result;
use rustc_hash::FxHashSet;
use ruff_diagnostics::Edit;
use ruff_python_ast::call_path::from_qualified_name;
use ruff_python_ast::helpers::{map_callable, map_subscript};
use ruff_python_ast::helpers::map_callable;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_codegen::Stylist;
use ruff_python_codegen::{Generator, Stylist};
use ruff_python_semantic::{
Binding, BindingId, BindingKind, NodeId, ResolvedReference, SemanticModel,
analyze, Binding, BindingKind, NodeId, ResolvedReference, SemanticModel,
};
use ruff_source_file::Locator;
use ruff_text_size::Ranged;
@@ -59,57 +58,17 @@ pub(crate) fn runtime_required_class(
false
}
/// Return `true` if a class is a subclass of a runtime-required base class.
fn runtime_required_base_class(
class_def: &ast::StmtClassDef,
base_classes: &[String],
semantic: &SemanticModel,
) -> bool {
fn inner(
class_def: &ast::StmtClassDef,
base_classes: &[String],
semantic: &SemanticModel,
seen: &mut FxHashSet<BindingId>,
) -> bool {
class_def.bases().iter().any(|expr| {
// If the base class is itself runtime-required, then this is too.
// Ex) `class Foo(BaseModel): ...`
if semantic
.resolve_call_path(map_subscript(expr))
.is_some_and(|call_path| {
base_classes
.iter()
.any(|base_class| from_qualified_name(base_class) == call_path)
})
{
return true;
}
// If the base class extends a runtime-required class, then this does too.
// Ex) `class Bar(BaseModel): ...; class Foo(Bar): ...`
if let Some(id) = semantic.lookup_attribute(map_subscript(expr)) {
if seen.insert(id) {
let binding = semantic.binding(id);
if let Some(base_class) = binding
.kind
.as_class_definition()
.map(|id| &semantic.scopes[*id])
.and_then(|scope| scope.kind.as_class())
{
if inner(base_class, base_classes, semantic, seen) {
return true;
}
}
}
}
false
})
}
if base_classes.is_empty() {
return false;
}
inner(class_def, base_classes, semantic, &mut FxHashSet::default())
analyze::class::any_over_body(class_def, semantic, &|call_path| {
base_classes
.iter()
.any(|base_class| from_qualified_name(base_class) == call_path)
})
}
fn runtime_required_decorators(
@@ -215,6 +174,7 @@ pub(crate) fn quote_annotation(
semantic: &SemanticModel,
locator: &Locator,
stylist: &Stylist,
generator: Generator,
) -> Result<Edit> {
let expr = semantic.expression(node_id).expect("Expression not found");
if let Some(parent_id) = semantic.parent_expression_id(node_id) {
@@ -224,7 +184,7 @@ pub(crate) fn quote_annotation(
// If we're quoting the value of a subscript, we need to quote the entire
// expression. For example, when quoting `DataFrame` in `DataFrame[int]`, we
// should generate `"DataFrame[int]"`.
return quote_annotation(parent_id, semantic, locator, stylist);
return quote_annotation(parent_id, semantic, locator, stylist, generator);
}
}
Some(Expr::Attribute(parent)) => {
@@ -232,7 +192,7 @@ pub(crate) fn quote_annotation(
// If we're quoting the value of an attribute, we need to quote the entire
// expression. For example, when quoting `DataFrame` in `pd.DataFrame`, we
// should generate `"pd.DataFrame"`.
return quote_annotation(parent_id, semantic, locator, stylist);
return quote_annotation(parent_id, semantic, locator, stylist, generator);
}
}
Some(Expr::Call(parent)) => {
@@ -240,7 +200,7 @@ pub(crate) fn quote_annotation(
// If we're quoting the function of a call, we need to quote the entire
// expression. For example, when quoting `DataFrame` in `DataFrame()`, we
// should generate `"DataFrame()"`.
return quote_annotation(parent_id, semantic, locator, stylist);
return quote_annotation(parent_id, semantic, locator, stylist, generator);
}
}
Some(Expr::BinOp(parent)) => {
@@ -248,27 +208,44 @@ pub(crate) fn quote_annotation(
// If we're quoting the left or right side of a binary operation, we need to
// quote the entire expression. For example, when quoting `DataFrame` in
// `DataFrame | Series`, we should generate `"DataFrame | Series"`.
return quote_annotation(parent_id, semantic, locator, stylist);
return quote_annotation(parent_id, semantic, locator, stylist, generator);
}
}
_ => {}
}
}
let annotation = locator.slice(expr);
// If the annotation already contains a quote, avoid attempting to re-quote it. For example:
// ```python
// from typing import Literal
//
// Set[Literal["Foo"]]
// ```
if annotation.contains('\'') || annotation.contains('"') {
let text = locator.slice(expr);
if text.contains('\'') || text.contains('"') {
return Err(anyhow::anyhow!("Annotation already contains a quote"));
}
// If we're quoting a name, we need to quote the entire expression.
// Quote the entire expression.
let quote = stylist.quote();
let annotation = format!("{quote}{annotation}{quote}");
Ok(Edit::range_replacement(annotation, expr.range()))
let annotation = generator.expr(expr);
Ok(Edit::range_replacement(
format!("{quote}{annotation}{quote}"),
expr.range(),
))
}
/// Filter out any [`Edit`]s that are completely contained by any other [`Edit`].
pub(crate) fn filter_contained(edits: Vec<Edit>) -> Vec<Edit> {
let mut filtered: Vec<Edit> = Vec::with_capacity(edits.len());
for edit in edits {
if filtered
.iter()
.all(|filtered_edit| !filtered_edit.range().contains_range(edit.range()))
{
filtered.push(edit);
}
}
filtered
}

View File

@@ -35,6 +35,8 @@ mod tests {
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_8.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("TCH004_9.py"))]
#[test_case(Rule::RuntimeImportInTypeCheckingBlock, Path::new("quote.py"))]
#[test_case(Rule::RuntimeStringUnion, Path::new("TCH006_1.py"))]
#[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("snapshot.py"))]
@@ -104,6 +106,35 @@ mod tests {
Ok(())
}
#[test_case(
Rule::TypingOnlyStandardLibraryImport,
Path::new("exempt_type_checking_1.py")
)]
#[test_case(
Rule::TypingOnlyStandardLibraryImport,
Path::new("exempt_type_checking_2.py")
)]
#[test_case(
Rule::TypingOnlyStandardLibraryImport,
Path::new("exempt_type_checking_3.py")
)]
fn exempt_type_checking(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy());
let diagnostics = test_path(
Path::new("flake8_type_checking").join(path).as_path(),
&settings::LinterSettings {
flake8_type_checking: super::settings::Settings {
exempt_modules: vec![],
strict: true,
..Default::default()
},
..settings::LinterSettings::for_rule(rule_code)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(
Rule::RuntimeImportInTypeCheckingBlock,
Path::new("runtime_evaluated_base_classes_1.py")

View File

@@ -1,7 +1,9 @@
pub(crate) use empty_type_checking_block::*;
pub(crate) use runtime_import_in_type_checking_block::*;
pub(crate) use runtime_string_union::*;
pub(crate) use typing_only_runtime_import::*;
mod empty_type_checking_block;
mod runtime_import_in_type_checking_block;
mod runtime_string_union;
mod typing_only_runtime_import;

View File

@@ -12,7 +12,7 @@ use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix;
use crate::importer::ImportedMembers;
use crate::rules::flake8_type_checking::helpers::quote_annotation;
use crate::rules::flake8_type_checking::helpers::{filter_contained, quote_annotation};
use crate::rules::flake8_type_checking::imports::ImportBinding;
/// ## What it does
@@ -262,32 +262,33 @@ pub(crate) fn runtime_import_in_type_checking_block(
/// Generate a [`Fix`] to quote runtime usages for imports in a type-checking block.
fn quote_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) -> Result<Fix> {
let mut quote_reference_edits = imports
.iter()
.flat_map(|ImportBinding { binding, .. }| {
binding.references.iter().filter_map(|reference_id| {
let reference = checker.semantic().reference(*reference_id);
if reference.context().is_runtime() {
Some(quote_annotation(
reference.expression_id()?,
checker.semantic(),
checker.locator(),
checker.stylist(),
))
} else {
None
}
let quote_reference_edits = filter_contained(
imports
.iter()
.flat_map(|ImportBinding { binding, .. }| {
binding.references.iter().filter_map(|reference_id| {
let reference = checker.semantic().reference(*reference_id);
if reference.context().is_runtime() {
Some(quote_annotation(
reference.expression_id()?,
checker.semantic(),
checker.locator(),
checker.stylist(),
checker.generator(),
))
} else {
None
}
})
})
})
.collect::<Result<Vec<_>>>()?;
let quote_reference_edit = quote_reference_edits
.pop()
.expect("Expected at least one reference");
Ok(
Fix::unsafe_edits(quote_reference_edit, quote_reference_edits).isolate(Checker::isolation(
checker.semantic().parent_statement_id(node_id),
)),
)
.collect::<Result<Vec<_>>>()?,
);
let mut rest = quote_reference_edits.into_iter();
let head = rest.next().expect("Expected at least one reference");
Ok(Fix::unsafe_edits(head, rest).isolate(Checker::isolation(
checker.semantic().parent_statement_id(node_id),
)))
}
/// Generate a [`Fix`] to remove runtime imports from a type-checking block.

View File

@@ -0,0 +1,95 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, Operator};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the presence of string literals in `X | Y`-style union types.
///
/// ## Why is this bad?
/// [PEP 604] introduced a new syntax for union type annotations based on the
/// `|` operator.
///
/// While Python's type annotations can typically be wrapped in strings to
/// avoid runtime evaluation, the use of a string member within an `X | Y`-style
/// union type will cause a runtime error.
///
/// Instead, remove the quotes, wrap the _entire_ union in quotes, or use
/// `from __future__ import annotations` to disable runtime evaluation of
/// annotations entirely.
///
/// ## Example
/// ```python
/// var: str | "int"
/// ```
///
/// Use instead:
/// ```python
/// var: str | int
/// ```
///
/// Or, extend the quotes to include the entire union:
/// ```python
/// var: "str | int"
/// ```
///
/// ## References
/// - [PEP 535](https://peps.python.org/pep-0563/)
/// - [PEP 604](https://peps.python.org/pep-0604/)
///
/// [PEP 604]: https://peps.python.org/pep-0604/
#[violation]
pub struct RuntimeStringUnion;
impl Violation for RuntimeStringUnion {
#[derive_message_formats]
fn message(&self) -> String {
format!("Invalid string member in `X | Y`-style union type")
}
}
/// TCH006
pub(crate) fn runtime_string_union(checker: &mut Checker, expr: &Expr) {
if !checker.semantic().in_type_definition() {
return;
}
if !checker.semantic().execution_context().is_runtime() {
return;
}
// Search for strings within the binary operator.
let mut strings = Vec::new();
traverse_op(expr, &mut strings);
for string in strings {
checker
.diagnostics
.push(Diagnostic::new(RuntimeStringUnion, string.range()));
}
}
/// Collect all string members in possibly-nested binary `|` expressions.
fn traverse_op<'a>(expr: &'a Expr, strings: &mut Vec<&'a Expr>) {
match expr {
Expr::StringLiteral(_) => {
strings.push(expr);
}
Expr::BytesLiteral(_) => {
strings.push(expr);
}
Expr::BinOp(ast::ExprBinOp {
left,
right,
op: Operator::BitOr,
..
}) => {
traverse_op(left, strings);
traverse_op(right, strings);
}
_ => {}
}
}

View File

@@ -12,7 +12,9 @@ use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::fix;
use crate::importer::ImportedMembers;
use crate::rules::flake8_type_checking::helpers::{is_typing_reference, quote_annotation};
use crate::rules::flake8_type_checking::helpers::{
filter_contained, is_typing_reference, quote_annotation,
};
use crate::rules::flake8_type_checking::imports::ImportBinding;
use crate::rules::isort::{categorize, ImportSection, ImportType};
@@ -471,41 +473,47 @@ fn fix_imports(checker: &Checker, node_id: NodeId, imports: &[ImportBinding]) ->
)?;
// Step 2) Add the import to a `TYPE_CHECKING` block.
let add_import_edit = checker.importer().typing_import_edit(
&ImportedMembers {
statement,
names: member_names.iter().map(AsRef::as_ref).collect(),
},
at,
checker.semantic(),
checker.source_type,
)?;
let (type_checking_edit, add_import_edit) = checker
.importer()
.typing_import_edit(
&ImportedMembers {
statement,
names: member_names.iter().map(AsRef::as_ref).collect(),
},
at,
checker.semantic(),
checker.source_type,
)?
.into_edits();
// Step 3) Quote any runtime usages of the referenced symbol.
let quote_reference_edits = imports
.iter()
.flat_map(|ImportBinding { binding, .. }| {
binding.references.iter().filter_map(|reference_id| {
let reference = checker.semantic().reference(*reference_id);
if reference.context().is_runtime() {
Some(quote_annotation(
reference.expression_id()?,
checker.semantic(),
checker.locator(),
checker.stylist(),
))
} else {
None
}
let quote_reference_edits = filter_contained(
imports
.iter()
.flat_map(|ImportBinding { binding, .. }| {
binding.references.iter().filter_map(|reference_id| {
let reference = checker.semantic().reference(*reference_id);
if reference.context().is_runtime() {
Some(quote_annotation(
reference.expression_id()?,
checker.semantic(),
checker.locator(),
checker.stylist(),
checker.generator(),
))
} else {
None
}
})
})
})
.collect::<Result<Vec<_>>>()?;
.collect::<Result<Vec<_>>>()?,
);
Ok(Fix::unsafe_edits(
remove_import_edit,
type_checking_edit,
add_import_edit
.into_edits()
.into_iter()
.chain(std::iter::once(remove_import_edit))
.chain(quote_reference_edits),
)
.isolate(Checker::isolation(

View File

@@ -18,5 +18,7 @@ quote.py:64:28: TCH004 [*] Quote references to `pandas.DataFrame`. Import is in
66 |- def func(value: DataFrame):
66 |+ def func(value: "DataFrame"):
67 67 | ...
68 68 |
69 69 |

View File

@@ -196,4 +196,146 @@ quote.py:54:24: TCH002 Move third-party import `pandas.DataFrame` into a type-ch
|
= help: Move into type-checking block
quote.py:71:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
70 | def f():
71 | from pandas import DataFrame, Series
| ^^^^^^^^^ TCH002
72 |
73 | def baz() -> DataFrame | Series:
|
= help: Move into type-checking block
Unsafe fix
1 |+from typing import TYPE_CHECKING
2 |+
3 |+if TYPE_CHECKING:
4 |+ from pandas import DataFrame, Series
1 5 | def f():
2 6 | from pandas import DataFrame
3 7 |
--------------------------------------------------------------------------------
68 72 |
69 73 |
70 74 | def f():
71 |- from pandas import DataFrame, Series
72 75 |
73 |- def baz() -> DataFrame | Series:
76 |+ def baz() -> "DataFrame | Series":
74 77 | ...
75 78 |
76 79 |
quote.py:71:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
70 | def f():
71 | from pandas import DataFrame, Series
| ^^^^^^ TCH002
72 |
73 | def baz() -> DataFrame | Series:
|
= help: Move into type-checking block
Unsafe fix
1 |+from typing import TYPE_CHECKING
2 |+
3 |+if TYPE_CHECKING:
4 |+ from pandas import DataFrame, Series
1 5 | def f():
2 6 | from pandas import DataFrame
3 7 |
--------------------------------------------------------------------------------
68 72 |
69 73 |
70 74 | def f():
71 |- from pandas import DataFrame, Series
72 75 |
73 |- def baz() -> DataFrame | Series:
76 |+ def baz() -> "DataFrame | Series":
74 77 | ...
75 78 |
76 79 |
quote.py:78:24: TCH002 [*] Move third-party import `pandas.DataFrame` into a type-checking block
|
77 | def f():
78 | from pandas import DataFrame, Series
| ^^^^^^^^^ TCH002
79 |
80 | def baz() -> (
|
= help: Move into type-checking block
Unsafe fix
1 |+from typing import TYPE_CHECKING
2 |+
3 |+if TYPE_CHECKING:
4 |+ from pandas import DataFrame, Series
1 5 | def f():
2 6 | from pandas import DataFrame
3 7 |
--------------------------------------------------------------------------------
75 79 |
76 80 |
77 81 | def f():
78 |- from pandas import DataFrame, Series
79 82 |
80 83 | def baz() -> (
81 |- DataFrame |
82 |- Series
84 |+ "DataFrame | Series"
83 85 | ):
84 86 | ...
85 87 |
86 88 | class C:
87 |- x: DataFrame[
88 |- int
89 |- ] = 1
89 |+ x: "DataFrame[int]" = 1
90 90 |
91 |- def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
91 |+ def func() -> "DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]":
92 92 | ...
quote.py:78:35: TCH002 [*] Move third-party import `pandas.Series` into a type-checking block
|
77 | def f():
78 | from pandas import DataFrame, Series
| ^^^^^^ TCH002
79 |
80 | def baz() -> (
|
= help: Move into type-checking block
Unsafe fix
1 |+from typing import TYPE_CHECKING
2 |+
3 |+if TYPE_CHECKING:
4 |+ from pandas import DataFrame, Series
1 5 | def f():
2 6 | from pandas import DataFrame
3 7 |
--------------------------------------------------------------------------------
75 79 |
76 80 |
77 81 | def f():
78 |- from pandas import DataFrame, Series
79 82 |
80 83 | def baz() -> (
81 |- DataFrame |
82 |- Series
84 |+ "DataFrame | Series"
83 85 | ):
84 86 | ...
85 87 |
86 88 | class C:
87 |- x: DataFrame[
88 |- int
89 |- ] = 1
89 |+ x: "DataFrame[int]" = 1
90 90 |
91 |- def func() -> DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]:
91 |+ def func() -> "DataFrame[[DataFrame[_P, _R]], DataFrame[_P, _R]]":
92 92 | ...

View File

@@ -0,0 +1,12 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TCH006_1.py:18:30: TCH006 Invalid string member in `X | Y`-style union type
|
16 | type A = Value["int" | str] # OK
17 |
18 | OldS = TypeVar('OldS', int | 'str', str) # TCH006
| ^^^^^ TCH006
|

View File

@@ -0,0 +1,41 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
TCH006_2.py:4:4: TCH006 Invalid string member in `X | Y`-style union type
|
4 | x: "int" | str # TCH006
| ^^^^^ TCH006
5 | x: ("int" | str) | "bool" # TCH006
|
TCH006_2.py:5:5: TCH006 Invalid string member in `X | Y`-style union type
|
4 | x: "int" | str # TCH006
5 | x: ("int" | str) | "bool" # TCH006
| ^^^^^ TCH006
|
TCH006_2.py:5:20: TCH006 Invalid string member in `X | Y`-style union type
|
4 | x: "int" | str # TCH006
5 | x: ("int" | str) | "bool" # TCH006
| ^^^^^^ TCH006
|
TCH006_2.py:12:20: TCH006 Invalid string member in `X | Y`-style union type
|
12 | z: list[str, str | "int"] = [] # TCH006
| ^^^^^ TCH006
13 |
14 | type A = Value["int" | str] # OK
|
TCH006_2.py:16:30: TCH006 Invalid string member in `X | Y`-style union type
|
14 | type A = Value["int" | str] # OK
15 |
16 | OldS = TypeVar('OldS', int | 'str', str) # TCH006
| ^^^^^ TCH006
|

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
exempt_type_checking_1.py:5:20: TCH003 [*] Move standard library import `typing.Final` into a type-checking block
|
3 | from __future__ import annotations
4 |
5 | from typing import Final
| ^^^^^ TCH003
6 |
7 | Const: Final[dict] = {}
|
= help: Move into type-checking block
Unsafe fix
2 2 |
3 3 | from __future__ import annotations
4 4 |
5 |-from typing import Final
5 |+from typing import TYPE_CHECKING
6 |+
7 |+if TYPE_CHECKING:
8 |+ from typing import Final
6 9 |
7 10 | Const: Final[dict] = {}

View File

@@ -0,0 +1,27 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
exempt_type_checking_2.py:5:20: TCH003 [*] Move standard library import `typing.Final` into a type-checking block
|
3 | from __future__ import annotations
4 |
5 | from typing import Final, TYPE_CHECKING
| ^^^^^ TCH003
6 |
7 | Const: Final[dict] = {}
|
= help: Move into type-checking block
Unsafe fix
2 2 |
3 3 | from __future__ import annotations
4 4 |
5 |-from typing import Final, TYPE_CHECKING
5 |+from typing import TYPE_CHECKING
6 |+
7 |+if TYPE_CHECKING:
8 |+ from typing import Final
6 9 |
7 10 | Const: Final[dict] = {}

View File

@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs
---
exempt_type_checking_3.py:5:20: TCH003 [*] Move standard library import `typing.Final` into a type-checking block
|
3 | from __future__ import annotations
4 |
5 | from typing import Final, Mapping
| ^^^^^ TCH003
6 |
7 | Const: Final[dict] = {}
|
= help: Move into type-checking block
Unsafe fix
2 2 |
3 3 | from __future__ import annotations
4 4 |
5 |-from typing import Final, Mapping
5 |+from typing import Mapping
6 |+from typing import TYPE_CHECKING
7 |+
8 |+if TYPE_CHECKING:
9 |+ from typing import Final
6 10 |
7 11 | Const: Final[dict] = {}

View File

@@ -35,7 +35,8 @@ mod tests {
#[test_case(Rule::LineTooLong, Path::new("E501_3.py"))]
#[test_case(Rule::MixedSpacesAndTabs, Path::new("E101.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E40.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_0.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402_1.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.ipynb"))]
#[test_case(Rule::MultipleImportsOnOneLine, Path::new("E40.py"))]
#[test_case(Rule::MultipleStatementsOnOneLineColon, Path::new("E70.py"))]
@@ -65,7 +66,7 @@ mod tests {
}
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.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<()> {
let snapshot = format!(

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