Compare commits

...

41 Commits

Author SHA1 Message Date
Charlie Marsh
92fa77ee53 Avoid mutable-class-default (RUF012) for fully untyped classes 2023-12-12 16:48:49 -05:00
Dhruv Manilawala
6c0068eeec Remove ExprFormattedValue formatting impl (#9108) 2023-12-12 21:16:01 +00:00
Samuel Cormier-Iijima
c306f85691 F841: support fixing unused assignments in tuples by renaming variables (#9107)
## Summary

A fairly common pattern which triggers F841 is unused variables from
tuple assignments, e.g.:

    user, created = User.objects.get_or_create(...)
          ^ F841: Local variable `created` is assigned to but never used

This error is currently not auto-fixable.

This PR adds support for fixing the error automatically by renaming the
unused variable to have a leading underscore (i.e. `_created`) **iff**
the `dummy-variable-rgx` setting would match it.

I considered using `renamers::Renamer` here, but because by the nature
of the error there should be no references to it, that seemed like
overkill. Also note that the fix might break by shadowing the new name
if it is already used elsewhere in the scope. I left it as is because

1. the renamed variable matches the "unused" regex, so it should
hopefully not already be used,
2. the fix is marked as unsafe so it should be reviewed manually
anyways, and
3. I'm not actually sure how to check the scope for the new variable
name 😅
2023-12-12 13:23:46 -05:00
Andrew Gallant
b972455ac7 ruff_python_formatter: implement "dynamic" line width mode for docstring code formatting (#9098)
## Summary

This PR changes the internal `docstring-code-line-width` setting to
additionally accept a string value `dynamic`. When `dynamic` is set, the
line width is dynamically adjusted when reformatting code snippets in
docstrings based on the indent level of the docstring. The result is
that the reformatted lines from the code snippet should not exceed the
"global" line width configuration for the surrounding source.

This PR does not change the default behavior, although I suspect the
default should probably be `dynamic`.

## Test Plan

I added a new configuration to the existing docstring code tests and
also added a new set of tests dedicated to the new `dynamic` mode.
2023-12-12 09:58:07 -05:00
Micha Reiser
5559827a78 Use the latest poetry ref in the ecosystem formatter check script (#9104) 2023-12-12 06:32:47 +00:00
Shantanu
cb8eea64a8 [pylint] Add fix for subprocess-run-without-check (PLW1510) (#6708) 2023-12-12 05:08:17 +00:00
Zanie Blue
8e9bf84047 Hide unsafe fix suggestions when explicitly disabled (#9095)
Hides hints about unsafe fixes when they are disabled e.g. with
`--no-unsafe-fixes` or `unsafe-fixes = false`. By default, unsafe fix
hints are still displayed. This seems like a nice way to remove the nag
for users who have chosen not to apply unsafe fixes.

Inspired by comment at
https://github.com/astral-sh/ruff/issues/9063#issuecomment-1850289675
2023-12-11 15:42:53 -06:00
Charlie Marsh
2993c342d2 Add beta note to the formatter docs (#9097)
Closes https://github.com/astral-sh/ruff/issues/9092.
2023-12-11 15:50:01 -05:00
dependabot[bot]
108260298f Bump actions/setup-python from 4 to 5 (#9084) 2023-12-11 15:49:32 -05:00
Tuomas Siipola
a53d59f6bd Support floating-point base in FURB163 (#9100)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

- Does this pull request include a summary of the change? (See below.)
- Does this pull request include a descriptive title?
- Does this pull request include references to any relevant issues?
-->

## Summary

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

Check floating-point numbers similarly to integers in FURB163. For
example, both `math.log(x, 10)` and `math.log(x, 10.0)` should be
changed to `math.log10(x)`.

## Test Plan

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

Added couple of test cases.
2023-12-11 15:47:37 -05:00
Samuel Cormier-Iijima
1026ece946 E274: allow tab indentation before keyword (#9099)
## Summary

E274 currently flags any keyword at the start of a line indented with
tabs. This turns out to be due to a bug in `Whitespace::trailing` that
never considers any whitespace containing a tab as indentation.

## Test Plan

Added a simple test case.
2023-12-11 15:25:31 -05:00
Charlie Marsh
f452bf8cad Allow matplotlib.use calls to intersperse imports (#9094)
This PR allows `matplotlib.use` calls to intersperse imports without
triggering `E402`. This is a pragmatic choice as it's common to require
`matplotlib.use` calls prior to importing from within `matplotlib`
itself.

Closes https://github.com/astral-sh/ruff/issues/9091.
2023-12-11 17:06:25 +00:00
Andrew Gallant
07380e0657 ruff_python_formatter: add docstring-code-line-width internal setting (#9055)
## Summary

This does the light plumbing necessary to add a new internal option that
permits setting the line width of code examples in docstrings. The plan
is to add the corresponding user facing knob in #8854.

Note that this effectively removes the `same-as-global` configuration
style discussed [in this
comment](https://github.com/astral-sh/ruff/issues/8855#issuecomment-1847230440).
It replaces it with the `{integer}` configuration style only.

There are a lot of commits here, but they are each tiny to make review
easier because of the changes to snapshots.

## Test Plan

I added a new docstring test configuration that sets
`docstring-code-line-width = 60` and examined the differences.
2023-12-11 08:20:59 -05:00
dependabot[bot]
3aa6a30395 Bump serde-wasm-bindgen from 0.6.1 to 0.6.3 (#9089)
Bumps
[serde-wasm-bindgen](https://github.com/RReverser/serde-wasm-bindgen)
from 0.6.1 to 0.6.3.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="e65f027ed7"><code>e65f027</code></a>
chore: Release</li>
<li><a
href="0cf8879399"><code>0cf8879</code></a>
Fix find-replace typo in docs</li>
<li><a
href="ff83666343"><code>ff83666</code></a>
Fix doc annotation</li>
<li><a
href="014e415d41"><code>014e415</code></a>
chore: Release</li>
<li><a
href="34aab01dcb"><code>34aab01</code></a>
Use Wasm target for docs.rs</li>
<li><a
href="455d55645f"><code>455d556</code></a>
More consistent docs + hide internal fields</li>
<li><a
href="ce7669e1d1"><code>ce7669e</code></a>
Use field indices for struct deserialization</li>
<li><a
href="a7e4c5b5aa"><code>a7e4c5b</code></a>
Bump deps</li>
<li><a
href="b4b4965c63"><code>b4b4965</code></a>
Don't use --profiling for benchmarks</li>
<li><a
href="3dfe7271ba"><code>3dfe727</code></a>
Speed up integer decoding</li>
<li>Additional commits viewable in <a
href="https://github.com/RReverser/serde-wasm-bindgen/compare/v0.6.1...v0.6.3">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde-wasm-bindgen&package-manager=cargo&previous-version=0.6.1&new-version=0.6.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-11 08:52:07 +00:00
dependabot[bot]
efb76ffa64 Bump is-macro from 0.3.0 to 0.3.1 (#9090)
Bumps [is-macro](https://github.com/kdy1/is-macro) from 0.3.0 to 0.3.1.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a
href="https://github.com/kdy1/is-macro/commits/v0.3.1">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=is-macro&package-manager=cargo&previous-version=0.3.0&new-version=0.3.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-11 08:51:57 +00:00
dependabot[bot]
4e461cbf03 Bump syn from 2.0.39 to 2.0.40 (#9086)
Bumps [syn](https://github.com/dtolnay/syn) from 2.0.39 to 2.0.40.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dtolnay/syn/releases">syn's
releases</a>.</em></p>
<blockquote>
<h2>2.0.40</h2>
<ul>
<li>Fix some edge cases of handling None-delimited groups in expression
parser (<a
href="https://redirect.github.com/dtolnay/syn/issues/1539">#1539</a>, <a
href="https://redirect.github.com/dtolnay/syn/issues/1541">#1541</a>, <a
href="https://redirect.github.com/dtolnay/syn/issues/1542">#1542</a>, <a
href="https://redirect.github.com/dtolnay/syn/issues/1543">#1543</a>, <a
href="https://redirect.github.com/dtolnay/syn/issues/1544">#1544</a>, <a
href="https://redirect.github.com/dtolnay/syn/issues/1545">#1545</a>)</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="cf7f40a96a"><code>cf7f40a</code></a>
Release 2.0.40</li>
<li><a
href="1ce8ccf5cd"><code>1ce8ccf</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/syn/issues/1538">#1538</a>
from dtolnay/testinvisible</li>
<li><a
href="d06bff8883"><code>d06bff8</code></a>
Add test for parsing Delimiter::None in expressions</li>
<li><a
href="9ec66d42bb"><code>9ec66d4</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/syn/issues/1545">#1545</a>
from dtolnay/groupedlet</li>
<li><a
href="384621acc6"><code>384621a</code></a>
Fix None-delimited let expression in stmt position</li>
<li><a
href="5325b6d171"><code>5325b6d</code></a>
Add test of let expr surrounded in None-delimited group</li>
<li><a
href="0ddfc27cf7"><code>0ddfc27</code></a>
Merge pull request <a
href="https://redirect.github.com/dtolnay/syn/issues/1544">#1544</a>
from dtolnay/nonedelimloop</li>
<li><a
href="9c99b3f62e"><code>9c99b3f</code></a>
Fix stmt boundary after None-delimited group containing loop</li>
<li><a
href="2781584ea8"><code>2781584</code></a>
Add test of None-delimited group containing loop in match arm</li>
<li><a
href="d332928084"><code>d332928</code></a>
Simplify token stream construction in Delimiter::None tests</li>
<li>Additional commits viewable in <a
href="https://github.com/dtolnay/syn/compare/2.0.39...2.0.40">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=syn&package-manager=cargo&previous-version=2.0.39&new-version=2.0.40)](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-11 08:50:22 +00:00
dependabot[bot]
93417b5644 Bump serde from 1.0.190 to 1.0.193 (#9087)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.190 to
1.0.193.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/serde-rs/serde/releases">serde's
releases</a>.</em></p>
<blockquote>
<h2>v1.0.193</h2>
<ul>
<li>Fix field names used for the deserialization of
<code>RangeFrom</code> and <code>RangeTo</code> (<a
href="https://redirect.github.com/serde-rs/serde/issues/2653">#2653</a>,
<a
href="https://redirect.github.com/serde-rs/serde/issues/2654">#2654</a>,
<a
href="https://redirect.github.com/serde-rs/serde/issues/2655">#2655</a>,
thanks <a
href="https://github.com/emilbonnek"><code>@​emilbonnek</code></a>)</li>
</ul>
<h2>v1.0.192</h2>
<ul>
<li>Allow internal tag field in untagged variant (<a
href="https://redirect.github.com/serde-rs/serde/issues/2646">#2646</a>,
thanks <a
href="https://github.com/robsdedude"><code>@​robsdedude</code></a>)</li>
</ul>
<h2>v1.0.191</h2>
<ul>
<li>Documentation improvements</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="44613c7d01"><code>44613c7</code></a>
Release 1.0.193</li>
<li><a
href="c706281df3"><code>c706281</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2655">#2655</a>
from dtolnay/rangestartend</li>
<li><a
href="65d75b8fe3"><code>65d75b8</code></a>
Add RangeFrom and RangeTo tests</li>
<li><a
href="332b0cba40"><code>332b0cb</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2654">#2654</a>
from dtolnay/rangestartend</li>
<li><a
href="8c4af41296"><code>8c4af41</code></a>
Fix more RangeFrom / RangeEnd mixups</li>
<li><a
href="24a78f071b"><code>24a78f0</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2653">#2653</a>
from emilbonnek/fix/range-to-from-de-mixup</li>
<li><a
href="c91c33436d"><code>c91c334</code></a>
Fix Range{From,To} deserialize mixup</li>
<li><a
href="2083f43a28"><code>2083f43</code></a>
Update ui test suite to nightly-2023-11-19</li>
<li><a
href="4676abdc9e"><code>4676abd</code></a>
Release 1.0.192</li>
<li><a
href="35700eb23e"><code>35700eb</code></a>
Merge pull request <a
href="https://redirect.github.com/serde-rs/serde/issues/2646">#2646</a>
from robsdedude/fix/2643/allow-tag-field-in-untagged</li>
<li>Additional commits viewable in <a
href="https://github.com/serde-rs/serde/compare/v1.0.190...v1.0.193">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=serde&package-manager=cargo&previous-version=1.0.190&new-version=1.0.193)](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-11 08:50:09 +00:00
dependabot[bot]
b8dd499b2a Bump filetime from 0.2.22 to 0.2.23 (#9088)
Bumps [filetime](https://github.com/alexcrichton/filetime) from 0.2.22
to 0.2.23.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="3e0deed809"><code>3e0deed</code></a>
Bump to 0.2.23</li>
<li><a
href="c681afa9df"><code>c681afa</code></a>
unix/birthtime: use broad support of libstd (<a
href="https://redirect.github.com/alexcrichton/filetime/issues/100">#100</a>)</li>
<li><a
href="b53dd77c67"><code>b53dd77</code></a>
Update redox_syscall to 0.4.1 and add Redox build to CI (<a
href="https://redirect.github.com/alexcrichton/filetime/issues/101">#101</a>)</li>
<li><a
href="d31dc01d6c"><code>d31dc01</code></a>
windows-sys 0.52 (<a
href="https://redirect.github.com/alexcrichton/filetime/issues/102">#102</a>)</li>
<li>See full diff in <a
href="https://github.com/alexcrichton/filetime/compare/0.2.22...0.2.23">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=filetime&package-manager=cargo&previous-version=0.2.22&new-version=0.2.23)](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-11 08:50:00 +00:00
konsti
cd2bf26845 Fix alpine CI (#9085)
The builds are failing with

> error: externally-managed-environment

I've added a venv
2023-12-11 09:47:38 +01:00
Simon Brugman
6e36dcfefe [refurb] Implement hashlib-digest-hex (FURB181) (#9077)
## Summary

Implementation of  Refurb FURB181
Part of https://github.com/astral-sh/ruff/issues/1348

## Test Plan

Test cases from Refurb
2023-12-10 02:00:11 +00:00
Charlie Marsh
febc69ab48 Avoid trailing comma for single-argument with positional separator (#9076)
## Summary

In https://github.com/astral-sh/ruff/pull/8921, we changed our parameter
formatting behavior to add a trailing comma whenever a single-argument
function breaks. This introduced a deviation in the case that a function
contains a single argument, but _also_ includes a positional-only or
keyword-only separator.

Closes https://github.com/astral-sh/ruff/issues/9074.
2023-12-09 18:03:31 -05:00
asafamr-mm
6c2613b44e Detect unused-asyncio-dangling-task (RUF006) on unused assignments (#9060)
## Summary

Fixes #8863 : Detect asyncio-dangling-task (RUF006) when discarding
return value

## Test Plan

added new two testcases, changed result of an old one that was made more
specific
2023-12-09 21:10:38 +00:00
Charlie Marsh
cb8a2f5615 Add fix for comment-related whitespace rules (#9075)
Closes https://github.com/astral-sh/ruff/issues/9067.

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

Closes https://github.com/astral-sh/ruff/issues/8119.
2023-12-09 15:18:07 -05:00
Sai-Suraj-27
b7b137abc8 Fix: Fixed a line in docs to make it more clear (#9073)
## Summary
I was using `ruff` on one of my repo's and found this small error. I
think the sentence can be made more clear.
2023-12-09 14:52:57 -05:00
Charlie Marsh
f69a35a021 Add fix for unexpected-spaces-around-keyword-parameter-equals (#9072)
Closes https://github.com/astral-sh/ruff/issues/9066.
2023-12-09 18:15:28 +00:00
Charlie Marsh
829a808526 Upgrade ahash (#9071)
The version we're using now was yanked.
2023-12-09 11:23:34 -05:00
Dimitri Papadopoulos Orfanos
85fc57e7f9 Fix typo in documentation (#9069)
## Summary

Fix a couple typos:
- I'm certain about `It's is` → `It is`.
- Not sure about `is it's` → `if it's` because I don't understand the
sentence.

## Test Plan

No tests.
2023-12-09 16:06:49 +00:00
Charlie Marsh
20e33bf514 Allow class names when apps.get_model is a non-string (#9065)
See:
https://github.com/astral-sh/ruff/issues/7675#issuecomment-1848206022
2023-12-08 22:59:05 -05:00
Dhruv Manilawala
b7dd2b5941 Allow EM fixes even if msg variable is defined (#9059)
This PR updates the `EM` rules to generate the auto-fix even if the
`msg` variable is defined in the current scope.

As discussed in https://github.com/astral-sh/ruff/issues/9052.
2023-12-08 15:16:15 -06:00
Charlie Marsh
e043bd46b5 Make math-constant rule more targeted (#9054)
## Summary

We now only flag `math.pi` if the value is in `[3.14, 3.15)`, and apply
similar rules to the other constants.

Closes https://github.com/astral-sh/ruff/issues/9049.
2023-12-08 12:42:18 -05:00
Micha Reiser
d0d88d9375 Fix handling of trailing target comment (#9051) 2023-12-08 05:00:36 +00:00
Andrew Gallant
a224f19903 ruff_python_formatter: add test for extraneous info string text (#9050)
@ofek asked [about this][ref]. I did specifically add support for it,
but neglected to add a test. This PR adds a test.

[ref]:
https://github.com/astral-sh/ruff/pull/9030#issuecomment-1846054764
2023-12-07 19:52:14 -05:00
Samuel Cormier-Iijima
2414298289 Add "preserve" quote-style to mimic Black's skip-string-normalization (#8822)
Co-authored-by: Micha Reiser <micha@reiser.io>
2023-12-07 23:59:22 +00:00
Dhruv Manilawala
6bbabceead Allow transparent cell magics (#8911)
## Summary

This PR updates the logic for `is_magic_cell` to include certain cell
magics. These cell magics would contain Python code following the line
defining the command. The code could define a variable which can then be
referenced in other cells. Currently, we would ignore the cell
completely leading to undefined-name violation.

As discussed in
https://github.com/astral-sh/ruff/issues/8354#issuecomment-1832221009

## Test Plan

Add new test case to validate this scenario.
2023-12-07 14:15:43 -06:00
Andrew Gallant
04ec11a73d ruff_python_formatter: support reformatting Markdown code blocks (#9030)
(This is not possible to actually use until
https://github.com/astral-sh/ruff/pull/8854 is merged.)

This commit slots in support for formatting Markdown fenced code
blocks[1]. With the refactoring done for reStructuredText previously,
this ended up being pretty easy to add. Markdown code blocks are also
quite a bit easier to parse and recognize correctly.

One point of contention in #8860 is whether to assume that unlabeled
Markdown code fences are Python or not by default. In this PR, we make
such an assumption. This follows what `rustdoc` does. The mitigation
here is that if an unlabeled code block isn't Python, then it probably
won't parse as Python. And we'll end up skipping it. So in the vast
majority of cases, the worst thing that can happen is a little bit of
wasted work.

Closes #8860

[1]: https://spec.commonmark.org/0.30/#fenced-code-blocks
2023-12-07 14:30:43 -05:00
Charlie Marsh
b021ede481 Allow sys.path modifications between imports (#9047)
## Summary

It's common to interleave a `sys.path` modification between imports at
the top of a file. This is a frequent cause of `# noqa: E402` false
positives, as seen in the ecosystem checks. This PR modifies E402 to
omit such modifications when determining the "import boundary".

(We could consider linting against `sys.path` modifications, but that
should be a separate rule.)

Closes: https://github.com/astral-sh/ruff/issues/5557.
2023-12-07 13:35:55 -05:00
Dhruv Manilawala
96ae9fe685 Introduce StringLike enum (#9016)
## Summary

This PR introduces a new `StringLike` enum which is a narrow type to
indicate string-like nodes. These includes the string literals, bytes
literals, and the literal parts of f-strings.

The main motivation behind this is to avoid repetition of rule calling
in the AST checker. We add a new `analyze::string_like` function which
takes in the enum and calls all the respective rule functions which
expects atleast 2 of the variants of this enum.

I'm open to discarding this if others think it's not that useful at this
stage as currently only 3 rules require these nodes.

As suggested
[here](https://github.com/astral-sh/ruff/pull/8835#discussion_r1414746934)
and
[here](https://github.com/astral-sh/ruff/pull/8835#discussion_r1414750204).

## Test Plan

`cargo test`
2023-12-07 16:39:13 +00:00
Dhruv Manilawala
cdac90ef68 New AST nodes for f-string elements (#8835)
Rebase of #6365 authored by @davidszotten.

## Summary

This PR updates the AST structure for an f-string elements.

The main **motivation** behind this change is to have a dedicated node
for the string part of an f-string. Previously, the existing
`ExprStringLiteral` node was used for this purpose which isn't exactly
correct. The `ExprStringLiteral` node should include the quotes as well
in the range but the f-string literal element doesn't include the quote
as it's a specific part within an f-string. For example,

```python
f"foo {x}"
# ^^^^
# This is the literal part of an f-string
```

The introduction of `FStringElement` enum is helpful which represent
either the literal part or the expression part of an f-string.

### Rule Updates

This means that there'll be two nodes representing a string depending on
the context. One for a normal string literal while the other is a string
literal within an f-string. The AST checker is updated to accommodate
this change. The rules which work on string literal are updated to check
on the literal part of f-string as well.

#### Notes

1. The `Expr::is_literal_expr` method would check for
`ExprStringLiteral` and return true if so. But now that we don't
represent the literal part of an f-string using that node, this improves
the method's behavior and confines to the actual expression. We do have
the `FStringElement::is_literal` method.
2. We avoid checking if we're in a f-string context before adding to
`string_type_definitions` because the f-string literal is now a
dedicated node and not part of `Expr`.
3. Annotations cannot use f-string so we avoid changing any rules which
work on annotation and checks for `ExprStringLiteral`.

## Test Plan

- All references of `Expr::StringLiteral` were checked to see if any of
the rules require updating to account for the f-string literal element
node.
- New test cases are added for rules which check against the literal
part of an f-string.
- Check the ecosystem results and ensure it remains unchanged.

## Performance

There's a performance penalty in the parser. The reason for this remains
unknown as it seems that the generated assembly code is now different
for the `__reduce154` function. The reduce function body is just popping
the `ParenthesizedExpr` on top of the stack and pushing it with the new
location.

- The size of `FStringElement` enum is the same as `Expr` which is what
it replaces in `FString::format_spec`
- The size of `FStringExpressionElement` is the same as
`ExprFormattedValue` which is what it replaces

I tried reducing the `Expr` enum from 80 bytes to 72 bytes but it hardly
resulted in any performance gain. The difference can be seen here:
- Original profile: https://share.firefox.dev/3Taa7ES
- Profile after boxing some node fields:
https://share.firefox.dev/3GsNXpD

### Backtracking

I tried backtracking the changes to see if any of the isolated change
produced this regression. The problem here is that the overall change is
so small that there's only a single checkpoint where I can backtrack and
that checkpoint results in the same regression. This checkpoint is to
revert using `Expr` to the `FString::format_spec` field. After this
point, the change would revert back to the original implementation.

## Review process

The review process is similar to #7927. The first set of commits update
the node structure, parser, and related AST files. Then, further commits
update the linter and formatter part to account for the AST change.

---------

Co-authored-by: David Szotten <davidszotten@gmail.com>
2023-12-07 10:28:05 -06:00
Eli Schwartz
fcc08894cf Fix documentation snafu that recommended invalid settings (#9018) 2023-12-07 05:01:55 +00:00
Charlie Marsh
ebc7ac31cb Avoid invalid combination of force-sort-within-types and lines-between-types (#9041)
Closes https://github.com/astral-sh/ruff/issues/8792.
2023-12-06 23:56:14 -05:00
Micha Reiser
981a0703ed Use double quotes for all docstrings, including single-quoted docstrings (#9020) 2023-12-07 04:41:00 +00:00
195 changed files with 15158 additions and 2614 deletions

View File

@@ -215,7 +215,7 @@ jobs:
}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
@@ -338,7 +338,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -362,7 +362,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Install Rust toolchain"
@@ -392,7 +392,7 @@ jobs:
MKDOCS_INSIDERS_SSH_KEY_EXISTS: ${{ secrets.MKDOCS_INSIDERS_SSH_KEY != '' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.8.0
@@ -455,7 +455,7 @@ jobs:
with:
repository: "astral-sh/ruff-lsp"
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}

View File

@@ -20,7 +20,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
- name: "Add SSH key"
if: ${{ env.MKDOCS_INSIDERS_SSH_KEY_EXISTS == 'true' }}
uses: webfactory/ssh-agent@v0.8.0

View File

@@ -20,7 +20,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -43,7 +43,7 @@ jobs:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -69,7 +69,7 @@ jobs:
target: [x64, x86]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.target }}
@@ -97,7 +97,7 @@ jobs:
target: [x86_64, i686]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -124,7 +124,7 @@ jobs:
target: [aarch64, armv7, s390x, ppc64le, ppc64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Build wheels"
@@ -161,7 +161,7 @@ jobs:
- i686-unknown-linux-musl
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -197,7 +197,7 @@ jobs:
arch: armv7
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Build wheels"
@@ -237,7 +237,7 @@ jobs:
- uses: actions/download-artifact@v3
with:
name: wheels
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
- name: "Publish to PyPi"
env:
TWINE_USERNAME: __token__

View File

@@ -36,7 +36,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -63,7 +63,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -103,7 +103,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -151,7 +151,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: ${{ matrix.platform.arch }}
@@ -199,7 +199,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -258,7 +258,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -313,7 +313,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
architecture: x64
@@ -332,10 +332,10 @@ jobs:
image: alpine:latest
options: -v ${{ github.workspace }}:/io -w /io
run: |
apk add py3-pip
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links /io/dist/ --force-reinstall
ruff --help
python -m ruff --help
apk add python3
python -m venv .venv
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:
@@ -369,7 +369,7 @@ jobs:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.sha }}
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: "Prep README.md"
@@ -388,10 +388,11 @@ jobs:
distro: alpine_latest
githubToken: ${{ github.token }}
install: |
apk add py3-pip
apk add python3
run: |
pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
ruff check --help
python -m venv .venv
.venv/bin/pip3 install ${{ env.PACKAGE_NAME }} --no-index --find-links dist/ --force-reinstall
.venv/bin/ruff check --help
- name: "Upload wheels"
uses: actions/upload-artifact@v3
with:

153
Cargo.lock generated
View File

@@ -16,14 +16,15 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "ahash"
version = "0.8.3"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
@@ -381,7 +382,7 @@ dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -606,7 +607,7 @@ dependencies = [
"proc-macro2",
"quote",
"strsim",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -617,7 +618,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [
"darling_core",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -790,14 +791,14 @@ dependencies = [
[[package]]
name = "filetime"
version = "0.2.22"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0"
checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd"
dependencies = [
"cfg-if",
"libc",
"redox_syscall 0.3.5",
"windows-sys 0.48.0",
"redox_syscall 0.4.1",
"windows-sys 0.52.0",
]
[[package]]
@@ -1122,15 +1123,15 @@ dependencies = [
[[package]]
name = "is-macro"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4467ed1321b310c2625c5aa6c1b1ffc5de4d9e42668cf697a08fb033ee8265e"
checksum = "bc74b7abae208af9314a406bd7dcc65091230b6e749c09e07a645885fecf34f9"
dependencies = [
"Inflector",
"pmutil 0.6.1",
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -1708,7 +1709,7 @@ checksum = "52a40bc70c2c58040d2d8b167ba9a5ff59fc9dab7ad44771cfde3dcfde7a09c6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -2269,7 +2270,7 @@ dependencies = [
"proc-macro2",
"quote",
"ruff_python_trivia",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -2676,18 +2677,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090"
[[package]]
name = "serde"
version = "1.0.190"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
checksum = "25dd9975e68d0cb5aa1120c288333fc98731bd1dd12f561e468ea4728c042b89"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.1"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17ba92964781421b6cef36bf0d7da26d201e96d84e1b10e7ae6ed416e516906d"
checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132"
dependencies = [
"js-sys",
"serde",
@@ -2696,13 +2697,13 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.190"
version = "1.0.193"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
checksum = "43576ca501357b9b071ac53cdc7da8ef0cbd9493d8df094cd821777ea6e894d3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -2764,7 +2765,7 @@ dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -2868,7 +2869,7 @@ dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -2884,9 +2885,9 @@ dependencies = [
[[package]]
name = "syn"
version = "2.0.39"
version = "2.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
checksum = "13fa70a4ee923979ffb522cacce59d34421ebdea5625e1073c4326ef9d2dd42e"
dependencies = [
"proc-macro2",
"quote",
@@ -2973,7 +2974,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -2985,7 +2986,7 @@ dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
"test-case-core",
]
@@ -3006,7 +3007,7 @@ checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -3168,7 +3169,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -3386,7 +3387,7 @@ checksum = "f49e7f3f3db8040a100710a11932239fd30697115e2ba4107080d8252939845e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -3480,7 +3481,7 @@ dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
"wasm-bindgen-shared",
]
@@ -3514,7 +3515,7 @@ checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
@@ -3547,7 +3548,7 @@ checksum = "493fcbab756bb764fa37e6bee8cec2dd709eb4273d06d0c282a5e74275ded735"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.39",
"syn 2.0.40",
]
[[package]]
@@ -3644,6 +3645,15 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.0",
]
[[package]]
name = "windows-targets"
version = "0.42.2"
@@ -3674,6 +3684,21 @@ dependencies = [
"windows_x86_64_msvc 0.48.5",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm 0.52.0",
"windows_aarch64_msvc 0.52.0",
"windows_i686_gnu 0.52.0",
"windows_i686_msvc 0.52.0",
"windows_x86_64_gnu 0.52.0",
"windows_x86_64_gnullvm 0.52.0",
"windows_x86_64_msvc 0.52.0",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
@@ -3686,6 +3711,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -3698,6 +3729,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -3710,6 +3747,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -3722,6 +3765,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -3734,6 +3783,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -3746,6 +3801,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -3758,6 +3819,12 @@ version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"
[[package]]
name = "winnow"
version = "0.5.15"
@@ -3796,3 +3863,23 @@ checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1"
dependencies = [
"winapi",
]
[[package]]
name = "zerocopy"
version = "0.7.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "306dca4455518f1f31635ec308b6b3e4eb1b11758cefafc782827d0aa7acb5c7"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be912bf68235a88fbefd1b73415cb218405958d1655b2ece9035a19920bdf6ba"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.40",
]

View File

@@ -17,12 +17,12 @@ 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" }
filetime = { version = "0.2.20" }
filetime = { version = "0.2.23" }
glob = { version = "0.3.1" }
globset = { version = "0.4.14" }
ignore = { version = "0.4.20" }
insta = { version = "1.34.0", feature = ["filters", "glob"] }
is-macro = { version = "0.3.0" }
is-macro = { version = "0.3.1" }
itertools = { version = "0.11.0" }
libcst = { version = "1.1.0", default-features = false }
log = { version = "0.4.17" }
@@ -34,7 +34,7 @@ quote = { version = "1.0.23" }
regex = { version = "1.10.2" }
rustc-hash = { version = "1.1.0" }
schemars = { version = "0.8.16" }
serde = { version = "1.0.190", features = ["derive"] }
serde = { version = "1.0.193", features = ["derive"] }
serde_json = { version = "1.0.108" }
shellexpand = { version = "3.0.0" }
similar = { version = "2.3.0", features = ["inline"] }
@@ -42,7 +42,7 @@ smallvec = { version = "1.11.2" }
static_assertions = "1.1.0"
strum = { version = "0.25.0", features = ["strum_macros"] }
strum_macros = { version = "0.25.3" }
syn = { version = "2.0.39" }
syn = { version = "2.0.40" }
test-case = { version = "3.2.1" }
thiserror = { version = "1.0.50" }
toml = { version = "0.7.8" }

View File

@@ -125,15 +125,7 @@ impl Printer {
if let Some(fixables) = fixables {
let fix_prefix = format!("[{}]", "*".cyan());
if self.unsafe_fixes.is_enabled() {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the --fix option.",
fixables.applicable
)?;
}
} else {
if self.unsafe_fixes.is_hint() {
if fixables.applicable > 0 && fixables.unapplicable_unsafe > 0 {
let es = if fixables.unapplicable_unsafe == 1 {
""
@@ -163,6 +155,14 @@ impl Printer {
fixables.unapplicable_unsafe
)?;
}
} else {
if fixables.applicable > 0 {
writeln!(
writer,
"{fix_prefix} {} fixable with the --fix option.",
fixables.applicable
)?;
}
}
}
} else {

View File

@@ -1158,6 +1158,44 @@ fn check_hints_hidden_unsafe_fixes_with_no_safe_fixes() {
"###);
}
#[test]
fn check_no_hint_for_hidden_unsafe_fixes_when_disabled() {
let mut cmd = RuffCheck::default()
.args(["--select", "F601,UP034", "--no-unsafe-fixes"])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("x = {'a': 1, 'a': 1}\nprint(('foo'))\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
-:2:7: UP034 [*] Avoid extraneous parentheses
Found 2 errors.
[*] 1 fixable with the --fix option.
----- stderr -----
"###);
}
#[test]
fn check_no_hint_for_hidden_unsafe_fixes_with_no_safe_fixes_when_disabled() {
let mut cmd = RuffCheck::default()
.args(["--select", "F601", "--no-unsafe-fixes"])
.build();
assert_cmd_snapshot!(cmd
.pass_stdin("x = {'a': 1, 'a': 1}\n"),
@r###"
success: false
exit_code: 1
----- stdout -----
-:1:14: F601 Dictionary key literal `'a'` repeated
Found 1 error.
----- stderr -----
"###);
}
#[test]
fn check_shows_unsafe_fixes_with_opt_in() {
let mut cmd = RuffCheck::default()

View File

@@ -8,6 +8,7 @@ def func(address):
# Error
"0.0.0.0"
'0.0.0.0'
f"0.0.0.0"
# Error

View File

@@ -5,6 +5,9 @@ with open("/abc/tmp", "w") as f:
with open("/tmp/abc", "w") as f:
f.write("def")
with open(f"/tmp/abc", "w") as f:
f.write("def")
with open("/var/tmp/123", "w") as f:
f.write("def")

View File

@@ -27,7 +27,7 @@ def f_ok():
raise RuntimeError(msg)
def f_unfixable():
def f_msg_defined():
msg = "hello"
raise RuntimeError("This is an example exception")

View File

@@ -32,6 +32,7 @@ def f8(x: bytes = b"50 character byte stringgggggggggggggggggggggggggg\xff") ->
foo: str = "50 character stringggggggggggggggggggggggggggggggg"
bar: str = "51 character stringgggggggggggggggggggggggggggggggg"
baz: str = f"51 character stringgggggggggggggggggggggggggggggggg"
baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg"

View File

@@ -29,6 +29,10 @@ baz: bytes = b"50 character byte stringgggggggggggggggggggggggggg" # OK
qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
class Demo:
"""Docstrings are excluded from this rule. Some padding.""" # OK

View File

@@ -0,0 +1,4 @@
from a import x
import b
from c import y
import d

View File

@@ -55,3 +55,6 @@ def model_assign() -> None:
Bad = apps.get_model() # N806
Bad = apps.get_model(model_name="Stream") # N806
Address: Type = apps.get_model("zerver", variable) # OK
ValidationError = import_string(variable) # N806

View File

@@ -72,3 +72,15 @@ a = 42 # (Two spaces)
# EF Means test is giving error and Failing
#! Means test is segfaulting
# 8 Means test runs forever
#: Colon prefix is okay
###This is a variable ###
# We should strip the space, but preserve the hashes.
#: E266:1:3
## Foo
a = 1 ## Foo
a = 1 #:Foo

View File

@@ -60,3 +60,6 @@ def f():
if (a and
b):
pass
#: Okay
def f():
return 1

View File

@@ -19,21 +19,32 @@ if x > 0:
else:
import e
__some__magic = 1
import sys
sys.path.insert(0, "some/path")
import f
import matplotlib
matplotlib.use("Agg")
import g
__some__magic = 1
import h
def foo() -> None:
import e
import i
if __name__ == "__main__":
import g
import j
import h; import i
import k; import l
if __name__ == "__main__":
import j; \
import k
import m; \
import n

View File

@@ -3,6 +3,11 @@ import subprocess
# Errors.
subprocess.run("ls")
subprocess.run("ls", shell=True)
subprocess.run(
["ls"],
shell=False,
)
subprocess.run(["ls"], **kwargs)
# Non-errors.
subprocess.run("ls", check=True)

View File

@@ -5,3 +5,11 @@ A = 3.14 * r ** 2 # FURB152
C = 6.28 * r # FURB152
e = 2.71 # FURB152
r = 3.15 # OK
r = 3.141 # FURB152
r = 3.1415 # FURB152
e = 2.7 # OK

View File

@@ -16,6 +16,8 @@ special_log(1, 2)
special_log(1, 10)
special_log(1, math.e)
special_log(1, special_e)
math.log(1, 2.0)
math.log(1, 10.0)
# Ok.
math.log2(1)
@@ -45,3 +47,6 @@ def log(*args):
log(1, 2)
log(1, 10)
log(1, math.e)
math.log(1, 2.0001)
math.log(1, 10.0001)

View File

@@ -0,0 +1,57 @@
import hashlib
from hashlib import (
blake2b,
blake2s,
md5,
sha1,
sha3_224,
sha3_256,
sha3_384,
sha3_512,
sha224,
)
from hashlib import sha256
from hashlib import sha256 as hash_algo
from hashlib import sha384, sha512, shake_128, shake_256
# these will match
blake2b().digest().hex()
blake2s().digest().hex()
md5().digest().hex()
sha1().digest().hex()
sha224().digest().hex()
sha256().digest().hex()
sha384().digest().hex()
sha3_224().digest().hex()
sha3_256().digest().hex()
sha3_384().digest().hex()
sha3_512().digest().hex()
sha512().digest().hex()
shake_128().digest(10).hex()
shake_256().digest(10).hex()
hashlib.sha256().digest().hex()
sha256(b"text").digest().hex()
hash_algo().digest().hex()
# not yet supported
h = sha256()
h.digest().hex()
# these will not
sha256().digest()
sha256().digest().hex("_")
sha256().digest().hex(bytes_per_sep=4)
sha256().hexdigest()
class Hash:
def digest(self) -> bytes:
return b""
Hash().digest().hex()

View File

@@ -63,11 +63,29 @@ def f():
tasks = [asyncio.create_task(task) for task in tasks]
# OK (false negative)
# Error
def f():
task = asyncio.create_task(coordinator.ws_connect())
# Error
def f():
loop = asyncio.get_running_loop()
task: asyncio.Task = loop.create_task(coordinator.ws_connect())
# OK (potential false negative)
def f():
task = asyncio.create_task(coordinator.ws_connect())
background_tasks.add(task)
# OK
async def f():
task = asyncio.create_task(coordinator.ws_connect())
await task
# OK (potential false negative)
def f():
do_nothing_with_the_task(asyncio.create_task(coordinator.ws_connect()))
@@ -88,3 +106,19 @@ def f():
def f():
loop = asyncio.get_running_loop()
loop.do_thing(coordinator.ws_connect())
# OK
async def f():
task = unused = asyncio.create_task(coordinator.ws_connect())
await task
# OK (false negative)
async def f():
task = unused = asyncio.create_task(coordinator.ws_connect())
# OK
async def f():
task[i] = asyncio.create_task(coordinator.ws_connect())

View File

@@ -59,3 +59,7 @@ class F(BaseSettings):
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
class E:
without_annotation = []

View File

@@ -3,11 +3,12 @@ use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint};
use crate::rules::{flake8_import_conventions, flake8_pyi, pyflakes, pylint, ruff};
/// 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,
@@ -71,5 +72,12 @@ 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

@@ -356,6 +356,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::FString,
// flynt
Rule::StaticJoinToFString,
// refurb
Rule::HashlibDigestHex,
]) {
if let Expr::Attribute(ast::ExprAttribute { value, attr, .. }) = func.as_ref() {
let attr = attr.as_str();
@@ -581,6 +583,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::HashlibInsecureHashFunction) {
flake8_bandit::rules::hashlib_insecure_hash_functions(checker, call);
}
if checker.enabled(Rule::HashlibDigestHex) {
refurb::rules::hashlib_digest_hex(checker, call);
}
if checker.enabled(Rule::RequestWithoutTimeout) {
flake8_bandit::rules::request_without_timeout(checker, call);
}
@@ -1270,32 +1275,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
refurb::rules::math_constant(checker, number_literal);
}
}
Expr::BytesLiteral(_) => {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
Expr::StringLiteral(string) => {
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
if let Some(diagnostic) =
flake8_bandit::rules::hardcoded_bind_all_interfaces(string)
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::HardcodedTempFile) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, string);
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if checker.enabled(Rule::UnicodeKindPrefix) {
for string_part in string.value.parts() {
for string_part in value.parts() {
pyupgrade::rules::unicode_kind_prefix(checker, string_part);
}
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
}
Expr::IfExp(
if_exp @ ast::ExprIfExp {

View File

@@ -10,6 +10,7 @@ pub(super) use module::module;
pub(super) use parameter::parameter;
pub(super) use parameters::parameters;
pub(super) use statement::statement;
pub(super) use string_like::string_like;
pub(super) use suite::suite;
pub(super) use unresolved_references::unresolved_references;
@@ -25,5 +26,6 @@ mod module;
mod parameter;
mod parameters;
mod statement;
mod string_like;
mod suite;
mod unresolved_references;

View File

@@ -1571,7 +1571,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pylint::rules::named_expr_without_context(checker, value);
}
if checker.enabled(Rule::AsyncioDanglingTask) {
ruff::rules::asyncio_dangling_task(checker, value);
if let Some(diagnostic) =
ruff::rules::asyncio_dangling_task(value, checker.semantic())
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::RepeatedAppend) {
refurb::rules::repeated_append(checker, stmt);

View File

@@ -0,0 +1,20 @@
use ruff_python_ast::StringLike;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bandit, flake8_pyi};
/// Run lint rules over a [`StringLike`] syntax nodes.
pub(crate) fn string_like(string_like: StringLike, checker: &mut Checker) {
if checker.enabled(Rule::HardcodedBindAllInterfaces) {
flake8_bandit::rules::hardcoded_bind_all_interfaces(checker, string_like);
}
if checker.enabled(Rule::HardcodedTempFile) {
flake8_bandit::rules::hardcoded_tmp_directory(checker, string_like);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, string_like);
}
}
}

View File

@@ -44,12 +44,12 @@ use ruff_python_ast::helpers::{
};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::str::trailing_quote;
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::visitor::{walk_except_handler, walk_f_string_element, walk_pattern, Visitor};
use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Quote, Stylist};
use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::analyze::{imports, typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, Snapshot,
@@ -303,9 +303,12 @@ where
}
_ => {
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !self.semantic.seen_import_boundary()
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !helpers::in_nested_block(self.semantic.current_statements())
if !(self.semantic.seen_import_boundary()
|| helpers::is_assignment_to_a_dunder(stmt)
|| helpers::in_nested_block(self.semantic.current_statements())
|| imports::is_matplotlib_activation(stmt, self.semantic())
|| self.settings.preview.is_enabled()
&& imports::is_sys_path_modification(stmt, self.semantic()))
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@@ -815,8 +818,7 @@ where
fn visit_expr(&mut self, expr: &'b Expr) {
// Step 0: Pre-processing
if !self.semantic.in_f_string()
&& !self.semantic.in_typing_literal()
if !self.semantic.in_typing_literal()
&& !self.semantic.in_deferred_type_definition()
&& self.semantic.in_type_definition()
&& self.semantic.future_annotations()
@@ -1238,10 +1240,7 @@ where
}
}
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => {
if self.semantic.in_type_definition()
&& !self.semantic.in_typing_literal()
&& !self.semantic.in_f_string()
{
if self.semantic.in_type_definition() && !self.semantic.in_typing_literal() {
self.deferred.string_type_definitions.push((
expr.range(),
value.to_str(),
@@ -1271,6 +1270,13 @@ where
// Step 4: Analysis
analyze::expression(expr, self);
match expr {
Expr::StringLiteral(string_literal) => {
analyze::string_like(string_literal.into(), self);
}
Expr::BytesLiteral(bytes_literal) => analyze::string_like(bytes_literal.into(), self),
_ => {}
}
self.semantic.flags = flags_snapshot;
self.semantic.pop_node();
@@ -1326,17 +1332,6 @@ where
self.semantic.flags = flags_snapshot;
}
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::FString(ast::ExprFString { value, .. }) => {
for expr in value.elements() {
self.visit_expr(expr);
}
}
_ => unreachable!("Unexpected expression for format_spec"),
}
}
fn visit_parameters(&mut self, parameters: &'b Parameters) {
// Step 1: Binding.
// Bind, but intentionally avoid walking default expressions, as we handle them
@@ -1446,6 +1441,16 @@ where
.push((bound, self.semantic.snapshot()));
}
}
fn visit_f_string_element(&mut self, f_string_element: &'b ast::FStringElement) {
// Step 2: Traversal
walk_f_string_element(self, f_string_element);
// Step 4: Analysis
if let Some(literal) = f_string_element.as_literal() {
analyze::string_like(literal.into(), self);
}
}
}
impl<'a> Checker<'a> {

View File

@@ -965,6 +965,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Refurb, "169") => (RuleGroup::Preview, rules::refurb::rules::TypeNoneComparison),
(Refurb, "171") => (RuleGroup::Preview, rules::refurb::rules::SingleItemMembershipTest),
(Refurb, "177") => (RuleGroup::Preview, rules::refurb::rules::ImplicitCwd),
(Refurb, "181") => (RuleGroup::Preview, rules::refurb::rules::HashlibDigestHex),
// flake8-logging
(Flake8Logging, "001") => (RuleGroup::Preview, rules::flake8_logging::rules::DirectLoggerInstantiation),

View File

@@ -1,6 +1,9 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::ExprStringLiteral;
use ruff_python_ast::{self as ast, StringLike};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for hardcoded bindings to all network interfaces (`0.0.0.0`).
@@ -34,10 +37,16 @@ impl Violation for HardcodedBindAllInterfaces {
}
/// S104
pub(crate) fn hardcoded_bind_all_interfaces(string: &ExprStringLiteral) -> Option<Diagnostic> {
if string.value.to_str() == "0.0.0.0" {
Some(Diagnostic::new(HardcodedBindAllInterfaces, string.range))
} else {
None
pub(crate) fn hardcoded_bind_all_interfaces(checker: &mut Checker, string: StringLike) {
let is_bind_all_interface = match string {
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value == "0.0.0.0",
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value == "0.0.0.0",
StringLike::BytesLiteral(_) => return,
};
if is_bind_all_interface {
checker
.diagnostics
.push(Diagnostic::new(HardcodedBindAllInterfaces, string.range()));
}
}

View File

@@ -1,4 +1,5 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast::{self as ast, Expr, StringLike};
use ruff_text_size::Ranged;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -51,13 +52,19 @@ impl Violation for HardcodedTempFile {
}
/// S108
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprStringLiteral) {
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: StringLike) {
let value = match string {
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.to_str(),
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => value,
StringLike::BytesLiteral(_) => return,
};
if !checker
.settings
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| string.value.to_str().starts_with(prefix))
.any(|prefix| value.starts_with(prefix))
{
return;
}
@@ -76,8 +83,8 @@ pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, string: &ast::ExprS
checker.diagnostics.push(Diagnostic::new(
HardcodedTempFile {
string: string.value.to_string(),
string: value.to_string(),
},
string.range,
string.range(),
));
}

View File

@@ -7,6 +7,7 @@ S104.py:9:1: S104 Possible binding to all interfaces
9 | "0.0.0.0"
| ^^^^^^^^^ S104
10 | '0.0.0.0'
11 | f"0.0.0.0"
|
S104.py:10:1: S104 Possible binding to all interfaces
@@ -15,21 +16,30 @@ S104.py:10:1: S104 Possible binding to all interfaces
9 | "0.0.0.0"
10 | '0.0.0.0'
| ^^^^^^^^^ S104
11 | f"0.0.0.0"
|
S104.py:14:6: S104 Possible binding to all interfaces
S104.py:11:3: S104 Possible binding to all interfaces
|
13 | # Error
14 | func("0.0.0.0")
9 | "0.0.0.0"
10 | '0.0.0.0'
11 | f"0.0.0.0"
| ^^^^^^^ S104
|
S104.py:15:6: S104 Possible binding to all interfaces
|
14 | # Error
15 | func("0.0.0.0")
| ^^^^^^^^^ S104
|
S104.py:18:9: S104 Possible binding to all interfaces
S104.py:19:9: S104 Possible binding to all interfaces
|
17 | def my_func():
18 | x = "0.0.0.0"
18 | def my_func():
19 | x = "0.0.0.0"
| ^^^^^^^^^ S104
19 | print(x)
20 | print(x)
|

View File

@@ -10,22 +10,31 @@ S108.py:5:11: S108 Probable insecure usage of temporary file or directory: "/tmp
6 | f.write("def")
|
S108.py:8:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
S108.py:8:13: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
6 | f.write("def")
7 |
8 | with open("/var/tmp/123", "w") as f:
| ^^^^^^^^^^^^^^ S108
8 | with open(f"/tmp/abc", "w") as f:
| ^^^^^^^^ S108
9 | f.write("def")
|
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
|
9 | f.write("def")
10 |
11 | with open("/dev/shm/unit/test", "w") as f:
| ^^^^^^^^^^^^^^^^^^^^ S108
11 | with open("/var/tmp/123", "w") as f:
| ^^^^^^^^^^^^^^ S108
12 | f.write("def")
|
S108.py:14:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
|
12 | f.write("def")
13 |
14 | with open("/dev/shm/unit/test", "w") as f:
| ^^^^^^^^^^^^^^^^^^^^ S108
15 | f.write("def")
|

View File

@@ -10,30 +10,39 @@ S108.py:5:11: S108 Probable insecure usage of temporary file or directory: "/tmp
6 | f.write("def")
|
S108.py:8:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
S108.py:8:13: S108 Probable insecure usage of temporary file or directory: "/tmp/abc"
|
6 | f.write("def")
7 |
8 | with open("/var/tmp/123", "w") as f:
| ^^^^^^^^^^^^^^ S108
8 | with open(f"/tmp/abc", "w") as f:
| ^^^^^^^^ S108
9 | f.write("def")
|
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
S108.py:11:11: S108 Probable insecure usage of temporary file or directory: "/var/tmp/123"
|
9 | f.write("def")
10 |
11 | with open("/dev/shm/unit/test", "w") as f:
| ^^^^^^^^^^^^^^^^^^^^ S108
11 | with open("/var/tmp/123", "w") as f:
| ^^^^^^^^^^^^^^ S108
12 | f.write("def")
|
S108.py:15:11: S108 Probable insecure usage of temporary file or directory: "/foo/bar"
S108.py:14:11: S108 Probable insecure usage of temporary file or directory: "/dev/shm/unit/test"
|
14 | # not ok by config
15 | with open("/foo/bar", "w") as f:
12 | f.write("def")
13 |
14 | with open("/dev/shm/unit/test", "w") as f:
| ^^^^^^^^^^^^^^^^^^^^ S108
15 | f.write("def")
|
S108.py:18:11: S108 Probable insecure usage of temporary file or directory: "/foo/bar"
|
17 | # not ok by config
18 | with open("/foo/bar", "w") as f:
| ^^^^^^^^^^ S108
16 | f.write("def")
19 | f.write("def")
|

View File

@@ -1083,7 +1083,7 @@ pub(crate) fn fix_unnecessary_map(
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if matches!(object_type, ObjectType::Set | ObjectType::Dict) {
if parent.is_some_and(Expr::is_formatted_value_expr) {
if parent.is_some_and(Expr::is_f_string_expr) {
content = format!(" {content} ");
}
}

View File

@@ -191,15 +191,13 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
if let Some(indentation) =
whitespace::indentation(checker.locator(), stmt)
{
if checker.semantic().is_available("msg") {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
checker.diagnostics.push(diagnostic);
}
@@ -211,15 +209,13 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
let mut diagnostic = Diagnostic::new(FStringInException, first.range());
if let Some(indentation) = whitespace::indentation(checker.locator(), stmt)
{
if checker.semantic().is_available("msg") {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
checker.diagnostics.push(diagnostic);
}
@@ -236,15 +232,13 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
if let Some(indentation) =
whitespace::indentation(checker.locator(), stmt)
{
if checker.semantic().is_available("msg") {
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
diagnostic.set_fix(generate_fix(
stmt,
first,
indentation,
checker.stylist(),
checker.locator(),
));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -59,15 +59,26 @@ EM.py:22:24: EM103 [*] Exception must not use a `.format()` string directly, ass
24 25 |
25 26 | def f_ok():
EM.py:32:24: EM101 Exception must not use a string literal, assign to variable first
EM.py:32:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
30 | def f_unfixable():
30 | def f_msg_defined():
31 | msg = "hello"
32 | raise RuntimeError("This is an example exception")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101
|
= help: Assign to variable; remove string literal
Unsafe fix
29 29 |
30 30 | def f_msg_defined():
31 31 | msg = "hello"
32 |- raise RuntimeError("This is an example exception")
32 |+ msg = "This is an example exception"
33 |+ raise RuntimeError(msg)
33 34 |
34 35 |
35 36 | def f_msg_in_nested_scope():
EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
37 | msg = "hello"
@@ -88,7 +99,7 @@ EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variab
41 42 |
42 43 | def f_msg_in_parent_scope():
EM.py:46:28: EM101 Exception must not use a string literal, assign to variable first
EM.py:46:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
45 | def nested():
46 | raise RuntimeError("This is an example exception")
@@ -96,6 +107,17 @@ EM.py:46:28: EM101 Exception must not use a string literal, assign to variable f
|
= help: Assign to variable; remove string literal
Unsafe fix
43 43 | msg = "hello"
44 44 |
45 45 | def nested():
46 |- raise RuntimeError("This is an example exception")
46 |+ msg = "This is an example exception"
47 |+ raise RuntimeError(msg)
47 48 |
48 49 |
49 50 | def f_fix_indentation_check(foo):
EM.py:51:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
49 | def f_fix_indentation_check(foo):

View File

@@ -97,15 +97,26 @@ EM.py:22:24: EM103 [*] Exception must not use a `.format()` string directly, ass
24 25 |
25 26 | def f_ok():
EM.py:32:24: EM101 Exception must not use a string literal, assign to variable first
EM.py:32:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
30 | def f_unfixable():
30 | def f_msg_defined():
31 | msg = "hello"
32 | raise RuntimeError("This is an example exception")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ EM101
|
= help: Assign to variable; remove string literal
Unsafe fix
29 29 |
30 30 | def f_msg_defined():
31 31 | msg = "hello"
32 |- raise RuntimeError("This is an example exception")
32 |+ msg = "This is an example exception"
33 |+ raise RuntimeError(msg)
33 34 |
34 35 |
35 36 | def f_msg_in_nested_scope():
EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variable first
|
37 | msg = "hello"
@@ -126,7 +137,7 @@ EM.py:39:24: EM101 [*] Exception must not use a string literal, assign to variab
41 42 |
42 43 | def f_msg_in_parent_scope():
EM.py:46:28: EM101 Exception must not use a string literal, assign to variable first
EM.py:46:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
45 | def nested():
46 | raise RuntimeError("This is an example exception")
@@ -134,6 +145,17 @@ EM.py:46:28: EM101 Exception must not use a string literal, assign to variable f
|
= help: Assign to variable; remove string literal
Unsafe fix
43 43 | msg = "hello"
44 44 |
45 45 | def nested():
46 |- raise RuntimeError("This is an example exception")
46 |+ msg = "This is an example exception"
47 |+ raise RuntimeError(msg)
47 48 |
48 49 |
49 50 | def f_fix_indentation_check(foo):
EM.py:51:28: EM101 [*] Exception must not use a string literal, assign to variable first
|
49 | def f_fix_indentation_check(foo):

View File

@@ -1,8 +1,7 @@
use ruff_python_ast::{self as ast, Expr};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
use ruff_python_ast::{self as ast, StringLike};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
@@ -44,25 +43,27 @@ impl AlwaysFixableViolation for StringOrBytesTooLong {
}
/// PYI053
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, expr: &Expr) {
pub(crate) fn string_or_bytes_too_long(checker: &mut Checker, string: StringLike) {
// Ignore docstrings.
if is_docstring_stmt(checker.semantic().current_statement()) {
return;
}
let length = match expr {
Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
Expr::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
_ => return,
let length = match string {
StringLike::StringLiteral(ast::ExprStringLiteral { value, .. }) => value.chars().count(),
StringLike::BytesLiteral(ast::ExprBytesLiteral { value, .. }) => value.len(),
StringLike::FStringLiteral(ast::FStringLiteralElement { value, .. }) => {
value.chars().count()
}
};
if length <= 50 {
return;
}
let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, expr.range());
let mut diagnostic = Diagnostic::new(StringOrBytesTooLong, string.range());
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
"...".to_string(),
expr.range(),
string.range(),
)));
checker.diagnostics.push(diagnostic);
}

View File

@@ -90,7 +90,7 @@ PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters
30 | qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
31 |
32 | class Demo:
32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
|
= help: Replace with `...`
@@ -101,7 +101,28 @@ PYI053.pyi:30:14: PYI053 [*] String and bytes literals longer than 50 characters
30 |-qux: bytes = b"51 character byte stringggggggggggggggggggggggggggg\xff" # Error: PYI053
30 |+qux: bytes = ... # Error: PYI053
31 31 |
32 32 | class Demo:
33 33 | """Docstrings are excluded from this rule. Some padding.""" # OK
32 32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
33 33 |
PYI053.pyi:34:15: PYI053 [*] String and bytes literals longer than 50 characters are not permitted
|
32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
33 |
34 | fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ PYI053
35 |
36 | class Demo:
|
= help: Replace with `...`
Safe fix
31 31 |
32 32 | ffoo: str = f"50 character stringggggggggggggggggggggggggggggggg" # OK
33 33 |
34 |-fbar: str = f"51 character stringgggggggggggggggggggggggggggggggg" # Error: PYI053
34 |+fbar: str = f"..." # Error: PYI053
35 35 |
36 36 | class Demo:
37 37 | """Docstrings are excluded from this rule. Some padding.""" # OK

View File

@@ -33,7 +33,7 @@ use super::unittest_assert::UnittestAssert;
/// Checks for assertions that combine multiple independent conditions.
///
/// ## Why is this bad?
/// Composite assertion statements are harder debug upon failure, as the
/// Composite assertion statements are harder to debug upon failure, as the
/// failure message will not indicate which condition failed.
///
/// ## Example

View File

@@ -58,15 +58,25 @@ pub(super) fn is_empty_or_null_string(expr: &Expr) -> bool {
Expr::FString(ast::ExprFString { value, .. }) => {
value.parts().all(|f_string_part| match f_string_part {
ast::FStringPart::Literal(literal) => literal.is_empty(),
ast::FStringPart::FString(f_string) => {
f_string.values.iter().all(is_empty_or_null_string)
}
ast::FStringPart::FString(f_string) => f_string
.elements
.iter()
.all(is_empty_or_null_fstring_element),
})
}
_ => false,
}
}
fn is_empty_or_null_fstring_element(element: &ast::FStringElement) -> bool {
match element {
ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => value.is_empty(),
ast::FStringElement::Expression(ast::FStringExpressionElement { expression, .. }) => {
is_empty_or_null_string(expression)
}
}
}
pub(super) fn split_names(names: &str) -> Vec<&str> {
// Match the following pytest code:
// [x.strip() for x in argnames.split(",") if x.strip()]

View File

@@ -1,25 +1,23 @@
use ruff_python_ast::{self as ast, Arguments, ConversionFlag, Expr};
use ruff_text_size::TextRange;
/// Wrap an expression in a `FormattedValue` with no special formatting.
fn to_formatted_value_expr(inner: &Expr) -> Expr {
let node = ast::ExprFormattedValue {
value: Box::new(inner.clone()),
/// Wrap an expression in a [`ast::FStringElement::Expression`] with no special formatting.
fn to_f_string_expression_element(inner: &Expr) -> ast::FStringElement {
ast::FStringElement::Expression(ast::FStringExpressionElement {
expression: Box::new(inner.clone()),
debug_text: None,
conversion: ConversionFlag::None,
format_spec: None,
range: TextRange::default(),
};
node.into()
})
}
/// Convert a string to a constant string expression.
pub(super) fn to_constant_string(s: &str) -> Expr {
let node = ast::StringLiteral {
value: s.to_string(),
..ast::StringLiteral::default()
};
node.into()
/// Convert a string to a [`ast::FStringElement::Literal`].
pub(super) fn to_f_string_literal_element(s: &str) -> ast::FStringElement {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value: s.to_owned(),
range: TextRange::default(),
})
}
/// Figure out if `expr` represents a "simple" call
@@ -51,15 +49,19 @@ fn is_simple_callee(func: &Expr) -> bool {
}
/// Convert an expression to a f-string element (if it looks like a good idea).
pub(super) fn to_f_string_element(expr: &Expr) -> Option<Expr> {
pub(super) fn to_f_string_element(expr: &Expr) -> Option<ast::FStringElement> {
match expr {
// These are directly handled by `unparse_f_string_element`:
Expr::StringLiteral(_) | Expr::FString(_) | Expr::FormattedValue(_) => Some(expr.clone()),
Expr::StringLiteral(ast::ExprStringLiteral { value, range }) => {
Some(ast::FStringElement::Literal(ast::FStringLiteralElement {
value: value.to_string(),
range: *range,
}))
}
// These should be pretty safe to wrap in a formatted value.
Expr::NumberLiteral(_) | Expr::BooleanLiteral(_) | Expr::Name(_) | Expr::Attribute(_) => {
Some(to_formatted_value_expr(expr))
Some(to_f_string_expression_element(expr))
}
Expr::Call(_) if is_simple_call(expr) => Some(to_formatted_value_expr(expr)),
Expr::Call(_) if is_simple_call(expr) => Some(to_f_string_expression_element(expr)),
_ => None,
}
}

View File

@@ -78,7 +78,7 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
return Some(node.into());
}
let mut fstring_elems = Vec::with_capacity(joinees.len() * 2);
let mut f_string_elements = Vec::with_capacity(joinees.len() * 2);
let mut first = true;
for expr in joinees {
@@ -88,13 +88,13 @@ fn build_fstring(joiner: &str, joinees: &[Expr]) -> Option<Expr> {
return None;
}
if !std::mem::take(&mut first) {
fstring_elems.push(helpers::to_constant_string(joiner));
f_string_elements.push(helpers::to_f_string_literal_element(joiner));
}
fstring_elems.push(helpers::to_f_string_element(expr)?);
f_string_elements.push(helpers::to_f_string_element(expr)?);
}
let node = ast::FString {
values: fstring_elems,
elements: f_string_elements,
range: TextRange::default(),
};
Some(node.into())
@@ -127,7 +127,7 @@ pub(crate) fn static_join_to_fstring(checker: &mut Checker, expr: &Expr, joiner:
};
// Try to build the fstring (internally checks whether e.g. the elements are
// convertible to f-string parts).
// convertible to f-string elements).
let Some(new_expr) = build_fstring(joiner, joinees) else {
return;
};

View File

@@ -200,6 +200,7 @@ fn format_import_block(
// Add a blank lines between direct and from imports.
if settings.from_first
&& lines_between_types > 0
&& !settings.force_sort_within_sections
&& line_insertion == Some(LineInsertion::Necessary)
{
for _ in 0..lines_between_types {
@@ -225,6 +226,7 @@ fn format_import_block(
// Add a blank lines between direct and from imports.
if !settings.from_first
&& lines_between_types > 0
&& !settings.force_sort_within_sections
&& line_insertion == Some(LineInsertion::Necessary)
{
for _ in 0..lines_between_types {
@@ -722,6 +724,26 @@ mod tests {
Ok(())
}
#[test_case(Path::new("force_sort_within_sections_lines_between.py"))]
fn force_sort_within_sections_lines_between(path: &Path) -> Result<()> {
let snapshot = format!("force_sort_within_sections_{}", path.to_string_lossy());
let mut diagnostics = test_path(
Path::new("isort").join(path).as_path(),
&LinterSettings {
isort: super::settings::Settings {
force_sort_within_sections: true,
lines_between_types: 2,
..super::settings::Settings::default()
},
src: vec![test_resource_path("fixtures/isort")],
..LinterSettings::for_rule(Rule::UnsortedImports)
},
)?;
diagnostics.sort_by_key(Ranged::start);
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test_case(Path::new("comment.py"))]
#[test_case(Path::new("comments_and_newlines.py"))]
#[test_case(Path::new("docstring.py"))]

View File

@@ -112,7 +112,11 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti
arguments.find_argument("model_name", arguments.args.len().saturating_sub(1))
{
if let Some(string_literal) = argument.as_string_literal_expr() {
return string_literal.value.to_str() == name;
if string_literal.value.to_str() == name {
return true;
}
} else {
return true;
}
}
}
@@ -127,7 +131,9 @@ pub(super) fn is_django_model_import(name: &str, stmt: &Stmt, semantic: &Semanti
if let Some(argument) = arguments.find_argument("dotted_path", 0) {
if let Some(string_literal) = argument.as_string_literal_expr() {
if let Some((.., model)) = string_literal.value.to_str().rsplit_once('.') {
return model == name;
if model == name {
return true;
}
}
}
}

View File

@@ -52,6 +52,15 @@ N806.py:57:5: N806 Variable `Bad` in function should be lowercase
56 | Bad = apps.get_model() # N806
57 | Bad = apps.get_model(model_name="Stream") # N806
| ^^^ N806
58 |
59 | Address: Type = apps.get_model("zerver", variable) # OK
|
N806.py:60:5: N806 Variable `ValidationError` in function should be lowercase
|
59 | Address: Type = apps.get_model("zerver", variable) # OK
60 | ValidationError = import_string(variable) # N806
| ^^^^^^^^^^^^^^^ N806
|

View File

@@ -65,6 +65,7 @@ mod tests {
}
#[test_case(Rule::IsLiteral, Path::new("constant_literals.py"))]
#[test_case(Rule::ModuleImportNotAtTopOfFile, Path::new("E402.py"))]
#[test_case(Rule::TypeComparison, Path::new("E721.py"))]
fn preview_rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!(

View File

@@ -41,6 +41,10 @@ impl Violation for MultipleImportsOnOneLine {
/// According to [PEP 8], "imports are always put at the top of the file, just after any
/// module comments and docstrings, and before module globals and constants."
///
/// In [preview], this rule makes an exception for `sys.path` modifications,
/// allowing for `sys.path.insert`, `sys.path.append`, and similar
/// modifications between import statements.
///
/// ## Example
/// ```python
/// "One string"

View File

@@ -381,20 +381,16 @@ impl Whitespace {
}
}
if has_tabs {
if len == content.text_len() {
// All whitespace up to the start of the line -> Indent
(Self::None, TextSize::default())
} else if has_tabs {
(Self::Tab, len)
} else {
match count {
0 => (Self::None, TextSize::default()),
1 => (Self::Single, len),
_ => {
if len == content.text_len() {
// All whitespace up to the start of the line -> Indent
(Self::None, TextSize::default())
} else {
(Self::Many, len)
}
}
_ => (Self::Many, len),
}
}
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_parser::TokenKind;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -34,11 +34,15 @@ use crate::rules::pycodestyle::rules::logical_lines::{LogicalLine, LogicalLineTo
#[violation]
pub struct UnexpectedSpacesAroundKeywordParameterEquals;
impl Violation for UnexpectedSpacesAroundKeywordParameterEquals {
impl AlwaysFixableViolation for UnexpectedSpacesAroundKeywordParameterEquals {
#[derive_message_formats]
fn message(&self) -> String {
format!("Unexpected spaces around keyword / parameter equals")
}
fn fix_title(&self) -> String {
format!("Remove whitespace")
}
}
/// ## What it does
@@ -165,22 +169,31 @@ pub(crate) fn whitespace_around_named_parameter_equals(
}
}
} else {
// If there's space between the preceding token and the equals sign, report it.
if token.start() != prev_end {
context.push(
let mut diagnostic = Diagnostic::new(
UnexpectedSpacesAroundKeywordParameterEquals,
TextRange::new(prev_end, token.start()),
);
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(prev_end, token.start())));
context.push_diagnostic(diagnostic);
}
// If there's space between the equals sign and the following token, report it.
while let Some(next) = iter.peek() {
if next.kind() == TokenKind::NonLogicalNewline {
iter.next();
} else {
if next.start() != token.end() {
context.push(
let mut diagnostic = Diagnostic::new(
UnexpectedSpacesAroundKeywordParameterEquals,
TextRange::new(token.end(), next.start()),
);
diagnostic.set_fix(Fix::safe_edit(Edit::deletion(
token.end(),
next.start(),
)));
context.push_diagnostic(diagnostic);
}
break;
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_parser::TokenKind;
use ruff_python_trivia::PythonWhitespace;
@@ -66,11 +66,15 @@ impl AlwaysFixableViolation for TooFewSpacesBeforeInlineComment {
#[violation]
pub struct NoSpaceAfterInlineComment;
impl Violation for NoSpaceAfterInlineComment {
impl AlwaysFixableViolation for NoSpaceAfterInlineComment {
#[derive_message_formats]
fn message(&self) -> String {
format!("Inline comment should start with `# `")
}
fn fix_title(&self) -> String {
format!("Format space")
}
}
/// ## What it does
@@ -98,11 +102,15 @@ impl Violation for NoSpaceAfterInlineComment {
#[violation]
pub struct NoSpaceAfterBlockComment;
impl Violation for NoSpaceAfterBlockComment {
impl AlwaysFixableViolation for NoSpaceAfterBlockComment {
#[derive_message_formats]
fn message(&self) -> String {
format!("Block comment should start with `# `")
}
fn fix_title(&self) -> String {
format!("Format space")
}
}
/// ## What it does
@@ -130,11 +138,15 @@ impl Violation for NoSpaceAfterBlockComment {
#[violation]
pub struct MultipleLeadingHashesForBlockComment;
impl Violation for MultipleLeadingHashesForBlockComment {
impl AlwaysFixableViolation for MultipleLeadingHashesForBlockComment {
#[derive_message_formats]
fn message(&self) -> String {
format!("Too many leading `#` before block comment")
}
fn fix_title(&self) -> String {
format!("Remove leading `#`")
}
}
/// E261, E262, E265, E266
@@ -184,14 +196,30 @@ pub(crate) fn whitespace_before_comment(
if is_inline_comment {
if bad_prefix.is_some() || comment.chars().next().is_some_and(char::is_whitespace) {
context.push(NoSpaceAfterInlineComment, range);
let mut diagnostic = Diagnostic::new(NoSpaceAfterInlineComment, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format_leading_space(token_text),
range,
)));
context.push_diagnostic(diagnostic);
}
} else if let Some(bad_prefix) = bad_prefix {
if bad_prefix != '!' || !line.is_start_of_file() {
if bad_prefix != '#' {
context.push(NoSpaceAfterBlockComment, range);
let mut diagnostic = Diagnostic::new(NoSpaceAfterBlockComment, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format_leading_space(token_text),
range,
)));
context.push_diagnostic(diagnostic);
} else if !comment.is_empty() {
context.push(MultipleLeadingHashesForBlockComment, range);
let mut diagnostic =
Diagnostic::new(MultipleLeadingHashesForBlockComment, range);
diagnostic.set_fix(Fix::safe_edit(Edit::range_replacement(
format_leading_hashes(token_text),
range,
)));
context.push_diagnostic(diagnostic);
}
}
}
@@ -200,3 +228,17 @@ pub(crate) fn whitespace_before_comment(
}
}
}
/// Format a comment to have a single space after the `#`.
fn format_leading_space(comment: &str) -> String {
if let Some(rest) = comment.strip_prefix("#:") {
format!("#: {}", rest.trim_start())
} else {
format!("# {}", comment.trim_start_matches('#').trim_start())
}
}
/// Format a comment to strip multiple leading `#` characters.
fn format_leading_hashes(comment: &str) -> String {
format!("# {}", comment.trim_start_matches('#').trim_start())
}

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E25.py:2:12: E251 Unexpected spaces around keyword / parameter equals
E25.py:2:12: E251 [*] Unexpected spaces around keyword / parameter equals
|
1 | #: E251 E251
2 | def foo(bar = False):
@@ -9,8 +9,17 @@ E25.py:2:12: E251 Unexpected spaces around keyword / parameter equals
3 | '''Test function with an error in declaration'''
4 | pass
|
= help: Remove whitespace
E25.py:2:14: E251 Unexpected spaces around keyword / parameter equals
Safe fix
1 1 | #: E251 E251
2 |-def foo(bar = False):
2 |+def foo(bar= False):
3 3 | '''Test function with an error in declaration'''
4 4 | pass
5 5 | #: E251
E25.py:2:14: E251 [*] Unexpected spaces around keyword / parameter equals
|
1 | #: E251 E251
2 | def foo(bar = False):
@@ -18,8 +27,17 @@ E25.py:2:14: E251 Unexpected spaces around keyword / parameter equals
3 | '''Test function with an error in declaration'''
4 | pass
|
= help: Remove whitespace
E25.py:6:9: E251 Unexpected spaces around keyword / parameter equals
Safe fix
1 1 | #: E251 E251
2 |-def foo(bar = False):
2 |+def foo(bar =False):
3 3 | '''Test function with an error in declaration'''
4 4 | pass
5 5 | #: E251
E25.py:6:9: E251 [*] Unexpected spaces around keyword / parameter equals
|
4 | pass
5 | #: E251
@@ -28,8 +46,19 @@ E25.py:6:9: E251 Unexpected spaces around keyword / parameter equals
7 | #: E251
8 | foo(bar =True)
|
= help: Remove whitespace
E25.py:8:8: E251 Unexpected spaces around keyword / parameter equals
Safe fix
3 3 | '''Test function with an error in declaration'''
4 4 | pass
5 5 | #: E251
6 |-foo(bar= True)
6 |+foo(bar=True)
7 7 | #: E251
8 8 | foo(bar =True)
9 9 | #: E251 E251
E25.py:8:8: E251 [*] Unexpected spaces around keyword / parameter equals
|
6 | foo(bar= True)
7 | #: E251
@@ -38,8 +67,19 @@ E25.py:8:8: E251 Unexpected spaces around keyword / parameter equals
9 | #: E251 E251
10 | foo(bar = True)
|
= help: Remove whitespace
E25.py:10:8: E251 Unexpected spaces around keyword / parameter equals
Safe fix
5 5 | #: E251
6 6 | foo(bar= True)
7 7 | #: E251
8 |-foo(bar =True)
8 |+foo(bar=True)
9 9 | #: E251 E251
10 10 | foo(bar = True)
11 11 | #: E251
E25.py:10:8: E251 [*] Unexpected spaces around keyword / parameter equals
|
8 | foo(bar =True)
9 | #: E251 E251
@@ -48,8 +88,19 @@ E25.py:10:8: E251 Unexpected spaces around keyword / parameter equals
11 | #: E251
12 | y = bar(root= "sdasd")
|
= help: Remove whitespace
E25.py:10:10: E251 Unexpected spaces around keyword / parameter equals
Safe fix
7 7 | #: E251
8 8 | foo(bar =True)
9 9 | #: E251 E251
10 |-foo(bar = True)
10 |+foo(bar= True)
11 11 | #: E251
12 12 | y = bar(root= "sdasd")
13 13 | #: E251:2:29
E25.py:10:10: E251 [*] Unexpected spaces around keyword / parameter equals
|
8 | foo(bar =True)
9 | #: E251 E251
@@ -58,8 +109,19 @@ E25.py:10:10: E251 Unexpected spaces around keyword / parameter equals
11 | #: E251
12 | y = bar(root= "sdasd")
|
= help: Remove whitespace
E25.py:12:14: E251 Unexpected spaces around keyword / parameter equals
Safe fix
7 7 | #: E251
8 8 | foo(bar =True)
9 9 | #: E251 E251
10 |-foo(bar = True)
10 |+foo(bar =True)
11 11 | #: E251
12 12 | y = bar(root= "sdasd")
13 13 | #: E251:2:29
E25.py:12:14: E251 [*] Unexpected spaces around keyword / parameter equals
|
10 | foo(bar = True)
11 | #: E251
@@ -68,8 +130,19 @@ E25.py:12:14: E251 Unexpected spaces around keyword / parameter equals
13 | #: E251:2:29
14 | parser.add_argument('--long-option',
|
= help: Remove whitespace
E25.py:15:29: E251 Unexpected spaces around keyword / parameter equals
Safe fix
9 9 | #: E251 E251
10 10 | foo(bar = True)
11 11 | #: E251
12 |-y = bar(root= "sdasd")
12 |+y = bar(root="sdasd")
13 13 | #: E251:2:29
14 14 | parser.add_argument('--long-option',
15 15 | default=
E25.py:15:29: E251 [*] Unexpected spaces around keyword / parameter equals
|
13 | #: E251:2:29
14 | parser.add_argument('--long-option',
@@ -80,8 +153,20 @@ E25.py:15:29: E251 Unexpected spaces around keyword / parameter equals
17 | #: E251:1:45
18 | parser.add_argument('--long-option', default
|
= help: Remove whitespace
E25.py:18:45: E251 Unexpected spaces around keyword / parameter equals
Safe fix
12 12 | y = bar(root= "sdasd")
13 13 | #: E251:2:29
14 14 | parser.add_argument('--long-option',
15 |- default=
16 |- "/rather/long/filesystem/path/here/blah/blah/blah")
15 |+ default="/rather/long/filesystem/path/here/blah/blah/blah")
17 16 | #: E251:1:45
18 17 | parser.add_argument('--long-option', default
19 18 | ="/rather/long/filesystem/path/here/blah/blah/blah")
E25.py:18:45: E251 [*] Unexpected spaces around keyword / parameter equals
|
16 | "/rather/long/filesystem/path/here/blah/blah/blah")
17 | #: E251:1:45
@@ -92,8 +177,20 @@ E25.py:18:45: E251 Unexpected spaces around keyword / parameter equals
20 | #: E251:3:8 E251:3:10
21 | foo(True,
|
= help: Remove whitespace
E25.py:23:8: E251 Unexpected spaces around keyword / parameter equals
Safe fix
15 15 | default=
16 16 | "/rather/long/filesystem/path/here/blah/blah/blah")
17 17 | #: E251:1:45
18 |-parser.add_argument('--long-option', default
19 |- ="/rather/long/filesystem/path/here/blah/blah/blah")
18 |+parser.add_argument('--long-option', default="/rather/long/filesystem/path/here/blah/blah/blah")
20 19 | #: E251:3:8 E251:3:10
21 20 | foo(True,
22 21 | baz=(1, 2),
E25.py:23:8: E251 [*] Unexpected spaces around keyword / parameter equals
|
21 | foo(True,
22 | baz=(1, 2),
@@ -102,8 +199,19 @@ E25.py:23:8: E251 Unexpected spaces around keyword / parameter equals
24 | )
25 | #: Okay
|
= help: Remove whitespace
E25.py:23:10: E251 Unexpected spaces around keyword / parameter equals
Safe fix
20 20 | #: E251:3:8 E251:3:10
21 21 | foo(True,
22 22 | baz=(1, 2),
23 |- biz = 'foo'
23 |+ biz= 'foo'
24 24 | )
25 25 | #: Okay
26 26 | foo(bar=(1 == 1))
E25.py:23:10: E251 [*] Unexpected spaces around keyword / parameter equals
|
21 | foo(True,
22 | baz=(1, 2),
@@ -112,5 +220,16 @@ E25.py:23:10: E251 Unexpected spaces around keyword / parameter equals
24 | )
25 | #: Okay
|
= help: Remove whitespace
Safe fix
20 20 | #: E251:3:8 E251:3:10
21 21 | foo(True,
22 22 | baz=(1, 2),
23 |- biz = 'foo'
23 |+ biz ='foo'
24 24 | )
25 25 | #: Okay
26 26 | foo(bar=(1 == 1))

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E26.py:4:12: E262 Inline comment should start with `# `
E26.py:4:12: E262 [*] Inline comment should start with `# `
|
2 | pass # an inline comment
3 | #: E262:1:12
@@ -10,8 +10,19 @@ E26.py:4:12: E262 Inline comment should start with `# `
5 | #: E262:1:12
6 | x = x + 1 # Increment x
|
= help: Format space
E26.py:6:12: E262 Inline comment should start with `# `
Safe fix
1 1 | #: E261:1:5
2 2 | pass # an inline comment
3 3 | #: E262:1:12
4 |-x = x + 1 #Increment x
4 |+x = x + 1 # Increment x
5 5 | #: E262:1:12
6 6 | x = x + 1 # Increment x
7 7 | #: E262:1:12
E26.py:6:12: E262 [*] Inline comment should start with `# `
|
4 | x = x + 1 #Increment x
5 | #: E262:1:12
@@ -20,8 +31,19 @@ E26.py:6:12: E262 Inline comment should start with `# `
7 | #: E262:1:12
8 | x = y + 1 #: Increment x
|
= help: Format space
E26.py:8:12: E262 Inline comment should start with `# `
Safe fix
3 3 | #: E262:1:12
4 4 | x = x + 1 #Increment x
5 5 | #: E262:1:12
6 |-x = x + 1 # Increment x
6 |+x = x + 1 # Increment x
7 7 | #: E262:1:12
8 8 | x = y + 1 #: Increment x
9 9 | #: E265:1:1
E26.py:8:12: E262 [*] Inline comment should start with `# `
|
6 | x = x + 1 # Increment x
7 | #: E262:1:12
@@ -30,8 +52,19 @@ E26.py:8:12: E262 Inline comment should start with `# `
9 | #: E265:1:1
10 | #Block comment
|
= help: Format space
E26.py:63:9: E262 Inline comment should start with `# `
Safe fix
5 5 | #: E262:1:12
6 6 | x = x + 1 # Increment x
7 7 | #: E262:1:12
8 |-x = y + 1 #: Increment x
8 |+x = y + 1 #: Increment x
9 9 | #: E265:1:1
10 10 | #Block comment
11 11 | a = 1
E26.py:63:9: E262 [*] Inline comment should start with `# `
|
61 | # -*- coding: utf8 -*-
62 | #  (One space one NBSP) Ok for block comment
@@ -40,8 +73,19 @@ E26.py:63:9: E262 Inline comment should start with `# `
64 | #: E262:2:9
65 | # (Two spaces) Ok for block comment
|
= help: Format space
E26.py:66:9: E262 Inline comment should start with `# `
Safe fix
60 60 | #: E262:3:9
61 61 | # -*- coding: utf8 -*-
62 62 | #  (One space one NBSP) Ok for block comment
63 |-a = 42 #  (One space one NBSP)
63 |+a = 42 # (One space one NBSP)
64 64 | #: E262:2:9
65 65 | # (Two spaces) Ok for block comment
66 66 | a = 42 # (Two spaces)
E26.py:66:9: E262 [*] Inline comment should start with `# `
|
64 | #: E262:2:9
65 | # (Two spaces) Ok for block comment
@@ -50,5 +94,52 @@ E26.py:66:9: E262 Inline comment should start with `# `
67 |
68 | #: E265:5:1
|
= help: Format space
Safe fix
63 63 | a = 42 #  (One space one NBSP)
64 64 | #: E262:2:9
65 65 | # (Two spaces) Ok for block comment
66 |-a = 42 # (Two spaces)
66 |+a = 42 # (Two spaces)
67 67 |
68 68 | #: E265:5:1
69 69 | ### Means test is not done yet
E26.py:84:8: E262 [*] Inline comment should start with `# `
|
82 | ## Foo
83 |
84 | a = 1 ## Foo
| ^^^^^^ E262
85 |
86 | a = 1 #:Foo
|
= help: Format space
Safe fix
81 81 | #: E266:1:3
82 82 | ## Foo
83 83 |
84 |-a = 1 ## Foo
84 |+a = 1 # Foo
85 85 |
86 86 | a = 1 #:Foo
E26.py:86:8: E262 [*] Inline comment should start with `# `
|
84 | a = 1 ## Foo
85 |
86 | a = 1 #:Foo
| ^^^^^ E262
|
= help: Format space
Safe fix
83 83 |
84 84 | a = 1 ## Foo
85 85 |
86 |-a = 1 #:Foo
86 |+a = 1 #: Foo

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E26.py:10:1: E265 Block comment should start with `# `
E26.py:10:1: E265 [*] Block comment should start with `# `
|
8 | x = y + 1 #: Increment x
9 | #: E265:1:1
@@ -10,8 +10,19 @@ E26.py:10:1: E265 Block comment should start with `# `
11 | a = 1
12 | #: E265:2:1
|
= help: Format space
E26.py:14:1: E265 Block comment should start with `# `
Safe fix
7 7 | #: E262:1:12
8 8 | x = y + 1 #: Increment x
9 9 | #: E265:1:1
10 |-#Block comment
10 |+# Block comment
11 11 | a = 1
12 12 | #: E265:2:1
13 13 | m = 42
E26.py:14:1: E265 [*] Block comment should start with `# `
|
12 | #: E265:2:1
13 | m = 42
@@ -20,8 +31,19 @@ E26.py:14:1: E265 Block comment should start with `# `
15 | mx = 42 - 42
16 | #: E266:3:5 E266:6:5
|
= help: Format space
E26.py:25:1: E265 Block comment should start with `# `
Safe fix
11 11 | a = 1
12 12 | #: E265:2:1
13 13 | m = 42
14 |-#! This is important
14 |+# ! This is important
15 15 | mx = 42 - 42
16 16 | #: E266:3:5 E266:6:5
17 17 | def how_it_feel(r):
E26.py:25:1: E265 [*] Block comment should start with `# `
|
23 | return
24 | #: E265:1:1 E266:2:1
@@ -30,8 +52,19 @@ E26.py:25:1: E265 Block comment should start with `# `
26 | ## logging.error()
27 | #: W291:1:42
|
= help: Format space
E26.py:32:1: E265 Block comment should start with `# `
Safe fix
22 22 | ### Of course it is unused
23 23 | return
24 24 | #: E265:1:1 E266:2:1
25 |-##if DEBUG:
25 |+# if DEBUG:
26 26 | ## logging.error()
27 27 | #: W291:1:42
28 28 | #########################################
E26.py:32:1: E265 [*] Block comment should start with `# `
|
31 | #: Okay
32 | #!/usr/bin/env python
@@ -39,8 +72,19 @@ E26.py:32:1: E265 Block comment should start with `# `
33 |
34 | pass # an inline comment
|
= help: Format space
E26.py:73:1: E265 Block comment should start with `# `
Safe fix
29 29 | #:
30 30 |
31 31 | #: Okay
32 |-#!/usr/bin/env python
32 |+# !/usr/bin/env python
33 33 |
34 34 | pass # an inline comment
35 35 | x = x + 1 # Increment x
E26.py:73:1: E265 [*] Block comment should start with `# `
|
71 | # F Means test is failing (F)
72 | # EF Means test is giving error and Failing
@@ -48,5 +92,37 @@ E26.py:73:1: E265 Block comment should start with `# `
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ E265
74 | # 8 Means test runs forever
|
= help: Format space
Safe fix
70 70 | # E Means test is giving error (E)
71 71 | # F Means test is failing (F)
72 72 | # EF Means test is giving error and Failing
73 |-#! Means test is segfaulting
73 |+# ! Means test is segfaulting
74 74 | # 8 Means test runs forever
75 75 |
76 76 | #: Colon prefix is okay
E26.py:78:1: E265 [*] Block comment should start with `# `
|
76 | #: Colon prefix is okay
77 |
78 | ###This is a variable ###
| ^^^^^^^^^^^^^^^^^^^^^^^^^ E265
79 |
80 | # We should strip the space, but preserve the hashes.
|
= help: Format space
Safe fix
75 75 |
76 76 | #: Colon prefix is okay
77 77 |
78 |-###This is a variable ###
78 |+# This is a variable ###
79 79 |
80 80 | # We should strip the space, but preserve the hashes.
81 81 | #: E266:1:3

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E26.py:19:5: E266 Too many leading `#` before block comment
E26.py:19:5: E266 [*] Too many leading `#` before block comment
|
17 | def how_it_feel(r):
18 |
@@ -9,8 +9,19 @@ E26.py:19:5: E266 Too many leading `#` before block comment
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ E266
20 | a = 42
|
= help: Remove leading `#`
E26.py:22:5: E266 Too many leading `#` before block comment
Safe fix
16 16 | #: E266:3:5 E266:6:5
17 17 | def how_it_feel(r):
18 18 |
19 |- ### This is a variable ###
19 |+ # This is a variable ###
20 20 | a = 42
21 21 |
22 22 | ### Of course it is unused
E26.py:22:5: E266 [*] Too many leading `#` before block comment
|
20 | a = 42
21 |
@@ -19,8 +30,19 @@ E26.py:22:5: E266 Too many leading `#` before block comment
23 | return
24 | #: E265:1:1 E266:2:1
|
= help: Remove leading `#`
E26.py:26:1: E266 Too many leading `#` before block comment
Safe fix
19 19 | ### This is a variable ###
20 20 | a = 42
21 21 |
22 |- ### Of course it is unused
22 |+ # Of course it is unused
23 23 | return
24 24 | #: E265:1:1 E266:2:1
25 25 | ##if DEBUG:
E26.py:26:1: E266 [*] Too many leading `#` before block comment
|
24 | #: E265:1:1 E266:2:1
25 | ##if DEBUG:
@@ -29,8 +51,19 @@ E26.py:26:1: E266 Too many leading `#` before block comment
27 | #: W291:1:42
28 | #########################################
|
= help: Remove leading `#`
E26.py:69:1: E266 Too many leading `#` before block comment
Safe fix
23 23 | return
24 24 | #: E265:1:1 E266:2:1
25 25 | ##if DEBUG:
26 |-## logging.error()
26 |+# logging.error()
27 27 | #: W291:1:42
28 28 | #########################################
29 29 | #:
E26.py:69:1: E266 [*] Too many leading `#` before block comment
|
68 | #: E265:5:1
69 | ### Means test is not done yet
@@ -38,5 +71,37 @@ E26.py:69:1: E266 Too many leading `#` before block comment
70 | # E Means test is giving error (E)
71 | # F Means test is failing (F)
|
= help: Remove leading `#`
Safe fix
66 66 | a = 42 # (Two spaces)
67 67 |
68 68 | #: E265:5:1
69 |-### Means test is not done yet
69 |+# Means test is not done yet
70 70 | # E Means test is giving error (E)
71 71 | # F Means test is failing (F)
72 72 | # EF Means test is giving error and Failing
E26.py:82:1: E266 [*] Too many leading `#` before block comment
|
80 | # We should strip the space, but preserve the hashes.
81 | #: E266:1:3
82 | ## Foo
| ^^^^^^^ E266
83 |
84 | a = 1 ## Foo
|
= help: Remove leading `#`
Safe fix
79 79 |
80 80 | # We should strip the space, but preserve the hashes.
81 81 | #: E266:1:3
82 |-## Foo
82 |+# Foo
83 83 |
84 84 | a = 1 ## Foo
85 85 |

View File

@@ -1,27 +1,57 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402.py:24:1: E402 Module level import not at top of file
E402.py:25:1: E402 Module level import not at top of file
|
22 | __some__magic = 1
23 |
24 | import f
23 | sys.path.insert(0, "some/path")
24 |
25 | import f
| ^^^^^^^^ E402
26 |
27 | import matplotlib
|
E402.py:27:1: E402 Module level import not at top of file
|
25 | import f
26 |
27 | import matplotlib
| ^^^^^^^^^^^^^^^^^ E402
28 |
29 | matplotlib.use("Agg")
|
E402.py:31:1: E402 Module level import not at top of file
|
29 | matplotlib.use("Agg")
30 |
31 | import g
| ^^^^^^^^ E402
32 |
33 | __some__magic = 1
|
E402.py:35:1: E402 Module level import not at top of file
|
33 | __some__magic = 1
34 |
35 | import h
| ^^^^^^^^ E402
|
E402.py:34:1: E402 Module level import not at top of file
E402.py:45:1: E402 Module level import not at top of file
|
32 | import g
33 |
34 | import h; import i
43 | import j
44 |
45 | import k; import l
| ^^^^^^^^ E402
|
E402.py:34:11: E402 Module level import not at top of file
E402.py:45:11: E402 Module level import not at top of file
|
32 | import g
33 |
34 | import h; import i
43 | import j
44 |
45 | import k; import l
| ^^^^^^^^ E402
|

View File

@@ -0,0 +1,28 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
E402.py:35:1: E402 Module level import not at top of file
|
33 | __some__magic = 1
34 |
35 | import h
| ^^^^^^^^ E402
|
E402.py:45:1: E402 Module level import not at top of file
|
43 | import j
44 |
45 | import k; import l
| ^^^^^^^^ E402
|
E402.py:45:11: E402 Module level import not at top of file
|
43 | import j
44 |
45 | import k; import l
| ^^^^^^^^ E402
|

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pycodestyle/mod.rs
---
shebang.py:3:1: E265 Block comment should start with `# `
shebang.py:3:1: E265 [*] Block comment should start with `# `
|
1 | #!/usr/bin/python
2 | #
@@ -9,5 +9,13 @@ shebang.py:3:1: E265 Block comment should start with `# `
| ^^ E265
4 | #:
|
= help: Format space
Safe fix
1 1 | #!/usr/bin/python
2 2 | #
3 |-#!
3 |+# !
4 4 | #:

View File

@@ -1,6 +1,6 @@
use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr};
use ruff_python_ast as ast;
use ruff_source_file::Locator;
use ruff_text_size::{Ranged, TextRange, TextSize};
@@ -47,11 +47,12 @@ impl AlwaysFixableViolation for FStringMissingPlaceholders {
/// F541
pub(crate) fn f_string_missing_placeholders(checker: &mut Checker, expr: &ast::ExprFString) {
if expr
.value
.f_strings()
.any(|f_string| f_string.values.iter().any(Expr::is_formatted_value_expr))
{
if expr.value.f_strings().any(|f_string| {
f_string
.elements
.iter()
.any(ast::FStringElement::is_expression)
}) {
return;
}

View File

@@ -247,6 +247,14 @@ fn remove_unused_variable(binding: &Binding, checker: &Checker) -> Option<Fix> {
Some(Fix::unsafe_edit(edit).isolate(isolation))
};
}
} else {
let name = binding.name(checker.locator());
let renamed = format!("_{name}");
if checker.settings.dummy_variable_rgx.is_match(&renamed) {
let edit = Edit::range_replacement(renamed, binding.range());
return Some(Fix::unsafe_edit(edit).isolate(isolation));
}
}
}

View File

@@ -57,7 +57,7 @@ F841_0.py:20:5: F841 [*] Local variable `foo` is assigned to but never used
22 21 |
23 22 | bar = (1, 2)
F841_0.py:21:6: F841 Local variable `a` is assigned to but never used
F841_0.py:21:6: F841 [*] Local variable `a` is assigned to but never used
|
19 | def f():
20 | foo = (1, 2)
@@ -68,7 +68,17 @@ F841_0.py:21:6: F841 Local variable `a` is assigned to but never used
|
= help: Remove assignment to unused variable `a`
F841_0.py:21:9: F841 Local variable `b` is assigned to but never used
Unsafe fix
18 18 |
19 19 | def f():
20 20 | foo = (1, 2)
21 |- (a, b) = (1, 2)
21 |+ (_a, b) = (1, 2)
22 22 |
23 23 | bar = (1, 2)
24 24 | (c, d) = bar
F841_0.py:21:9: F841 [*] Local variable `b` is assigned to but never used
|
19 | def f():
20 | foo = (1, 2)
@@ -79,6 +89,16 @@ F841_0.py:21:9: F841 Local variable `b` is assigned to but never used
|
= help: Remove assignment to unused variable `b`
Unsafe fix
18 18 |
19 19 | def f():
20 20 | foo = (1, 2)
21 |- (a, b) = (1, 2)
21 |+ (a, _b) = (1, 2)
22 22 |
23 23 | bar = (1, 2)
24 24 | (c, d) = bar
F841_0.py:26:14: F841 [*] Local variable `baz` is assigned to but never used
|
24 | (c, d) = bar

View File

@@ -1,7 +1,7 @@
---
source: crates/ruff_linter/src/rules/pyflakes/mod.rs
---
F841_1.py:6:5: F841 Local variable `x` is assigned to but never used
F841_1.py:6:5: F841 [*] Local variable `x` is assigned to but never used
|
5 | def f():
6 | x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
@@ -9,7 +9,17 @@ F841_1.py:6:5: F841 Local variable `x` is assigned to but never used
|
= help: Remove assignment to unused variable `x`
F841_1.py:6:8: F841 Local variable `y` is assigned to but never used
Unsafe fix
3 3 |
4 4 |
5 5 | def f():
6 |- x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
6 |+ _x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
7 7 |
8 8 |
9 9 | def f():
F841_1.py:6:8: F841 [*] Local variable `y` is assigned to but never used
|
5 | def f():
6 | x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
@@ -17,6 +27,16 @@ F841_1.py:6:8: F841 Local variable `y` is assigned to but never used
|
= help: Remove assignment to unused variable `y`
Unsafe fix
3 3 |
4 4 |
5 5 | def f():
6 |- x, y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
6 |+ x, _y = 1, 2 # this triggers F841 as it's just a simple assignment where unpacking isn't needed
7 7 |
8 8 |
9 9 | def f():
F841_1.py:16:14: F841 [*] Local variable `coords` is assigned to but never used
|
15 | def f():
@@ -53,7 +73,7 @@ F841_1.py:20:5: F841 [*] Local variable `coords` is assigned to but never used
22 22 |
23 23 | def f():
F841_1.py:24:6: F841 Local variable `a` is assigned to but never used
F841_1.py:24:6: F841 [*] Local variable `a` is assigned to but never used
|
23 | def f():
24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
@@ -61,7 +81,14 @@ F841_1.py:24:6: F841 Local variable `a` is assigned to but never used
|
= help: Remove assignment to unused variable `a`
F841_1.py:24:9: F841 Local variable `b` is assigned to but never used
Unsafe fix
21 21 |
22 22 |
23 23 | def f():
24 |- (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
24 |+ (_a, b) = (x, y) = 1, 2 # this triggers F841 on everything
F841_1.py:24:9: F841 [*] Local variable `b` is assigned to but never used
|
23 | def f():
24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
@@ -69,7 +96,14 @@ F841_1.py:24:9: F841 Local variable `b` is assigned to but never used
|
= help: Remove assignment to unused variable `b`
F841_1.py:24:15: F841 Local variable `x` is assigned to but never used
Unsafe fix
21 21 |
22 22 |
23 23 | def f():
24 |- (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
24 |+ (a, _b) = (x, y) = 1, 2 # this triggers F841 on everything
F841_1.py:24:15: F841 [*] Local variable `x` is assigned to but never used
|
23 | def f():
24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
@@ -77,7 +111,14 @@ F841_1.py:24:15: F841 Local variable `x` is assigned to but never used
|
= help: Remove assignment to unused variable `x`
F841_1.py:24:18: F841 Local variable `y` is assigned to but never used
Unsafe fix
21 21 |
22 22 |
23 23 | def f():
24 |- (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
24 |+ (a, b) = (_x, y) = 1, 2 # this triggers F841 on everything
F841_1.py:24:18: F841 [*] Local variable `y` is assigned to but never used
|
23 | def f():
24 | (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
@@ -85,4 +126,11 @@ F841_1.py:24:18: F841 Local variable `y` is assigned to but never used
|
= help: Remove assignment to unused variable `y`
Unsafe fix
21 21 |
22 22 |
23 23 | def f():
24 |- (a, b) = (x, y) = 1, 2 # this triggers F841 on everything
24 |+ (a, b) = (x, _y) = 1, 2 # this triggers F841 on everything

View File

@@ -156,7 +156,7 @@ F841_3.py:27:46: F841 [*] Local variable `z3` is assigned to but never used
29 29 |
30 30 |
F841_3.py:32:6: F841 Local variable `x1` is assigned to but never used
F841_3.py:32:6: F841 [*] Local variable `x1` is assigned to but never used
|
31 | def f():
32 | (x1, y1) = (1, 2)
@@ -166,7 +166,17 @@ F841_3.py:32:6: F841 Local variable `x1` is assigned to but never used
|
= help: Remove assignment to unused variable `x1`
F841_3.py:32:10: F841 Local variable `y1` is assigned to but never used
Unsafe fix
29 29 |
30 30 |
31 31 | def f():
32 |- (x1, y1) = (1, 2)
32 |+ (_x1, y1) = (1, 2)
33 33 | (x2, y2) = coords2 = (1, 2)
34 34 | coords3 = (x3, y3) = (1, 2)
35 35 |
F841_3.py:32:10: F841 [*] Local variable `y1` is assigned to but never used
|
31 | def f():
32 | (x1, y1) = (1, 2)
@@ -176,6 +186,16 @@ F841_3.py:32:10: F841 Local variable `y1` is assigned to but never used
|
= help: Remove assignment to unused variable `y1`
Unsafe fix
29 29 |
30 30 |
31 31 | def f():
32 |- (x1, y1) = (1, 2)
32 |+ (x1, _y1) = (1, 2)
33 33 | (x2, y2) = coords2 = (1, 2)
34 34 | coords3 = (x3, y3) = (1, 2)
35 35 |
F841_3.py:33:16: F841 [*] Local variable `coords2` is assigned to but never used
|
31 | def f():

View File

@@ -20,7 +20,7 @@ F841_4.py:12:5: F841 [*] Local variable `a` is assigned to but never used
14 14 |
15 15 |
F841_4.py:13:5: F841 Local variable `b` is assigned to but never used
F841_4.py:13:5: F841 [*] Local variable `b` is assigned to but never used
|
11 | def bar():
12 | a = foo()
@@ -29,7 +29,17 @@ F841_4.py:13:5: F841 Local variable `b` is assigned to but never used
|
= help: Remove assignment to unused variable `b`
F841_4.py:13:8: F841 Local variable `c` is assigned to but never used
Unsafe fix
10 10 |
11 11 | def bar():
12 12 | a = foo()
13 |- b, c = foo()
13 |+ _b, c = foo()
14 14 |
15 15 |
16 16 | def baz():
F841_4.py:13:8: F841 [*] Local variable `c` is assigned to but never used
|
11 | def bar():
12 | a = foo()
@@ -38,4 +48,14 @@ F841_4.py:13:8: F841 Local variable `c` is assigned to but never used
|
= help: Remove assignment to unused variable `c`
Unsafe fix
10 10 |
11 11 | def bar():
12 12 | a = foo()
13 |- b, c = foo()
13 |+ b, _c = foo()
14 14 |
15 15 |
16 16 | def baz():

View File

@@ -72,24 +72,26 @@ pub(crate) fn assert_on_string_literal(checker: &mut Checker, test: &Expr) {
Expr::FString(ast::ExprFString { value, .. }) => {
let kind = if value.parts().all(|f_string_part| match f_string_part {
ast::FStringPart::Literal(literal) => literal.is_empty(),
ast::FStringPart::FString(f_string) => f_string.values.iter().all(|value| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = value {
value.is_empty()
} else {
false
}
}),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().all(|element| match element {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) => value.is_empty(),
ast::FStringElement::Expression(_) => false,
})
}
}) {
Kind::Empty
} else if value.parts().any(|f_string_part| match f_string_part {
ast::FStringPart::Literal(literal) => !literal.is_empty(),
ast::FStringPart::FString(f_string) => f_string.values.iter().any(|value| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = value {
!value.is_empty()
} else {
false
}
}),
ast::FStringPart::FString(f_string) => {
f_string.elements.iter().any(|element| match element {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) => !value.is_empty(),
ast::FStringElement::Expression(_) => false,
})
}
}) {
Kind::NonEmpty
} else {

View File

@@ -1,9 +1,10 @@
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_diagnostics::{AlwaysFixableViolation, Applicability, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
use crate::fix::edits::add_argument;
/// ## What it does
/// Checks for uses of `subprocess.run` without an explicit `check` argument.
@@ -36,16 +37,25 @@ use crate::checkers::ast::Checker;
/// subprocess.run(["ls", "nonexistent"], check=False) # Explicitly no check.
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe for function 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: `subprocess.run`](https://docs.python.org/3/library/subprocess.html#subprocess.run)
#[violation]
pub struct SubprocessRunWithoutCheck;
impl Violation for SubprocessRunWithoutCheck {
impl AlwaysFixableViolation for SubprocessRunWithoutCheck {
#[derive_message_formats]
fn message(&self) -> String {
format!("`subprocess.run` without explicit `check` argument")
}
fn fix_title(&self) -> String {
"Add explicit `check=False`".to_string()
}
}
/// PLW1510
@@ -56,10 +66,27 @@ pub(crate) fn subprocess_run_without_check(checker: &mut Checker, call: &ast::Ex
.is_some_and(|call_path| matches!(call_path.as_slice(), ["subprocess", "run"]))
{
if call.arguments.find_keyword("check").is_none() {
checker.diagnostics.push(Diagnostic::new(
SubprocessRunWithoutCheck,
call.func.range(),
let mut diagnostic = Diagnostic::new(SubprocessRunWithoutCheck, call.func.range());
diagnostic.set_fix(Fix::applicable_edit(
add_argument(
"check=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,22 +1,87 @@
---
source: crates/ruff_linter/src/rules/pylint/mod.rs
---
subprocess_run_without_check.py:4:1: PLW1510 `subprocess.run` without explicit `check` argument
subprocess_run_without_check.py:4:1: PLW1510 [*] `subprocess.run` without explicit `check` argument
|
3 | # Errors.
4 | subprocess.run("ls")
| ^^^^^^^^^^^^^^ PLW1510
5 | subprocess.run("ls", shell=True)
6 | subprocess.run(
|
= help: Add explicit `check=False`
subprocess_run_without_check.py:5:1: PLW1510 `subprocess.run` without explicit `check` argument
Safe fix
1 1 | import subprocess
2 2 |
3 3 | # Errors.
4 |-subprocess.run("ls")
4 |+subprocess.run("ls", check=False)
5 5 | subprocess.run("ls", shell=True)
6 6 | subprocess.run(
7 7 | ["ls"],
subprocess_run_without_check.py:5:1: PLW1510 [*] `subprocess.run` without explicit `check` argument
|
3 | # Errors.
4 | subprocess.run("ls")
5 | subprocess.run("ls", shell=True)
| ^^^^^^^^^^^^^^ PLW1510
6 |
7 | # Non-errors.
6 | subprocess.run(
7 | ["ls"],
|
= help: Add explicit `check=False`
Safe fix
2 2 |
3 3 | # Errors.
4 4 | subprocess.run("ls")
5 |-subprocess.run("ls", shell=True)
5 |+subprocess.run("ls", shell=True, check=False)
6 6 | subprocess.run(
7 7 | ["ls"],
8 8 | shell=False,
subprocess_run_without_check.py:6:1: PLW1510 [*] `subprocess.run` without explicit `check` argument
|
4 | subprocess.run("ls")
5 | subprocess.run("ls", shell=True)
6 | subprocess.run(
| ^^^^^^^^^^^^^^ PLW1510
7 | ["ls"],
8 | shell=False,
|
= help: Add explicit `check=False`
Safe fix
5 5 | subprocess.run("ls", shell=True)
6 6 | subprocess.run(
7 7 | ["ls"],
8 |- shell=False,
8 |+ shell=False, check=False,
9 9 | )
10 10 | subprocess.run(["ls"], **kwargs)
11 11 |
subprocess_run_without_check.py:10:1: PLW1510 [*] `subprocess.run` without explicit `check` argument
|
8 | shell=False,
9 | )
10 | subprocess.run(["ls"], **kwargs)
| ^^^^^^^^^^^^^^ PLW1510
11 |
12 | # Non-errors.
|
= help: Add explicit `check=False`
Unsafe fix
7 7 | ["ls"],
8 8 | shell=False,
9 9 | )
10 |-subprocess.run(["ls"], **kwargs)
10 |+subprocess.run(["ls"], **kwargs, check=False)
11 11 |
12 12 | # Non-errors.
13 13 | subprocess.run("ls", check=True)

View File

@@ -180,7 +180,6 @@ fn is_allowed_value(expr: &Expr) -> bool {
| Expr::GeneratorExp(_)
| Expr::Compare(_)
| Expr::Call(_)
| Expr::FormattedValue(_)
| Expr::FString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)

View File

@@ -29,6 +29,7 @@ mod tests {
#[test_case(Rule::IsinstanceTypeNone, Path::new("FURB168.py"))]
#[test_case(Rule::TypeNoneComparison, Path::new("FURB169.py"))]
#[test_case(Rule::RedundantLogBase, Path::new("FURB163.py"))]
#[test_case(Rule::HashlibDigestHex, Path::new("FURB181.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(

View File

@@ -14,7 +14,7 @@ use crate::rules::refurb::helpers::generate_method_call;
/// dictionary.
///
/// ## Why is this bad?
/// It's is faster and more succinct to remove all items via the `clear()`
/// It is faster and more succinct to remove all items via the `clear()`
/// method.
///
/// ## Known problems

View File

@@ -0,0 +1,120 @@
use ruff_diagnostics::{Diagnostic, Edit, Fix, FixAvailability, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, ExprAttribute, ExprCall};
use ruff_text_size::{Ranged, TextRange};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the use of `.digest().hex()` on a hashlib hash, like `sha512`.
///
/// ## Why is this bad?
/// When generating a hex digest from a hash, it's preferable to use the
/// `.hexdigest()` method, rather than calling `.digest()` and then `.hex()`,
/// as the former is more concise and readable.
///
/// ## Example
/// ```python
/// from hashlib import sha512
///
/// hashed = sha512(b"some data").digest().hex()
/// ```
///
/// Use instead:
/// ```python
/// from hashlib import sha512
///
/// hashed = sha512(b"some data").hexdigest()
/// ```
///
/// ## Fix safety
/// This rule's fix is marked as unsafe, as the target of the `.digest()` call
/// could be a user-defined class that implements a `.hex()` method, rather
/// than a hashlib hash object.
///
/// ## References
/// - [Python documentation: `hashlib`](https://docs.python.org/3/library/hashlib.html)
#[violation]
pub struct HashlibDigestHex;
impl Violation for HashlibDigestHex {
const FIX_AVAILABILITY: FixAvailability = FixAvailability::Sometimes;
#[derive_message_formats]
fn message(&self) -> String {
format!("Use of hashlib's `.digest().hex()`")
}
fn fix_title(&self) -> Option<String> {
Some("Replace with `.hexdigest()`".to_string())
}
}
/// FURB181
pub(crate) fn hashlib_digest_hex(checker: &mut Checker, call: &ExprCall) {
if !call.arguments.is_empty() {
return;
}
let Expr::Attribute(ExprAttribute { attr, value, .. }) = call.func.as_ref() else {
return;
};
if attr.as_str() != "hex" {
return;
}
let Expr::Call(ExprCall {
func, arguments, ..
}) = value.as_ref()
else {
return;
};
let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() else {
return;
};
if attr.as_str() != "digest" {
return;
}
let Expr::Call(ExprCall { func, .. }) = value.as_ref() else {
return;
};
if checker.semantic().resolve_call_path(func).is_some_and(
|call_path: smallvec::SmallVec<[&str; 8]>| {
matches!(
call_path.as_slice(),
[
"hashlib",
"md5"
| "sha1"
| "sha224"
| "sha256"
| "sha384"
| "sha512"
| "blake2b"
| "blake2s"
| "sha3_224"
| "sha3_256"
| "sha3_384"
| "sha3_512"
| "shake_128"
| "shake_256"
| "_Hash"
]
)
},
) {
let mut diagnostic = Diagnostic::new(HashlibDigestHex, call.range());
if arguments.is_empty() {
diagnostic.set_fix(Fix::unsafe_edit(Edit::range_replacement(
".hexdigest".to_string(),
TextRange::new(value.end(), call.func.end()),
)));
}
checker.diagnostics.push(diagnostic);
}
}

View File

@@ -53,23 +53,17 @@ pub(crate) fn math_constant(checker: &mut Checker, literal: &ast::ExprNumberLite
let Number::Float(value) = literal.value else {
return;
};
for (real_value, constant) in [
(std::f64::consts::PI, "pi"),
(std::f64::consts::E, "e"),
(std::f64::consts::TAU, "tau"),
] {
if (value - real_value).abs() < 1e-2 {
let mut diagnostic = Diagnostic::new(
MathConstant {
literal: checker.locator().slice(literal).into(),
constant,
},
literal.range(),
);
diagnostic.try_set_fix(|| convert_to_constant(literal, constant, checker));
checker.diagnostics.push(diagnostic);
return;
}
if let Some(constant) = Constant::from_value(value) {
let mut diagnostic = Diagnostic::new(
MathConstant {
literal: checker.locator().slice(literal).into(),
constant: constant.name(),
},
literal.range(),
);
diagnostic.try_set_fix(|| convert_to_constant(literal, constant.name(), checker));
checker.diagnostics.push(diagnostic);
}
}
@@ -88,3 +82,33 @@ fn convert_to_constant(
[edit],
))
}
#[derive(Debug, Clone, Copy)]
enum Constant {
Pi,
E,
Tau,
}
impl Constant {
#[allow(clippy::approx_constant)]
fn from_value(value: f64) -> Option<Self> {
if (3.14..3.15).contains(&value) {
Some(Self::Pi)
} else if (2.71..2.72).contains(&value) {
Some(Self::E)
} else if (6.28..6.29).contains(&value) {
Some(Self::Tau)
} else {
None
}
}
fn name(self) -> &'static str {
match self {
Constant::Pi => "pi",
Constant::E => "e",
Constant::Tau => "tau",
}
}
}

View File

@@ -1,5 +1,6 @@
pub(crate) use check_and_remove_from_set::*;
pub(crate) use delete_full_slice::*;
pub(crate) use hashlib_digest_hex::*;
pub(crate) use if_expr_min_max::*;
pub(crate) use implicit_cwd::*;
pub(crate) use isinstance_type_none::*;
@@ -16,6 +17,7 @@ pub(crate) use unnecessary_enumerate::*;
mod check_and_remove_from_set;
mod delete_full_slice;
mod hashlib_digest_hex;
mod if_expr_min_max;
mod implicit_cwd;
mod isinstance_type_none;

View File

@@ -130,6 +130,9 @@ fn is_number_literal(expr: &Expr, value: i8) -> bool {
if let Expr::NumberLiteral(number_literal) = expr {
if let Number::Int(number) = &number_literal.value {
return number.as_i8().is_some_and(|number| number == value);
} else if let Number::Float(number) = number_literal.value {
#[allow(clippy::float_cmp)]
return number == f64::from(value);
}
}
false

View File

@@ -43,6 +43,7 @@ FURB152.py:5:5: FURB152 [*] Replace `6.28` with `math.tau`
6 |+C = math.tau * r # FURB152
6 7 |
7 8 | e = 2.71 # FURB152
8 9 |
FURB152.py:7:5: FURB152 [*] Replace `2.71` with `math.e`
|
@@ -50,6 +51,8 @@ FURB152.py:7:5: FURB152 [*] Replace `2.71` with `math.e`
6 |
7 | e = 2.71 # FURB152
| ^^^^ FURB152
8 |
9 | r = 3.15 # OK
|
= help: Use `math.e`
@@ -63,5 +66,59 @@ FURB152.py:7:5: FURB152 [*] Replace `2.71` with `math.e`
6 7 |
7 |-e = 2.71 # FURB152
8 |+e = math.e # FURB152
8 9 |
9 10 | r = 3.15 # OK
10 11 |
FURB152.py:11:5: FURB152 [*] Replace `3.141` with `math.pi`
|
9 | r = 3.15 # OK
10 |
11 | r = 3.141 # FURB152
| ^^^^^ FURB152
12 |
13 | r = 3.1415 # FURB152
|
= help: Use `math.pi`
Safe fix
1 |+import math
1 2 | r = 3.1 # OK
2 3 |
3 4 | A = 3.14 * r ** 2 # FURB152
--------------------------------------------------------------------------------
8 9 |
9 10 | r = 3.15 # OK
10 11 |
11 |-r = 3.141 # FURB152
12 |+r = math.pi # FURB152
12 13 |
13 14 | r = 3.1415 # FURB152
14 15 |
FURB152.py:13:5: FURB152 [*] Replace `3.1415` with `math.pi`
|
11 | r = 3.141 # FURB152
12 |
13 | r = 3.1415 # FURB152
| ^^^^^^ FURB152
14 |
15 | e = 2.7 # OK
|
= help: Use `math.pi`
Safe fix
1 |+import math
1 2 | r = 3.1 # OK
2 3 |
3 4 | A = 3.14 * r ** 2 # FURB152
--------------------------------------------------------------------------------
10 11 |
11 12 | r = 3.141 # FURB152
12 13 |
13 |-r = 3.1415 # FURB152
14 |+r = math.pi # FURB152
14 15 |
15 16 | e = 2.7 # OK

View File

@@ -187,7 +187,7 @@ FURB163.py:16:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redun
16 |+math.log10(1)
17 17 | special_log(1, math.e)
18 18 | special_log(1, special_e)
19 19 |
19 19 | math.log(1, 2.0)
FURB163.py:17:1: FURB163 [*] Prefer `math.log(1)` over `math.log` with a redundant base
|
@@ -196,6 +196,7 @@ FURB163.py:17:1: FURB163 [*] Prefer `math.log(1)` over `math.log` with a redunda
17 | special_log(1, math.e)
| ^^^^^^^^^^^^^^^^^^^^^^ FURB163
18 | special_log(1, special_e)
19 | math.log(1, 2.0)
|
= help: Replace with `math.log(1)`
@@ -206,8 +207,8 @@ FURB163.py:17:1: FURB163 [*] Prefer `math.log(1)` over `math.log` with a redunda
17 |-special_log(1, math.e)
17 |+math.log(1)
18 18 | special_log(1, special_e)
19 19 |
20 20 | # Ok.
19 19 | math.log(1, 2.0)
20 20 | math.log(1, 10.0)
FURB163.py:18:1: FURB163 [*] Prefer `math.log(1)` over `math.log` with a redundant base
|
@@ -215,8 +216,8 @@ FURB163.py:18:1: FURB163 [*] Prefer `math.log(1)` over `math.log` with a redunda
17 | special_log(1, math.e)
18 | special_log(1, special_e)
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB163
19 |
20 | # Ok.
19 | math.log(1, 2.0)
20 | math.log(1, 10.0)
|
= help: Replace with `math.log(1)`
@@ -226,8 +227,49 @@ FURB163.py:18:1: FURB163 [*] Prefer `math.log(1)` over `math.log` with a redunda
17 17 | special_log(1, math.e)
18 |-special_log(1, special_e)
18 |+math.log(1)
19 19 |
20 20 | # Ok.
21 21 | math.log2(1)
19 19 | math.log(1, 2.0)
20 20 | math.log(1, 10.0)
21 21 |
FURB163.py:19:1: FURB163 [*] Prefer `math.log2(1)` over `math.log` with a redundant base
|
17 | special_log(1, math.e)
18 | special_log(1, special_e)
19 | math.log(1, 2.0)
| ^^^^^^^^^^^^^^^^ FURB163
20 | math.log(1, 10.0)
|
= help: Replace with `math.log2(1)`
Safe fix
16 16 | special_log(1, 10)
17 17 | special_log(1, math.e)
18 18 | special_log(1, special_e)
19 |-math.log(1, 2.0)
19 |+math.log2(1)
20 20 | math.log(1, 10.0)
21 21 |
22 22 | # Ok.
FURB163.py:20:1: FURB163 [*] Prefer `math.log10(1)` over `math.log` with a redundant base
|
18 | special_log(1, special_e)
19 | math.log(1, 2.0)
20 | math.log(1, 10.0)
| ^^^^^^^^^^^^^^^^^ FURB163
21 |
22 | # Ok.
|
= help: Replace with `math.log10(1)`
Safe fix
17 17 | special_log(1, math.e)
18 18 | special_log(1, special_e)
19 19 | math.log(1, 2.0)
20 |-math.log(1, 10.0)
20 |+math.log10(1)
21 21 |
22 22 | # Ok.
23 23 | math.log2(1)

View File

@@ -0,0 +1,339 @@
---
source: crates/ruff_linter/src/rules/refurb/mod.rs
---
FURB181.py:19:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
17 | # these will match
18 |
19 | blake2b().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
20 | blake2s().digest().hex()
21 | md5().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
16 16 |
17 17 | # these will match
18 18 |
19 |-blake2b().digest().hex()
19 |+blake2b().hexdigest()
20 20 | blake2s().digest().hex()
21 21 | md5().digest().hex()
22 22 | sha1().digest().hex()
FURB181.py:20:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
19 | blake2b().digest().hex()
20 | blake2s().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
21 | md5().digest().hex()
22 | sha1().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
17 17 | # these will match
18 18 |
19 19 | blake2b().digest().hex()
20 |-blake2s().digest().hex()
20 |+blake2s().hexdigest()
21 21 | md5().digest().hex()
22 22 | sha1().digest().hex()
23 23 | sha224().digest().hex()
FURB181.py:21:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
19 | blake2b().digest().hex()
20 | blake2s().digest().hex()
21 | md5().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^ FURB181
22 | sha1().digest().hex()
23 | sha224().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
18 18 |
19 19 | blake2b().digest().hex()
20 20 | blake2s().digest().hex()
21 |-md5().digest().hex()
21 |+md5().hexdigest()
22 22 | sha1().digest().hex()
23 23 | sha224().digest().hex()
24 24 | sha256().digest().hex()
FURB181.py:22:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
20 | blake2s().digest().hex()
21 | md5().digest().hex()
22 | sha1().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^ FURB181
23 | sha224().digest().hex()
24 | sha256().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
19 19 | blake2b().digest().hex()
20 20 | blake2s().digest().hex()
21 21 | md5().digest().hex()
22 |-sha1().digest().hex()
22 |+sha1().hexdigest()
23 23 | sha224().digest().hex()
24 24 | sha256().digest().hex()
25 25 | sha384().digest().hex()
FURB181.py:23:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
21 | md5().digest().hex()
22 | sha1().digest().hex()
23 | sha224().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB181
24 | sha256().digest().hex()
25 | sha384().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
20 20 | blake2s().digest().hex()
21 21 | md5().digest().hex()
22 22 | sha1().digest().hex()
23 |-sha224().digest().hex()
23 |+sha224().hexdigest()
24 24 | sha256().digest().hex()
25 25 | sha384().digest().hex()
26 26 | sha3_224().digest().hex()
FURB181.py:24:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
22 | sha1().digest().hex()
23 | sha224().digest().hex()
24 | sha256().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB181
25 | sha384().digest().hex()
26 | sha3_224().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
21 21 | md5().digest().hex()
22 22 | sha1().digest().hex()
23 23 | sha224().digest().hex()
24 |-sha256().digest().hex()
24 |+sha256().hexdigest()
25 25 | sha384().digest().hex()
26 26 | sha3_224().digest().hex()
27 27 | sha3_256().digest().hex()
FURB181.py:25:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
23 | sha224().digest().hex()
24 | sha256().digest().hex()
25 | sha384().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB181
26 | sha3_224().digest().hex()
27 | sha3_256().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
22 22 | sha1().digest().hex()
23 23 | sha224().digest().hex()
24 24 | sha256().digest().hex()
25 |-sha384().digest().hex()
25 |+sha384().hexdigest()
26 26 | sha3_224().digest().hex()
27 27 | sha3_256().digest().hex()
28 28 | sha3_384().digest().hex()
FURB181.py:26:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
24 | sha256().digest().hex()
25 | sha384().digest().hex()
26 | sha3_224().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
27 | sha3_256().digest().hex()
28 | sha3_384().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
23 23 | sha224().digest().hex()
24 24 | sha256().digest().hex()
25 25 | sha384().digest().hex()
26 |-sha3_224().digest().hex()
26 |+sha3_224().hexdigest()
27 27 | sha3_256().digest().hex()
28 28 | sha3_384().digest().hex()
29 29 | sha3_512().digest().hex()
FURB181.py:27:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
25 | sha384().digest().hex()
26 | sha3_224().digest().hex()
27 | sha3_256().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
28 | sha3_384().digest().hex()
29 | sha3_512().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
24 24 | sha256().digest().hex()
25 25 | sha384().digest().hex()
26 26 | sha3_224().digest().hex()
27 |-sha3_256().digest().hex()
27 |+sha3_256().hexdigest()
28 28 | sha3_384().digest().hex()
29 29 | sha3_512().digest().hex()
30 30 | sha512().digest().hex()
FURB181.py:28:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
26 | sha3_224().digest().hex()
27 | sha3_256().digest().hex()
28 | sha3_384().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
29 | sha3_512().digest().hex()
30 | sha512().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
25 25 | sha384().digest().hex()
26 26 | sha3_224().digest().hex()
27 27 | sha3_256().digest().hex()
28 |-sha3_384().digest().hex()
28 |+sha3_384().hexdigest()
29 29 | sha3_512().digest().hex()
30 30 | sha512().digest().hex()
31 31 | shake_128().digest(10).hex()
FURB181.py:29:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
27 | sha3_256().digest().hex()
28 | sha3_384().digest().hex()
29 | sha3_512().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
30 | sha512().digest().hex()
31 | shake_128().digest(10).hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
26 26 | sha3_224().digest().hex()
27 27 | sha3_256().digest().hex()
28 28 | sha3_384().digest().hex()
29 |-sha3_512().digest().hex()
29 |+sha3_512().hexdigest()
30 30 | sha512().digest().hex()
31 31 | shake_128().digest(10).hex()
32 32 | shake_256().digest(10).hex()
FURB181.py:30:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
28 | sha3_384().digest().hex()
29 | sha3_512().digest().hex()
30 | sha512().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^ FURB181
31 | shake_128().digest(10).hex()
32 | shake_256().digest(10).hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
27 27 | sha3_256().digest().hex()
28 28 | sha3_384().digest().hex()
29 29 | sha3_512().digest().hex()
30 |-sha512().digest().hex()
30 |+sha512().hexdigest()
31 31 | shake_128().digest(10).hex()
32 32 | shake_256().digest(10).hex()
33 33 |
FURB181.py:31:1: FURB181 Use of hashlib's `.digest().hex()`
|
29 | sha3_512().digest().hex()
30 | sha512().digest().hex()
31 | shake_128().digest(10).hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
32 | shake_256().digest(10).hex()
|
= help: Replace with `.hexdigest()`
FURB181.py:32:1: FURB181 Use of hashlib's `.digest().hex()`
|
30 | sha512().digest().hex()
31 | shake_128().digest(10).hex()
32 | shake_256().digest(10).hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
33 |
34 | hashlib.sha256().digest().hex()
|
= help: Replace with `.hexdigest()`
FURB181.py:34:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
32 | shake_256().digest(10).hex()
33 |
34 | hashlib.sha256().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
35 |
36 | sha256(b"text").digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
31 31 | shake_128().digest(10).hex()
32 32 | shake_256().digest(10).hex()
33 33 |
34 |-hashlib.sha256().digest().hex()
34 |+hashlib.sha256().hexdigest()
35 35 |
36 36 | sha256(b"text").digest().hex()
37 37 |
FURB181.py:36:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
34 | hashlib.sha256().digest().hex()
35 |
36 | sha256(b"text").digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
37 |
38 | hash_algo().digest().hex()
|
= help: Replace with `.hexdigest()`
Unsafe fix
33 33 |
34 34 | hashlib.sha256().digest().hex()
35 35 |
36 |-sha256(b"text").digest().hex()
36 |+sha256(b"text").hexdigest()
37 37 |
38 38 | hash_algo().digest().hex()
39 39 |
FURB181.py:38:1: FURB181 [*] Use of hashlib's `.digest().hex()`
|
36 | sha256(b"text").digest().hex()
37 |
38 | hash_algo().digest().hex()
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ FURB181
39 |
40 | # not yet supported
|
= help: Replace with `.hexdigest()`
Unsafe fix
35 35 |
36 36 | sha256(b"text").digest().hex()
37 37 |
38 |-hash_algo().digest().hex()
38 |+hash_algo().hexdigest()
39 39 |
40 40 | # not yet supported
41 41 | h = sha256()

View File

@@ -1,14 +1,12 @@
use std::fmt;
use ruff_python_ast::{self as ast, Expr};
use ast::Stmt;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::analyze::typing;
use ruff_python_ast::{self as ast, Expr};
use ruff_python_semantic::{analyze::typing, Binding, SemanticModel};
use ruff_text_size::Ranged;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for `asyncio.create_task` and `asyncio.ensure_future` calls
/// that do not store a reference to the returned result.
@@ -66,35 +64,34 @@ impl Violation for AsyncioDanglingTask {
}
/// RUF006
pub(crate) fn asyncio_dangling_task(checker: &mut Checker, expr: &Expr) {
pub(crate) fn asyncio_dangling_task(expr: &Expr, semantic: &SemanticModel) -> Option<Diagnostic> {
let Expr::Call(ast::ExprCall { func, .. }) = expr else {
return;
return None;
};
// Ex) `asyncio.create_task(...)`
if let Some(method) = checker
.semantic()
.resolve_call_path(func)
.and_then(|call_path| match call_path.as_slice() {
["asyncio", "create_task"] => Some(Method::CreateTask),
["asyncio", "ensure_future"] => Some(Method::EnsureFuture),
_ => None,
})
if let Some(method) =
semantic
.resolve_call_path(func)
.and_then(|call_path| match call_path.as_slice() {
["asyncio", "create_task"] => Some(Method::CreateTask),
["asyncio", "ensure_future"] => Some(Method::EnsureFuture),
_ => None,
})
{
checker.diagnostics.push(Diagnostic::new(
return Some(Diagnostic::new(
AsyncioDanglingTask { method },
expr.range(),
));
return;
}
// Ex) `loop = asyncio.get_running_loop(); loop.create_task(...)`
if let Expr::Attribute(ast::ExprAttribute { attr, value, .. }) = func.as_ref() {
if attr == "create_task" {
if typing::resolve_assignment(value, checker.semantic()).is_some_and(|call_path| {
if typing::resolve_assignment(value, semantic).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["asyncio", "get_running_loop"])
}) {
checker.diagnostics.push(Diagnostic::new(
return Some(Diagnostic::new(
AsyncioDanglingTask {
method: Method::CreateTask,
},
@@ -103,6 +100,28 @@ pub(crate) fn asyncio_dangling_task(checker: &mut Checker, expr: &Expr) {
}
}
}
None
}
/// RUF006
pub(crate) fn asyncio_dangling_binding(
binding: &Binding,
semantic: &SemanticModel,
) -> Option<Diagnostic> {
if binding.is_used() || !binding.kind.is_assignment() {
return None;
}
let source = binding.source?;
match semantic.statement(source) {
Stmt::Assign(ast::StmtAssign { value, targets, .. }) if targets.len() == 1 => {
asyncio_dangling_task(value, semantic)
}
Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
}) => asyncio_dangling_task(value, semantic),
_ => None,
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]

View File

@@ -53,10 +53,12 @@ impl AlwaysFixableViolation for ExplicitFStringTypeConversion {
/// RUF010
pub(crate) fn explicit_f_string_type_conversion(checker: &mut Checker, f_string: &ast::FString) {
for (index, expr) in f_string.values.iter().enumerate() {
let Some(ast::ExprFormattedValue {
value, conversion, ..
}) = expr.as_formatted_value_expr()
for (index, element) in f_string.elements.iter().enumerate() {
let Some(ast::FStringExpressionElement {
expression,
conversion,
..
}) = element.as_expression()
else {
continue;
};
@@ -75,7 +77,7 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &mut Checker, f_string:
range: _,
},
..
}) = value.as_ref()
}) = expression.as_ref()
else {
continue;
};
@@ -110,7 +112,7 @@ pub(crate) fn explicit_f_string_type_conversion(checker: &mut Checker, f_string:
continue;
}
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range());
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, expression.range());
diagnostic.try_set_fix(|| {
convert_call_to_conversion_flag(f_string, index, checker.locator(), checker.stylist())
});

View File

@@ -24,6 +24,10 @@ use crate::rules::ruff::rules::helpers::{
/// `typing.ClassVar`. When mutability is not required, values should be
/// immutable types, like `tuple` or `frozenset`.
///
/// As a heuristic, this rule is only applied to classes with at least one
/// annotated attribute, as unannotated classes are assumed to be deliberately
/// untyped.
///
/// ## Examples
/// ```python
/// class A:
@@ -52,6 +56,10 @@ impl Violation for MutableClassDefault {
/// RUF012
pub(crate) fn mutable_class_default(checker: &mut Checker, class_def: &ast::StmtClassDef) {
if !class_def.body.iter().any(Stmt::is_ann_assign_stmt) {
return;
}
for statement in &class_def.body {
match statement {
Stmt::AnnAssign(ast::StmtAnnAssign {

View File

@@ -635,7 +635,6 @@ impl<'stmt> BasicBlocksBuilder<'stmt> {
| Expr::Set(_)
| Expr::Compare(_)
| Expr::Call(_)
| Expr::FormattedValue(_)
| Expr::FString(_)
| Expr::StringLiteral(_)
| Expr::BytesLiteral(_)

View File

@@ -17,11 +17,27 @@ RUF006.py:11:5: RUF006 Store a reference to the return value of `asyncio.ensure_
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF006
|
RUF006.py:79:5: RUF006 Store a reference to the return value of `asyncio.create_task`
RUF006.py:68:12: RUF006 Store a reference to the return value of `asyncio.create_task`
|
77 | def f():
78 | loop = asyncio.get_running_loop()
79 | loop.create_task(coordinator.ws_connect()) # Error
66 | # Error
67 | def f():
68 | task = asyncio.create_task(coordinator.ws_connect())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF006
|
RUF006.py:74:26: RUF006 Store a reference to the return value of `asyncio.create_task`
|
72 | def f():
73 | loop = asyncio.get_running_loop()
74 | task: asyncio.Task = loop.create_task(coordinator.ws_connect())
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF006
|
RUF006.py:97:5: RUF006 Store a reference to the return value of `asyncio.create_task`
|
95 | def f():
96 | loop = asyncio.get_running_loop()
97 | loop.create_task(coordinator.ws_connect()) # Error
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ RUF006
|

View File

@@ -98,8 +98,12 @@ fn contains_message(expr: &Expr) -> bool {
}
}
ast::FStringPart::FString(f_string) => {
for value in &f_string.values {
if contains_message(value) {
for literal in f_string
.elements
.iter()
.filter_map(|element| element.as_literal())
{
if literal.chars().any(char::is_whitespace) {
return true;
}
}

View File

@@ -119,16 +119,21 @@ impl From<bool> for PreviewMode {
}
}
/// Toggle for unsafe fixes.
/// `Hint` will not apply unsafe fixes but a message will be shown when they are available.
/// `Disabled` will not apply unsafe fixes or show a message.
/// `Enabled` will apply unsafe fixes.
#[derive(Debug, Copy, Clone, CacheKey, Default, PartialEq, Eq, is_macro::Is)]
pub enum UnsafeFixes {
#[default]
Hint,
Disabled,
Enabled,
}
impl From<bool> for UnsafeFixes {
fn from(version: bool) -> Self {
if version {
fn from(value: bool) -> Self {
if value {
UnsafeFixes::Enabled
} else {
UnsafeFixes::Disabled
@@ -140,7 +145,7 @@ impl UnsafeFixes {
pub fn required_applicability(&self) -> Applicability {
match self {
Self::Enabled => Applicability::Unsafe,
Self::Disabled => Applicability::Safe,
Self::Disabled | Self::Hint => Applicability::Safe,
}
}
}

View File

@@ -19,5 +19,20 @@ ipy_escape_command.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
5 |-import os
6 5 |
7 6 | _ = math.pi
8 7 | %%timeit
ipy_escape_command.ipynb:cell 2:2:8: F401 [*] `sys` imported but unused
|
1 | %%timeit
2 | import sys
| ^^^ F401
|
= help: Remove unused import: `sys`
Safe fix
6 6 |
7 7 | _ = math.pi
8 8 | %%timeit
9 |-import sys

View File

@@ -4,5 +4,10 @@
"id": "1",
"metadata": {},
"outputs": [],
"source": ["%%timeit\n", "print('hello world')"]
"source": [
"%%script bash\n",
"for i in 1 2 3; do\n",
" echo $i\n",
"done"
]
}

View File

@@ -0,0 +1,11 @@
{
"execution_count": null,
"cell_type": "code",
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"print('hello world')"
]
}

View File

@@ -26,6 +26,18 @@
"%%timeit\n",
"import sys"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "36dedfd1-6c03-4894-bea6-6c1687b82b3c",
"metadata": {},
"outputs": [],
"source": [
"%%random\n",
"# This cell is ignored\n",
"import pathlib"
]
}
],
"metadata": {

View File

@@ -22,8 +22,19 @@
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
"%%timeit"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4b6d7faa-72b3-4087-8670-fe6d35e41fb6",
"metadata": {},
"outputs": [],
"source": [
"%%random\n",
"# This cell is ignored\n",
"import pathlib"
]
}
],

View File

@@ -170,7 +170,50 @@ impl Cell {
}
// Detect cell magics (which operate on multiple lines).
lines.any(|line| line.trim_start().starts_with("%%"))
lines.any(|line| {
line.split_whitespace().next().is_some_and(|first| {
if first.len() < 2 {
return false;
}
let (token, command) = first.split_at(2);
// These cell magics are special in that the lines following them are valid
// Python code and the variables defined in that scope are available to the
// rest of the notebook.
//
// For example:
//
// Cell 1:
// ```python
// x = 1
// ```
//
// Cell 2:
// ```python
// %%time
// y = x
// ```
//
// Cell 3:
// ```python
// print(y) # Here, `y` is available.
// ```
//
// This is to avoid false positives when these variables are referenced
// elsewhere in the notebook.
token == "%%"
&& !matches!(
command,
"capture"
| "debug"
| "prun"
| "pypy"
| "python"
| "python3"
| "time"
| "timeit"
)
})
})
}
}

View File

@@ -426,6 +426,7 @@ mod tests {
#[test_case(Path::new("code_and_magic.json"), true; "code_and_magic")]
#[test_case(Path::new("only_code.json"), true; "only_code")]
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
#[test_case(Path::new("valid_cell_magic.json"), true; "valid_cell_magic")]
#[test_case(Path::new("automagic.json"), false; "automagic")]
#[test_case(Path::new("automagics.json"), false; "automagics")]
#[test_case(Path::new("automagic_before_code.json"), false; "automagic_before_code")]

View File

@@ -509,6 +509,41 @@ impl<'a> From<&'a ast::ExceptHandler> for ComparableExceptHandler<'a> {
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum ComparableFStringElement<'a> {
Literal(&'a str),
FStringExpressionElement(FStringExpressionElement<'a>),
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct FStringExpressionElement<'a> {
expression: ComparableExpr<'a>,
debug_text: Option<&'a ast::DebugText>,
conversion: ast::ConversionFlag,
format_spec: Option<Vec<ComparableFStringElement<'a>>>,
}
impl<'a> From<&'a ast::FStringElement> for ComparableFStringElement<'a> {
fn from(fstring_element: &'a ast::FStringElement) -> Self {
match fstring_element {
ast::FStringElement::Literal(ast::FStringLiteralElement { value, .. }) => {
Self::Literal(value)
}
ast::FStringElement::Expression(formatted_value) => {
Self::FStringExpressionElement(FStringExpressionElement {
expression: (&formatted_value.expression).into(),
debug_text: formatted_value.debug_text.as_ref(),
conversion: formatted_value.conversion,
format_spec: formatted_value
.format_spec
.as_ref()
.map(|spec| spec.elements.iter().map(Into::into).collect()),
})
}
}
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableElifElseClause<'a> {
test: Option<ComparableExpr<'a>>,
@@ -562,13 +597,13 @@ impl<'a> From<ast::LiteralExpressionRef<'a>> for ComparableLiteral<'a> {
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ComparableFString<'a> {
values: Vec<ComparableExpr<'a>>,
elements: Vec<ComparableFStringElement<'a>>,
}
impl<'a> From<&'a ast::FString> for ComparableFString<'a> {
fn from(fstring: &'a ast::FString) -> Self {
Self {
values: fstring.values.iter().map(Into::into).collect(),
elements: fstring.elements.iter().map(Into::into).collect(),
}
}
}
@@ -717,11 +752,11 @@ pub struct ExprCall<'a> {
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub struct ExprFormattedValue<'a> {
pub struct ExprFStringExpressionElement<'a> {
value: Box<ComparableExpr<'a>>,
debug_text: Option<&'a ast::DebugText>,
conversion: ast::ConversionFlag,
format_spec: Option<Box<ComparableExpr<'a>>>,
format_spec: Vec<ComparableFStringElement<'a>>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -813,7 +848,7 @@ pub enum ComparableExpr<'a> {
YieldFrom(ExprYieldFrom<'a>),
Compare(ExprCompare<'a>),
Call(ExprCall<'a>),
FormattedValue(ExprFormattedValue<'a>),
FStringExpressionElement(ExprFStringExpressionElement<'a>),
FString(ExprFString<'a>),
StringLiteral(ExprStringLiteral<'a>),
BytesLiteral(ExprBytesLiteral<'a>),
@@ -975,18 +1010,6 @@ impl<'a> From<&'a ast::Expr> for ComparableExpr<'a> {
func: func.into(),
arguments: arguments.into(),
}),
ast::Expr::FormattedValue(ast::ExprFormattedValue {
value,
conversion,
debug_text,
format_spec,
range: _,
}) => Self::FormattedValue(ExprFormattedValue {
value: value.into(),
conversion: *conversion,
debug_text: debug_text.as_ref(),
format_spec: format_spec.as_ref().map(Into::into),
}),
ast::Expr::FString(ast::ExprFString { value, range: _ }) => {
Self::FString(ExprFString {
parts: value.parts().map(Into::into).collect(),

View File

@@ -23,7 +23,6 @@ pub enum ExpressionRef<'a> {
YieldFrom(&'a ast::ExprYieldFrom),
Compare(&'a ast::ExprCompare),
Call(&'a ast::ExprCall),
FormattedValue(&'a ast::ExprFormattedValue),
FString(&'a ast::ExprFString),
StringLiteral(&'a ast::ExprStringLiteral),
BytesLiteral(&'a ast::ExprBytesLiteral),
@@ -67,7 +66,6 @@ impl<'a> From<&'a Expr> for ExpressionRef<'a> {
Expr::YieldFrom(value) => ExpressionRef::YieldFrom(value),
Expr::Compare(value) => ExpressionRef::Compare(value),
Expr::Call(value) => ExpressionRef::Call(value),
Expr::FormattedValue(value) => ExpressionRef::FormattedValue(value),
Expr::FString(value) => ExpressionRef::FString(value),
Expr::StringLiteral(value) => ExpressionRef::StringLiteral(value),
Expr::BytesLiteral(value) => ExpressionRef::BytesLiteral(value),
@@ -172,11 +170,6 @@ impl<'a> From<&'a ast::ExprCall> for ExpressionRef<'a> {
Self::Call(value)
}
}
impl<'a> From<&'a ast::ExprFormattedValue> for ExpressionRef<'a> {
fn from(value: &'a ast::ExprFormattedValue) -> Self {
Self::FormattedValue(value)
}
}
impl<'a> From<&'a ast::ExprFString> for ExpressionRef<'a> {
fn from(value: &'a ast::ExprFString) -> Self {
Self::FString(value)
@@ -273,7 +266,6 @@ impl<'a> From<ExpressionRef<'a>> for AnyNodeRef<'a> {
ExpressionRef::YieldFrom(expression) => AnyNodeRef::ExprYieldFrom(expression),
ExpressionRef::Compare(expression) => AnyNodeRef::ExprCompare(expression),
ExpressionRef::Call(expression) => AnyNodeRef::ExprCall(expression),
ExpressionRef::FormattedValue(expression) => AnyNodeRef::ExprFormattedValue(expression),
ExpressionRef::FString(expression) => AnyNodeRef::ExprFString(expression),
ExpressionRef::StringLiteral(expression) => AnyNodeRef::ExprStringLiteral(expression),
ExpressionRef::BytesLiteral(expression) => AnyNodeRef::ExprBytesLiteral(expression),
@@ -317,7 +309,6 @@ impl Ranged for ExpressionRef<'_> {
ExpressionRef::YieldFrom(expression) => expression.range(),
ExpressionRef::Compare(expression) => expression.range(),
ExpressionRef::Call(expression) => expression.range(),
ExpressionRef::FormattedValue(expression) => expression.range(),
ExpressionRef::FString(expression) => expression.range(),
ExpressionRef::StringLiteral(expression) => expression.range(),
ExpressionRef::BytesLiteral(expression) => expression.range(),
@@ -402,3 +393,41 @@ impl LiteralExpressionRef<'_> {
}
}
}
/// An enum that holds a reference to a string-like literal from the AST.
/// This includes string literals, bytes literals, and the literal parts of
/// f-strings.
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum StringLike<'a> {
StringLiteral(&'a ast::ExprStringLiteral),
BytesLiteral(&'a ast::ExprBytesLiteral),
FStringLiteral(&'a ast::FStringLiteralElement),
}
impl<'a> From<&'a ast::ExprStringLiteral> for StringLike<'a> {
fn from(value: &'a ast::ExprStringLiteral) -> Self {
StringLike::StringLiteral(value)
}
}
impl<'a> From<&'a ast::ExprBytesLiteral> for StringLike<'a> {
fn from(value: &'a ast::ExprBytesLiteral) -> Self {
StringLike::BytesLiteral(value)
}
}
impl<'a> From<&'a ast::FStringLiteralElement> for StringLike<'a> {
fn from(value: &'a ast::FStringLiteralElement) -> Self {
StringLike::FStringLiteral(value)
}
}
impl Ranged for StringLike<'_> {
fn range(&self) -> TextRange {
match self {
StringLike::StringLiteral(literal) => literal.range(),
StringLike::BytesLiteral(literal) => literal.range(),
StringLike::FStringLiteral(literal) => literal.range(),
}
}
}

View File

@@ -12,8 +12,8 @@ use crate::parenthesize::parenthesized_range;
use crate::statement_visitor::StatementVisitor;
use crate::visitor::Visitor;
use crate::{
self as ast, Arguments, CmpOp, ExceptHandler, Expr, MatchCase, Operator, Pattern, Stmt,
TypeParam,
self as ast, Arguments, CmpOp, ExceptHandler, Expr, FStringElement, MatchCase, Operator,
Pattern, Stmt, TypeParam,
};
use crate::{AnyNodeRef, ExprContext};
@@ -136,9 +136,9 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
Expr::BoolOp(ast::ExprBoolOp { values, .. }) => {
values.iter().any(|expr| any_over_expr(expr, func))
}
Expr::FString(ast::ExprFString { value, .. }) => {
value.elements().any(|expr| any_over_expr(expr, func))
}
Expr::FString(ast::ExprFString { value, .. }) => value
.elements()
.any(|expr| any_over_f_string_element(expr, func)),
Expr::NamedExpr(ast::ExprNamedExpr {
target,
value,
@@ -231,14 +231,6 @@ pub fn any_over_expr(expr: &Expr, func: &dyn Fn(&Expr) -> bool) -> bool {
.iter()
.any(|keyword| any_over_expr(&keyword.value, func))
}
Expr::FormattedValue(ast::ExprFormattedValue {
value, format_spec, ..
}) => {
any_over_expr(value, func)
|| format_spec
.as_ref()
.is_some_and(|value| any_over_expr(value, func))
}
Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => {
any_over_expr(value, func) || any_over_expr(slice, func)
}
@@ -315,6 +307,24 @@ pub fn any_over_pattern(pattern: &Pattern, func: &dyn Fn(&Expr) -> bool) -> bool
}
}
pub fn any_over_f_string_element(element: &FStringElement, func: &dyn Fn(&Expr) -> bool) -> bool {
match element {
FStringElement::Literal(_) => false,
FStringElement::Expression(ast::FStringExpressionElement {
expression,
format_spec,
..
}) => {
any_over_expr(expression, func)
|| format_spec.as_ref().is_some_and(|spec| {
spec.elements
.iter()
.any(|spec_element| any_over_f_string_element(spec_element, func))
})
}
}
}
pub fn any_over_stmt(stmt: &Stmt, func: &dyn Fn(&Expr) -> bool) -> bool {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
@@ -1318,16 +1328,18 @@ impl Truthiness {
Expr::FString(ast::ExprFString { value, .. }) => {
if value.parts().all(|part| match part {
ast::FStringPart::Literal(string_literal) => string_literal.is_empty(),
ast::FStringPart::FString(f_string) => f_string.values.is_empty(),
ast::FStringPart::FString(f_string) => f_string.elements.is_empty(),
}) {
Self::Falsey
} else if value.elements().any(|expr| {
if let Expr::StringLiteral(ast::ExprStringLiteral { value, .. }) = &expr {
!value.is_empty()
} else {
false
}
}) {
} else if value
.elements()
.any(|f_string_element| match f_string_element {
ast::FStringElement::Literal(ast::FStringLiteralElement {
value, ..
}) => !value.is_empty(),
ast::FStringElement::Expression(_) => true,
})
{
Self::Truthy
} else {
Self::Unknown

View File

@@ -1,7 +1,7 @@
use crate::visitor::preorder::PreorderVisitor;
use crate::{
self as ast, Alias, ArgOrKeyword, Arguments, Comprehension, Decorator, ExceptHandler, Expr,
Keyword, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern,
FStringElement, Keyword, MatchCase, Mod, Parameter, ParameterWithDefault, Parameters, Pattern,
PatternArguments, PatternKeyword, Stmt, TypeParam, TypeParamParamSpec, TypeParamTypeVar,
TypeParamTypeVarTuple, TypeParams, WithItem,
};
@@ -71,7 +71,6 @@ pub enum AnyNode {
ExprYieldFrom(ast::ExprYieldFrom),
ExprCompare(ast::ExprCompare),
ExprCall(ast::ExprCall),
ExprFormattedValue(ast::ExprFormattedValue),
ExprFString(ast::ExprFString),
ExprStringLiteral(ast::ExprStringLiteral),
ExprBytesLiteral(ast::ExprBytesLiteral),
@@ -88,6 +87,8 @@ pub enum AnyNode {
ExprSlice(ast::ExprSlice),
ExprIpyEscapeCommand(ast::ExprIpyEscapeCommand),
ExceptHandlerExceptHandler(ast::ExceptHandlerExceptHandler),
FStringExpressionElement(ast::FStringExpressionElement),
FStringLiteralElement(ast::FStringLiteralElement),
PatternMatchValue(ast::PatternMatchValue),
PatternMatchSingleton(ast::PatternMatchSingleton),
PatternMatchSequence(ast::PatternMatchSequence),
@@ -166,7 +167,8 @@ impl AnyNode {
| AnyNode::ExprYieldFrom(_)
| AnyNode::ExprCompare(_)
| AnyNode::ExprCall(_)
| AnyNode::ExprFormattedValue(_)
| AnyNode::FStringExpressionElement(_)
| AnyNode::FStringLiteralElement(_)
| AnyNode::ExprFString(_)
| AnyNode::ExprStringLiteral(_)
| AnyNode::ExprBytesLiteral(_)
@@ -233,7 +235,6 @@ impl AnyNode {
AnyNode::ExprYieldFrom(node) => Some(Expr::YieldFrom(node)),
AnyNode::ExprCompare(node) => Some(Expr::Compare(node)),
AnyNode::ExprCall(node) => Some(Expr::Call(node)),
AnyNode::ExprFormattedValue(node) => Some(Expr::FormattedValue(node)),
AnyNode::ExprFString(node) => Some(Expr::FString(node)),
AnyNode::ExprStringLiteral(node) => Some(Expr::StringLiteral(node)),
AnyNode::ExprBytesLiteral(node) => Some(Expr::BytesLiteral(node)),
@@ -278,6 +279,8 @@ impl AnyNode {
| AnyNode::StmtContinue(_)
| AnyNode::StmtIpyEscapeCommand(_)
| AnyNode::ExceptHandlerExceptHandler(_)
| AnyNode::FStringExpressionElement(_)
| AnyNode::FStringLiteralElement(_)
| AnyNode::PatternMatchValue(_)
| AnyNode::PatternMatchSingleton(_)
| AnyNode::PatternMatchSequence(_)
@@ -356,7 +359,8 @@ impl AnyNode {
| AnyNode::ExprYieldFrom(_)
| AnyNode::ExprCompare(_)
| AnyNode::ExprCall(_)
| AnyNode::ExprFormattedValue(_)
| AnyNode::FStringExpressionElement(_)
| AnyNode::FStringLiteralElement(_)
| AnyNode::ExprFString(_)
| AnyNode::ExprStringLiteral(_)
| AnyNode::ExprBytesLiteral(_)
@@ -459,7 +463,8 @@ impl AnyNode {
| AnyNode::ExprYieldFrom(_)
| AnyNode::ExprCompare(_)
| AnyNode::ExprCall(_)
| AnyNode::ExprFormattedValue(_)
| AnyNode::FStringExpressionElement(_)
| AnyNode::FStringLiteralElement(_)
| AnyNode::ExprFString(_)
| AnyNode::ExprStringLiteral(_)
| AnyNode::ExprBytesLiteral(_)
@@ -547,7 +552,8 @@ impl AnyNode {
| AnyNode::ExprYieldFrom(_)
| AnyNode::ExprCompare(_)
| AnyNode::ExprCall(_)
| AnyNode::ExprFormattedValue(_)
| AnyNode::FStringExpressionElement(_)
| AnyNode::FStringLiteralElement(_)
| AnyNode::ExprFString(_)
| AnyNode::ExprStringLiteral(_)
| AnyNode::ExprBytesLiteral(_)
@@ -660,7 +666,8 @@ impl AnyNode {
Self::ExprYieldFrom(node) => AnyNodeRef::ExprYieldFrom(node),
Self::ExprCompare(node) => AnyNodeRef::ExprCompare(node),
Self::ExprCall(node) => AnyNodeRef::ExprCall(node),
Self::ExprFormattedValue(node) => AnyNodeRef::ExprFormattedValue(node),
Self::FStringExpressionElement(node) => AnyNodeRef::FStringExpressionElement(node),
Self::FStringLiteralElement(node) => AnyNodeRef::FStringLiteralElement(node),
Self::ExprFString(node) => AnyNodeRef::ExprFString(node),
Self::ExprStringLiteral(node) => AnyNodeRef::ExprStringLiteral(node),
Self::ExprBytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node),
@@ -2621,12 +2628,12 @@ impl AstNode for ast::ExprCall {
visitor.visit_arguments(arguments);
}
}
impl AstNode for ast::ExprFormattedValue {
impl AstNode for ast::FStringExpressionElement {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::ExprFormattedValue(node) = kind {
if let AnyNode::FStringExpressionElement(node) = kind {
Some(node)
} else {
None
@@ -2634,7 +2641,7 @@ impl AstNode for ast::ExprFormattedValue {
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::ExprFormattedValue(node) = kind {
if let AnyNodeRef::FStringExpressionElement(node) = kind {
Some(node)
} else {
None
@@ -2653,16 +2660,54 @@ impl AstNode for ast::ExprFormattedValue {
where
V: PreorderVisitor<'a> + ?Sized,
{
let ast::ExprFormattedValue {
value, format_spec, ..
let ast::FStringExpressionElement {
expression,
format_spec,
..
} = self;
visitor.visit_expr(value);
visitor.visit_expr(expression);
if let Some(expr) = format_spec {
visitor.visit_format_spec(expr);
if let Some(format_spec) = format_spec {
for spec_part in &format_spec.elements {
visitor.visit_f_string_element(spec_part);
}
}
}
}
impl AstNode for ast::FStringLiteralElement {
fn cast(kind: AnyNode) -> Option<Self>
where
Self: Sized,
{
if let AnyNode::FStringLiteralElement(node) = kind {
Some(node)
} else {
None
}
}
fn cast_ref(kind: AnyNodeRef) -> Option<&Self> {
if let AnyNodeRef::FStringLiteralElement(node) = kind {
Some(node)
} else {
None
}
}
fn as_any_node_ref(&self) -> AnyNodeRef {
AnyNodeRef::from(self)
}
fn into_any_node(self) -> AnyNode {
AnyNode::from(self)
}
fn visit_preorder<'a, V>(&'a self, _visitor: &mut V)
where
V: PreorderVisitor<'a> + ?Sized,
{
}
}
impl AstNode for ast::ExprFString {
fn cast(kind: AnyNode) -> Option<Self>
where
@@ -4339,10 +4384,10 @@ impl AstNode for ast::FString {
where
V: PreorderVisitor<'a> + ?Sized,
{
let ast::FString { values, range: _ } = self;
let ast::FString { elements, range: _ } = self;
for expr in values {
visitor.visit_expr(expr);
for fstring_element in elements {
visitor.visit_f_string_element(fstring_element);
}
}
}
@@ -4467,7 +4512,6 @@ impl From<Expr> for AnyNode {
Expr::YieldFrom(node) => AnyNode::ExprYieldFrom(node),
Expr::Compare(node) => AnyNode::ExprCompare(node),
Expr::Call(node) => AnyNode::ExprCall(node),
Expr::FormattedValue(node) => AnyNode::ExprFormattedValue(node),
Expr::FString(node) => AnyNode::ExprFString(node),
Expr::StringLiteral(node) => AnyNode::ExprStringLiteral(node),
Expr::BytesLiteral(node) => AnyNode::ExprBytesLiteral(node),
@@ -4496,6 +4540,15 @@ impl From<Mod> for AnyNode {
}
}
impl From<FStringElement> for AnyNode {
fn from(element: FStringElement) -> Self {
match element {
FStringElement::Literal(node) => AnyNode::FStringLiteralElement(node),
FStringElement::Expression(node) => AnyNode::FStringExpressionElement(node),
}
}
}
impl From<Pattern> for AnyNode {
fn from(pattern: Pattern) -> Self {
match pattern {
@@ -4789,9 +4842,15 @@ impl From<ast::ExprCall> for AnyNode {
}
}
impl From<ast::ExprFormattedValue> for AnyNode {
fn from(node: ast::ExprFormattedValue) -> Self {
AnyNode::ExprFormattedValue(node)
impl From<ast::FStringExpressionElement> for AnyNode {
fn from(node: ast::FStringExpressionElement) -> Self {
AnyNode::FStringExpressionElement(node)
}
}
impl From<ast::FStringLiteralElement> for AnyNode {
fn from(node: ast::FStringLiteralElement) -> Self {
AnyNode::FStringLiteralElement(node)
}
}
@@ -5089,7 +5148,8 @@ impl Ranged for AnyNode {
AnyNode::ExprYieldFrom(node) => node.range(),
AnyNode::ExprCompare(node) => node.range(),
AnyNode::ExprCall(node) => node.range(),
AnyNode::ExprFormattedValue(node) => node.range(),
AnyNode::FStringExpressionElement(node) => node.range(),
AnyNode::FStringLiteralElement(node) => node.range(),
AnyNode::ExprFString(node) => node.range(),
AnyNode::ExprStringLiteral(node) => node.range(),
AnyNode::ExprBytesLiteral(node) => node.range(),
@@ -5184,7 +5244,8 @@ pub enum AnyNodeRef<'a> {
ExprYieldFrom(&'a ast::ExprYieldFrom),
ExprCompare(&'a ast::ExprCompare),
ExprCall(&'a ast::ExprCall),
ExprFormattedValue(&'a ast::ExprFormattedValue),
FStringExpressionElement(&'a ast::FStringExpressionElement),
FStringLiteralElement(&'a ast::FStringLiteralElement),
ExprFString(&'a ast::ExprFString),
ExprStringLiteral(&'a ast::ExprStringLiteral),
ExprBytesLiteral(&'a ast::ExprBytesLiteral),
@@ -5278,7 +5339,8 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ExprYieldFrom(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprCompare(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprCall(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprFormattedValue(node) => NonNull::from(*node).cast(),
AnyNodeRef::FStringExpressionElement(node) => NonNull::from(*node).cast(),
AnyNodeRef::FStringLiteralElement(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprFString(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprStringLiteral(node) => NonNull::from(*node).cast(),
AnyNodeRef::ExprBytesLiteral(node) => NonNull::from(*node).cast(),
@@ -5378,7 +5440,8 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ExprYieldFrom(_) => NodeKind::ExprYieldFrom,
AnyNodeRef::ExprCompare(_) => NodeKind::ExprCompare,
AnyNodeRef::ExprCall(_) => NodeKind::ExprCall,
AnyNodeRef::ExprFormattedValue(_) => NodeKind::ExprFormattedValue,
AnyNodeRef::FStringExpressionElement(_) => NodeKind::FStringExpressionElement,
AnyNodeRef::FStringLiteralElement(_) => NodeKind::FStringLiteralElement,
AnyNodeRef::ExprFString(_) => NodeKind::ExprFString,
AnyNodeRef::ExprStringLiteral(_) => NodeKind::ExprStringLiteral,
AnyNodeRef::ExprBytesLiteral(_) => NodeKind::ExprBytesLiteral,
@@ -5473,7 +5536,8 @@ impl<'a> AnyNodeRef<'a> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::ExprFormattedValue(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
@@ -5540,7 +5604,6 @@ impl<'a> AnyNodeRef<'a> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::ExprFormattedValue(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
@@ -5585,6 +5648,8 @@ impl<'a> AnyNodeRef<'a> {
| AnyNodeRef::StmtContinue(_)
| AnyNodeRef::StmtIpyEscapeCommand(_)
| AnyNodeRef::ExceptHandlerExceptHandler(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::PatternMatchValue(_)
| AnyNodeRef::PatternMatchSingleton(_)
| AnyNodeRef::PatternMatchSequence(_)
@@ -5662,7 +5727,8 @@ impl<'a> AnyNodeRef<'a> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::ExprFormattedValue(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
@@ -5765,7 +5831,8 @@ impl<'a> AnyNodeRef<'a> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::ExprFormattedValue(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
@@ -5853,7 +5920,8 @@ impl<'a> AnyNodeRef<'a> {
| AnyNodeRef::ExprYieldFrom(_)
| AnyNodeRef::ExprCompare(_)
| AnyNodeRef::ExprCall(_)
| AnyNodeRef::ExprFormattedValue(_)
| AnyNodeRef::FStringExpressionElement(_)
| AnyNodeRef::FStringLiteralElement(_)
| AnyNodeRef::ExprFString(_)
| AnyNodeRef::ExprStringLiteral(_)
| AnyNodeRef::ExprBytesLiteral(_)
@@ -5975,7 +6043,8 @@ impl<'a> AnyNodeRef<'a> {
AnyNodeRef::ExprYieldFrom(node) => node.visit_preorder(visitor),
AnyNodeRef::ExprCompare(node) => node.visit_preorder(visitor),
AnyNodeRef::ExprCall(node) => node.visit_preorder(visitor),
AnyNodeRef::ExprFormattedValue(node) => node.visit_preorder(visitor),
AnyNodeRef::FStringExpressionElement(node) => node.visit_preorder(visitor),
AnyNodeRef::FStringLiteralElement(node) => node.visit_preorder(visitor),
AnyNodeRef::ExprFString(node) => node.visit_preorder(visitor),
AnyNodeRef::ExprStringLiteral(node) => node.visit_preorder(visitor),
AnyNodeRef::ExprBytesLiteral(node) => node.visit_preorder(visitor),
@@ -6354,9 +6423,15 @@ impl<'a> From<&'a ast::ExprCall> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a ast::ExprFormattedValue> for AnyNodeRef<'a> {
fn from(node: &'a ast::ExprFormattedValue) -> Self {
AnyNodeRef::ExprFormattedValue(node)
impl<'a> From<&'a ast::FStringExpressionElement> for AnyNodeRef<'a> {
fn from(node: &'a ast::FStringExpressionElement) -> Self {
AnyNodeRef::FStringExpressionElement(node)
}
}
impl<'a> From<&'a ast::FStringLiteralElement> for AnyNodeRef<'a> {
fn from(node: &'a ast::FStringLiteralElement) -> Self {
AnyNodeRef::FStringLiteralElement(node)
}
}
@@ -6615,7 +6690,6 @@ impl<'a> From<&'a Expr> for AnyNodeRef<'a> {
Expr::YieldFrom(node) => AnyNodeRef::ExprYieldFrom(node),
Expr::Compare(node) => AnyNodeRef::ExprCompare(node),
Expr::Call(node) => AnyNodeRef::ExprCall(node),
Expr::FormattedValue(node) => AnyNodeRef::ExprFormattedValue(node),
Expr::FString(node) => AnyNodeRef::ExprFString(node),
Expr::StringLiteral(node) => AnyNodeRef::ExprStringLiteral(node),
Expr::BytesLiteral(node) => AnyNodeRef::ExprBytesLiteral(node),
@@ -6644,6 +6718,15 @@ impl<'a> From<&'a Mod> for AnyNodeRef<'a> {
}
}
impl<'a> From<&'a FStringElement> for AnyNodeRef<'a> {
fn from(element: &'a FStringElement) -> Self {
match element {
FStringElement::Expression(node) => AnyNodeRef::FStringExpressionElement(node),
FStringElement::Literal(node) => AnyNodeRef::FStringLiteralElement(node),
}
}
}
impl<'a> From<&'a Pattern> for AnyNodeRef<'a> {
fn from(pattern: &'a Pattern) -> Self {
match pattern {
@@ -6772,7 +6855,8 @@ impl Ranged for AnyNodeRef<'_> {
AnyNodeRef::ExprYieldFrom(node) => node.range(),
AnyNodeRef::ExprCompare(node) => node.range(),
AnyNodeRef::ExprCall(node) => node.range(),
AnyNodeRef::ExprFormattedValue(node) => node.range(),
AnyNodeRef::FStringExpressionElement(node) => node.range(),
AnyNodeRef::FStringLiteralElement(node) => node.range(),
AnyNodeRef::ExprFString(node) => node.range(),
AnyNodeRef::ExprStringLiteral(node) => node.range(),
AnyNodeRef::ExprBytesLiteral(node) => node.range(),
@@ -6869,7 +6953,8 @@ pub enum NodeKind {
ExprYieldFrom,
ExprCompare,
ExprCall,
ExprFormattedValue,
FStringExpressionElement,
FStringLiteralElement,
ExprFString,
ExprStringLiteral,
ExprBytesLiteral,

View File

@@ -590,8 +590,6 @@ pub enum Expr {
Compare(ExprCompare),
#[is(name = "call_expr")]
Call(ExprCall),
#[is(name = "formatted_value_expr")]
FormattedValue(ExprFormattedValue),
#[is(name = "f_string_expr")]
FString(ExprFString),
#[is(name = "string_literal_expr")]
@@ -919,19 +917,51 @@ impl From<ExprCall> for Expr {
}
}
/// See also [FormattedValue](https://docs.python.org/3/library/ast.html#ast.FormattedValue)
#[derive(Clone, Debug, PartialEq)]
pub struct ExprFormattedValue {
pub struct FStringFormatSpec {
pub range: TextRange,
pub value: Box<Expr>,
pub debug_text: Option<DebugText>,
pub conversion: ConversionFlag,
pub format_spec: Option<Box<Expr>>,
pub elements: Vec<FStringElement>,
}
impl From<ExprFormattedValue> for Expr {
fn from(payload: ExprFormattedValue) -> Self {
Expr::FormattedValue(payload)
impl Ranged for FStringFormatSpec {
fn range(&self) -> TextRange {
self.range
}
}
/// See also [FormattedValue](https://docs.python.org/3/library/ast.html#ast.FormattedValue)
#[derive(Clone, Debug, PartialEq)]
pub struct FStringExpressionElement {
pub range: TextRange,
pub expression: Box<Expr>,
pub debug_text: Option<DebugText>,
pub conversion: ConversionFlag,
pub format_spec: Option<Box<FStringFormatSpec>>,
}
impl Ranged for FStringExpressionElement {
fn range(&self) -> TextRange {
self.range
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FStringLiteralElement {
pub range: TextRange,
pub value: String,
}
impl Ranged for FStringLiteralElement {
fn range(&self) -> TextRange {
self.range
}
}
impl Deref for FStringLiteralElement {
type Target = str;
fn deref(&self) -> &Self::Target {
self.value.as_str()
}
}
@@ -1064,7 +1094,7 @@ impl FStringValue {
self.parts().filter_map(|part| part.as_f_string())
}
/// Returns an iterator over all the f-string elements contained in this value.
/// Returns an iterator over all the [`FStringElement`] contained in this value.
///
/// An f-string element is what makes up an [`FString`] i.e., it is either a
/// string literal or an expression. In the following example,
@@ -1075,8 +1105,8 @@ impl FStringValue {
///
/// The f-string elements returned would be string literal (`"bar "`),
/// expression (`x`) and string literal (`"qux"`).
pub fn elements(&self) -> impl Iterator<Item = &Expr> {
self.f_strings().flat_map(|fstring| fstring.values.iter())
pub fn elements(&self) -> impl Iterator<Item = &FStringElement> {
self.f_strings().flat_map(|fstring| fstring.elements.iter())
}
}
@@ -1113,7 +1143,7 @@ impl Ranged for FStringPart {
#[derive(Clone, Debug, PartialEq)]
pub struct FString {
pub range: TextRange,
pub values: Vec<Expr>,
pub elements: Vec<FStringElement>,
}
impl Ranged for FString {
@@ -1132,6 +1162,21 @@ impl From<FString> for Expr {
}
}
#[derive(Clone, Debug, PartialEq, is_macro::Is)]
pub enum FStringElement {
Literal(FStringLiteralElement),
Expression(FStringExpressionElement),
}
impl Ranged for FStringElement {
fn range(&self) -> TextRange {
match self {
FStringElement::Literal(node) => node.range(),
FStringElement::Expression(node) => node.range(),
}
}
}
/// An AST node that represents either a single string literal or an implicitly
/// concatenated string literals.
#[derive(Clone, Debug, Default, PartialEq)]
@@ -3483,11 +3528,6 @@ impl Ranged for crate::nodes::ExprCall {
self.range
}
}
impl Ranged for crate::nodes::ExprFormattedValue {
fn range(&self) -> TextRange {
self.range
}
}
impl Ranged for crate::nodes::ExprFString {
fn range(&self) -> TextRange {
self.range
@@ -3553,7 +3593,6 @@ impl Ranged for crate::Expr {
Self::YieldFrom(node) => node.range(),
Self::Compare(node) => node.range(),
Self::Call(node) => node.range(),
Self::FormattedValue(node) => node.range(),
Self::FString(node) => node.range(),
Self::StringLiteral(node) => node.range(),
Self::BytesLiteral(node) => node.range(),

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