Compare commits

...

102 Commits

Author SHA1 Message Date
Charlie Marsh
89d9f1e124 Use PatternMatchAs for mapping rest node 2023-08-24 00:44:50 -04:00
Harutaka Kawamura
205d234856 Format PatternMatchStar (#6653) 2023-08-24 01:58:05 +00:00
Charlie Marsh
4889b84338 Document logger-objects setting in flake8-logging-format rules (#6832)
Closes https://github.com/astral-sh/ruff/issues/6764.
2023-08-24 00:18:51 +00:00
Charlie Marsh
847432cacf Avoid attempting to fix PT018 in multi-statement lines (#6829)
## Summary

These fixes will _always_ fail, so we should avoid trying to construct
them in the first place.

Closes https://github.com/astral-sh/ruff/issues/6812.
2023-08-23 19:09:34 -04:00
Charlie Marsh
9b6e008cf1 Remove remaining usages of try_set_fix (#6828)
There are just a few remaining usages of this deprecated method.
2023-08-23 22:36:09 +00:00
Charlie Marsh
39c6665ff9 Remove remaining usage of set_fix_from_edit (#6827)
This method is deprecated; we have one last usage, so removing it.
2023-08-23 18:25:39 -04:00
konsti
19a87c220a Document ecosystem_all_check.sh input json (#6824)
Document the origin of `github_search.jsonl` in `ecosystem_all_check.sh`
2023-08-23 20:08:52 +00:00
Zanie Blue
0688883404 Fix uncessary-coding-comment fix when there's leading content (#6775)
Closes https://github.com/astral-sh/ruff/issues/6756

Including whitespace, code, and continuations.
2023-08-23 12:00:06 -05:00
Zanie Blue
3bb638875f Fix native-literals handling of int literal with attribute access (#6792)
Closes https://github.com/astral-sh/ruff/issues/6788 by special casing
integer literals with attribute access — either retaining parenthesis
for literals with values (e.g. `int(7).denominator` to
`(7).denominator)` or leaving calls without values (e.g.
`int().denominator`) unchanged.
2023-08-23 11:22:05 -05:00
Charlie Marsh
26e63ab137 Remove lexing from flake8-pytest-style (#6795)
## Summary

Another drive-by change to remove unnecessary custom lexing. We just
need to know the parenthesized range, so we can use...
`parenthesized_range`. I've also updated `parenthesized_range` to
support nested parentheses.

## Test Plan

`cargo test`
2023-08-23 15:54:11 +00:00
Zanie Blue
417a1d0717 Update mutable-argument-default (B006) to use extend-immutable-calls when determining if annotations are immutable (#6781)
Part of https://github.com/astral-sh/ruff/issues/3762
2023-08-23 15:44:35 +00:00
Micha Reiser
34b2ae73b4 Extend BestFitting with mode (#6814) 2023-08-23 17:23:45 +02:00
Charlie Marsh
71c25e4f9d Implement FormatPatternMatchValue (#6799)
## Summary

This is effectively #6608, but with additional tests.

We aren't properly handling parenthesized patterns, but that needs to be
dealt with separately as it's somewhat involved.

Closes #6555
2023-08-23 14:01:14 +00:00
Micha Reiser
4bdd99f882 Fix: Re-add missing node start positions (#6780) 2023-08-23 09:59:36 +02:00
Charlie Marsh
1e6d1182bf Improve comment handling around PatternMatchAs (#6797)
## Summary

Follows up on
https://github.com/astral-sh/ruff/pull/6652#discussion_r1300871033 with
some modifications to the `PatternMatchAs` comment handling.
Specifically, any comments between the `as` and the end are now
formatted as dangling, and we now insert some newlines in the
appropriate places.

## Test Plan

`cargo test`
2023-08-23 04:48:20 +00:00
Charlie Marsh
d08f697a04 Remove lexing for colon-matching use cases (#6803)
It's much simpler to just search ahead for the first colon.
2023-08-23 04:44:51 +00:00
Charlie Marsh
4bc5eddf91 Handle open-parenthesis comments on match case (#6798)
## Summary

Ensures that we retain the open-parenthesis comment in cases like:
```python
match pattern_comments:
    case (  # leading
        only_leading
    ):
        ...
```

Previously, this was treated as a leading comment on `only_leading`.

## Test Plan

`cargo test`
2023-08-23 00:40:18 -04:00
Charlie Marsh
5f5de52aba Confine repeated-equality-comparison-target to names and attributes (#6802)
Empirically, Pylint does this, so seems reasonable to follow.
2023-08-23 03:56:37 +00:00
Tom Kuson
1cb1bd731c Extend repeated-equality-comparison-target to check for mixed orderings and Yoda conditions. (#6691) 2023-08-23 03:45:30 +00:00
Dhruv Manilawala
db2e548f4f Simplify ANN204 autofix to use Parameters range (#6793)
## Summary

This PR fixes the bug where the decorator parentheses weren't being considered
when computing the autofix for `ANN204`. The existing logic would only look
for balanced parentheses and not multiple pairs of parentheses.

The solution is to remove the logic to generate the autofix and use the
`Parameters` end range directly which includes the parentheses as well.

## Test Plan

Add test case for `ANN204` with decorator being called

fixes: #6790
2023-08-23 09:05:46 +05:30
Harutaka Kawamura
94f5f18ddb Format PatternMatchSequence (#6676) 2023-08-23 00:44:33 +00:00
Luc Khai Hai
c34a342ab4 Format PatternMatchAs (#6652)
## Summary

Add formatting for `PatternMatchAs`.

This closes #6641.

## Test Plan

Add tests for comments.
2023-08-22 23:58:15 +00:00
Charlie Marsh
42ff833d00 Remove comment lexing from isort (#6794)
## Summary

No need to lex to find comments -- we already know their locations via
`Indexer`.
2023-08-22 21:26:38 +00:00
Konrad Listwan-Ciesielski
e1f4438498 Add docs for DTZ007 (#6757) 2023-08-22 21:12:50 +00:00
Charlie Marsh
1acdec3e29 Add a note on __future__ imports and keep-runtime-typing to pyupgrade rules (#6746)
Closes https://github.com/astral-sh/ruff/issues/6740.
2023-08-22 17:04:00 -04:00
Charlie Marsh
ca2bb20063 Fallback to end-of-file if ends in trailing continuation (#6789)
## Summary

Given:

```python
def end_of_file():
    if False:
        return 1
    x = 2 \

```

Then when searching for the end of the `x = 2` statement, we'd reach a
panic as we'd hit the last line (`\\`) and abort, since the universal
iterator doesn't return trailing newlines. Instead, we should just use
the end of the file as the fallback.

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

## Test Plan

`cargo test`
2023-08-22 15:12:26 -04:00
Dhruv Manilawala
2e00983762 Avoid C417 for lambda with default and variadic parameters (#6752)
## Summary

Avoid `C417` for `lambda` with default and variadic parameters.

## Test Plan

`cargo test` and checking if it generates any autofix errors as test
cases
for `lambda` with default parameters already exists.

fixes: #6715
2023-08-23 00:38:08 +05:30
Dhruv Manilawala
fb7caf43c8 Update lexer tests to use snapshots (#6658)
## Summary

This PR updates the lexer tests to use the snapshot testing framework.
It also
makes the following changes:
* Remove the use of macros in the lexer tests
* Use `test_case` for EOL tests

## Test Plan

```
cargo test --package ruff_python_parser --lib --all-features -- lexer::tests --no-capture
```
2023-08-22 18:23:19 +00:00
konsti
e53bf25616 Update formatter ecosystem checks revisions (#6770)
With https://github.com/django/django/pull/17181 merged, this removes an
odd edge case (tuple expression statements aka bogus trailing commas
after statements that turn them into a tuple without you noticing) that
we don't want to care about because the input code is ~wrong from the
similarity index. I've took this opportunity to update the revisions of
all projects we test.

main

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75477          |
| django       | 0.99814          |
| transformers | 0.99621          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

this PR

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75996          |
| django       | 0.99819          |
| transformers | 0.99622          |
| twine        | 0.99876          |
| typeshed     | 0.99953          |
| warehouse    | 0.99607          |
| zulip        | 0.99729          |
2023-08-22 14:19:10 -04:00
Charlie Marsh
214eb707a6 Parenthesize expressions prior to LibCST parsing (#6742)
<!--
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

This PR adds a utility for transforming expressions via LibCST that
automatically wraps the expression in parentheses, applies a
user-provided transformation, then strips the parentheses from the
generated code. LibCST can't parse arbitrary expression ranges, since
some expressions may require parenthesization in order to be parsed
properly. For example:

```python
option = (
    '{name}={value}'
    .format(nam=name, value=value)
)
```

In this case, the expression range is:

```python
'{name}={value}'
    .format(nam=name, value=value)
```

Which isn't valid on its own. So, instead, we add "fake" parentheses
around the expression.

We were already doing this in a few places, so this is mostly
formalizing and DRYing up that pattern.

Closes https://github.com/astral-sh/ruff/issues/6720.
2023-08-22 17:45:05 +00:00
Zanie Blue
5c1f7fd5dd Add networkx to conventional aliases (#6778)
Closes https://github.com/astral-sh/ruff/issues/6763
2023-08-22 11:49:04 -05:00
Charlie Marsh
cc278c24e2 Allow up to two empty lines after top-level imports (#6777)
## Summary

For imports, we enforce that there's _at least_ one empty line after an
import (assuming the next statement is _not_ an import), but allow up to
two at the module level.

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

## Test Plan

`cargo test`
2023-08-22 12:27:40 -04:00
Charlie Marsh
558b56f8a8 Avoid fixing D200 for docstrings that end in escapes (#6779)
Appease the fuzzers! Closes
https://github.com/astral-sh/ruff/issues/6755.
2023-08-22 16:25:37 +00:00
Charlie Marsh
749da6589a Fix isolation groups for unused imports (#6774)
## Summary

The isolation group for unused imports was relying on
`checker.semantic().current_statement()`, which isn't valid for that
rule, since it runs over the _scope_, not the statement. Instead, we
need to lookup the isolation group based on the `NodeId` of the
statement.

Our tests didn't catch this, because we mostly have cases that look like
this:

```python
if TYPE_CHECKING:
    import shelve
    import importlib
```

In this case, the two fixes to remove the two unused imports are
considered overlapping (since we delete the _full_ line, and the two
_full_ lines touch, and we consider exactly-adjacent fixes to be
overlapping), and so they don't run in a single pass due to the
non-overlapping-fixes requirement. That is: the isolation groups aren't
required for this case. They are, however, required for cases like:

```python
if TYPE_CHECKING:
    import shelve

    import importlib
```

...where the fixes don't overlap.

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

## Test Plan

`cargo test`
2023-08-22 11:55:27 -04:00
Charlie Marsh
d2eace3377 Prefer range_* edit methods (#6751) 2023-08-22 15:46:04 +00:00
Micha Reiser
ccac9681e1 Preserve yield parentheses (#6766) 2023-08-22 10:27:20 +00:00
Micha Reiser
b52cc84df6 Omit tuple parentheses in for statements except when absolutely necessary (#6765) 2023-08-22 12:18:59 +02:00
Micha Reiser
fec6fc2fab Preserve empty lines between try clause headers (#6759) 2023-08-22 11:50:28 +02:00
konsti
ba4c27598a Document IO Error (#6712)
`IOError` is special, it is not actually a lint but an error before
linting. I'm not entirely sure how to document it since it does not
match the general lint rule pattern (`Checks that the file can be read
in its entirety.` is imho worse).

I added the in my experience two most common reasons for io errors on
unix systems and linked two tutorials on how to fix them.

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

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-08-22 11:46:18 +02:00
Victor Hugo Gomes
0f9ccfcad9 Format PatternMatchSingleton (#6741) 2023-08-22 08:23:47 +02:00
Charlie Marsh
fa32cd9b6f Truncate some messages in diagnostics (#6748)
## Summary

I noticed this in the ecosystem CI check from
https://github.com/astral-sh/ruff/pull/6742. If we include source code
directly in a diagnostic, we need to be careful to avoid rendering
multi-line diagnostics or even excessively long diagnostics.

## Test Plan

`cargo test`
2023-08-21 23:46:24 -04:00
Victor Hugo Gomes
0aad0c41f6 [pylint] Implement no-self-use (R6301) (#6574) 2023-08-22 03:44:38 +00:00
Charlie Marsh
424b8d4ad2 Use a single node hierarchy to track statements and expressions (#6709)
## Summary

This PR is a follow-up to the suggestion in
https://github.com/astral-sh/ruff/pull/6345#discussion_r1285470953 to
use a single stack to store all statements and expressions, rather than
using separate vectors for each, which gives us something closer to a
full-fidelity chain. (We can then generalize this concept to include all
other AST nodes too.)

This is in part made possible by the removal of the hash map from
`&Stmt` to `StatementId` (#6694), which makes it much cheaper to store
these using a single interface (since doing so no longer introduces the
requirement that we hash all expressions).

I'll follow-up with some profiling, but a few notes on how the data
requirements have changed:

- We now store a `BranchId` for every expression, not just every
statement, so that's an extra `u32`.
- We now store a single `NodeId` on every snapshot, rather than separate
`StatementId` and `ExpressionId` IDs, so that's one fewer `u32` for each
snapshot.
- We're probably doing a few more lookups in general, since any calls to
`current_statement()` etc. now have to iterate up the node hierarchy
until they identify the first statement.

## Test Plan

`cargo test`
2023-08-21 21:32:57 -04:00
Charlie Marsh
abc5065fc7 Avoid E231 if comma is at end-of-line (#6747)
## Summary

I don't know how this could come up in valid Python, but anyway...

Closes https://github.com/astral-sh/ruff/issues/6738.
2023-08-21 20:47:20 -04:00
Victor Hugo Gomes
37f4920e1e Don't trigger eq-without-hash when __hash__ is explicitly set to None (#6739) 2023-08-21 23:51:21 +00:00
Charlie Marsh
c0df99b965 Avoid attempting to fix unconventional submodule imports (#6745)
## Summary

Avoid attempting to rewrite `import matplotlib.pyplot` as `import
matplotlib.pyplot as plt`. We can't support these right now, since we
don't track references at the attribute level (like
`matplotlib.pyplot`).

Closes https://github.com/astral-sh/ruff/issues/6719.
2023-08-21 23:45:32 +00:00
Charlie Marsh
7650c6ee45 Support C419 autofixes for set comprehensions (#6744)
Closes https://github.com/astral-sh/ruff/issues/6713.
2023-08-21 23:41:13 +00:00
Charlie Marsh
7b14d17e39 Ignore star imports when importing symbols in fixes (#6743)
## Summary

Given:

```python
from sys import *

exit(0)
```

We can't add `exit` to `from sys import *`, so we should just ignore it.
Ideally, we'd just resolve `exit` in the first place (since it's
imported from `from sys import *`), but as long as we don't support
wildcard imports, this is more consistent.

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

## Test Plan

`cargo test`
2023-08-21 23:31:30 +00:00
Charlie Marsh
4678f7dafe Remove parenthesis lexing in RSE102 (#6732)
## Summary

Now that we have an `Arguments` node, we can just use the range of the
arguments directly to find the parentheses in `raise Error()`.
2023-08-21 20:59:06 +00:00
konsti
b182368008 Simplify suite formatting (#6722)
Avoid the nesting in a macro by using the new `WithNodeLevel` to
`PyFormatter` deref. No changes otherwise.

I wanted to follow this up with quickly fixing the typeshed empty line
rules but they turned out a lot more complex than i had anticipated.
2023-08-21 21:01:51 +02:00
Charlie Marsh
e032fbd2e7 Remove remove_super_arguments (#6735)
Now that we have an `Arguments` node, we can use it directly to get the
range.
2023-08-21 13:04:07 -04:00
dependabot[bot]
575b77aa52 ci(deps): bump cloudflare/wrangler-action from 3.0.2 to 3.1.0 (#6736)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 3.0.2 to 3.1.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/releases">cloudflare/wrangler-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.1.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/154">#154</a>
<a
href="3f40637a1c"><code>3f40637</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- feat: Quiet mode
Some of the stderr, stdout, info &amp; groupings can be a little noisy
for some users and use cases.
This feature allows for a option to be passed 'quiet: true' this would
significantly reduce the noise.</p>
<p>There will still be output that lets the user know Wrangler Installed
and Wrangler Action completed successfully.
Any failure status will still be output to the user as well, to prevent
silent failures.</p>
<p>resolves <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/142">#142</a></p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md">cloudflare/wrangler-action's
changelog</a>.</em></p>
<blockquote>
<h2>3.1.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/154">#154</a>
<a
href="3f40637a1c"><code>3f40637</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- feat: Quiet mode
Some of the stderr, stdout, info &amp; groupings can be a little noisy
for some users and use cases.
This feature allows for a option to be passed 'quiet: true' this would
significantly reduce the noise.</p>
<p>There will still be output that lets the user know Wrangler Installed
and Wrangler Action completed successfully.
Any failure status will still be output to the user as well, to prevent
silent failures.</p>
<p>resolves <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/142">#142</a></p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="c25aadc965"><code>c25aadc</code></a>
Automatic compilation</li>
<li><a
href="fcf648c789"><code>fcf648c</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/158">#158</a>
from cloudflare/changeset-release/main</li>
<li><a
href="fcbabec21e"><code>fcbabec</code></a>
Version Packages</li>
<li><a
href="0aa12f0c2b"><code>0aa12f0</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/154">#154</a>
from cloudflare/jacobmgevans/silence-mode</li>
<li><a
href="ad7441b6ad"><code>ad7441b</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/157">#157</a>
from EstebanBorai/main</li>
<li><a
href="3f40637a1c"><code>3f40637</code></a>
Quiet feature</li>
<li><a
href="4132892387"><code>4132892</code></a>
fix: use <code>wrangler@3.5.1</code> by default</li>
<li><a
href="62ce9d23a3"><code>62ce9d2</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/155">#155</a>
from ethanppl/fix-readme</li>
<li><a
href="f089b0a195"><code>f089b0a</code></a>
Update README.md</li>
<li><a
href="4318a2fb97"><code>4318a2f</code></a>
Fix examples in README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/cloudflare/wrangler-action/compare/v3.0.2...v3.1.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cloudflare/wrangler-action&package-manager=github_actions&previous-version=3.0.2&new-version=3.1.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-21 16:35:59 +00:00
Micha Reiser
17a26e6ff3 Fix fmt:skip for function with return type (#6733) 2023-08-21 17:45:23 +02:00
Charlie Marsh
d5a51b4e45 Allow ctypes.WinError() in flake8-raise (#6731)
Closes https://github.com/astral-sh/ruff/issues/6730.
2023-08-21 14:57:34 +00:00
Charlie Marsh
83f68891e0 Allow next in FBT exclusions (#6729)
Closes https://github.com/astral-sh/ruff/issues/6711.
2023-08-21 14:56:38 +00:00
konsti
aafde6db28 Remove some indexing (#6728)
**Summary** A common pattern in the code used to be
```rust
if statements.len() != 1 {
    return;
}
use_single_entry(statements[0])?;
```
which can be better expressed as
```rust
let [statement] = statements else {
    return;
};
use_single_entry(statements)?;
```

Direct indexing can cause panics if you don't manually take care of
checking the length, while matching (such as if-let or let-else) can
never panic.

This isn't a complete refactor, i've just removed some of the obvious
cases. I've specifically looked for `.len() != 1` and fixed those.

**Test Plan** No functional changes
2023-08-21 16:56:15 +02:00
Charlie Marsh
2405536d03 Remove unnecessary LibCST usage in key-in-dict (#6727)
## Summary

We're using LibCST to ensure that we return the full parenthesized range
of an expression, for display purposes. We can just use
`parenthesized_range` which is more efficient and removes one LibCST
dependency.

## Test Plan

`cargo test`
2023-08-21 10:32:09 -04:00
Micha Reiser
f017555d53 Parenthesize NamedExpr if target breaks (#6714) 2023-08-21 16:29:26 +02:00
Charlie Marsh
be96e0041a Accept empty inner calls in C414 (#6725)
Closes https://github.com/astral-sh/ruff/issues/6716.
2023-08-21 14:05:09 +00:00
Harutaka Kawamura
3c2dd5e42e Remove confusing comment on get_parametrize_name_range (#6724) 2023-08-21 08:52:48 -04:00
Micha Reiser
8b347cdaa9 Simplify IfRequired needs parentheses condition (#6678) 2023-08-21 07:11:31 +00:00
Tom Kuson
2a8d24dd4b Format function and class definitions into a single line if its body is an ellipsis (#6592) 2023-08-21 09:02:23 +02:00
Charlie Marsh
bb5fbb1b5c Use simple lexer for argument removal (#6710) 2023-08-21 04:16:29 +00:00
Harutaka Kawamura
086e11087f [flake8-pytest-style] Autofix PT014 (#6698) 2023-08-21 03:45:12 +00:00
Charlie Marsh
1b7e4a12a9 Refactor remove_unused_variable to take &Binding (#6707) 2023-08-20 15:50:57 +00:00
Charlie Marsh
da1697121e Add BranchId to the model snapshot (#6706)
This _probably_ never matters given the set of rules we support and in
fact I'm having trouble thinking of a test-case for it, but it's
definitely incorrect _not_ to pass on the `BranchId` here.
2023-08-20 15:35:49 +00:00
Harutaka Kawamura
419615f29b Add docs for E275, E231, E251, and E252 (#6700) 2023-08-20 14:51:50 +00:00
Charlie Marsh
a742a562fd Ignore multi-comparisons in repeated-equality-comparison-target (#6705)
Given `foo == "a" == "b" or foo == "c"`, we were suggesting `foo in
{"a", "b", "c"}`.
2023-08-20 14:41:10 +00:00
Harutaka Kawamura
129b19050a Refactor flake8_pytest_style/rules/parametrize.rs (#6703) 2023-08-20 14:30:26 +00:00
Konrad Listwan-Ciesielski
0dc23da1d0 Add docs for DTZ011 and DTZ012 (#6688) 2023-08-20 10:21:10 -04:00
Harutaka Kawamura
c62e544cba Add doc for E999 (#6699) 2023-08-20 14:14:22 +00:00
Charlie Marsh
7e9023b6f8 Use typing_extensions.TypeAlias for PYI026 fixes on pre-3.10 (#6696)
Closes https://github.com/astral-sh/ruff/issues/6695.
2023-08-19 22:16:44 +00:00
Harutaka Kawamura
a489b96a65 [flake8-pie] Implement unnecessary-range-start (PIE808) (#6690) 2023-08-19 21:59:11 +00:00
Charlie Marsh
17af12e57c Add branch detection to the semantic model (#6694)
## Summary

We have a few rules that rely on detecting whether two statements are in
different branches -- for example, different arms of an `if`-`else`.
Historically, the way this was implemented is that, given two statement
IDs, we'd find the common parent (by traversing upwards via our
`Statements` abstraction); then identify branches "manually" by matching
the parents against `try`, `if`, and `match`, and returning iterators
over the arms; then check if there's an arm for which one of the
statements is a child, and the other is not.

This has a few drawbacks:

1. First, the code is generally a bit hard to follow (Konsti mentioned
this too when working on the `ElifElseClause` refactor).

2. Second, this is the only place in the codebase where we need to go
from `&Stmt` to `StatementID` -- _everywhere_ else, we only need to go
in the _other_ direction. Supporting these lookups means we need to
maintain a mapping from `&Stmt` to `StatementID` that includes every
`&Stmt` in the program. (We _also_ end up maintaining a `depth` level
for every statement.) I'd like to get rid of these requirements to
improve efficiency, reduce complexity, and enable us to treat AST modes
more generically in the future. (When I looked at adding the `&Expr` to
our existing statement-tracking infrastructure, maintaining a hash map
with all the statements noticeably hurt performance.)

The solution implemented here instead makes branches a first-class
concept in the semantic model. Like with `Statements`, we now have a
`Branches` abstraction, where each branch points to its optional parent.
When we store statements, we store the `BranchID` alongside each
statement. When we need to detect whether two statements are in the same
branch, we just realize each statement's branch path and compare the
two. (Assuming that the two statements are in the same scope, then
they're on the same branch IFF one branch path is a subset of the other,
starting from the top.) We then add some calls to the visitor to push
and pop branches in the appropriate places, for `if`, `try`, and `match`
statements.

Note that a branch is not 1:1 with a statement; instead, each branch is
closer to a suite, but not _every_ suite is a branch. For example, each
arm in an `if`-`elif`-`else` is a branch, but the `else` in a `for` loop
is not considered a branch.

In addition to being much simpler, this should also be more efficient,
since we've shed the entire `&Stmt` hash map, plus the `depth` that we
track on `StatementWithParent` in favor of a single `Option<BranchID>`
on `StatementWithParent` plus a single vector for all branches. The
lookups should be faster too, since instead of doing a bunch of jumps
around with the hash map + repeated recursive calls to find the common
parents, we instead just do a few simple lookups in the `Branches`
vector to realize and compare the branch paths.

## Test Plan

`cargo test` -- we have a lot of coverage for this, which we inherited
from PyFlakes
2023-08-19 21:28:17 +00:00
Chris Pryer
648333b8b2 ruff_formatter crate doc comment fixes (#6677) 2023-08-19 17:42:02 +01:00
Charlie Marsh
3849fa0cf1 Rewrite yield-in-for-loop to avoid recursing over body (#6692)
## Summary

This is much simpler and avoids (1) multiple passes over the entire
function body, (2) requiring the rule to do its own binding tracking (we
can just use the semantic model), and (3) a usage of `StatementKey`.

In general, where we can, we should try to remove these kinds of custom
visitors that track name references, and instead rely on the semantic
model.

## Test Plan

`cargo test`
2023-08-19 11:25:29 -04:00
Victor Hugo Gomes
59e533047a Fix typo in ruff_python_formatter documentation (#6687)
## Summary

In the documentation was written `Javascript` but we are working with
`Python` here :)

## Test Plan

n/a
2023-08-18 19:16:09 -04:00
Charlie Marsh
053b1145f0 Avoid panic in unused arguments rule for parameter-free lambda (#6679)
## Summary

This was just a mistake in pattern-matching with no test coverage.

## Test Plan

`cargo test`
2023-08-18 18:29:31 +00:00
Charlie Marsh
6a5acde226 Make Parameters an optional field on ExprLambda (#6669)
## Summary

If a lambda doesn't contain any parameters, or any parameter _tokens_
(like `*`), we can use `None` for the parameters. This feels like a
better representation to me, since, e.g., what should the `TextRange` be
for a non-existent set of parameters? It also allows us to remove
several sites where we check if the `Parameters` is empty by seeing if
it contains any arguments, so semantically, we're already trying to
detect and model around this elsewhere.

Changing this also fixes a number of issues with dangling comments in
parameter-less lambdas, since those comments are now automatically
marked as dangling on the lambda. (As-is, we were also doing something
not-great whereby the lambda was responsible for formatting dangling
comments on the parameters, which has been removed.)

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

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

## Test Plan

`cargo test`
2023-08-18 15:34:54 +00:00
Micha Reiser
ea72d5feba Refactor SourceKind to store file content (#6640) 2023-08-18 13:45:38 +00:00
Charlie Marsh
2aeb27334d Avoid cloning source code multiple times (#6629)
## Summary

In working on https://github.com/astral-sh/ruff/pull/6628, I noticed
that we clone the source code contents, potentially multiple times,
prior to linting. The issue is that `SourceKind::Python` takes a
`String`, so we first have to provide it with a `String`. In the stdin
case, that means cloning. However, on top of this, we then have to clone
`source_kind.contents()` because `SourceKind` gets mutated. So for
stdin, we end up cloning twice. For non-stdin, we end up cloning once,
but unnecessarily (since the _contents_ don't get mutated, only the
kind).

This PR removes the `String` from `source_kind`, instead requiring that
we parse it out elsewhere. It reduces the number of clones down to 1 for
Jupyter Notebooks, and zero otherwise.
2023-08-18 09:32:18 -04:00
Micha Reiser
0cea4975fc Rename Comments methods (#6649) 2023-08-18 06:37:01 +00:00
Charlie Marsh
3ceb6fbeb0 Remove some unnecessary ampersands in the formatter (#6667) 2023-08-18 04:18:26 +00:00
Charlie Marsh
8e18f8018f Remove some trailing commas in write calls (#6666) 2023-08-18 00:14:44 -04:00
Charlie Marsh
8228429a70 Convert comment to rustdoc in placement.rs (#6665) 2023-08-18 04:11:38 +00:00
Charlie Marsh
1811312722 Improve with statement comment handling and expression breaking (#6621)
## Summary

The motivating code here was:

```python
with test as (
    # test
foo):
    pass
```

Which we were formatting as:

```python
with test as
# test
(foo):
    pass
```

`with` statements are oddly difficult. This PR makes a bunch of subtle
modifications and adds a more extensive test suite. For example, we now
only preserve parentheses if there's more than one `WithItem` _or_ a
trailing comma; before, we always preserved.

Our formatting is_not_ the same as Black, but here's a diff of our
formatted code vs. Black's for the `with.py` test suite. The primary
difference is that we tend to break parentheses when they contain
comments rather than move them to the end of the life (this is a
consistent difference that we make across the codebase):

```diff
diff --git a/crates/ruff_python_formatter/foo.py b/crates/ruff_python_formatter/foo.py
index 85e761080..31625c876 100644
--- a/crates/ruff_python_formatter/foo.py
+++ b/crates/ruff_python_formatter/foo.py
@@ -1,6 +1,4 @@
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-), aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:
     ...
     # trailing
 
@@ -16,28 +14,33 @@ with (
     # trailing
 
 
-with a, b:  # a  # comma  # c  # colon
+with (
+    a,  # a  # comma
+    b,  # c
+):  # colon
     ...
 
 
 with (
-    a as  # a  # as
-    # own line
-    b,  # b  # comma
+    a as (  # a  # as
+        # own line
+        b
+    ),  # b  # comma
     c,  # c
 ):  # colon
     ...  # body
     # body trailing own
 
-with (
-    a as  # a  # as
+with a as (  # a  # as
     # own line
-    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb  # b
-):
+    bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+):  # b
     pass
 
 
-with (a,):  # magic trailing comma
+with (
+    a,
+):  # magic trailing comma
     ...
 
 
@@ -47,6 +50,7 @@ with a:  # should remove brackets
 with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as c:
     ...
 
+
 with (
     # leading comment
     a
@@ -74,8 +78,7 @@ with (
 with (
     a  # trailing same line comment
     # trailing own line comment
-    as b
-):
+) as b:
     ...
 
 with (
@@ -87,7 +90,9 @@ with (
 with (
     a
     # trailing own line comment
-) as b:  # trailing as same line comment  # trailing b same line comment
+) as (  # trailing as same line comment
+    b
+):  # trailing b same line comment
     ...
 
 with (
@@ -124,18 +129,24 @@ with (  # comment
     ...
 
 with (  # outer comment
-    CtxManager1() as example1,  # inner comment
+    (  # inner comment
+        CtxManager1()
+    ) as example1,
     CtxManager2() as example2,
     CtxManager3() as example3,
 ):
     ...
 
-with CtxManager() as example:  # outer comment
+with (  # outer comment
+    CtxManager()
+) as example:
     ...
 
 with (  # outer comment
     CtxManager()
-) as example, CtxManager2() as example2:  # inner comment
+) as example, (  # inner comment
+    CtxManager2()
+) as example2:
     ...
 
 with (  # outer comment
@@ -145,7 +156,9 @@ with (  # outer comment
     ...
 
 with (  # outer comment
-    (CtxManager1()),  # inner comment
+    (  # inner comment
+        CtxManager1()
+    ),
     CtxManager2(),
 ) as example:
     ...
@@ -179,7 +192,9 @@ with (
 ):
     pass
 
-with a as (b):  # foo
+with a as (  # foo
+    b
+):
     pass
 
 with f(
@@ -209,17 +224,13 @@ with f(
 ) as b, c as d:
     pass
 
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b:
     pass
 
 with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b:
     pass
 
-with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b, c as d:
+with aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b, c as d:
     pass
 
 with (
@@ -230,6 +241,8 @@ with (
     pass
 
 with (
-    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
-) as b, c as d:
+    aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+    + bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb as b,
+    c as d,
+):
     pass
```

Closes https://github.com/astral-sh/ruff/issues/6600.
## Test Plan

Before:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74292          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

After:

| project      | similarity index |
|--------------|------------------|
| cpython      | 0.75473          |
| django       | 0.99804          |
| transformers | 0.99618          |
| twine        | 0.99876          |
| typeshed     | 0.74292          |
| warehouse    | 0.99601          |
| zulip        | 0.99727          |

`cargo test`
2023-08-18 03:30:38 +00:00
Charlie Marsh
26bba11be6 Manually format comments around := in named expressions (#6634)
## Summary

Attaches comments around the `:=` operator in a named expression as
dangling, and formats them manually in the `named_expr.rs` formatter.

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

## Test Plan

`cargo test`
2023-08-18 03:10:45 +00:00
Shantanu
a128fe5148 Apply RUF017 when start is passed via position (#6664)
As discussed in
https://github.com/astral-sh/ruff/pull/6489#discussion_r1297858919.
Linking https://github.com/astral-sh/ruff/issues/5073
2023-08-17 20:10:07 -04:00
Zanie Blue
5892c691ea Bump version to 0.0.285 (#6660)
Requires
- https://github.com/astral-sh/ruff/pull/6655
- https://github.com/astral-sh/ruff/pull/6657
2023-08-17 15:46:28 -05:00
Zanie Blue
82e0a97b34 Clarify behavior of PLW3201 (#6657)
Otherwise it is unclear that violations will be raised for methods like
`_foo_`
2023-08-17 14:41:55 -05:00
Zanie Blue
a8d7bbae6f Remove experimental label from Jupyter docs (#6655) 2023-08-17 14:40:50 -05:00
Charlie Marsh
1050142a58 Expand expressions to include parentheses in E712 (#6575)
## Summary

This PR exposes our `is_expression_parenthesized` logic such that we can
use it to expand expressions when autofixing to include their
parenthesized ranges.

This solution has a few drawbacks: (1) we need to compute parenthesized
ranges in more places, which also relies on backwards lexing; and (2) we
need to make use of this in any relevant fixes.

However, I still think it's worth pursuing. On (1), the implementation
is very contained, so IMO we can easily swap this out for a more
performant solution in the future if needed. On (2), this improves
correctness and fixes some bad syntax errors detected by fuzzing, which
means it has value even if it's not as robust as an _actual_
`ParenthesizedExpression` node in the AST itself.

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

## Test Plan

`cargo test` with new cases that previously failed the fuzzer.
2023-08-17 15:51:09 +00:00
Charlie Marsh
db1c556508 Implement Ranged on more structs (#6639)
## Summary

I noticed some inconsistencies around uses of `.range.start()`, structs
that have a `TextRange` field but don't implement `Ranged`, etc.

## Test Plan

`cargo test`
2023-08-17 11:22:39 -04:00
Charlie Marsh
a70807e1e1 Expand NamedExpr range to include full range of parenthesized value (#6632)
## Summary

Given:

```python
if (
    x
    :=
    (  # 4
        y # 5
    )  # 6
):
    pass
```

It turns out the parser ended the range of the `NamedExpr` at the end of
`y`, rather than the end of the parenthesis that encloses `y`. This just
seems like a bug -- the range should be from the start of the name on
the left, to the end of the parenthesized node on the right.

## Test Plan

`cargo test`
2023-08-17 14:34:05 +00:00
dependabot[bot]
d9bb51dee4 ci(deps): bump cloudflare/wrangler-action from 3.0.0 to 3.0.2 (#6565)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 3.0.0 to 3.0.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/releases">cloudflare/wrangler-action's
releases</a>.</em></p>
<blockquote>
<h2>v3.0.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Added more error logging when a command fails to execute
Previously, we prevented any error logs from propagating too far to
prevent leaking of any potentially sensitive information. However, this
made it difficult for developers to debug their code.</p>
<p>In this release, we have updated our error handling to allow for more
error messaging from pre/post and custom commands. We still discourage
the use of these commands for secrets or other sensitive information,
but we believe this change will make it easier for developers to debug
their code.</p>
<p>Relates to <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/137">#137</a></p>
</li>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Adding Changesets</p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/HEAD/#version-300">Version
3.0.0</a></p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/HEAD/#version-200">Version
2.0.0</a></p>
</li>
</ul>
<h2>v3.0.1</h2>
<p>Automating Build &amp; Release</p>
<h2>What's Changed</h2>
<ul>
<li>Artifacts are now ESM supported with <code>.mjs</code></li>
<li>Update publish to deploy by <a
href="https://github.com/lrapoport-cf"><code>@​lrapoport-cf</code></a>
in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/124">cloudflare/wrangler-action#124</a></li>
<li>Automatically add issues to workers-sdk GH project by <a
href="https://github.com/lrapoport-cf"><code>@​lrapoport-cf</code></a>
in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/127">cloudflare/wrangler-action#127</a></li>
<li>Automate Action Release by <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>
in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/128">cloudflare/wrangler-action#128</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/lrapoport-cf"><code>@​lrapoport-cf</code></a>
made their first contribution in <a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/124">cloudflare/wrangler-action#124</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/cloudflare/wrangler-action/compare/3.0.0...3.0.1">https://github.com/cloudflare/wrangler-action/compare/3.0.0...3.0.1</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/cloudflare/wrangler-action/blob/main/CHANGELOG.md">cloudflare/wrangler-action's
changelog</a>.</em></p>
<blockquote>
<h2>3.0.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Added more error logging when a command fails to execute
Previously, we prevented any error logs from propagating too far to
prevent leaking of any potentially sensitive information. However, this
made it difficult for developers to debug their code.</p>
<p>In this release, we have updated our error handling to allow for more
error messaging from pre/post and custom commands. We still discourage
the use of these commands for secrets or other sensitive information,
but we believe this change will make it easier for developers to debug
their code.</p>
<p>Relates to <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/137">#137</a></p>
</li>
<li>
<p><a
href="https://redirect.github.com/cloudflare/wrangler-action/pull/147">#147</a>
<a
href="58f274b9f7"><code>58f274b</code></a>
Thanks <a
href="https://github.com/JacobMGEvans"><code>@​JacobMGEvans</code></a>!
- Adding Changesets</p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/main/#version-300">Version
3.0.0</a></p>
</li>
<li>
<p><a
href="https://github.com/cloudflare/wrangler-action/blob/main/#version-200">Version
2.0.0</a></p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="80501a7b4d"><code>80501a7</code></a>
Automatic compilation</li>
<li><a
href="3252711404"><code>3252711</code></a>
hotfix</li>
<li><a
href="2faabf36a2"><code>2faabf3</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/150">#150</a>
from cloudflare/jacobmgevans/changesets-tag-cli</li>
<li><a
href="b734d85d74"><code>b734d85</code></a>
Changesets needs a tag created for release</li>
<li><a
href="8ca2ff1612"><code>8ca2ff1</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/148">#148</a>
from cloudflare/changeset-release/main</li>
<li><a
href="7d08a8657e"><code>7d08a86</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/149">#149</a>
from cloudflare/jacobmgevans/changesets-needs-publish</li>
<li><a
href="e193627f19"><code>e193627</code></a>
Spoofing publish for Changeset Action</li>
<li><a
href="55b80c5f62"><code>55b80c5</code></a>
Version Packages</li>
<li><a
href="f61dc4d5a9"><code>f61dc4d</code></a>
Merge pull request <a
href="https://redirect.github.com/cloudflare/wrangler-action/issues/147">#147</a>
from cloudflare/revert-146-changeset-release/main</li>
<li><a
href="58f274b9f7"><code>58f274b</code></a>
Revert &quot;Version Packages&quot;</li>
<li>Additional commits viewable in <a
href="https://github.com/cloudflare/wrangler-action/compare/3.0.0...v3.0.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=cloudflare/wrangler-action&package-manager=github_actions&previous-version=3.0.0&new-version=3.0.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-17 09:11:04 -05:00
Zanie Blue
d0f2a8e424 Add support for nested replacements inside format specifications (#6616)
Closes https://github.com/astral-sh/ruff/issues/6442

Python string formatting like `"hello {place}".format(place="world")`
supports format specifications for replaced content such as `"hello
{place:>10}".format(place="world")` which will align the text to the
right in a container filled up to ten characters.

Ruff parses formatted strings into `FormatPart`s each of which is either
a `Field` (content in `{...}`) or a `Literal` (the normal content).
Fields are parsed into name and format specifier sections (we'll ignore
conversion specifiers for now).

There are a myriad of specifiers that can be used in a `FormatSpec`.
Unfortunately for linters, the specifier values can be dynamically set.
For example, `"hello {place:{align}{width}}".format(place="world",
align=">", width=10)` and `"hello {place:{fmt}}".format(place="world",
fmt=">10")` will yield the same string as before but variables can be
used to determine the formatting. In this case, when parsing the format
specifier we can't know what _kind_ of specifier is being used as their
meaning is determined by both position and value.

Ruff does not support nested replacements and our current data model
does not support the concept. Here the data model is updated to support
this concept, although linting of specifications with replacements will
be inherently limited. We could split format specifications into two
types, one without any replacements that we can perform lints with and
one with replacements that we cannot inspect. However, it seems
excessive to drop all parsing of format specifiers due to the presence
of a replacement. Instead, I've opted to parse replacements eagerly and
ignore their possible effect on other format specifiers. This will allow
us to retain a simple interface for `FormatSpec` and most syntax checks.
We may need to add some handling to relax errors if a replacement was
seen previously.

It's worth noting that the nested replacement _can_ also include a
format specification although it may fail at runtime if you produce an
invalid outer format specification. For example, `"hello
{place:{fmt:<2}}".format(place="world", fmt=">10")` is valid so we need
to represent each nested replacement as a full `FormatPart`.

## Test plan

Adding unit tests for `FormatSpec` parsing and snapshots for PLE1300
2023-08-17 09:07:30 -05:00
Charlie Marsh
1334232168 Introduce ExpressionRef (#6637)
## Summary

This PR revives the `ExpressionRef` concept introduced in
https://github.com/astral-sh/ruff/pull/5644, motivated by the change we
want to make in https://github.com/astral-sh/ruff/pull/6575 to narrow
the type of the expression that can be passed to `parenthesized_range`.

## Test Plan

`cargo test`
2023-08-17 10:07:16 -04:00
Micha Reiser
fa7442da2f Support fmt: skip on compound statements (#6593) 2023-08-17 06:05:41 +00:00
Micha Reiser
4dc32a00d0 Support fmt: skip for simple-statements and decorators (#6561) 2023-08-17 05:58:19 +00:00
Evan Rittenhouse
e3ecbe660e [ruff] Implement quadratic-list-summation rule (RUF017) (#6489)
## Summary

Adds `RUF017`. Closes #5073 

## Test Plan

`cargo t`
2023-08-16 23:13:05 -04:00
Harutaka Kawamura
8c3a8c4fc6 Support glob patterns for raises_require_match_for and raises_require_match_for (#6635)
## Summary

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

Support glob patterns for `raises_require_match_for` and
`raises_require_match_for`. Resolve #6473

## Test Plan

New tests + existing tests
2023-08-17 02:15:50 +00:00
Charlie Marsh
dcc7226685 Make lambda-assignment fix always-manual in class bodies (#6626)
## Summary

Related to https://github.com/astral-sh/ruff/issues/6620 (although that
will only be truly closed once we respect manual fixes on the CLI).
2023-08-16 21:24:48 -04:00
493 changed files with 31577 additions and 24916 deletions

View File

@@ -40,7 +40,7 @@ jobs:
run: mkdocs build --strict -f mkdocs.generated.yml
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@3.0.0
uses: cloudflare/wrangler-action@v3.1.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

View File

@@ -40,7 +40,7 @@ jobs:
working-directory: playground
- name: "Deploy to Cloudflare Pages"
if: ${{ env.CF_API_TOKEN_EXISTS == 'true' }}
uses: cloudflare/wrangler-action@3.0.0
uses: cloudflare/wrangler-action@v3.1.0
with:
apiToken: ${{ secrets.CF_API_TOKEN }}
accountId: ${{ secrets.CF_ACCOUNT_ID }}

7
Cargo.lock generated
View File

@@ -812,7 +812,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.284"
version = "0.0.285"
dependencies = [
"anyhow",
"clap",
@@ -2064,7 +2064,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.284"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2164,7 +2164,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.284"
version = "0.0.285"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2399,6 +2399,7 @@ dependencies = [
"ruff_text_size",
"rustc-hash",
"static_assertions",
"test-case",
"tiny-keccak",
"unic-emoji-char",
"unic-ucd-ident",

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.284
rev: v0.0.285
hooks:
- id: ruff
```

View File

@@ -1,6 +1,6 @@
[package]
name = "flake8-to-ruff"
version = "0.0.284"
version = "0.0.285"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "ruff"
version = "0.0.284"
version = "0.0.285"
publish = false
authors = { workspace = true }
edition = { workspace = true }

View File

@@ -152,3 +152,9 @@ def f(a: Union[str, bytes, Any]) -> None: ...
def f(a: Optional[Any]) -> None: ...
def f(a: Annotated[Any, ...]) -> None: ...
def f(a: "Union[str, bytes, Any]") -> None: ...
class Foo:
@decorator()
def __init__(self: "Foo", foo: int):
...

View File

@@ -69,6 +69,7 @@ g_action.set_enabled(True)
settings.set_enable_developer_extras(True)
foo.is_(True)
bar.is_not(False)
next(iter([]), False)
class Registry:
def __init__(self) -> None:

View File

@@ -0,0 +1,18 @@
import custom
from custom import ImmutableTypeB
def okay(foo: ImmutableTypeB = []):
...
def okay(foo: custom.ImmutableTypeA = []):
...
def okay(foo: custom.ImmutableTypeB = []):
...
def error_due_to_missing_import(foo: ImmutableTypeA = []):
...

View File

@@ -22,6 +22,10 @@ tuple(
"o"]
)
)
set(set())
set(list())
set(tuple())
sorted(reversed())
# Nested sorts with differing keyword arguments. Not flagged.
sorted(sorted(x, key=lambda y: y))

View File

@@ -15,11 +15,6 @@ filter(func, map(lambda v: v, nums))
_ = f"{set(map(lambda x: x % 2 == 0, nums))}"
_ = f"{dict(map(lambda v: (v, v**2), nums))}"
# Error, but unfixable.
# For simple expressions, this could be: `(x if x else 1 for x in nums)`.
# For more complex expressions, this would differ: `(x + 2 if x else 3 for x in nums)`.
map(lambda x=1: x, nums)
# False negatives.
map(lambda x=2, y=1: x + y, nums, nums)
set(map(lambda x, y: x, nums, nums))
@@ -37,3 +32,8 @@ map(lambda x: lambda: x, range(4))
# Error: the `x` is overridden by the inner lambda.
map(lambda x: lambda x: x, range(4))
# Ok because of the default parameters, and variadic arguments.
map(lambda x=1: x, nums)
map(lambda *args: len(args), range(4))
map(lambda **kwargs: len(kwargs), range(4))

View File

@@ -1,22 +1,31 @@
import math # not checked
def not_checked():
import math
import altair # unconventional
import matplotlib.pyplot # unconventional
import numpy # unconventional
import pandas # unconventional
import seaborn # unconventional
import tkinter # unconventional
import altair as altr # unconventional
import matplotlib.pyplot as plot # unconventional
import numpy as nmp # unconventional
import pandas as pdas # unconventional
import seaborn as sbrn # unconventional
import tkinter as tkr # unconventional
def unconventional():
import altair
import matplotlib.pyplot
import numpy
import pandas
import seaborn
import tkinter
import networkx
import altair as alt # conventional
import matplotlib.pyplot as plt # conventional
import numpy as np # conventional
import pandas as pd # conventional
import seaborn as sns # conventional
import tkinter as tk # conventional
def unconventional_aliases():
import altair as altr
import matplotlib.pyplot as plot
import numpy as nmp
import pandas as pdas
import seaborn as sbrn
import tkinter as tkr
import networkx as nxy
def conventional_aliases():
import altair as alt
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
import tkinter as tk
import networkx as nx

View File

@@ -0,0 +1,13 @@
# PIE808
range(0, 10)
# OK
range(x, 10)
range(-15, 10)
range(10)
range(0)
range(0, 10, x)
range(0, 10, 1)
range(0, 10, step=1)
range(start=0, stop=10)
range(0, stop=10)

View File

@@ -64,3 +64,8 @@ def test_implicit_str_concat_no_parens(param1, param2, param3):
@pytest.mark.parametrize((("param1, " "param2, " "param3")), [(1, 2, 3), (4, 5, 6)])
def test_implicit_str_concat_with_multi_parens(param1, param2, param3):
...
@pytest.mark.parametrize(("param1,param2"), [(1, 2), (3, 4)])
def test_csv_with_parens(param1, param2):
...

View File

@@ -1,3 +1,4 @@
from pickle import PicklingError, UnpicklingError
import socket
import pytest
@@ -20,6 +21,12 @@ def test_error_no_argument_given():
with pytest.raises(socket.error):
raise ValueError("Can't divide 1 by 0")
with pytest.raises(PicklingError):
raise PicklingError("Can't pickle")
with pytest.raises(UnpicklingError):
raise UnpicklingError("Can't unpickle")
def test_error_match_is_empty():
with pytest.raises(ValueError, match=None):

View File

@@ -16,11 +16,38 @@ def test_error_expr_simple(x):
...
@pytest.mark.parametrize("x", [(a, b), (a, b), (b, c)])
@pytest.mark.parametrize(
"x",
[
(a, b),
# comment
(a, b),
(b, c),
],
)
def test_error_expr_complex(x):
...
@pytest.mark.parametrize("x", [a, b, (a), c, ((a))])
def test_error_parentheses(x):
...
@pytest.mark.parametrize(
"x",
[
a,
b,
(a),
c,
((a)),
],
)
def test_error_parentheses_trailing_comma(x):
...
@pytest.mark.parametrize("x", [1, 2])
def test_ok(x):
...

View File

@@ -43,3 +43,12 @@ message
assert something # OK
assert something and something_else # Error
assert something and something_else and something_third # Error
def test_multiline():
assert something and something_else; x = 1
x = 1; assert something and something_else
x = 1; \
assert something and something_else

View File

@@ -19,11 +19,20 @@ raise TypeError ()
raise TypeError \
()
# RSE102
raise TypeError \
();
# RSE102
raise TypeError(
)
# RSE102
raise (TypeError) (
)
# RSE102
raise TypeError(
# Hello, world!
@@ -52,3 +61,10 @@ class Class:
# OK
raise Class.error()
import ctypes
# OK
raise ctypes.WinError(1)

View File

@@ -320,3 +320,9 @@ def end_of_statement():
if True:
return "" \
; # type: ignore
def end_of_file():
if False:
return 1
x = 2 \

View File

@@ -31,6 +31,8 @@ for key in list(obj.keys()):
key in (obj or {}).keys() # SIM118
(key) in (obj or {}).keys() # SIM118
from typing import KeysView

View File

@@ -27,6 +27,8 @@ def f(cls, x):
###
lambda x: print("Hello, world!")
lambda: print("Hello, world!")
class C:
###

View File

@@ -28,3 +28,6 @@ mdtypes_template = {
'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],
'tag_smalldata':[('byte_count_mdtype', 'u4'), ('data', 'S4')],
}
#: Okay
a = (1,

View File

@@ -25,6 +25,12 @@ if (True) == TrueElement or x == TrueElement:
if res == True != False:
pass
if(True) == TrueElement or x == TrueElement:
pass
if (yield i) == True:
print("even")
#: Okay
if x not in y:
pass

View File

@@ -133,3 +133,8 @@ def scope():
from collections.abc import Callable
f: Callable[[str, int, list[str]], list[str]] = lambda a, b, /, c: [*c, a * b]
class TemperatureScales(Enum):
CELSIUS = (lambda deg_c: deg_c)
FAHRENHEIT = (lambda deg_c: deg_c * 9 / 5 + 32)

View File

@@ -0,0 +1,13 @@
def func():
"""\
"""
def func():
"""\\
"""
def func():
"""\ \
"""

View File

@@ -99,3 +99,16 @@ import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))
# Test: isolated deletions.
if TYPE_CHECKING:
import a1
import a2
match *0, 1, *2:
case 0,:
import b1
import b2

View File

@@ -2,6 +2,5 @@
"{bar}{}".format(1, bar=2, spam=3) # F522
"{bar:{spam}}".format(bar=2, spam=3) # No issues
"{bar:{spam}}".format(bar=2, spam=3, eggs=4, ham=5) # F522
# Not fixable
(''
.format(x=2))
.format(x=2)) # F522

View File

@@ -28,6 +28,6 @@
"{1}{3}".format(1, 2, 3, 4) # F523, # F524
"{1} {8}".format(0, 1) # F523, # F524
# Not fixable
# Multiline
(''
.format(2))

View File

@@ -154,3 +154,14 @@ def f() -> None:
print("hello")
except A as e :
print("oh no!")
def f():
x = 1
y = 2
def f():
x = 1
y = 2

View File

@@ -14,7 +14,10 @@
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered")
"{:*^30s}".format("centered") # OK
"{:{s}}".format("hello", s="s") # OK (nested replacement value not checked)
"{:{s:y}}".format("hello", s="s") # [bad-format-character] (nested replacement format spec checked)
## f-strings

View File

@@ -1,10 +1,11 @@
class Person:
class Person: # [eq-without-hash]
def __init__(self):
self.name = "monty"
def __eq__(self, other):
return isinstance(other, Person) and other.name == self.name
# OK
class Language:
def __init__(self):
self.name = "python"
@@ -14,3 +15,9 @@ class Language:
def __hash__(self):
return hash(self.name)
class MyClass:
def __eq__(self, other):
return True
__hash__ = None

View File

@@ -0,0 +1,62 @@
import abc
class Person:
def developer_greeting(self, name): # [no-self-use]
print(f"Greetings {name}!")
def greeting_1(self): # [no-self-use]
print("Hello!")
def greeting_2(self): # [no-self-use]
print("Hi!")
# OK
def developer_greeting():
print("Greetings developer!")
# OK
class Person:
name = "Paris"
def __init__(self):
pass
def __cmp__(self, other):
print(24)
def __repr__(self):
return "Person"
def func(self):
...
def greeting_1(self):
print(f"Hello from {self.name} !")
@staticmethod
def greeting_2():
print("Hi!")
class Base(abc.ABC):
"""abstract class"""
@abstractmethod
def abstract_method(self):
"""abstract method could not be a function"""
raise NotImplementedError
class Sub(Base):
@override
def abstract_method(self):
print("concret method")
class Prop:
@property
def count(self):
return 24

View File

@@ -9,13 +9,22 @@ foo != "a" and foo != "b" and foo != "c"
foo == a or foo == "b" or foo == 3 # Mixed types.
# False negatives (the current implementation doesn't support Yoda conditions).
"a" == foo or "b" == foo or "c" == foo
"a" != foo and "b" != foo and "c" != foo
"a" == foo or foo == "b" or "c" == foo
foo == bar or baz == foo or qux == foo
foo == "a" or "b" == foo or foo == "c"
foo != "a" and "b" != foo and foo != "c"
foo == "a" or foo == "b" or "c" == bar or "d" == bar # Multiple targets
foo.bar == "a" or foo.bar == "b" # Attributes.
# OK
foo == "a" and foo == "b" and foo == "c" # `and` mixed with `==`.
@@ -32,3 +41,13 @@ foo not in {"a", "b", "c"} # Uses membership test already.
foo == "a" # Single comparison.
foo != "a" # Single comparison.
foo == "a" == "b" or foo == "c" # Multiple comparisons.
foo == bar == "b" or foo == "c" # Multiple comparisons.
foo == foo or foo == bar # Self-comparison.
foo[0] == "a" or foo[0] == "b" # Subscripts.
foo() == "a" or foo() == "b" # Calls.

View File

@@ -0,0 +1,3 @@
from sys import *
exit(0)

View File

@@ -0,0 +1,7 @@
"""
# coding=utf8""" # empty comment
"""
Invalid coding declaration since it is nested inside a docstring
The following empty comment tests for false positives as our implementation visits comments
"""

View File

@@ -0,0 +1,7 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading space must be removed to prevent invalid syntax.
"""

View File

@@ -0,0 +1,7 @@
# coding=utf8
print("Hello world")
"""
Regression test for https://github.com/astral-sh/ruff/issues/6756
The leading tab must be removed to prevent invalid syntax.
"""

View File

@@ -0,0 +1,6 @@
print("foo") # coding=utf8
print("Hello world")
"""
Invalid coding declaration due to a statement before the comment
"""

View File

@@ -0,0 +1,7 @@
x = 1 \
# coding=utf8
x = 2
"""
Invalid coding declaration due to continuation on preceding line
"""

View File

@@ -31,6 +31,7 @@ bool("foo")
bool("")
bool(b"")
bool(1.0)
int().denominator
# These become string or byte literals
str()
@@ -49,3 +50,6 @@ float(1.0)
bool()
bool(True)
bool(False)
# These become a literal but retain parentheses
int(1).denominator

View File

@@ -0,0 +1,14 @@
x = [1, 2, 3]
y = [4, 5, 6]
# RUF017
sum([x, y], start=[])
sum([x, y], [])
sum([[1, 2, 3], [4, 5, 6]], start=[])
sum([[1, 2, 3], [4, 5, 6]], [])
sum([[1, 2, 3], [4, 5, 6]],
[])
# OK
sum([x, y])
sum([[1, 2, 3], [4, 5, 6]])

View File

@@ -1,17 +1,17 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use anyhow::{Context, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{
self as ast, Arguments, ExceptHandler, Expr, Keyword, PySourceType, Ranged, Stmt,
};
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::{lexer, AsMode};
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
use ruff_python_trivia::{
has_leading_content, is_python_whitespace, PythonWhitespace, SimpleTokenKind, SimpleTokenizer,
};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_text_size::{TextLen, TextSize};
use crate::autofix::codemods;
@@ -48,7 +48,7 @@ pub(crate) fn delete_stmt(
} else if has_leading_content(stmt.start(), locator) {
Edit::range_deletion(stmt.range())
} else if let Some(start) = indexer.preceded_by_continuations(stmt.start(), locator) {
Edit::range_deletion(TextRange::new(start, stmt.end()))
Edit::deletion(start, stmt.end())
} else {
let range = locator.full_lines_range(stmt.range());
Edit::range_deletion(range)
@@ -89,83 +89,52 @@ pub(crate) fn remove_argument<T: Ranged>(
argument: &T,
arguments: &Arguments,
parentheses: Parentheses,
locator: &Locator,
source_type: PySourceType,
source: &str,
) -> Result<Edit> {
// TODO(sbrugman): Preserve trailing comments.
if arguments.keywords.len() + arguments.args.len() > 1 {
let mut fix_start = None;
let mut fix_end = None;
// Partition into arguments before and after the argument to remove.
let (before, after): (Vec<_>, Vec<_>) = arguments
.args
.iter()
.map(Expr::range)
.chain(arguments.keywords.iter().map(Keyword::range))
.filter(|range| argument.range() != *range)
.partition(|range| range.start() < argument.start());
if arguments
.args
.iter()
.map(Expr::start)
.chain(arguments.keywords.iter().map(Keyword::start))
.any(|location| location > argument.start())
{
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
arguments.start(),
)
.flatten()
{
if seen_comma {
if tok.is_non_logical_newline() {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if tok.is_newline() {
range.end()
} else {
range.start()
});
break;
}
if range.start() == argument.start() {
fix_start = Some(range.start());
}
if fix_start.is_some() && tok.is_comma() {
seen_comma = true;
}
}
} else {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
arguments.start(),
)
.flatten()
{
if range.start() == argument.start() {
fix_end = Some(argument.end());
break;
}
if tok.is_comma() {
fix_start = Some(range.start());
}
}
}
if !after.is_empty() {
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut tokenizer = SimpleTokenizer::starts_at(argument.end(), source);
match (fix_start, fix_end) {
(Some(start), Some(end)) => Ok(Edit::deletion(start, end)),
_ => {
bail!("No fix could be constructed")
}
}
// Find the trailing comma.
tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
// Find the next non-whitespace token.
let next = tokenizer
.find(|token| {
token.kind != SimpleTokenKind::Whitespace && token.kind != SimpleTokenKind::Newline
})
.context("Unable to find next token")?;
Ok(Edit::deletion(argument.start(), next.start()))
} else if let Some(previous) = before.iter().map(Ranged::end).max() {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
let mut tokenizer = SimpleTokenizer::starts_at(previous, source);
// Find the trailing comma.
let comma = tokenizer
.find(|token| token.kind == SimpleTokenKind::Comma)
.context("Unable to find trailing comma")?;
Ok(Edit::deletion(comma.start(), argument.end()))
} else {
// Only one argument; remove it (but preserve parentheses, if needed).
// Case 3: argument or keyword is the only node, so delete the arguments (but preserve
// parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
}
Parentheses::Remove => Edit::range_deletion(arguments.range()),
Parentheses::Preserve => Edit::range_replacement("()".to_string(), arguments.range()),
})
}
}
@@ -257,25 +226,25 @@ fn trailing_semicolon(offset: TextSize, locator: &Locator) -> Option<TextSize> {
fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
let start_location = semicolon + TextSize::from(1);
let contents = &locator.contents()[usize::from(start_location)..];
for line in NewlineWithTrailingNewline::from(contents) {
for line in
NewlineWithTrailingNewline::with_offset(locator.after(start_location), start_location)
{
let trimmed = line.trim_whitespace();
// Skip past any continuations.
if trimmed.starts_with('\\') {
continue;
}
return start_location
+ if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
return if trimmed.is_empty() {
// If the line is empty, then despite the previous statement ending in a
// semicolon, we know that it's not a multi-statement line.
line.start()
} else {
// Otherwise, find the start of the next statement. (Or, anything that isn't
// whitespace.)
let relative_offset = line.find(|c: char| !is_python_whitespace(c)).unwrap();
line.start() + TextSize::try_from(relative_offset).unwrap()
};
}
locator.line_end(start_location)

View File

@@ -13,6 +13,7 @@ use crate::registry::{AsRule, Rule};
pub(crate) mod codemods;
pub(crate) mod edits;
pub(crate) mod snippet;
pub(crate) mod source_map;
pub(crate) struct FixResult {

View File

@@ -0,0 +1,36 @@
use unicode_width::UnicodeWidthStr;
/// A snippet of source code for user-facing display, as in a diagnostic.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct SourceCodeSnippet(String);
impl SourceCodeSnippet {
pub(crate) fn new(source_code: String) -> Self {
Self(source_code)
}
/// Return the full snippet for user-facing display, or `None` if the snippet should be
/// truncated.
pub(crate) fn full_display(&self) -> Option<&str> {
if Self::should_truncate(&self.0) {
None
} else {
Some(&self.0)
}
}
/// Return a truncated snippet for user-facing display.
pub(crate) fn truncated_display(&self) -> &str {
if Self::should_truncate(&self.0) {
"..."
} else {
&self.0
}
}
/// Returns `true` if the source code should be truncated when included in a user-facing
/// diagnostic.
fn should_truncate(source_code: &str) -> bool {
source_code.width() > 50 || source_code.contains(['\r', '\n'])
}
}

View File

@@ -1,4 +1,5 @@
use ruff_diagnostics::{Diagnostic, Fix};
use ruff_python_ast::Ranged;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
@@ -29,7 +30,7 @@ pub(crate) fn bindings(checker: &mut Checker) {
pyflakes::rules::UnusedVariable {
name: binding.name(checker.locator).to_string(),
},
binding.range,
binding.range(),
);
if checker.patch(Rule::UnusedVariable) {
diagnostic.try_set_fix(|| {

View File

@@ -7,10 +7,6 @@ use crate::rules::flake8_simplify;
/// Run lint rules over a [`Comprehension`] syntax nodes.
pub(crate) fn comprehension(comprehension: &Comprehension, checker: &mut Checker) {
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(
checker,
&comprehension.target,
&comprehension.iter,
);
flake8_simplify::rules::key_in_dict_comprehension(checker, comprehension);
}
}

View File

@@ -1,8 +1,8 @@
use ruff_python_ast::{self as ast, Stmt};
use ruff_python_ast::Stmt;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, perflint};
use crate::rules::{flake8_bugbear, perflint, pyupgrade};
/// Run lint rules over all deferred for-loops in the [`SemanticModel`].
pub(crate) fn deferred_for_loops(checker: &mut Checker) {
@@ -11,18 +11,18 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
let Stmt::For(ast::StmtFor {
target, iter, body, ..
}) = checker.semantic.current_statement()
else {
let Stmt::For(stmt_for) = checker.semantic.current_statement() else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
flake8_bugbear::rules::unused_loop_control_variable(checker, stmt_for);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
perflint::rules::incorrect_dict_iterator(checker, stmt_for);
}
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt_for);
}
}
}

View File

@@ -1,5 +1,6 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_ast::Ranged;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
use crate::checkers::ast::Checker;
@@ -29,6 +30,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
Rule::UnusedPrivateTypedDict,
Rule::UnusedStaticMethodArgument,
Rule::UnusedVariable,
Rule::NoSelfUse,
]) {
return;
}
@@ -81,7 +83,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pylint::rules::GlobalVariableNotAssigned {
name: (*name).to_string(),
},
binding.range,
binding.range(),
));
}
}
@@ -111,25 +113,21 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
checker.semantic.different_branches(left, right)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.range.start());
let line = checker.locator.compute_line_index(shadowed.start());
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::ImportShadowedByLoopVar {
name: name.to_string(),
line,
},
binding.range,
binding.range(),
));
}
}
@@ -171,7 +169,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
let Some(statement_id) = shadowed.source else {
let Some(node_id) = shadowed.source else {
continue;
};
@@ -179,7 +177,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(statement_id)
.statement(node_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
@@ -207,24 +205,20 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
checker.semantic.different_branches(left, right)
})
}) {
continue;
}
#[allow(deprecated)]
let line = checker.locator.compute_line_index(shadowed.range.start());
let line = checker.locator.compute_line_index(shadowed.start());
let mut diagnostic = Diagnostic::new(
pyflakes::rules::RedefinedWhileUnused {
name: (*name).to_string(),
line,
},
binding.range,
binding.range(),
);
if let Some(range) = binding.parent_range(&checker.semantic) {
diagnostic.set_parent(range.start());
@@ -309,6 +303,12 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_import(checker, scope, &mut diagnostics);
}
}
if scope.kind.is_function() {
if checker.enabled(Rule::NoSelfUse) {
pylint::rules::no_self_use(checker, scope, &mut diagnostics);
}
}
}
checker.diagnostics.extend(diagnostics);
}

View File

@@ -381,34 +381,34 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Ok(summary) => {
if checker.enabled(Rule::StringDotFormatExtraNamedArguments) {
pyflakes::rules::string_dot_format_extra_named_arguments(
checker, &summary, keywords, location,
checker, call, &summary, keywords,
);
}
if checker
.enabled(Rule::StringDotFormatExtraPositionalArguments)
{
pyflakes::rules::string_dot_format_extra_positional_arguments(
checker, &summary, args, location,
checker, call, &summary, args,
);
}
if checker.enabled(Rule::StringDotFormatMissingArguments) {
pyflakes::rules::string_dot_format_missing_argument(
checker, &summary, args, keywords, location,
checker, call, &summary, args, keywords,
);
}
if checker.enabled(Rule::StringDotFormatMixingAutomatic) {
pyflakes::rules::string_dot_format_mixing_automatic(
checker, &summary, location,
checker, call, &summary,
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, call);
pyupgrade::rules::format_literals(checker, call, &summary);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
checker,
call,
&summary,
expr,
value,
checker.settings.line_length,
);
@@ -432,7 +432,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::deprecated_unittest_alias(checker, func);
}
if checker.enabled(Rule::SuperCallWithParameters) {
pyupgrade::rules::super_call_with_parameters(checker, expr, func, args);
pyupgrade::rules::super_call_with_parameters(checker, call);
}
if checker.enabled(Rule::UnnecessaryEncodeUTF8) {
pyupgrade::rules::unnecessary_encode_utf8(checker, call);
@@ -441,7 +441,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::redundant_open_modes(checker, call);
}
if checker.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
pyupgrade::rules::native_literals(
checker,
call,
checker.semantic().current_expression_parent(),
);
}
if checker.enabled(Rule::OpenAlias) {
pyupgrade::rules::open_alias(checker, expr, func);
@@ -531,6 +535,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
}
if checker.enabled(Rule::UnnecessaryRangeStart) {
flake8_pie::rules::unnecessary_range_start(checker, call);
}
if checker.enabled(Rule::ExecBuiltin) {
flake8_bandit::rules::exec_used(checker, func);
}
@@ -873,6 +880,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
}
if checker.enabled(Rule::QuadraticListSummation) {
ruff::rules::quadratic_list_summation(checker, call);
}
}
Expr::Dict(ast::ExprDict {
keys,
@@ -1172,7 +1182,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::magic_value_comparison(checker, left, comparators);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_compare(checker, expr, left, ops, comparators);
flake8_simplify::rules::key_in_dict_compare(checker, compare);
}
if checker.enabled(Rule::YodaConditions) {
flake8_simplify::rules::yoda_conditions(checker, expr, left, ops, comparators);

View File

@@ -338,9 +338,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::FStringDocstring) {
flake8_bugbear::rules::f_string_docstring(checker, body);
}
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt);
}
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
@@ -467,17 +464,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
if checker.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(checker, stmt, body);
flake8_pyi::rules::pass_in_class_body(checker, class_def);
}
}
if checker.enabled(Rule::EllipsisInNonEmptyClassBody) {
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, stmt, body);
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, body);
}
if checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle) {
flake8_pytest_style::rules::marks(checker, decorator_list);
}
if checker.enabled(Rule::DuplicateClassFieldDefinition) {
flake8_pie::rules::duplicate_class_field_definition(checker, stmt, body);
flake8_pie::rules::duplicate_class_field_definition(checker, body);
}
if checker.enabled(Rule::NonUniqueEnums) {
flake8_pie::rules::non_unique_enums(checker, stmt, body);
@@ -1142,7 +1139,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
Stmt::With(with_stmt @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1152,7 +1149,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
with_,
with_stmt,
checker.semantic.current_statement_parent(),
);
}
@@ -1171,15 +1168,21 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
perflint::rules::try_except_in_loop(checker, body);
}
}
Stmt::For(ast::StmtFor {
target,
body,
iter,
orelse,
..
}) => {
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
{
Stmt::For(
for_stmt @ ast::StmtFor {
target,
body,
iter,
orelse,
is_async,
..
},
) => {
if checker.any_enabled(&[
Rule::UnusedLoopControlVariable,
Rule::IncorrectDictIterator,
Rule::YieldInForLoop,
]) {
checker.deferred.for_loops.push(checker.semantic.snapshot());
}
if checker.enabled(Rule::LoopVariableOverridesIterator) {
@@ -1200,17 +1203,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::IterationOverSet) {
pylint::rules::iteration_over_set(checker, iter);
}
if stmt.is_for_stmt() {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(checker, target, iter);
}
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
}
if checker.enabled(Rule::ManualListComprehension) {
perflint::rules::manual_list_comprehension(checker, target, body);
}
@@ -1220,6 +1212,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(checker, iter);
}
if !is_async {
if checker.enabled(Rule::ReimplementedBuiltin) {
flake8_simplify::rules::convert_for_loop_to_any_all(checker, stmt);
}
if checker.enabled(Rule::InDictKeys) {
flake8_simplify::rules::key_in_dict_for(checker, for_stmt);
}
if checker.enabled(Rule::TryExceptInLoop) {
perflint::rules::try_except_in_loop(checker, body);
}
}
}
Stmt::Try(ast::StmtTry {
body,

View File

@@ -32,8 +32,8 @@ use itertools::Itertools;
use log::error;
use ruff_python_ast::{
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt,
Suite, UnaryOp,
ExprContext, Keyword, MatchCase, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged,
Stmt, Suite, UnaryOp,
};
use ruff_text_size::{TextRange, TextSize};
@@ -52,7 +52,8 @@ use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
use ruff_python_semantic::analyze::{typing, visibility};
use ruff_python_semantic::{
BindingFlags, BindingId, BindingKind, Exceptions, Export, FromImport, Globals, Import, Module,
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
ModuleKind, NodeId, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport,
SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_source_file::Locator;
@@ -193,20 +194,6 @@ impl<'a> Checker<'a> {
}
}
/// Returns the [`IsolationLevel`] for fixes in the current context.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
parent
.and_then(|stmt| self.semantic.statement_id(stmt))
.map_or(IsolationLevel::default(), |node_id| {
IsolationLevel::Group(node_id.into())
})
}
/// The [`Locator`] for the current file, which enables extraction of source code from byte
/// offsets.
pub(crate) const fn locator(&self) -> &'a Locator<'a> {
@@ -255,6 +242,18 @@ impl<'a> Checker<'a> {
pub(crate) const fn any_enabled(&self, rules: &[Rule]) -> bool {
self.settings.rules.any_enabled(rules)
}
/// Returns the [`IsolationLevel`] to isolate fixes for a given node.
///
/// The primary use-case for fix isolation is to ensure that we don't delete all statements
/// in a given indented block, which would cause a syntax error. We therefore need to ensure
/// that we delete at most one statement per indented block per fixer pass. Fix isolation should
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(node_id: Option<NodeId>) -> IsolationLevel {
node_id
.map(|node_id| IsolationLevel::Group(node_id.into()))
.unwrap_or_default()
}
}
impl<'a, 'b> Visitor<'b> for Checker<'a>
@@ -263,7 +262,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_statement(stmt);
self.semantic.push_node(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -619,16 +618,28 @@ where
}
}
// Iterate over the `body`, then the `handlers`, then the `orelse`, then the
// `finalbody`, but treat the body and the `orelse` as a single branch for
// flow analysis purposes.
let branch = self.semantic.push_branch();
self.semantic.handled_exceptions.push(handled_exceptions);
self.visit_body(body);
self.semantic.handled_exceptions.pop();
self.semantic.pop_branch();
for except_handler in handlers {
self.semantic.push_branch();
self.visit_except_handler(except_handler);
self.semantic.pop_branch();
}
self.semantic.set_branch(branch);
self.visit_body(orelse);
self.semantic.pop_branch();
self.semantic.push_branch();
self.visit_body(finalbody);
self.semantic.pop_branch();
}
Stmt::AnnAssign(ast::StmtAnnAssign {
target,
@@ -708,6 +719,7 @@ where
) => {
self.visit_boolean_test(test);
self.semantic.push_branch();
if typing::is_type_checking_block(stmt_if, &self.semantic) {
if self.semantic.at_top_level() {
self.importer.visit_type_checking_block(stmt);
@@ -716,9 +728,12 @@ where
} else {
self.visit_body(body);
}
self.semantic.pop_branch();
for clause in elif_else_clauses {
self.semantic.push_branch();
self.visit_elif_else_clause(clause);
self.semantic.pop_branch();
}
}
_ => visitor::walk_stmt(self, stmt),
@@ -759,7 +774,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_statement();
self.semantic.pop_node();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -795,7 +810,7 @@ where
return;
}
self.semantic.push_expression(expr);
self.semantic.push_node(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -874,18 +889,20 @@ where
},
) => {
// Visit the default arguments, but avoid the body, which will be deferred.
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
if let Some(parameters) = parameters {
for ParameterWithDefault {
default,
parameter: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
}
}
}
@@ -1213,7 +1230,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_expression();
self.semantic.pop_node();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@@ -1318,23 +1335,46 @@ where
fn visit_pattern(&mut self, pattern: &'b Pattern) {
// Step 1: Binding
if let Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name), ..
})
| Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
range: _,
})
| Pattern::MatchMapping(ast::PatternMatchMapping {
rest: Some(name), ..
}) = pattern
{
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
match &pattern {
Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name),
pattern: _,
range: _,
}) => {
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
Pattern::MatchStar(ast::PatternMatchStar {
name: Some(name),
range: _,
}) => {
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
Pattern::MatchMapping(ast::PatternMatchMapping {
rest: Some(rest), ..
}) => {
if let Pattern::MatchAs(ast::PatternMatchAs {
name: Some(name), ..
}) = rest.as_ref()
{
self.add_binding(
name,
name.range(),
BindingKind::Assignment,
BindingFlags::empty(),
);
}
}
_ => {}
}
// Step 2: Traversal
@@ -1351,6 +1391,17 @@ where
}
}
fn visit_match_case(&mut self, match_case: &'b MatchCase) {
self.visit_pattern(&match_case.pattern);
if let Some(expr) = &match_case.guard {
self.visit_expr(expr);
}
self.semantic.push_branch();
self.visit_body(&match_case.body);
self.semantic.pop_branch();
}
fn visit_type_param(&mut self, type_param: &'b ast::TypeParam) {
// Step 1: Binding
match type_param {
@@ -1834,7 +1885,9 @@ impl<'a> Checker<'a> {
range: _,
}) = expr
{
self.visit_parameters(parameters);
if let Some(parameters) = parameters {
self.visit_parameters(parameters);
}
self.visit_expr(body);
} else {
unreachable!("Expected Expr::Lambda");
@@ -1855,7 +1908,7 @@ impl<'a> Checker<'a> {
.map(|binding_id| &self.semantic.bindings[binding_id])
.filter_map(|binding| match &binding.kind {
BindingKind::Export(Export { names }) => {
Some(names.iter().map(|name| (*name, binding.range)))
Some(names.iter().map(|name| (*name, binding.range())))
}
_ => None,
})

View File

@@ -1,10 +1,10 @@
use ruff_python_parser::lexer::LexResult;
use ruff_text_size::TextRange;
use ruff_diagnostics::{Diagnostic, DiagnosticKind};
use ruff_python_ast::Ranged;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::TokenKind;
use ruff_source_file::Locator;
use ruff_text_size::TextRange;
use crate::registry::{AsRule, Rule};
use crate::rules::pycodestyle::rules::logical_lines::{

View File

@@ -110,8 +110,8 @@ pub(crate) fn check_noqa(
let mut diagnostic =
Diagnostic::new(UnusedNOQA { codes: None }, directive.range());
if settings.rules.should_fix(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix_from_edit(delete_noqa(directive.range(), locator));
diagnostic
.set_fix(Fix::automatic(delete_noqa(directive.range(), locator)));
}
diagnostics.push(diagnostic);
}
@@ -175,12 +175,12 @@ pub(crate) fn check_noqa(
);
if settings.rules.should_fix(diagnostic.kind.rule()) {
if valid_codes.is_empty() {
#[allow(deprecated)]
diagnostic
.set_fix_from_edit(delete_noqa(directive.range(), locator));
diagnostic.set_fix(Fix::automatic(delete_noqa(
directive.range(),
locator,
)));
} else {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
format!("# noqa: {}", valid_codes.join(", ")),
directive.range(),
)));

View File

@@ -216,6 +216,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "R1722") => (RuleGroup::Unspecified, rules::pylint::rules::SysExitAlias),
(Pylint, "R2004") => (RuleGroup::Unspecified, rules::pylint::rules::MagicValueComparison),
(Pylint, "R5501") => (RuleGroup::Unspecified, rules::pylint::rules::CollapsibleElseIf),
(Pylint, "R6301") => (RuleGroup::Nursery, rules::pylint::rules::NoSelfUse),
(Pylint, "W0120") => (RuleGroup::Unspecified, rules::pylint::rules::UselessElseOnLoop),
(Pylint, "W0127") => (RuleGroup::Unspecified, rules::pylint::rules::SelfAssigningVariable),
(Pylint, "W0129") => (RuleGroup::Unspecified, rules::pylint::rules::AssertOnStringLiteral),
@@ -707,6 +708,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pie, "800") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessarySpread),
(Flake8Pie, "804") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryDictKwargs),
(Flake8Pie, "807") => (RuleGroup::Unspecified, rules::flake8_pie::rules::ReimplementedListBuiltin),
(Flake8Pie, "808") => (RuleGroup::Unspecified, rules::flake8_pie::rules::UnnecessaryRangeStart),
(Flake8Pie, "810") => (RuleGroup::Unspecified, rules::flake8_pie::rules::MultipleStartsEndsWith),
// flake8-commas
@@ -816,6 +818,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Ruff, "014") => (RuleGroup::Nursery, rules::ruff::rules::UnreachableCode),
(Ruff, "015") => (RuleGroup::Unspecified, rules::ruff::rules::UnnecessaryIterableAllocationForFirstElement),
(Ruff, "016") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidIndexType),
(Ruff, "017") => (RuleGroup::Nursery, rules::ruff::rules::QuadraticListSummation),
(Ruff, "100") => (RuleGroup::Unspecified, rules::ruff::rules::UnusedNOQA),
(Ruff, "200") => (RuleGroup::Unspecified, rules::ruff::rules::InvalidPyprojectToml),

View File

@@ -1,9 +1,11 @@
use crate::autofix::codemods::CodegenStylist;
use anyhow::{bail, Result};
use libcst_native::{
Arg, Attribute, Call, Comparison, CompoundStatement, Dict, Expression, FunctionDef,
GeneratorExp, If, Import, ImportAlias, ImportFrom, ImportNames, IndentedBlock, Lambda,
ListComp, Module, Name, SmallStatement, Statement, Suite, Tuple, With,
};
use ruff_python_codegen::Stylist;
pub(crate) fn match_module(module_text: &str) -> Result<Module> {
match libcst_native::parse_module(module_text, None) {
@@ -12,13 +14,6 @@ pub(crate) fn match_module(module_text: &str) -> Result<Module> {
}
}
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
pub(crate) fn match_statement(statement_text: &str) -> Result<Statement> {
match libcst_native::parse_statement(statement_text) {
Ok(statement) => Ok(statement),
@@ -205,3 +200,59 @@ pub(crate) fn match_if<'a, 'b>(statement: &'a mut Statement<'b>) -> Result<&'a m
bail!("Expected Statement::Compound")
}
}
/// Given the source code for an expression, return the parsed [`Expression`].
///
/// If the expression is not guaranteed to be valid as a standalone expression (e.g., if it may
/// span multiple lines and/or require parentheses), use [`transform_expression`] instead.
pub(crate) fn match_expression(expression_text: &str) -> Result<Expression> {
match libcst_native::parse_expression(expression_text) {
Ok(expression) => Ok(expression),
Err(_) => bail!("Failed to extract expression from source"),
}
}
/// Run a transformation function over an expression.
///
/// Passing an expression to [`match_expression`] directly can lead to parse errors if the
/// expression is not a valid standalone expression (e.g., it was parenthesized in the original
/// source). This method instead wraps the expression in "fake" parentheses, runs the
/// transformation, then removes the "fake" parentheses.
pub(crate) fn transform_expression(
source_code: &str,
stylist: &Stylist,
func: impl FnOnce(Expression) -> Result<Expression>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
let expression = match_expression(&source_code)?;
// Run the function on the expression.
let expression = func(expression)?;
// Codegen the expression.
let mut source_code = expression.codegen_stylist(stylist);
// Drop the outer parentheses.
source_code.drain(0..1);
source_code.drain(source_code.len() - 1..source_code.len());
Ok(source_code)
}
/// Like [`transform_expression`], but operates on the source code of the expression, rather than
/// the parsed [`Expression`]. This _shouldn't_ exist, but does to accommodate lifetime issues.
pub(crate) fn transform_expression_text(
source_code: &str,
func: impl FnOnce(String) -> Result<String>,
) -> Result<String> {
// Wrap the expression in parentheses.
let source_code = format!("({source_code})");
// Run the function on the expression.
let mut transformed = func(source_code)?;
// Drop the outer parentheses.
transformed.drain(0..1);
transformed.drain(transformed.len() - 1..transformed.len());
Ok(transformed)
}

View File

@@ -405,7 +405,7 @@ y = 2
z = x + 1";
assert_eq!(
noqa_mappings(contents),
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22)),])
NoqaMapping::from_iter([TextRange::new(TextSize::from(0), TextSize::from(22))])
);
let contents = "x = 1

View File

@@ -2,9 +2,8 @@ use std::fmt::{Debug, Formatter};
use std::ops::Deref;
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::{TextRange, TextSize};
use ruff_python_semantic::Definition;
use ruff_text_size::TextRange;
pub(crate) mod extraction;
pub(crate) mod google;
@@ -28,43 +27,34 @@ impl<'a> Docstring<'a> {
DocstringBody { docstring: self }
}
pub(crate) fn start(&self) -> TextSize {
self.expr.start()
}
pub(crate) fn end(&self) -> TextSize {
self.expr.end()
}
pub(crate) fn range(&self) -> TextRange {
self.expr.range()
}
pub(crate) fn leading_quote(&self) -> &'a str {
&self.contents[TextRange::up_to(self.body_range.start())]
}
}
impl Ranged for Docstring<'_> {
fn range(&self) -> TextRange {
self.expr.range()
}
}
#[derive(Copy, Clone)]
pub(crate) struct DocstringBody<'a> {
docstring: &'a Docstring<'a>,
}
impl<'a> DocstringBody<'a> {
#[inline]
pub(crate) fn start(self) -> TextSize {
self.range().start()
}
pub(crate) fn range(self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
pub(crate) fn as_str(self) -> &'a str {
&self.docstring.contents[self.docstring.body_range]
}
}
impl Ranged for DocstringBody<'_> {
fn range(&self) -> TextRange {
self.docstring.body_range + self.docstring.start()
}
}
impl Deref for DocstringBody<'_> {
type Target = str;

View File

@@ -2,6 +2,7 @@ use std::fmt::{Debug, Formatter};
use std::iter::FusedIterator;
use ruff_python_ast::docstrings::{leading_space, leading_words};
use ruff_python_ast::Ranged;
use ruff_text_size::{TextLen, TextRange, TextSize};
use strum_macros::EnumIter;
@@ -366,6 +367,12 @@ impl<'a> SectionContext<'a> {
}
}
impl Ranged for SectionContext<'_> {
fn range(&self) -> TextRange {
self.range()
}
}
impl Debug for SectionContext<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SectionContext")

View File

@@ -194,7 +194,7 @@ impl<'a> Importer<'a> {
// import and the current location, and thus the symbol would not be available). It's also
// unclear whether should add an import statement at the start of the file, since it could
// be shadowed between the import and the current location.
if imported_name.range().start() > at {
if imported_name.start() > at {
return Some(Err(ResolutionError::ImportAfterUsage));
}
@@ -301,12 +301,14 @@ impl<'a> Importer<'a> {
}
if let Stmt::ImportFrom(ast::StmtImportFrom {
module: name,
names,
level,
..
range: _,
}) = stmt
{
if level.map_or(true, |level| level.to_u32() == 0)
&& name.as_ref().is_some_and(|name| name == module)
&& names.iter().all(|alias| alias.name.as_str() != "*")
{
import_from = Some(*stmt);
}

View File

@@ -33,7 +33,7 @@ pub fn round_trip(path: &Path) -> anyhow::Result<String> {
err
)
})?;
let code = notebook.content().to_string();
let code = notebook.source_code().to_string();
notebook.update_cell_content(&code);
let mut writer = Vec::new();
notebook.write_inner(&mut writer)?;
@@ -103,7 +103,7 @@ pub struct Notebook {
/// separated by a newline and a trailing newline. The trailing newline
/// is added to make sure that each cell ends with a newline which will
/// be removed when updating the cell content.
content: String,
source_code: String,
/// The index of the notebook. This is used to map between the concatenated
/// source code and the original notebook.
index: OnceCell<JupyterIndex>,
@@ -132,8 +132,8 @@ impl Notebook {
}
/// Read the Jupyter Notebook from its JSON string.
pub fn from_contents(contents: &str) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(Cursor::new(contents))
pub fn from_source_code(source_code: &str) -> Result<Self, Box<Diagnostic>> {
Self::from_reader(Cursor::new(source_code))
}
/// Read a Jupyter Notebook from a [`Read`] implementor.
@@ -268,7 +268,7 @@ impl Notebook {
// The additional newline at the end is to maintain consistency for
// all cells. These newlines will be removed before updating the
// source code with the transformed content. Refer `update_cell_content`.
content: contents.join("\n") + "\n",
source_code: contents.join("\n") + "\n",
cell_offsets,
valid_code_cells,
trailing_newline,
@@ -404,8 +404,8 @@ impl Notebook {
/// Return the notebook content.
///
/// This is the concatenation of all Python code cells.
pub(crate) fn content(&self) -> &str {
&self.content
pub fn source_code(&self) -> &str {
&self.source_code
}
/// Return the Jupyter notebook index.
@@ -424,12 +424,13 @@ impl Notebook {
}
/// Update the notebook with the given sourcemap and transformed content.
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: &str) {
pub(crate) fn update(&mut self, source_map: &SourceMap, transformed: String) {
// Cell offsets must be updated before updating the cell content as
// it depends on the offsets to extract the cell content.
self.index.take();
self.update_cell_offsets(source_map);
self.update_cell_content(transformed);
self.content = transformed.to_string();
self.update_cell_content(&transformed);
self.source_code = transformed;
}
/// Return a slice of [`Cell`] in the Jupyter notebook.
@@ -476,14 +477,16 @@ mod tests {
use crate::jupyter::schema::Cell;
use crate::jupyter::Notebook;
use crate::registry::Rule;
use crate::test::{read_jupyter_notebook, test_notebook_path, test_resource_path};
use crate::test::{
read_jupyter_notebook, test_notebook_path, test_resource_path, TestedNotebook,
};
use crate::{assert_messages, settings};
/// Read a Jupyter cell from the `resources/test/fixtures/jupyter/cell` directory.
fn read_jupyter_cell(path: impl AsRef<Path>) -> Result<Cell> {
let path = test_resource_path("fixtures/jupyter/cell").join(path);
let contents = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&contents)?)
let source_code = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&source_code)?)
}
#[test]
@@ -536,7 +539,7 @@ mod tests {
fn test_concat_notebook() -> Result<()> {
let notebook = read_jupyter_notebook(Path::new("valid.ipynb"))?;
assert_eq!(
notebook.content,
notebook.source_code,
r#"def unused_variable():
x = 1
y = 2
@@ -578,49 +581,64 @@ print("after empty cells")
#[test]
fn test_import_sorting() -> Result<()> {
let path = "isort.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
&path,
Path::new("isort_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnsortedImports),
)?;
assert_messages!(diagnostics, path, source_kind);
assert_messages!(messages, path, source_notebook);
Ok(())
}
#[test]
fn test_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
&path,
Path::new("ipy_escape_command_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);
assert_messages!(messages, path, source_notebook);
Ok(())
}
#[test]
fn test_unused_variable() -> Result<()> {
let path = "unused_variable.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
let TestedNotebook {
messages,
source_notebook,
..
} = test_notebook_path(
&path,
Path::new("unused_variable_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedVariable),
)?;
assert_messages!(diagnostics, path, source_kind);
assert_messages!(messages, path, source_notebook);
Ok(())
}
#[test]
fn test_json_consistency() -> Result<()> {
let path = "before_fix.ipynb".to_string();
let (_, _, source_kind) = test_notebook_path(
let TestedNotebook {
linted_notebook: fixed_notebook,
..
} = test_notebook_path(
path,
Path::new("after_fix.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
let mut writer = Vec::new();
source_kind.expect_jupyter().write_inner(&mut writer)?;
fixed_notebook.write_inner(&mut writer)?;
let actual = String::from_utf8(writer)?;
let expected =
std::fs::read_to_string(test_resource_path("fixtures/jupyter/after_fix.ipynb"))?;

View File

@@ -63,7 +63,7 @@ pub struct FixerResult<'a> {
/// The result returned by the linter, after applying any fixes.
pub result: LinterResult<(Vec<Message>, Option<ImportMap>)>,
/// The resulting source code, after applying any fixes.
pub transformed: Cow<'a, str>,
pub transformed: Cow<'a, SourceKind>,
/// The number of fixes applied for each [`Rule`].
pub fixed: FixTable,
}
@@ -335,19 +335,19 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
/// Generate a [`Message`] for each [`Diagnostic`] triggered by the given source
/// code.
pub fn lint_only(
contents: &str,
path: &Path,
package: Option<&Path>,
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
source_kind: &SourceKind,
source_type: PySourceType,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(source_kind.source_code(), source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
let locator = Locator::new(source_kind.source_code());
// Detect the current code style (lazily).
let stylist = Stylist::from_tokens(&tokens, &locator);
@@ -374,7 +374,7 @@ pub fn lint_only(
&directives,
settings,
noqa,
source_kind,
Some(source_kind),
source_type,
);
@@ -416,15 +416,14 @@ fn diagnostics_to_messages(
/// Generate `Diagnostic`s from source code content, iteratively autofixing
/// until stable.
pub fn lint_fix<'a>(
contents: &'a str,
path: &Path,
package: Option<&Path>,
noqa: flags::Noqa,
settings: &Settings,
source_kind: &mut SourceKind,
source_kind: &'a SourceKind,
source_type: PySourceType,
) -> Result<FixerResult<'a>> {
let mut transformed = Cow::Borrowed(contents);
let mut transformed = Cow::Borrowed(source_kind);
// Track the number of fixed errors across iterations.
let mut fixed = FxHashMap::default();
@@ -439,10 +438,10 @@ pub fn lint_fix<'a>(
loop {
// Tokenize once.
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(&transformed, source_type.as_mode());
ruff_python_parser::tokenize(transformed.source_code(), source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(&transformed);
let locator = Locator::new(transformed.source_code());
// Detect the current code style (lazily).
let stylist = Stylist::from_tokens(&tokens, &locator);
@@ -482,7 +481,7 @@ pub fn lint_fix<'a>(
if parseable && result.error.is_some() {
report_autofix_syntax_error(
path,
&transformed,
transformed.source_code(),
&result.error.unwrap(),
fixed.keys().copied(),
);
@@ -503,12 +502,7 @@ pub fn lint_fix<'a>(
*fixed.entry(rule).or_default() += count;
}
if let SourceKind::Jupyter(notebook) = source_kind {
notebook.update(&source_map, &fixed_contents);
}
// Store the fixed contents.
transformed = Cow::Owned(fixed_contents);
transformed = Cow::Owned(transformed.updated(fixed_contents, &source_map));
// Increment the iteration count.
iterations += 1;
@@ -517,7 +511,7 @@ pub fn lint_fix<'a>(
continue;
}
report_failed_to_converge_error(path, &transformed, &result.data.0);
report_failed_to_converge_error(path, transformed.source_code(), &result.data.0);
}
return Ok(FixerResult {

View File

@@ -18,7 +18,7 @@ impl Emitter for AzureEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let location = if context.is_jupyter_notebook(message.filename()) {
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()

View File

@@ -20,7 +20,7 @@ impl Emitter for GithubEmitter {
) -> anyhow::Result<()> {
for message in messages {
let source_location = message.compute_start_location();
let location = if context.is_jupyter_notebook(message.filename()) {
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()

View File

@@ -63,7 +63,7 @@ impl Serialize for SerializedMessages<'_> {
let start_location = message.compute_start_location();
let end_location = message.compute_end_location();
let lines = if self.context.is_jupyter_notebook(message.filename()) {
let lines = if self.context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
json!({

View File

@@ -13,7 +13,6 @@ use crate::message::text::{MessageCodeFrame, RuleCodeAndBody};
use crate::message::{
group_messages_by_filename, Emitter, EmitterContext, Message, MessageWithLocation,
};
use crate::source_kind::SourceKind;
#[derive(Default)]
pub struct GroupedEmitter {
@@ -66,10 +65,7 @@ impl Emitter for GroupedEmitter {
writer,
"{}",
DisplayGroupedMessage {
jupyter_index: context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index),
jupyter_index: context.notebook(message.filename()).map(Notebook::index),
message,
show_fix_status: self.show_fix_status,
show_source: self.show_source,

View File

@@ -45,7 +45,7 @@ impl Emitter for JunitEmitter {
} = message;
let mut status = TestCaseStatus::non_success(NonSuccessKind::Failure);
status.set_message(message.kind.body.clone());
let location = if context.is_jupyter_notebook(message.filename()) {
let location = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
SourceLocation::default()

View File

@@ -3,10 +3,8 @@ use std::collections::BTreeMap;
use std::io::Write;
use std::ops::Deref;
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use crate::source_kind::SourceKind;
pub use azure::AzureEmitter;
pub use github::GithubEmitter;
pub use gitlab::GitlabEmitter;
@@ -17,8 +15,11 @@ pub use junit::JunitEmitter;
pub use pylint::PylintEmitter;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Fix};
use ruff_source_file::{SourceFile, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
pub use text::TextEmitter;
use crate::jupyter::Notebook;
mod azure;
mod diff;
mod github;
@@ -129,33 +130,31 @@ pub trait Emitter {
/// Context passed to [`Emitter`].
pub struct EmitterContext<'a> {
source_kind: &'a FxHashMap<String, SourceKind>,
notebooks: &'a FxHashMap<String, Notebook>,
}
impl<'a> EmitterContext<'a> {
pub fn new(source_kind: &'a FxHashMap<String, SourceKind>) -> Self {
Self { source_kind }
pub fn new(notebooks: &'a FxHashMap<String, Notebook>) -> Self {
Self { notebooks }
}
/// Tests if the file with `name` is a jupyter notebook.
pub fn is_jupyter_notebook(&self, name: &str) -> bool {
self.source_kind
.get(name)
.is_some_and(SourceKind::is_jupyter)
pub fn is_notebook(&self, name: &str) -> bool {
self.notebooks.contains_key(name)
}
pub fn source_kind(&self, name: &str) -> Option<&SourceKind> {
self.source_kind.get(name)
pub fn notebook(&self, name: &str) -> Option<&Notebook> {
self.notebooks.get(name)
}
}
#[cfg(test)]
mod tests {
use ruff_text_size::{TextRange, TextSize};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{Diagnostic, DiagnosticKind, Edit, Fix};
use ruff_source_file::SourceFileBuilder;
use ruff_text_size::{TextRange, TextSize};
use crate::message::{Emitter, EmitterContext, Message};

View File

@@ -19,7 +19,7 @@ impl Emitter for PylintEmitter {
context: &EmitterContext,
) -> anyhow::Result<()> {
for message in messages {
let row = if context.is_jupyter_notebook(message.filename()) {
let row = if context.is_notebook(message.filename()) {
// We can't give a reasonable location for the structured formats,
// so we show one that's clearly a fallback
OneIndexed::from_zero_indexed(0)

View File

@@ -6,9 +6,9 @@ use annotate_snippets::display_list::{DisplayList, FormatOptions};
use annotate_snippets::snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation};
use bitflags::bitflags;
use colored::Colorize;
use ruff_text_size::{TextRange, TextSize};
use ruff_source_file::{OneIndexed, SourceLocation};
use ruff_text_size::{TextRange, TextSize};
use crate::fs::relativize_path;
use crate::jupyter::{JupyterIndex, Notebook};
@@ -16,7 +16,6 @@ use crate::line_width::{LineWidth, TabSize};
use crate::message::diff::Diff;
use crate::message::{Emitter, EmitterContext, Message};
use crate::registry::AsRule;
use crate::source_kind::SourceKind;
bitflags! {
#[derive(Default)]
@@ -72,10 +71,7 @@ impl Emitter for TextEmitter {
)?;
let start_location = message.compute_start_location();
let jupyter_index = context
.source_kind(message.filename())
.and_then(SourceKind::notebook)
.map(Notebook::index);
let jupyter_index = context.notebook(message.filename()).map(Notebook::index);
// Check if we're working on a jupyter notebook and translate positions with cell accordingly
let diagnostic_location = if let Some(jupyter_index) = jupyter_index {

View File

@@ -562,7 +562,7 @@ fn add_noqa_inner(
let mut prev_end = TextSize::default();
for (offset, (rules, directive)) in matches_by_line {
output.push_str(&locator.contents()[TextRange::new(prev_end, offset)]);
output.push_str(locator.slice(TextRange::new(prev_end, offset)));
let line = locator.full_line(offset);
@@ -619,7 +619,7 @@ fn add_noqa_inner(
prev_end = offset + line.text_len();
}
output.push_str(&locator.contents()[usize::from(prev_end)..]);
output.push_str(locator.after(prev_end));
(count, output)
}

View File

@@ -4,6 +4,7 @@ use anyhow::{anyhow, Result};
use itertools::Itertools;
use ruff_diagnostics::Edit;
use ruff_python_ast::Ranged;
use ruff_python_semantic::{Binding, BindingKind, Scope, ScopeId, SemanticModel};
pub(crate) struct Renamer;
@@ -220,12 +221,12 @@ impl Renamer {
BindingKind::Import(_) | BindingKind::FromImport(_) => {
if binding.is_alias() {
// Ex) Rename `import pandas as alias` to `import pandas as pd`.
Some(Edit::range_replacement(target.to_string(), binding.range))
Some(Edit::range_replacement(target.to_string(), binding.range()))
} else {
// Ex) Rename `import pandas` to `import pandas as pd`.
Some(Edit::range_replacement(
format!("{name} as {target}"),
binding.range,
binding.range(),
))
}
}
@@ -234,7 +235,7 @@ impl Renamer {
let module_name = import.call_path.first().unwrap();
Some(Edit::range_replacement(
format!("{module_name} as {target}"),
binding.range,
binding.range(),
))
}
// Avoid renaming builtins and other "special" bindings.
@@ -254,7 +255,7 @@ impl Renamer {
| BindingKind::FunctionDefinition(_)
| BindingKind::Deletion
| BindingKind::UnboundException(_) => {
Some(Edit::range_replacement(target.to_string(), binding.range))
Some(Edit::range_replacement(target.to_string(), binding.range()))
}
}
}

View File

@@ -1,45 +0,0 @@
use anyhow::{bail, Result};
use ruff_python_ast::{PySourceType, Ranged};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_diagnostics::Edit;
use ruff_source_file::Locator;
/// ANN204
pub(crate) fn add_return_annotation<T: Ranged>(
statement: &T,
annotation: &str,
source_type: PySourceType,
locator: &Locator,
) -> Result<Edit> {
let contents = &locator.contents()[statement.range()];
// Find the colon (following the `def` keyword).
let mut seen_lpar = false;
let mut seen_rpar = false;
let mut count = 0u32;
for (tok, range) in
lexer::lex_starts_at(contents, source_type.as_mode(), statement.start()).flatten()
{
if seen_lpar && seen_rpar {
if matches!(tok, Tok::Colon) {
return Ok(Edit::insertion(format!(" -> {annotation}"), range.start()));
}
}
if matches!(tok, Tok::Lpar) {
if count == 0 {
seen_lpar = true;
}
count = count.saturating_add(1);
}
if matches!(tok, Tok::Rpar) {
count = count.saturating_sub(1);
if count == 0 {
seen_rpar = true;
}
}
}
bail!("Unable to locate colon in function definition");
}

View File

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

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
@@ -11,7 +11,6 @@ use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_annotations::fixes;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
/// ## What it does
@@ -704,15 +703,10 @@ pub(crate) fn definition(
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
diagnostic.set_fix(Fix::suggested(Edit::insertion(
" -> None".to_string(),
function.parameters.range().end(),
)));
}
diagnostics.push(diagnostic);
}
@@ -727,15 +721,10 @@ pub(crate) fn definition(
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(
function,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
diagnostic.set_fix(Fix::suggested(Edit::insertion(
format!(" -> {return_type}"),
function.parameters.range().end(),
)));
}
}
diagnostics.push(diagnostic);

View File

@@ -242,4 +242,22 @@ annotation_presence.py:154:10: ANN401 Dynamically typed expressions (typing.Any)
| ^^^^^^^^^^^^^^^^^^^^^^^^ ANN401
|
annotation_presence.py:159:9: ANN204 [*] Missing return type annotation for special method `__init__`
|
157 | class Foo:
158 | @decorator()
159 | def __init__(self: "Foo", foo: int):
| ^^^^^^^^ ANN204
160 | ...
|
= help: Add `None` return type
Suggested fix
156 156 |
157 157 | class Foo:
158 158 | @decorator()
159 |- def __init__(self: "Foo", foo: int):
159 |+ def __init__(self: "Foo", foo: int) -> None:
160 160 | ...

View File

@@ -4,7 +4,8 @@ use ruff_python_ast::{self as ast, Constant, Expr};
pub(super) fn is_allowed_func_call(name: &str) -> bool {
matches!(
name,
"append"
"__setattr__"
| "append"
| "assertEqual"
| "assertEquals"
| "assertNotEqual"
@@ -26,13 +27,13 @@ pub(super) fn is_allowed_func_call(name: &str) -> bool {
| "int"
| "is_"
| "is_not"
| "next"
| "param"
| "pop"
| "remove"
| "set_blocking"
| "set_enabled"
| "setattr"
| "__setattr__"
| "setdefault"
| "str"
)

View File

@@ -81,12 +81,12 @@ FBT.py:19:5: FBT001 Boolean-typed positional argument in function definition
21 | kwonly_nonvalued_nohint,
|
FBT.py:86:19: FBT001 Boolean-typed positional argument in function definition
FBT.py:87:19: FBT001 Boolean-typed positional argument in function definition
|
85 | # FBT001: Boolean positional arg in function definition
86 | def foo(self, value: bool) -> None:
86 | # FBT001: Boolean positional arg in function definition
87 | def foo(self, value: bool) -> None:
| ^^^^^ FBT001
87 | pass
88 | pass
|

View File

@@ -71,8 +71,27 @@ mod tests {
}
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
fn extend_immutable_calls_arg_annotation() -> Result<()> {
let snapshot = "extend_immutable_calls_arg_annotation".to_string();
let diagnostics = test_path(
Path::new("flake8_bugbear/B006_extended.py"),
&Settings {
flake8_bugbear: super::settings::Settings {
extend_immutable_calls: vec![
"custom.ImmutableTypeA".to_string(),
"custom.ImmutableTypeB".to_string(),
],
},
..Settings::for_rule(Rule::MutableArgumentDefault)
},
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls_arg_default() -> Result<()> {
let snapshot = "extend_immutable_calls_arg_default".to_string();
let diagnostics = test_path(
Path::new("flake8_bugbear/B008_extended.py"),
&Settings {

View File

@@ -90,8 +90,7 @@ impl AlwaysAutofixableViolation for DuplicateHandlerException {
#[derive_message_formats]
fn message(&self) -> String {
let DuplicateHandlerException { names } = self;
if names.len() == 1 {
let name = &names[0];
if let [name] = names.as_slice() {
format!("Exception handler with duplicate exception: `{name}`")
} else {
let names = names.iter().map(|name| format!("`{name}`")).join(", ");

View File

@@ -184,7 +184,10 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
return false;
}
if parameters.includes(&loaded.id) {
if parameters
.as_ref()
.is_some_and(|parameters| parameters.includes(&loaded.id))
{
return false;
}

View File

@@ -76,17 +76,20 @@ where
range: _,
}) => {
visitor::walk_expr(self, body);
for ParameterWithDefault {
parameter,
default: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
self.names.remove(parameter.name.as_str());
if let Some(parameters) = parameters {
for ParameterWithDefault {
parameter,
default: _,
range: _,
} in parameters
.posonlyargs
.iter()
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
self.names.remove(parameter.name.as_str());
}
}
}
_ => visitor::walk_expr(self, expr),

View File

@@ -1,3 +1,4 @@
use ast::call_path::{from_qualified_name, CallPath};
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::is_docstring_stmt;
@@ -25,6 +26,10 @@ use crate::registry::AsRule;
/// default, and initialize a new mutable object inside the function body
/// for each call.
///
/// Arguments with immutable type annotations will be ignored by this rule.
/// Types outside of the standard library can be marked as immutable with the
/// [`flake8-bugbear.extend-immutable-calls`] configuration option.
///
/// ## Example
/// ```python
/// def add_to_list(item, some_list=[]):
@@ -49,6 +54,9 @@ use crate::registry::AsRule;
/// l2 = add_to_list(1) # [1]
/// ```
///
/// ## Options
/// - `flake8-bugbear.extend-immutable-calls`
///
/// ## References
/// - [Python documentation: Default Argument Values](https://docs.python.org/3/tutorial/controlflow.html#default-argument-values)
#[violation]
@@ -84,11 +92,18 @@ pub(crate) fn mutable_argument_default(checker: &mut Checker, function_def: &ast
continue;
};
let extend_immutable_calls: Vec<CallPath> = checker
.settings
.flake8_bugbear
.extend_immutable_calls
.iter()
.map(|target| from_qualified_name(target))
.collect();
if is_mutable_expr(default, checker.semantic())
&& !parameter
.annotation
.as_ref()
.is_some_and(|expr| is_immutable_annotation(expr, checker.semantic()))
&& !parameter.annotation.as_ref().is_some_and(|expr| {
is_immutable_annotation(expr, checker.semantic(), extend_immutable_calls.as_slice())
})
{
let mut diagnostic = Diagnostic::new(MutableArgumentDefault, default.range());
@@ -125,7 +140,7 @@ fn move_initialization(
let mut body = function_def.body.iter();
let statement = body.next()?;
if indexer.preceded_by_multi_statement_line(statement, locator) {
if indexer.in_multi_statement_line(statement, locator) {
return None;
}
@@ -155,7 +170,7 @@ fn move_initialization(
if let Some(statement) = body.next() {
// If there's a second statement, insert _before_ it, but ensure this isn't a
// multi-statement line.
if indexer.preceded_by_multi_statement_line(statement, locator) {
if indexer.in_multi_statement_line(statement, locator) {
return None;
}
Edit::insertion(content, locator.line_start(statement.start()))

View File

@@ -79,7 +79,6 @@ pub(crate) fn redundant_tuple_in_exception_handler(
type_.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
checker.generator().expr(elt),
type_.range(),

View File

@@ -1,9 +1,9 @@
use ruff_python_ast::{self as ast, Expr, Ranged, Stmt};
use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::visitor::Visitor;
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_python_ast::{helpers, visitor};
use crate::checkers::ast::Checker;
@@ -105,16 +105,16 @@ where
}
/// B007
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr, body: &[Stmt]) {
pub(crate) fn unused_loop_control_variable(checker: &mut Checker, stmt_for: &ast::StmtFor) {
let control_names = {
let mut finder = NameFinder::new();
finder.visit_expr(target);
finder.visit_expr(stmt_for.target.as_ref());
finder.names
};
let used_names = {
let mut finder = NameFinder::new();
for stmt in body {
for stmt in &stmt_for.body {
finder.visit_stmt(stmt);
}
finder.names
@@ -132,9 +132,10 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
}
// Avoid fixing any variables that _may_ be used, but undetectably so.
let certainty = Certainty::from(!helpers::uses_magic_variable_access(body, |id| {
checker.semantic().is_builtin(id)
}));
let certainty =
Certainty::from(!helpers::uses_magic_variable_access(&stmt_for.body, |id| {
checker.semantic().is_builtin(id)
}));
// Attempt to rename the variable by prepending an underscore, but avoid
// applying the fix if doing so wouldn't actually cause us to ignore the
@@ -163,7 +164,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))
.filter(|binding| binding.range.start() >= expr.range().start())
.filter(|binding| binding.start() >= expr.start())
.all(|binding| !binding.is_used())
{
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(

View File

@@ -0,0 +1,22 @@
---
source: crates/ruff/src/rules/flake8_bugbear/mod.rs
---
B006_extended.py:17:55: B006 [*] Do not use mutable data structures for argument defaults
|
17 | def error_due_to_missing_import(foo: ImmutableTypeA = []):
| ^^ B006
18 | ...
|
= help: Replace with `None`; initialize within function
Possible fix
14 14 | ...
15 15 |
16 16 |
17 |-def error_due_to_missing_import(foo: ImmutableTypeA = []):
17 |+def error_due_to_missing_import(foo: ImmutableTypeA = None):
18 |+ if foo is None:
19 |+ foo = []
18 20 | ...

View File

@@ -765,13 +765,14 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
outer_call.args = match outer_call.args.split_first() {
Some((first, rest)) => {
let inner_call = match_call(&first.value)?;
if let Some(iterable) = inner_call.args.first() {
let mut args = vec![iterable.clone()];
args.extend_from_slice(rest);
args
} else {
bail!("Expected at least one argument in inner function call");
}
inner_call
.args
.iter()
.filter(|argument| argument.keyword.is_none())
.take(1)
.chain(rest.iter())
.cloned()
.collect::<Vec<_>>()
}
None => bail!("Expected at least one argument in outer function call"),
};
@@ -1044,8 +1045,26 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
let mut tree = match_expression(module_text)?;
let call = match_call_mut(&mut tree)?;
let Expression::ListComp(list_comp) = &call.args[0].value else {
bail!("Expected Expression::ListComp");
let (whitespace_after, whitespace_before, elt, for_in, lpar, rpar) = match &call.args[0].value {
Expression::ListComp(list_comp) => (
&list_comp.lbracket.whitespace_after,
&list_comp.rbracket.whitespace_before,
&list_comp.elt,
&list_comp.for_in,
&list_comp.lpar,
&list_comp.rpar,
),
Expression::SetComp(set_comp) => (
&set_comp.lbrace.whitespace_after,
&set_comp.rbrace.whitespace_before,
&set_comp.elt,
&set_comp.for_in,
&set_comp.lpar,
&set_comp.rpar,
),
_ => {
bail!("Expected Expression::ListComp | Expression::SetComp");
}
};
let mut new_empty_lines = vec![];
@@ -1054,7 +1073,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
first_line,
empty_lines,
..
}) = &list_comp.lbracket.whitespace_after
}) = &whitespace_after
{
// If there's a comment on the line after the opening bracket, we need
// to preserve it. The way we do this is by adding a new empty line
@@ -1143,7 +1162,7 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
..
},
..
}) = &list_comp.rbracket.whitespace_before
}) = &whitespace_before
{
Some(format!("{}{}", whitespace.0, comment.0))
} else {
@@ -1151,10 +1170,10 @@ pub(crate) fn fix_unnecessary_comprehension_any_all(
};
call.args[0].value = Expression::GeneratorExp(Box::new(GeneratorExp {
elt: list_comp.elt.clone(),
for_in: list_comp.for_in.clone(),
lpar: list_comp.lpar.clone(),
rpar: list_comp.rpar.clone(),
elt: elt.clone(),
for_in: for_in.clone(),
lpar: lpar.clone(),
rpar: rpar.clone(),
}));
let whitespace_after_arg = match &call.args[0].comma {

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{Expr, Keyword, Ranged};
@@ -86,8 +86,9 @@ pub(crate) fn unnecessary_collection_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_collection_call(checker, expr));
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_collection_call(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Comprehension, Expr, Ranged};
@@ -63,9 +63,9 @@ fn add_diagnostic(checker: &mut Checker, expr: &Expr) {
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -69,30 +69,31 @@ pub(crate) fn unnecessary_comprehension_any_all(
let Expr::Name(ast::ExprName { id, .. }) = func else {
return;
};
if (matches!(id.as_str(), "all" | "any")) && args.len() == 1 {
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = &args[0]
else {
return;
};
if contains_await(elt) {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, args[0].range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(
checker.locator(),
checker.stylist(),
expr,
)
});
}
checker.diagnostics.push(diagnostic);
if !matches!(id.as_str(), "all" | "any") {
return;
}
let [arg] = args else {
return;
};
let (Expr::ListComp(ast::ExprListComp { elt, .. })
| Expr::SetComp(ast::ExprSetComp { elt, .. })) = arg
else {
return;
};
if contains_await(elt) {
return;
}
if !checker.semantic().is_builtin(id) {
return;
}
let mut diagnostic = Diagnostic::new(UnnecessaryComprehensionAnyAll, arg.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_comprehension_any_all(checker.locator(), checker.stylist(), expr)
});
}
checker.diagnostics.push(diagnostic);
}
/// Return `true` if the [`Expr`] contains an `await` expression.

View File

@@ -1,4 +1,4 @@
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::comparable::ComparableKeyword;
use ruff_python_ast::{self as ast, Arguments, Expr, Keyword, Ranged};
@@ -130,13 +130,13 @@ pub(crate) fn unnecessary_double_cast_or_process(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_double_cast_or_process(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -59,9 +58,8 @@ pub(crate) fn unnecessary_generator_dict(
Expr::Tuple(ast::ExprTuple { elts, .. }) if elts.len() == 2 => {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_generator_dict(checker, expr)
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_dict(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_list(
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorList, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_list(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -60,9 +60,9 @@ pub(crate) fn unnecessary_generator_set(
if let Expr::GeneratorExp(_) = argument {
let mut diagnostic = Diagnostic::new(UnnecessaryGeneratorSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic
.try_set_fix_from_edit(|| fixes::fix_unnecessary_generator_set(checker, expr));
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_generator_set(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -56,9 +56,9 @@ pub(crate) fn unnecessary_list_call(
}
let mut diagnostic = Diagnostic::new(UnnecessaryListCall, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_call(checker.locator(), checker.stylist(), expr)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -66,9 +65,8 @@ pub(crate) fn unnecessary_list_comprehension_dict(
}
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionDict, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr)
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_dict(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -58,9 +58,8 @@ pub(crate) fn unnecessary_list_comprehension_set(
if argument.is_list_comp_expr() {
let mut diagnostic = Diagnostic::new(UnnecessaryListComprehensionSet, expr.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr)
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_list_comprehension_set(checker, expr).map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -1,7 +1,6 @@
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::{self as ast, Expr, Keyword, Ranged};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -81,8 +80,8 @@ pub(crate) fn unnecessary_literal_dict(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_dict(checker, expr));
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_dict(checker, expr).map(Fix::suggested));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -1,6 +1,6 @@
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -75,8 +75,8 @@ pub(crate) fn unnecessary_literal_set(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| fixes::fix_unnecessary_literal_set(checker, expr));
diagnostic
.try_set_fix(|| fixes::fix_unnecessary_literal_set(checker, expr).map(Fix::suggested));
}
checker.diagnostics.push(diagnostic);
}

View File

@@ -2,7 +2,7 @@ use std::fmt;
use ruff_python_ast::{Expr, Keyword, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
@@ -91,13 +91,13 @@ pub(crate) fn unnecessary_literal_within_dict_call(
expr.range(),
);
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.try_set_fix_from_edit(|| {
diagnostic.try_set_fix(|| {
fixes::fix_unnecessary_literal_within_dict_call(
checker.locator(),
checker.stylist(),
expr,
)
.map(Fix::suggested)
});
}
checker.diagnostics.push(diagnostic);

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