Compare commits

...

69 Commits

Author SHA1 Message Date
Charlie Marsh
fac6e83705 Avoid 2023-06-22 23:30:23 -04:00
Tom Kuson
1cf307c34c Fix collection-literal-concatenation documentation (#5320)
## Summary

Move `collection-literal-concatenation` markdown documentation to the
correct place.

Fixes error in #5262.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-22 18:37:54 -04:00
Charlie Marsh
7819b95d7f Avoid syntax errors when removing f-string prefixes (#5319)
Closes https://github.com/astral-sh/ruff/issues/5281.

Closes https://github.com/astral-sh/ruff/issues/4827.
2023-06-22 17:21:09 -04:00
Lukas Mayrhofer
4a81cfc51a Allow @Author format for "Missing Author" rule in flake8-todos (#4903)
## Summary

The TD-002 rule "Missing Author" was updated to allow another format
using "@". This reflects the current 0.3.0 version of flake8-todos.
2023-06-22 20:53:58 +00:00
qdegraaf
38e618cd18 [perflint] Add PERF101 with autofix (#5121)
## Summary

Adds PERF101 which checks for unnecessary casts to `list` in for loops. 

NOTE: Is not fully equal to its upstream implementation as this
implementation does not flag based on type annotations
(i.e.):
```python
def foo(x: List[str]):
    for y in list(x):
        ...
```

With the current set-up it's quite hard to get the annotation from a
function arg from its binding. Problem is best considered broader than
this implementation.

## Test Plan

Added fixture. 

## Issue links

Refers: https://github.com/astral-sh/ruff/issues/4789

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-06-22 20:44:26 +00:00
Charlie Marsh
50f0edd2cb Add dark- and light-mode image modifiers for custom MkDocs themes (#5318)
## Summary

Roughly following the docs
[here](https://squidfunk.github.io/mkdocs-material/reference/images/#custom-light-scheme).

Closes #5311.
2023-06-22 16:11:38 -04:00
Edgar R. M
e0e1d13d9f Fix diagnostics variable name in add_plugin.py script (#5317)
## Summary

Fix a variable name in the `add_plugin.py` script.

## Test Plan

I don't think there are any tests for the scripts, other than manual
confirmation
2023-06-22 20:06:47 +00:00
Charlie Marsh
8bc7378002 Add PythonVersion::Py312 (#5316)
Closes #5310.
2023-06-22 20:01:07 +00:00
Charlie Marsh
cdbd0bd5cd Respect abc decorators when classifying function types (#5315)
Closes #5307.
2023-06-22 19:52:36 +00:00
Charlie Marsh
5f88ff8a96 Allow __slots__ assignments in mutable-class-default (#5314)
Closes #5309.
2023-06-22 19:40:54 +00:00
Charlie Marsh
1c2be54b4a Support pydantic.BaseSettings in mutable-class-default (#5312)
Closes #5308.
2023-06-22 19:27:05 +00:00
Charlie Marsh
5dd00b19e6 Remove off-palette colors from code (#5305) 2023-06-22 16:31:22 +00:00
Charlie Marsh
c0f93fcf3e Publish GitHub release as draft (#5304)
I accidentally changed `draft: false` to `draft: true` in #5240. I
actually think Copilot did this without me realizing.
2023-06-22 16:11:43 +00:00
Charlie Marsh
3238a6ef1f Fix 'our' to 'your' typo (#5303) 2023-06-22 15:58:24 +00:00
Charlie Marsh
96ecfae1c5 Remove off-palette colors (#5302) 2023-06-22 15:52:03 +00:00
konstin
03694ef649 More stability checker options (#5299)
## Summary

This contains three changes:
* repos in `check_ecosystem.py` are stored as `org:name` instead of
`org/name` to create a flat directory layout
* `check_ecosystem.py` performs a maximum of 50 parallel jobs at the
same time to avoid consuming to much RAM
* `check-formatter-stability` gets a new option `--multi-project` so
it's possible to do `cargo run --bin ruff_dev --
check-formatter-stability --multi-project target/checkouts`
With these three changes it becomes easy to check the formatter
stability over a larger number of repositories. This is part of the
integration of integrating formatter regressions checks into the
ecosystem checks.

## Test Plan

```shell
python scripts/check_ecosystem.py --checkouts target/checkouts --projects github_search.jsonl -v $(which true) $(which true)
cargo run --bin ruff_dev -- check-formatter-stability --multi-project target/checkouts
```
2023-06-22 15:48:11 +00:00
Charlie Marsh
f9f0cf7524 Use __future__ imports in scripts (#5301) 2023-06-22 11:40:16 -04:00
Tom Kuson
eaa10ad2d9 Fix deprecated-import false positives (#5291)
## Summary

Remove recommendations to replace
`typing_extensions.dataclass_transform` and
`typing_extensions.SupportsIndex` with their `typing` library
counterparts.

Closes #5112.

## Test Plan

Added extra checks to the test fixture.

`cargo test`
2023-06-22 15:34:44 +00:00
Evan Rittenhouse
84259f5440 Add Applicability to pycodestyle (#5282) 2023-06-22 11:25:20 -04:00
trag1c
e8ebe0a425 Update docs to match updated logo and color palette (#5283)
![8511](https://github.com/astral-sh/ruff/assets/77130613/862d151f-ff1d-4da8-9230-8dd32f41f197)

## Summary

Supersedes #5277, includes redesigned dark mode.

## Test Plan

* `python scripts/generate_mkdocs.py`
* `mkdocs serve`
2023-06-22 11:19:34 -04:00
konstin
d407165aa7 Fix formatter panic with comment after parenthesized dict value (#5293)
## Summary

This snippet used to panic because it expected to see a comma or
something similar after the `2` but met the closing parentheses that is
not part of the range and panicked
```python
a = {
    1: (2),
    # comment
    3: True,
}
```

Originally found in
636a717ef0/testing/marionette/client/marionette_driver/geckoinstance.py (L109)

This snippet is also the test plan.
2023-06-22 16:52:48 +02:00
Micha Reiser
f7e1cf4b51 Format class definitions (#5289) 2023-06-22 09:09:43 +00:00
konstin
7d4f8e59da Improve FormatExprCall dummy (#5290)
This solves an instability when formatting cpython. It also introduces
another one, but i think it's still a worthwhile change for now.

There's no proper testing since this is just a dummy.
2023-06-22 10:59:30 +02:00
Charlie Marsh
2c63f8cdea Update reference to release step (#5280) 2023-06-22 02:04:43 +00:00
Charlie Marsh
1c0a3a467f Bump version to 0.0.275 (#5276) 2023-06-21 21:53:37 -04:00
Charlie Marsh
6b8b318d6b Use mod tests consistently (#5278)
As per the Rust documentation.
2023-06-22 01:50:28 +00:00
Charlie Marsh
c0c59b82ec Use 'Checks for uses' consistently (#5279) 2023-06-22 01:44:52 +00:00
Charlie Marsh
ac146e11f0 Allow typing.Final for mutable-class-default annotations (RUF012) (#5274)
## Summary

See: https://github.com/astral-sh/ruff/issues/5243.
2023-06-22 00:24:53 +00:00
Charlie Marsh
1229600e1d Ignore Pydantic classes when evaluating mutable-class-default (RUF012) (#5273)
Closes https://github.com/astral-sh/ruff/issues/5272.
2023-06-21 23:59:44 +00:00
Micha Reiser
ccf34aae8c Format Attribute Expression (#5259) 2023-06-21 21:33:53 +00:00
Tom Kuson
341b12d918 Complete documentation for Ruff-specific rules (#5262)
## Summary

Completes the documentation for the Ruff-specific ruleset. Related to
#2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-21 21:30:44 +00:00
Micha Reiser
3d7411bfaf Use trait for labels instead of TypeId (#5270) 2023-06-21 22:26:09 +01:00
David Szotten
1eccbbb60e Format StmtFor (#5163)
<!--
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

format StmtFor

still trying to learn how to help out with the formatter. trying
something slightly more advanced than [break](#5158)

mostly copied form StmtWhile

## Test Plan

snapshots
2023-06-21 23:00:31 +02:00
Charlie Marsh
e71f044f0d Avoid including nursery rules in linter-level selectors (#5268)
## Summary

Ensures that `--select PL` and `--select PLC` don't include `PLC1901`.
Previously, `--select PL` _did_, because it's a "linter-level selector"
(`--select PLC` is viewed as selecting the `C` prefix from `PL`), and we
were missing this filtering path.
2023-06-21 20:11:40 +00:00
James Berry
f194572be8 Remove visit_arg_with_default (#5265)
## Summary

This is a follow up to #5221. Turns out it was easy to restructure the
visitor to get the right order, I'm just dumb 🤷‍♂️ I've
removed `visit_arg_with_default` entirely from the `Visitor`, although
it still exists as part of `preorder::Visitor`.
2023-06-21 16:00:24 -04:00
Charlie Marsh
62e2c46f98 Move compare-to-empty-string to nursery (#5264)
## Summary

This rule has too many false positives. It has parity with the Pylint
version, but the Pylint version is part of an
[extension](https://pylint.readthedocs.io/en/stable/user_guide/messages/convention/compare-to-empty-string.html),
and so requires explicit opt-in.

I'm moving this rule to the nursery to require explicit opt-in, as with
Pylint.

Closes #4282.
2023-06-21 19:47:02 +00:00
konstin
9419d3f9c8 Special ExprTuple formatting option for for-loops (#5175)
## Motivation

While black keeps parentheses nearly everywhere, the notable exception
is in the body of for loops:
```python
for (a, b) in x:
    pass
```
becomes
```python
for a, b in x:
    pass
```

This currently blocks #5163, which this PR should unblock.

## Solution

This changes the `ExprTuple` formatting option to include one additional
option that removes the parentheses when not using magic trailing comma
and not breaking. It is supposed to be used through
```rust
#[derive(Debug)]
struct ExprTupleWithoutParentheses<'a>(&'a Expr);

impl Format<PyFormatContext<'_>> for ExprTupleWithoutParentheses<'_> {
    fn fmt(&self, f: &mut Formatter<PyFormatContext<'_>>) -> FormatResult<()> {
        match self.0 {
            Expr::Tuple(expr_tuple) => expr_tuple
                .format()
                .with_options(TupleParentheses::StripInsideForLoop)
                .fmt(f),
            other => other.format().with_options(Parenthesize::IfBreaks).fmt(f),
        }
    }
}
```


## Testing

The for loop formatting isn't merged due to missing this (and i didn't
want to create more git weirdness across two people), but I've confirmed
that when applying this to while loops instead of for loops, then
```rust
        write!(
            f,
            [
                text("while"),
                space(),
                ExprTupleWithoutParentheses(test.as_ref()),
                text(":"),
                trailing_comments(trailing_condition_comments),
                block_indent(&body.format())
            ]
        )?;
```
makes
```python
while (a, b):
    pass

while (
    ajssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssa,
    b,
):
    pass

while (a,b,):
    pass
```
formatted as
```python
while a, b:
    pass

while (
    ajssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssa,
    b,
):
    pass

while (
    a,
    b,
):
    pass
```
2023-06-21 21:17:47 +02:00
James Berry
9b5fb8f38f Fix AST visitor traversal order (#5221)
## Summary

According to the AST visitor documentation, the AST visitor "visits all
nodes in the AST recursively in evaluation-order". However, the current
traversal fails to meet this specification in a few places.

### Function traversal

```python
order = []
@(order.append("decorator") or (lambda x: x))
def f(
    posonly: order.append("posonly annotation") = order.append("posonly default"),
    /,
    arg: order.append("arg annotation") = order.append("arg default"),
    *args: order.append("vararg annotation"),
    kwarg: order.append("kwarg annotation") = order.append("kwarg default"),
    **kwargs: order.append("kwarg annotation")
) -> order.append("return annotation"):
    pass
print(order)
```

Executing the above snippet using CPython 3.10.6 prints the following
result (formatted for readability):
```python
[
    'decorator',
    'posonly default',
    'arg default',
    'kwarg default',
    'arg annotation',
    'posonly annotation',
    'vararg annotation',
    'kwarg annotation',
    'kwarg annotation',
    'return annotation',
]
```

Here we can see that decorators are evaluated first, followed by
argument defaults, and annotations are last. The current traversal of a
function's AST does not align with this order.

### Annotated assignment traversal
```python
order = []
x: order.append("annotation") = order.append("expression")
print(order)
```

Executing the above snippet using CPython 3.10.6 prints the following
result:
```python
['expression', 'annotation']
```

Here we can see that an annotated assignments annotation gets evaluated
after the assignment's expression. The current traversal of an annotated
assignment's AST does not align with this order.

## Why?

I'm slowly working on #3946 and porting over some of the logic and tests
from ssort. ssort is very sensitive to AST traversal order, so ensuring
the utmost correctness here is important.

## Test Plan

There doesn't seem to be existing tests for the AST visitor, so I didn't
bother adding tests for these very subtle changes. However, this
behavior will be captured in the tests for the PR which addresses #3946.
2023-06-21 14:40:58 -04:00
konstin
d7c7484618 Format function argument separator comments (#5211)
## Summary

This is a complete rewrite of the handling of `/` and `*` comment
handling in function signatures. The key problem is that slash and star
don't have a note. We now parse out the positions of slash and star and
their respective preceding and following note. I've left code comments
for each possible case of function signature structure and comment
placement

## Test Plan

I extended the function statement fixtures with cases that i found. If
you have more weird edge cases your input would be appreciated.
2023-06-21 17:56:47 +00:00
konstin
bc63cc9b3c Fix remaining CPython formatter errors except for function argument separator comments (#5210)
## Summary

This fixes two problems discovered when trying to format the cpython
repo with `cargo run --bin ruff_dev -- check-formatter-stability
projects/cpython`:

The first is to ignore try/except trailing comments for now since they
lead to unstable formatting on the dummy.

The second is to avoid dropping trailing if comments through placement:
This changes the placement to keep a comment trailing an if-elif or
if-elif-else to keep the comment a trailing comment on the entire if.
Previously the last comment would have been lost.
```python
if "first if":
    pass
elif "first elif":
    pass
```

The last remaining problem in cpython so far is function signature
argument separator comment placement which is its own PR on top of this.

## Test Plan

I added test fixtures of minimized examples with links back to the
original cpython location
2023-06-21 19:45:53 +02:00
Charlie Marsh
bf1a94ee54 Initialize caches for packages and standalone files (#5237)
## Summary

While fixing https://github.com/astral-sh/ruff/pull/5233, I noticed that
in FastAPI, 343 out of 823 files weren't hitting the cache. It turns out
these are standalone files in the documentation that lack a "package
root". Later, when looking up the cache entries, we fallback to the
package directory.

This PR ensures that we initialize the cache for both kinds of files:
those that are in a package, and those that aren't.

The total size of the FastAPI cache for me is now 388K. I also suspect
that this approach is much faster than as initially written, since
before, we were probably initializing one cache per _directory_.

## Test Plan

Ran `cargo run -p ruff_cli -- check ../fastapi --verbose`; verified
that, on second execution, there were no "Checking" entries in the logs.
2023-06-21 17:29:09 +00:00
Dhruv Manilawala
c792c10eaa Add support for nested quoted annotations in RUF013 (#5254)
## Summary

This is a follow up on #5235 to add support for nested quoted
annotations for RUF013.

## Test Plan

`cargo test`
2023-06-21 17:25:27 +00:00
Evan Rittenhouse
f9ffb3d50d Add Applicability to pylint (#5251) 2023-06-21 17:22:01 +00:00
Evan Rittenhouse
2b76d88bd3 Add Applicability to pandas_vet (#5252) 2023-06-21 17:12:47 +00:00
Evan Rittenhouse
41ef17b007 Add Applicability to pyflakes (#5253) 2023-06-21 17:04:55 +00:00
Charlie Marsh
0aa21277c6 Improve documentation for overlong-line rules (#5260)
Closes https://github.com/astral-sh/ruff/issues/5248.
2023-06-21 17:02:20 +00:00
Charlie Marsh
ecf61d49fa Restore existing bindings when unbinding caught exceptions (#5256)
## Summary

In the latest release, we made some improvements to the semantic model,
but our modifications to exception-unbinding are causing some
false-positives. For example:

```py
try:
    v = 3
except ImportError as v:
    print(v)
else:
    print(v)
```

In the latest release, we started unbinding `v` after the `except`
handler. (We used to restore the existing binding, the `v = 3`, but this
was quite complicated.) Because we don't have full branch analysis, we
can't then know that `v` is still bound in the `else` branch.

The solution here modifies `resolve_read` to skip-lookup when hitting
unbound exceptions. So when store the "unbind" for `except ImportError
as v`, we save the binding that it shadowed `v = 3`, and skip to that.

Closes #5249.

Closes #5250.
2023-06-21 12:53:58 -04:00
Charlie Marsh
d99b3bf661 Add some projects to the ecosystem CI check (#5258) 2023-06-21 12:42:58 -04:00
Micha Reiser
e47aa468d5 Format Identifier (#5255) 2023-06-21 17:35:37 +02:00
konstin
6155fd647d Format Slice Expressions (#5047)
This formats slice expressions and subscript expressions.

Spaces around the colons follows the same rules as black
(https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#slices):
```python
e00 = "e"[:]
e01 = "e"[:1]
e02 = "e"[: a()]
e10 = "e"[1:]
e11 = "e"[1:1]
e12 = "e"[1 : a()]
e20 = "e"[a() :]
e21 = "e"[a() : 1]
e22 = "e"[a() : a()]
e200 = "e"[a() : :]
e201 = "e"[a() :: 1]
e202 = "e"[a() :: a()]
e210 = "e"[a() : 1 :]
```

Comment placement is different due to our very different infrastructure.
If we have explicit bounds (e.g. `x[1:2]`) all comments get assigned as
leading or trailing to the bound expression. If a bound is missing
`[:]`, comments get marked as dangling and placed in the same section as
they were originally in:
```python
x = "x"[ # a
      # b
    :  # c
      # d
]
```
to
```python
x = "x"[
    # a
    # b
    :
    # c
    # d
]
```
Except for the potential trailing end-of-line comments, all comments get
formatted on their own line. This can be improved by keeping end-of-line
comments after the opening bracket or after a colon as such but the
changes were already complex enough.

I added tests for comment placement and spaces.
2023-06-21 15:09:39 +00:00
Charlie Marsh
4634560c80 Ensure release tagging has access to repo clone (#5240)
## Summary

The [release
failed](https://github.com/astral-sh/ruff/actions/runs/5329733171/jobs/9656004063),
but late enough that I was able to do the remaining steps manually. The
issue here is that the tagging step requires that we clone the repo. I
split the upload (to PyPI), tagging (in Git), and publishing (to GitHub
Releases) phases into their own steps, since they need different
resources + permissions anyway.
2023-06-21 10:24:33 -04:00
Charlie Marsh
10885d09a1 Add support for top-level quoted annotations in RUF013 (#5235)
## Summary

This PR adds support for autofixing annotations like:

```python
def f(x: "int" = None):
    ...
```

However, we don't yet support nested quotes, like:

```python
def f(x: Union["int", "str"] = None):
    ...
```

Closes #5231.
2023-06-21 10:23:37 -04:00
konstin
44156f6962 Improve debuggability of place_comment (#5209)
## Summary

I found it hard to figure out which function decides placement for a
specific comment. An explicit loop makes this easier to debug

## Test Plan

There should be no functional changes, no changes to the formatting of
the fixtures.
2023-06-21 09:52:13 +00:00
konstin
f551c9aad2 Unify benchmarking and profiling docs (#5145)
This moves all docs about benchmarking and profiling into
CONTRIBUTING.md by moving the readme of `ruff_benchmark` and adding more
information on profiling.

We need to somehow consolidate that documentation, but i'm not convinced
that this is the best way (i tried subpages in mkdocs, but that didn't
seem good either), so i'm happy to take suggestions.
2023-06-21 09:39:56 +00:00
Micha Reiser
653dbb6d17 Format BoolOp (#4986) 2023-06-21 09:27:57 +00:00
konstin
db301c14bd Consistently name comment own line/end-of-line line_position() (#5215)
## Summary

Previously, `DecoratedComment` used `text_position()` and
`SourceComment` used `position()`. This PR unifies this to
`line_position` everywhere.

## Test Plan

This is a rename refactoring.
2023-06-21 11:04:56 +02:00
Micha Reiser
1336ca601b Format UnaryExpr
<!--
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 basic formatting for unary expressions.

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

## Test Plan

I added a new `unary.py` with custom test cases
2023-06-21 10:09:47 +02:00
Micha Reiser
3973836420 Correctly handle left/right breaking of binary expression
<!--
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
Black supports for layouts when it comes to breaking binary expressions:

```rust
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
enum BinaryLayout {
    /// Put each operand on their own line if either side expands
    Default,

    /// Try to expand the left to make it fit. Add parentheses if the left or right don't fit.
    ///
    ///```python
    /// [
    ///     a,
    ///     b
    /// ] & c
    ///```
    ExpandLeft,

    /// Try to expand the right to make it fix. Add parentheses if the left or right don't fit.
    ///
    /// ```python
    /// a & [
    ///     b,
    ///     c
    /// ]
    /// ```
    ExpandRight,

    /// Both the left and right side can be expanded. Try in the following order:
    /// * expand the right side
    /// * expand the left side
    /// * expand both sides
    ///
    /// to make the expression fit
    ///
    /// ```python
    /// [
    ///     a,
    ///     b
    /// ] & [
    ///     c,
    ///     d
    /// ]
    /// ```
    ExpandRightThenLeft,
}
```

Our current implementation only handles `ExpandRight` and `Default` correctly. This PR adds support for `ExpandRightThenLeft` and `ExpandLeft`. 

## Test Plan

I added tests that play through all 4 binary expression layouts.
2023-06-21 09:40:05 +02:00
Charlie Marsh
a332f078db Checkout repo to support release tag validation (#5238)
## Summary

The
[release](https://github.com/astral-sh/ruff/actions/runs/5329340068/jobs/9655224008)
failed due to an inability to find `pyproject.toml`. This PR moves that
validation into its own step (so we can fail fast) and ensures we clone
the repo.
2023-06-21 03:16:35 +00:00
Charlie Marsh
e0339b538b Bump version to 0.0.274 (#5230) 2023-06-20 22:12:32 -04:00
Charlie Marsh
07b6b7401f Move copyright rules to flake8_copyright module (#5236)
## Summary

I initially wanted this category to be more general and decoupled from
the plugin, but I got some feedback that the titling felt inconsistent
with others.
2023-06-21 01:56:40 +00:00
Charlie Marsh
1db7d9e759 Avoid erroneous RUF013 violations for quoted annotations (#5234)
## Summary

Temporary fix for #5231: if we can't flag and fix these properly, just
disabling them for now.

\cc @dhruvmanila 

## Test Plan

`cargo test`
2023-06-21 01:29:12 +00:00
Charlie Marsh
621e9ace88 Use package roots rather than package members for cache initialization (#5233)
## Summary

This is a proper fix for the issue patched-over in
https://github.com/astral-sh/ruff/pull/5229, thanks to an extremely
helpful repro from @tlambert03 in that thread. It looks like we were
using the keys of `package_roots` rather than the values to initialize
the cache -- but it's a map from package to package root.

## Test Plan

Reverted #5229, then ran through the plan that @tlambert03 included in
https://github.com/astral-sh/ruff/pull/5229#issuecomment-1599723226.
Verified the panic before but not after this change.
2023-06-20 21:21:45 -04:00
Charlie Marsh
f9f77cf617 Revert change to RUF010 to remove unnecessary str calls (#5232)
## Summary

This PR reverts #4971 (aba073a791). It
turns out that `f"{str(x)}"` and `f"{x}"` are often but not exactly
equivalent, and performing that conversion automatically can lead to
subtle bugs, See the discussion in
https://github.com/astral-sh/ruff/issues/4958.
2023-06-20 21:15:17 -04:00
Charlie Marsh
1a2bd984f2 Avoid .unwrap() on cache access (#5229)
## Summary

I haven't been able to determine why / when this is happening, but in
some cases, users are reporting that this `unwrap()` is causing a panic.
It's fine to just return `None` here and fallback to "No cache",
certainly better than panicking (while we figure out the edge case).

Closes #5225.

Closes #5228.
2023-06-20 19:01:21 -04:00
Tom Kuson
4717d0779f Complete flake8-debugger documentation (#5223)
## Summary

Completes the documentation for the `flake8-debugger` ruleset. Related
to #2646.

## Test Plan

`python scripts/check_docs_formatted.py`
2023-06-20 21:04:32 +00:00
Florian Stasse
07409ce201 Fixed typo in numpy deprecated type alias rule documentation (#5224)
## Summary

It is a very simple typo fix in the "numy deprecated type alias"
documentation.
2023-06-20 16:51:51 -04:00
Addison Crump
2c0ec97782 Use cpython with fuzzer corpus (#5183)
Following #5055, add cpython as a member of the fuzzer corpus
unconditionally.
2023-06-20 16:51:06 -04:00
Micha Reiser
e520a3a721 Fix ArgWithDefault comments handling (#5204) 2023-06-20 20:48:07 +00:00
263 changed files with 8431 additions and 3344 deletions

View File

@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run skipping uploading artifact."
description: "The version to tag, without the leading 'v'. If omitted, will initiate a dry run (no uploads)."
type: string
sha:
description: "Optionally, the full sha of the commit to be released"
@@ -392,28 +392,14 @@ jobs:
*.tar.gz
*.sha256
release:
name: Release
validate-tag:
name: Validate tag
runs-on: ubuntu-latest
needs:
- macos-universal
- macos-x86_64
- windows
- linux
- linux-cross
- musllinux
- musllinux-cross
# If you don't set an input it's a dry run skipping uploading artifact
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
environment:
name: release
permissions:
# For pypi trusted publishing
id-token: write
# For GitHub release publishing
contents: write
steps:
- name: Consistency check tag
- uses: actions/checkout@v3
- name: Check tag consistency
run: |
version=$(grep "version = " pyproject.toml | sed -e 's/version = "\(.*\)"/\1/g')
if [ "${{ inputs.tag }}" != "${version}" ]; then
@@ -424,7 +410,7 @@ jobs:
else
echo "Releasing ${version}"
fi
- name: Consistency check sha
- name: Check SHA consistency
if: ${{ inputs.sha }}
run: |
git_sha=$(git rev-parse HEAD)
@@ -436,6 +422,27 @@ jobs:
else
echo "Releasing ${git_sha}"
fi
upload-release:
name: Upload to PyPI
runs-on: ubuntu-latest
needs:
- macos-universal
- macos-x86_64
- windows
- linux
- linux-cross
- musllinux
- musllinux-cross
- validate-tag
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
environment:
name: release
permissions:
# For pypi trusted publishing
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
@@ -446,10 +453,18 @@ jobs:
skip-existing: true
packages-dir: wheels
verbose: true
- uses: actions/download-artifact@v3
with:
name: binaries
path: binaries
tag-release:
name: Tag release
runs-on: ubuntu-latest
needs: upload-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For git tag
contents: write
steps:
- uses: actions/checkout@v3
- name: git tag
run: |
git config user.email "hey@astral.sh"
@@ -458,6 +473,21 @@ jobs:
# If there is duplicate tag, this will fail. The publish to pypi action will have been a noop (due to skip
# existing), so we make a non-destructive exit here
git push --tags
publish-release:
name: Publish to GitHub
runs-on: ubuntu-latest
needs: tag-release
# If you don't set an input tag, it's a dry run (no uploads).
if: ${{ inputs.tag }}
permissions:
# For GitHub release publishing
contents: write
steps:
- uses: actions/download-artifact@v3
with:
name: binaries
path: binaries
- name: "Publish to GitHub"
uses: softprops/action-gh-release@v1
with:
@@ -468,9 +498,9 @@ jobs:
# After the release has been published, we update downstream repositories
# This is separate because if this fails the release is still fine, we just need to do some manual workflow triggers
update-dependents:
name: Release
name: Update dependents
runs-on: ubuntu-latest
needs: release
needs: publish-release
steps:
- name: "Update pre-commit mirror"
uses: actions/github-script@v6

View File

@@ -12,7 +12,7 @@ Welcome! We're happy to have you here. Thank you in advance for your contributio
- [Example: Adding a new configuration option](#example-adding-a-new-configuration-option)
- [MkDocs](#mkdocs)
- [Release Process](#release-process)
- [Benchmarks](#benchmarks)
- [Benchmarks](#benchmarking-and-profiling)
## The Basics
@@ -307,7 +307,15 @@ downloading the [`known-github-tomls.json`](https://github.com/akx/ruff-usage-ag
as `github_search.jsonl` and following the instructions in [scripts/Dockerfile.ecosystem](https://github.com/astral-sh/ruff/blob/main/scripts/Dockerfile.ecosystem).
Note that this check will take a while to run.
## Benchmarks
## Benchmarking and Profiling
We have several ways of benchmarking and profiling Ruff:
- Our main performance benchmark comparing Ruff with other tools on the CPython codebase
- Microbenchmarks which the linter or the formatter on individual files. There run on pull requests.
- Profiling the linter on either the microbenchmarks or entire projects
### CPython Benchmark
First, clone [CPython](https://github.com/python/cpython). It's a large and diverse Python codebase,
which makes it a good target for benchmarking.
@@ -386,9 +394,9 @@ Summary
159.43 ± 2.48 times faster than 'pycodestyle crates/ruff/resources/test/cpython'
```
You can run `poetry install` from `./scripts` to create a working environment for the above. All
reported benchmarks were computed using the versions specified by `./scripts/pyproject.toml`
on Python 3.11.
You can run `poetry install` from `./scripts/benchmarks` to create a working environment for the
above. All reported benchmarks were computed using the versions specified by
`./scripts/benchmarks/pyproject.toml` on Python 3.11.
To benchmark Pylint, remove the following files from the CPython repository:
@@ -429,3 +437,116 @@ Benchmark 1: find . -type f -name "*.py" | xargs -P 0 pyupgrade --py311-plus
Time (mean ± σ): 30.119 s ± 0.195 s [User: 28.638 s, System: 0.390 s]
Range (min … max): 29.813 s … 30.356 s 10 runs
```
## Microbenchmarks
The `ruff_benchmark` crate benchmarks the linter and the formatter on individual files.
You can run the benchmarks with
```shell
cargo benchmark
```
### Benchmark driven Development
Ruff uses [Criterion.rs](https://bheisler.github.io/criterion.rs/book/) for benchmarks. You can use
`--save-baseline=<name>` to store an initial baseline benchmark (e.g. on `main`) and then use
`--benchmark=<name>` to compare against that benchmark. Criterion will print a message telling you
if the benchmark improved/regressed compared to that baseline.
```shell
# Run once on your "baseline" code
cargo benchmark --save-baseline=main
# Then iterate with
cargo benchmark --baseline=main
```
### PR Summary
You can use `--save-baseline` and `critcmp` to get a pretty comparison between two recordings.
This is useful to illustrate the improvements of a PR.
```shell
# On main
cargo benchmark --save-baseline=main
# After applying your changes
cargo benchmark --save-baseline=pr
critcmp main pr
```
You must install [`critcmp`](https://github.com/BurntSushi/critcmp) for the comparison.
```bash
cargo install critcmp
```
### Tips
- Use `cargo benchmark <filter>` to only run specific benchmarks. For example: `cargo benchmark linter/pydantic`
to only run the pydantic tests.
- Use `cargo benchmark --quiet` for a more cleaned up output (without statistical relevance)
- Use `cargo benchmark --quick` to get faster results (more prone to noise)
## Profiling Projects
You can either use the microbenchmarks from above or a project directory for benchmarking. There
are a lot of profiling tools out there,
[The Rust Performance Book](https://nnethercote.github.io/perf-book/profiling.html) lists some
examples.
### Linux
Install `perf` and build `ruff_benchmark` with the `release-debug` profile and then run it with perf
```shell
cargo bench -p ruff_benchmark --no-run --profile=release-debug && perf record -g -F 9999 cargo bench -p ruff_benchmark --profile=release-debug -- --profile-time=1
```
You can also use the `ruff_dev` launcher to run `ruff check` multiple times on a repository to
gather enough samples for a good flamegraph (change the 999, the sample rate, and the 30, the number
of checks, to your liking)
```shell
cargo build --bin ruff_dev --profile=release-debug
perf record -g -F 999 target/release-debug/ruff_dev repeat --repeat 30 --exit-zero --no-cache path/to/cpython > /dev/null
```
Then convert the recorded profile
```shell
perf script -F +pid > /tmp/test.perf
```
You can now view the converted file with [firefox profiler](https://profiler.firefox.com/), with a
more in-depth guide [here](https://profiler.firefox.com/docs/#/./guide-perf-profiling)
An alternative is to convert the perf data to `flamegraph.svg` using
[flamegraph](https://github.com/flamegraph-rs/flamegraph) (`cargo install flamegraph`):
```shell
flamegraph --perfdata perf.data
```
### Mac
Install [`cargo-instruments`](https://crates.io/crates/cargo-instruments):
```shell
cargo install cargo-instruments
```
Then run the profiler with
```shell
cargo instruments -t time --bench linter --profile release-debug -p ruff_benchmark -- --profile-time=1
```
- `-t`: Specifies what to profile. Useful options are `time` to profile the wall time and `alloc`
for profiling the allocations.
- You may want to pass an additional filter to run a single test file
Otherwise, follow the instructions from the linux section.

18
Cargo.lock generated
View File

@@ -733,7 +733,7 @@ dependencies = [
[[package]]
name = "flake8-to-ruff"
version = "0.0.273"
version = "0.0.275"
dependencies = [
"anyhow",
"clap",
@@ -1793,7 +1793,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.273"
version = "0.0.275"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -1889,7 +1889,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.273"
version = "0.0.275"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2105,7 +2105,7 @@ dependencies = [
[[package]]
name = "ruff_text_size"
version = "0.0.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11"
dependencies = [
"schemars",
"serde",
@@ -2183,7 +2183,7 @@ dependencies = [
[[package]]
name = "rustpython-ast"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11"
dependencies = [
"is-macro",
"num-bigint",
@@ -2194,7 +2194,7 @@ dependencies = [
[[package]]
name = "rustpython-format"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11"
dependencies = [
"bitflags 2.3.1",
"itertools",
@@ -2206,7 +2206,7 @@ dependencies = [
[[package]]
name = "rustpython-literal"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11"
dependencies = [
"hexf-parse",
"is-macro",
@@ -2218,7 +2218,7 @@ dependencies = [
[[package]]
name = "rustpython-parser"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11"
dependencies = [
"anyhow",
"is-macro",
@@ -2241,7 +2241,7 @@ dependencies = [
[[package]]
name = "rustpython-parser-core"
version = "0.2.0"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=ed3b4eb72b6e497bbdb4d19dec6621074d724130#ed3b4eb72b6e497bbdb4d19dec6621074d724130"
source = "git+https://github.com/astral-sh/RustPython-Parser.git?rev=f60e204b73b95bdb6ce87ccd0de34081b4a17c11#f60e204b73b95bdb6ce87ccd0de34081b4a17c11"
dependencies = [
"is-macro",
"memchr",

View File

@@ -50,15 +50,15 @@ toml = { version = "0.7.2" }
# v0.0.1
libcst = { git = "https://github.com/charliermarsh/LibCST", rev = "80e4c1399f95e5beb532fdd1e209ad2dbb470438" }
# v0.0.3
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" }
ruff_text_size = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" }
# v0.0.3
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]}
rustpython-ast = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" , default-features = false, features = ["all-nodes-with-ranges", "num-bigint"]}
# v0.0.3
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130", default-features = false, features = ["num-bigint"] }
rustpython-format = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11", default-features = false, features = ["num-bigint"] }
# v0.0.3
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130", default-features = false }
rustpython-literal = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11", default-features = false }
# v0.0.3
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "ed3b4eb72b6e497bbdb4d19dec6621074d724130" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] }
rustpython-parser = { git = "https://github.com/astral-sh/RustPython-Parser.git", rev = "f60e204b73b95bdb6ce87ccd0de34081b4a17c11" , default-features = false, features = ["full-lexer", "all-nodes-with-ranges", "num-bigint"] }
[profile.release]
lto = "fat"

View File

@@ -14,9 +14,9 @@ An extremely fast Python linter, written in Rust.
<p align="center">
<picture align="center">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/212613422-7faaf278-706b-4294-ad92-236ffcab3430.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/212613257-5f4bca12-6d6b-4c79-9bac-51a4c6d08928.svg">
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/1309177/232603514-c95e9b0f-6b31-43de-9a80-9e844173fd6a.svg">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/1309177/232603516-4fb4892d-585c-4b20-b810-3db9161831e4.svg">
<img alt="Shows a bar chart with benchmark results." src="https://user-images.githubusercontent.com/1309177/232603516-4fb4892d-585c-4b20-b810-3db9161831e4.svg">
</picture>
</p>
@@ -139,7 +139,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.273
rev: v0.0.275
hooks:
- id: ruff
```

View File

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

View File

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

View File

@@ -1,6 +1,12 @@
# T002 - accepted
# TODO (evanrittenhouse): this has an author
# TODO(evanrittenhouse): this also has an author
# TODO(evanrittenhouse): this has an author
# TODO (evanrittenhouse) and more: this has an author
# TODO(evanrittenhouse) and more: this has an author
# TODO@mayrholu: this has an author
# TODO @mayrholu: this has an author
# TODO@mayrholu and more: this has an author
# TODO @mayrholu and more: this has an author
# T002 - errors
# TODO: this has no author
# FIXME: neither does this

View File

@@ -1,4 +1,4 @@
from abc import ABCMeta
import abc
import pydantic
@@ -19,6 +19,10 @@ class Class:
def class_method(cls):
pass
@abc.abstractclassmethod
def abstract_class_method(cls):
pass
@staticmethod
def static_method(x):
return x
@@ -41,7 +45,7 @@ class Class:
...
class MetaClass(ABCMeta):
class MetaClass(abc.ABCMeta):
def bad_method(self):
pass

View File

@@ -1,4 +1,4 @@
from abc import ABCMeta
import abc
import pydantic
@@ -34,6 +34,23 @@ class Class:
def stillBad(cls, my_field: str) -> str:
pass
@classmethod
def badAllowed(cls):
pass
@classmethod
def stillBad(cls):
pass
@abc.abstractclassmethod
def badAllowed(cls):
pass
@abc.abstractclassmethod
def stillBad(cls):
pass
class PosOnlyClass:
def badAllowed(this, blah, /, self, something: str):
pass

View File

@@ -0,0 +1,52 @@
foo_tuple = (1, 2, 3)
foo_list = [1, 2, 3]
foo_set = {1, 2, 3}
foo_dict = {1: 2, 3: 4}
foo_int = 123
for i in list(foo_tuple): # PERF101
pass
for i in list(foo_list): # PERF101
pass
for i in list(foo_set): # PERF101
pass
for i in list((1, 2, 3)): # PERF101
pass
for i in list([1, 2, 3]): # PERF101
pass
for i in list({1, 2, 3}): # PERF101
pass
for i in list(
{
1,
2,
3,
}
):
pass
for i in list( # Comment
{1, 2, 3}
): # PERF101
pass
for i in list(foo_dict): # Ok
pass
for i in list(1): # Ok
pass
for i in list(foo_int): # Ok
pass
import itertools
for i in itertools.product(foo_int): # Ok
pass

View File

@@ -37,7 +37,10 @@ f"{{test}}"
f'{{ 40 }}'
f"{{a {{x}}"
f"{{{{x}}}}"
""f""
''f""
(""f""r"")
# To be fixed
# Error: f-string: single '}' is not allowed at line 41 column 8
# f"\{{x}}"
# f"\{{x}}"

View File

@@ -48,3 +48,12 @@ if True: from collections import (
# OK
from a import b
# Ok: `typing_extensions` contains backported improvements.
from typing_extensions import SupportsIndex
# Ok: `typing_extensions` contains backported improvements.
from typing_extensions import NamedTuple
# Ok: `typing_extensions` supports `frozen_default` (backported from 3.12).
from typing_extensions import dataclass_transform

View File

@@ -34,12 +34,3 @@ f"{ascii(bla)}" # OK
" intermediary content "
f" that flows {repr(obj)} of type {type(obj)}.{additional_message}" # RUF010
)
f"{str(bla)}" # RUF010
f"{str(bla):20}" # RUF010
f"{bla!s}" # RUF010
f"{bla!s:20}" # OK

View File

@@ -1,23 +1,16 @@
import typing
from typing import ClassVar, Sequence
KNOWINGLY_MUTABLE_DEFAULT = []
from typing import ClassVar, Sequence, Final
class A:
mutable_default: list[int] = []
immutable_annotation: typing.Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: typing.ClassVar[list[int]] = []
__slots__ = {
"mutable_default": "A mutable default value",
}
class B:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
from dataclasses import dataclass, field
@@ -28,6 +21,17 @@ class C:
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
correct_code: list[int] = KNOWINGLY_MUTABLE_DEFAULT
perfectly_fine: list[int] = field(default_factory=list)
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
from pydantic import BaseModel
class D(BaseModel):
mutable_default: list[int] = []
immutable_annotation: Sequence[int] = []
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []

View File

@@ -18,19 +18,19 @@ def f(arg: object = None):
pass
def f(arg: int = None): # RUF011
def f(arg: int = None): # RUF013
pass
def f(arg: str = None): # RUF011
def f(arg: str = None): # RUF013
pass
def f(arg: typing.List[str] = None): # RUF011
def f(arg: typing.List[str] = None): # RUF013
pass
def f(arg: Tuple[str] = None): # RUF011
def f(arg: Tuple[str] = None): # RUF013
pass
@@ -64,15 +64,15 @@ def f(arg: Union[int, str, Any] = None):
pass
def f(arg: Union = None): # RUF011
def f(arg: Union = None): # RUF013
pass
def f(arg: Union[int, str] = None): # RUF011
def f(arg: Union[int, str] = None): # RUF013
pass
def f(arg: typing.Union[int, str] = None): # RUF011
def f(arg: typing.Union[int, str] = None): # RUF013
pass
@@ -91,11 +91,11 @@ def f(arg: int | float | str | None = None):
pass
def f(arg: int | float = None): # RUF011
def f(arg: int | float = None): # RUF013
pass
def f(arg: int | float | str | bytes = None): # RUF011
def f(arg: int | float | str | bytes = None): # RUF013
pass
@@ -110,11 +110,11 @@ def f(arg: Literal[1, 2, None, 3] = None):
pass
def f(arg: Literal[1, "foo"] = None): # RUF011
def f(arg: Literal[1, "foo"] = None): # RUF013
pass
def f(arg: typing.Literal[1, "foo", True] = None): # RUF011
def f(arg: typing.Literal[1, "foo", True] = None): # RUF013
pass
@@ -133,11 +133,11 @@ def f(arg: Annotated[Any, ...] = None):
pass
def f(arg: Annotated[int, ...] = None): # RUF011
def f(arg: Annotated[int, ...] = None): # RUF013
pass
def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF011
def f(arg: Annotated[Annotated[int | str, ...], ...] = None): # RUF013
pass
@@ -153,9 +153,9 @@ def f(
def f(
arg1: int = None, # RUF011
arg2: Union[int, float] = None, # RUF011
arg3: Literal[1, 2, 3] = None, # RUF011
arg1: int = None, # RUF013
arg2: Union[int, float] = None, # RUF013
arg3: Literal[1, 2, 3] = None, # RUF013
):
pass
@@ -183,5 +183,41 @@ def f(arg: Union[Annotated[int, ...], Annotated[Optional[float], ...]] = None):
pass
def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF011
def f(arg: Union[Annotated[int, ...], Union[str, bytes]] = None): # RUF013
pass
# Quoted
def f(arg: "int" = None): # RUF013
pass
def f(arg: "str" = None): # RUF013
pass
def f(arg: "st" "r" = None): # RUF013
pass
def f(arg: "Optional[int]" = None):
pass
def f(arg: Union["int", "str"] = None): # RUF013
pass
def f(arg: Union["int", "None"] = None):
pass
def f(arg: Union["No" "ne", "int"] = None):
pass
# Avoid flagging when there's a parse error in the forward reference
def f(arg: Union["<>", "int"] = None):
pass

View File

@@ -1481,6 +1481,9 @@ where
if self.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(self, target, iter);
}
if self.enabled(Rule::UnnecessaryListCast) {
perflint::rules::unnecessary_list_cast(self, iter);
}
}
Stmt::Try(ast::StmtTry {
body,
@@ -3852,6 +3855,9 @@ where
);
}
// Store the existing binding, if any.
let existing_id = self.semantic.lookup(name);
// Add the bound exception name to the scope.
let binding_id = self.add_binding(
name,
@@ -3862,14 +3868,6 @@ where
walk_except_handler(self, except_handler);
// Remove it from the scope immediately after.
self.add_binding(
name,
range,
BindingKind::UnboundException,
BindingFlags::empty(),
);
// If the exception name wasn't used in the scope, emit a diagnostic.
if !self.semantic.is_used(binding_id) {
if self.enabled(Rule::UnusedVariable) {
@@ -3889,6 +3887,13 @@ where
self.diagnostics.push(diagnostic);
}
}
self.add_binding(
name,
range,
BindingKind::UnboundException(existing_id),
BindingFlags::empty(),
);
}
None => walk_except_handler(self, except_handler),
}
@@ -4236,7 +4241,7 @@ impl<'a> Checker<'a> {
let shadowed = &self.semantic.bindings[shadowed_id];
if !matches!(
shadowed.kind,
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException,
BindingKind::Builtin | BindingKind::Deletion | BindingKind::UnboundException(_),
) {
let references = shadowed.references.clone();
let is_global = shadowed.is_global();

View File

@@ -8,7 +8,7 @@ use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
use ruff_python_whitespace::UniversalNewlines;
use crate::registry::Rule;
use crate::rules::copyright::rules::missing_copyright_notice;
use crate::rules::flake8_copyright::rules::missing_copyright_notice;
use crate::rules::flake8_executable::helpers::{extract_shebang, ShebangDirective};
use crate::rules::flake8_executable::rules::{
shebang_missing, shebang_newline, shebang_not_executable, shebang_python, shebang_whitespace,

View File

@@ -157,7 +157,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
// pylint
(Pylint, "C0414") => (RuleGroup::Unspecified, rules::pylint::rules::UselessImportAlias),
(Pylint, "C1901") => (RuleGroup::Unspecified, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C1901") => (RuleGroup::Nursery, rules::pylint::rules::CompareToEmptyString),
(Pylint, "C3002") => (RuleGroup::Unspecified, rules::pylint::rules::UnnecessaryDirectLambdaCall),
(Pylint, "C0208") => (RuleGroup::Unspecified, rules::pylint::rules::IterationOverSet),
(Pylint, "E0100") => (RuleGroup::Unspecified, rules::pylint::rules::YieldInInit),
@@ -375,7 +375,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Simplify, "910") => (RuleGroup::Unspecified, rules::flake8_simplify::rules::DictGetWithNoneDefault),
// copyright
(Copyright, "001") => (RuleGroup::Nursery, rules::copyright::rules::MissingCopyrightNotice),
(Copyright, "001") => (RuleGroup::Nursery, rules::flake8_copyright::rules::MissingCopyrightNotice),
// pyupgrade
(Pyupgrade, "001") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UselessMetaclassType),
@@ -784,6 +784,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Airflow, "001") => (RuleGroup::Unspecified, rules::airflow::rules::AirflowVariableNameTaskIdMismatch),
// perflint
(Perflint, "101") => (RuleGroup::Unspecified, rules::perflint::rules::UnnecessaryListCast),
(Perflint, "102") => (RuleGroup::Unspecified, rules::perflint::rules::IncorrectDictIterator),
// flake8-fixme

View File

@@ -445,7 +445,7 @@ impl Notebook {
}
#[cfg(test)]
mod test {
mod tests {
use std::path::Path;
use anyhow::Result;

View File

@@ -78,7 +78,7 @@ fn detect_package_root_with_cache<'a>(
current
}
/// Return a mapping from Python file to its package root.
/// Return a mapping from Python package to its package root.
pub fn detect_package_roots<'a>(
files: &[&'a Path],
resolver: &'a Resolver,

View File

@@ -251,7 +251,7 @@ impl Renamer {
| BindingKind::ClassDefinition
| BindingKind::FunctionDefinition
| BindingKind::Deletion
| BindingKind::UnboundException => {
| BindingKind::UnboundException(_) => {
Some(Edit::range_replacement(target.to_string(), binding.range))
}
}

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
---

View File

@@ -1,4 +0,0 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
---

View File

@@ -1,4 +1,4 @@
use rustpython_parser::ast::{self, Expr, Stmt};
use rustpython_parser::ast::{self, Stmt};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
@@ -50,9 +50,9 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return;
};
let Expr::JoinedStr ( _) = value.as_ref() else {
if !value.is_joined_str_expr() {
return;
};
}
checker
.diagnostics
.push(Diagnostic::new(FStringDocstring, stmt.identifier()));

View File

@@ -75,7 +75,7 @@ import os
"#
.trim(),
&settings::Settings {
copyright: super::settings::Settings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
@@ -95,7 +95,7 @@ import os
"#
.trim(),
&settings::Settings {
copyright: super::settings::Settings {
flake8_copyright: super::settings::Settings {
author: Some("Ruff".to_string()),
..super::settings::Settings::default()
},
@@ -113,7 +113,7 @@ import os
"#
.trim(),
&settings::Settings {
copyright: super::settings::Settings {
flake8_copyright: super::settings::Settings {
min_file_size: 256,
..super::settings::Settings::default()
},

View File

@@ -28,7 +28,7 @@ pub(crate) fn missing_copyright_notice(
settings: &Settings,
) -> Option<Diagnostic> {
// Ignore files that are too small to contain a copyright notice.
if locator.len() < settings.copyright.min_file_size {
if locator.len() < settings.flake8_copyright.min_file_size {
return None;
}
@@ -40,8 +40,8 @@ pub(crate) fn missing_copyright_notice(
};
// Locate the copyright notice.
if let Some(match_) = settings.copyright.notice_rgx.find(contents) {
match settings.copyright.author {
if let Some(match_) = settings.flake8_copyright.notice_rgx.find(contents) {
match settings.flake8_copyright.author {
Some(ref author) => {
// Ensure that it's immediately followed by the author.
if contents[match_.end()..].trim_start().starts_with(author) {

View File

@@ -1,4 +1,4 @@
//! Settings for the `copyright` plugin.
//! Settings for the `flake8-copyright` plugin.
use once_cell::sync::Lazy;
use regex::Regex;
@@ -12,7 +12,7 @@ use ruff_macros::{CacheKey, CombineOptions, ConfigurationOptions};
#[serde(
deny_unknown_fields,
rename_all = "kebab-case",
rename = "CopyrightOptions"
rename = "Flake8CopyrightOptions"
)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct Options {

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---
<filename>:1:1: CPY001 Missing copyright notice at top of file
|

View File

@@ -1,5 +1,5 @@
---
source: crates/ruff/src/rules/copyright/mod.rs
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---
<filename>:1:1: CPY001 Missing copyright notice at top of file
|

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/flake8_copyright/mod.rs
---

View File

@@ -7,6 +7,28 @@ use ruff_python_ast::call_path::{format_call_path, from_unqualified_name, CallPa
use crate::checkers::ast::Checker;
use crate::rules::flake8_debugger::types::DebuggerUsingType;
/// ## What it does
/// Checks for the presence of debugger calls and imports.
///
/// ## Why is this bad?
/// Debugger calls and imports should be used for debugging purposes only. The
/// presence of a debugger call or import in production code is likely a
/// mistake and may cause unintended behavior, such as exposing sensitive
/// information or causing the program to hang.
///
/// Instead, consider using a logging library to log information about the
/// program's state, and writing tests to verify that the program behaves
/// as expected.
///
/// ## Example
/// ```python
/// def foo():
/// breakpoint()
/// ```
///
/// ## References
/// - [Python documentation: `pdb` — The Python Debugger](https://docs.python.org/3/library/pdb.html)
/// - [Python documentation: `logging` — Logging facility for Python](https://docs.python.org/3/library/logging.html)
#[violation]
pub struct Debugger {
using_type: DebuggerUsingType,

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of comparators other than `<` and `>=` for
/// Checks for uses of comparators other than `<` and `>=` for
/// `sys.version_info` checks in `.pyi` files. All other comparators, such
/// as `>`, `<=`, and `==`, are banned.
///

View File

@@ -7,7 +7,7 @@ use ruff_python_semantic::SemanticModel;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of the builtin `open()` function without an associated context
/// Checks for uses of the builtin `open()` function without an associated context
/// manager.
///
/// ## Why is this bad?

View File

@@ -67,7 +67,7 @@ pub struct MissingTodoAuthor;
impl Violation for MissingTodoAuthor {
#[derive_message_formats]
fn message(&self) -> String {
format!("Missing author in TODO; try: `# TODO(<author_name>): ...`")
format!("Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...`")
}
}
@@ -229,7 +229,7 @@ static ISSUE_LINK_REGEX_SET: Lazy<RegexSet> = Lazy::new(|| {
RegexSet::new([
r#"^#\s*(http|https)://.*"#, // issue link
r#"^#\s*\d+$"#, // issue code - like "003"
r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003" or "TD-003"
r#"^#\s*[A-Z]{1,6}\-?\d+$"#, // issue code - like "TD003"
])
.unwrap()
});
@@ -339,8 +339,7 @@ fn directive_errors(
}
}
/// Checks for "static" errors in the comment: missing colon, missing author, etc. This function
/// modifies `diagnostics` in-place.
/// Checks for "static" errors in the comment: missing colon, missing author, etc.
fn static_errors(
diagnostics: &mut Vec<Diagnostic>,
comment: &str,
@@ -358,6 +357,15 @@ fn static_errors(
} else {
trimmed.text_len()
}
} else if trimmed.starts_with('@') {
if let Some(end_index) = trimmed.find(|c: char| c.is_whitespace() || c == ':') {
TextSize::try_from(end_index).unwrap()
} else {
// TD002
diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range));
TextSize::new(0)
}
} else {
// TD002
diagnostics.push(Diagnostic::new(MissingTodoAuthor, directive.range));

View File

@@ -1,41 +1,41 @@
---
source: crates/ruff/src/rules/flake8_todos/mod.rs
---
TD002.py:5:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
3 | # TODO(evanrittenhouse): this also has an author
4 | # T002 - errors
5 | # TODO: this has no author
| ^^^^ TD002
6 | # FIXME: neither does this
7 | # TODO : and neither does this
|
TD002.py:11:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...`
|
9 | # TODO @mayrholu and more: this has an author
10 | # T002 - errors
11 | # TODO: this has no author
| ^^^^ TD002
12 | # FIXME: neither does this
13 | # TODO : and neither does this
|
TD002.py:6:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
4 | # T002 - errors
5 | # TODO: this has no author
6 | # FIXME: neither does this
| ^^^^^ TD002
7 | # TODO : and neither does this
8 | # foo # TODO: this doesn't either
|
TD002.py:12:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...`
|
10 | # T002 - errors
11 | # TODO: this has no author
12 | # FIXME: neither does this
| ^^^^^ TD002
13 | # TODO : and neither does this
14 | # foo # TODO: this doesn't either
|
TD002.py:7:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
5 | # TODO: this has no author
6 | # FIXME: neither does this
7 | # TODO : and neither does this
| ^^^^ TD002
8 | # foo # TODO: this doesn't either
|
TD002.py:13:3: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...`
|
11 | # TODO: this has no author
12 | # FIXME: neither does this
13 | # TODO : and neither does this
| ^^^^ TD002
14 | # foo # TODO: this doesn't either
|
TD002.py:8:9: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...`
|
6 | # FIXME: neither does this
7 | # TODO : and neither does this
8 | # foo # TODO: this doesn't either
| ^^^^ TD002
|
TD002.py:14:9: TD002 Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...`
|
12 | # FIXME: neither does this
13 | # TODO : and neither does this
14 | # foo # TODO: this doesn't either
| ^^^^ TD002
|

View File

@@ -1,6 +1,5 @@
#![allow(clippy::useless_format)]
pub mod airflow;
pub mod copyright;
pub mod eradicate;
pub mod flake8_2020;
pub mod flake8_annotations;
@@ -12,6 +11,7 @@ pub mod flake8_bugbear;
pub mod flake8_builtins;
pub mod flake8_commas;
pub mod flake8_comprehensions;
pub mod flake8_copyright;
pub mod flake8_datetimez;
pub mod flake8_debugger;
pub mod flake8_django;

View File

@@ -12,7 +12,7 @@ use crate::registry::AsRule;
/// ## Why is this bad?
/// NumPy's `np.int` has long been an alias of the builtin `int`. The same
/// goes for `np.float`, `np.bool`, and others. These aliases exist
/// primarily primarily for historic reasons, and have been a cause of
/// primarily for historic reasons, and have been a cause of
/// frequent confusion for newcomers.
///
/// These aliases were been deprecated in 1.20, and removed in 1.24.

View File

@@ -40,6 +40,5 @@ pub(super) fn convert_inplace_argument_to_assignment(
false,
)
.ok()?;
#[allow(deprecated)]
Some(Fix::unspecified_edits(insert_assignment, [remove_argument]))
Some(Fix::suggested_edits(insert_assignment, [remove_argument]))
}

View File

@@ -18,29 +18,29 @@ N805.py:12:30: N805 First argument of a method should be named `self`
13 | pass
|
N805.py:27:15: N805 First argument of a method should be named `self`
|
26 | @pydantic.validator
27 | def lower(cls, my_field: str) -> str:
| ^^^ N805
28 | pass
|
N805.py:31:15: N805 First argument of a method should be named `self`
|
30 | @pydantic.validator("my_field")
30 | @pydantic.validator
31 | def lower(cls, my_field: str) -> str:
| ^^^ N805
32 | pass
|
N805.py:60:29: N805 First argument of a method should be named `self`
N805.py:35:15: N805 First argument of a method should be named `self`
|
58 | pass
59 |
60 | def bad_method_pos_only(this, blah, /, self, something: str):
34 | @pydantic.validator("my_field")
35 | def lower(cls, my_field: str) -> str:
| ^^^ N805
36 | pass
|
N805.py:64:29: N805 First argument of a method should be named `self`
|
62 | pass
63 |
64 | def bad_method_pos_only(this, blah, /, self, something: str):
| ^^^^ N805
61 | pass
65 | pass
|

View File

@@ -18,13 +18,13 @@ N805.py:12:30: N805 First argument of a method should be named `self`
13 | pass
|
N805.py:60:29: N805 First argument of a method should be named `self`
N805.py:64:29: N805 First argument of a method should be named `self`
|
58 | pass
59 |
60 | def bad_method_pos_only(this, blah, /, self, something: str):
62 | pass
63 |
64 | def bad_method_pos_only(this, blah, /, self, something: str):
| ^^^^ N805
61 | pass
65 | pass
|

View File

@@ -35,13 +35,13 @@ N805.py:34:18: N805 First argument of a method should be named `self`
35 | pass
|
N805.py:41:18: N805 First argument of a method should be named `self`
N805.py:58:18: N805 First argument of a method should be named `self`
|
39 | pass
40 |
41 | def stillBad(this, blah, /, self, something: str):
56 | pass
57 |
58 | def stillBad(this, blah, /, self, something: str):
| ^^^^ N805
42 | pass
59 | pass
|

View File

@@ -13,6 +13,7 @@ mod tests {
use crate::settings::Settings;
use crate::test::test_path;
#[test_case(Rule::UnnecessaryListCast, Path::new("PERF101.py"))]
#[test_case(Rule::IncorrectDictIterator, Path::new("PERF102.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());

View File

@@ -1,3 +1,5 @@
pub(crate) use incorrect_dict_iterator::{incorrect_dict_iterator, IncorrectDictIterator};
pub(crate) use incorrect_dict_iterator::*;
pub(crate) use unnecessary_list_cast::*;
mod incorrect_dict_iterator;
mod unnecessary_list_cast;

View File

@@ -0,0 +1,129 @@
use ruff_text_size::TextRange;
use rustpython_parser::ast::{self, Expr};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::prelude::Stmt;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for explicit casts to `list` on for-loop iterables.
///
/// ## Why is this bad?
/// Using a `list()` call to eagerly iterate over an already-iterable type
/// (like a tuple, list, or set) is inefficient, as it forces Python to create
/// a new list unnecessarily.
///
/// Removing the `list()` call will not change the behavior of the code, but
/// may improve performance.
///
/// ## Example
/// ```python
/// items = (1, 2, 3)
/// for i in list(items):
/// print(i)
/// ```
///
/// Use instead:
/// ```python
/// items = (1, 2, 3)
/// for i in items:
/// print(i)
/// ```
#[violation]
pub struct UnnecessaryListCast;
impl AlwaysAutofixableViolation for UnnecessaryListCast {
#[derive_message_formats]
fn message(&self) -> String {
format!("Do not cast an iterable to `list` before iterating over it")
}
fn autofix_title(&self) -> String {
format!("Remove `list()` cast")
}
}
/// PERF101
pub(crate) fn unnecessary_list_cast(checker: &mut Checker, iter: &Expr) {
let Expr::Call(ast::ExprCall{ func, args, range: list_range, ..}) = iter else {
return;
};
if args.len() != 1 {
return;
}
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else{
return;
};
if !(id == "list" && checker.semantic().is_builtin("list")) {
return;
}
match &args[0] {
Expr::Tuple(ast::ExprTuple {
range: iterable_range,
..
})
| Expr::List(ast::ExprList {
range: iterable_range,
..
})
| Expr::Set(ast::ExprSet {
range: iterable_range,
..
}) => {
let mut diagnostic = Diagnostic::new(UnnecessaryListCast, *list_range);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(remove_cast(*list_range, *iterable_range));
}
checker.diagnostics.push(diagnostic);
}
Expr::Name(ast::ExprName {
id,
range: iterable_range,
..
}) => {
let scope = checker.semantic().scope();
if let Some(binding_id) = scope.get(id) {
let binding = checker.semantic().binding(binding_id);
if binding.kind.is_assignment() || binding.kind.is_named_expr_assignment() {
if let Some(parent_id) = binding.source {
let parent = checker.semantic().stmts[parent_id];
if let Stmt::Assign(ast::StmtAssign { value, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign {
value: Some(value), ..
})
| Stmt::AugAssign(ast::StmtAugAssign { value, .. }) = parent
{
if matches!(
value.as_ref(),
Expr::Tuple(_) | Expr::List(_) | Expr::Set(_)
) {
let mut diagnostic =
Diagnostic::new(UnnecessaryListCast, *list_range);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(remove_cast(*list_range, *iterable_range));
}
checker.diagnostics.push(diagnostic);
}
}
}
}
}
}
_ => {}
}
}
/// Generate a [`Fix`] to remove a `list` cast from an expression.
fn remove_cast(list_range: TextRange, iterable_range: TextRange) -> Fix {
Fix::automatic_edits(
Edit::deletion(list_range.start(), iterable_range.start()),
[Edit::deletion(iterable_range.end(), list_range.end())],
)
}

View File

@@ -0,0 +1,183 @@
---
source: crates/ruff/src/rules/perflint/mod.rs
---
PERF101.py:7:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
5 | foo_int = 123
6 |
7 | for i in list(foo_tuple): # PERF101
| ^^^^^^^^^^^^^^^ PERF101
8 | pass
|
= help: Remove `list()` cast
Fix
4 4 | foo_dict = {1: 2, 3: 4}
5 5 | foo_int = 123
6 6 |
7 |-for i in list(foo_tuple): # PERF101
7 |+for i in foo_tuple: # PERF101
8 8 | pass
9 9 |
10 10 | for i in list(foo_list): # PERF101
PERF101.py:10:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
8 | pass
9 |
10 | for i in list(foo_list): # PERF101
| ^^^^^^^^^^^^^^ PERF101
11 | pass
|
= help: Remove `list()` cast
Fix
7 7 | for i in list(foo_tuple): # PERF101
8 8 | pass
9 9 |
10 |-for i in list(foo_list): # PERF101
10 |+for i in foo_list: # PERF101
11 11 | pass
12 12 |
13 13 | for i in list(foo_set): # PERF101
PERF101.py:13:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
11 | pass
12 |
13 | for i in list(foo_set): # PERF101
| ^^^^^^^^^^^^^ PERF101
14 | pass
|
= help: Remove `list()` cast
Fix
10 10 | for i in list(foo_list): # PERF101
11 11 | pass
12 12 |
13 |-for i in list(foo_set): # PERF101
13 |+for i in foo_set: # PERF101
14 14 | pass
15 15 |
16 16 | for i in list((1, 2, 3)): # PERF101
PERF101.py:16:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
14 | pass
15 |
16 | for i in list((1, 2, 3)): # PERF101
| ^^^^^^^^^^^^^^^ PERF101
17 | pass
|
= help: Remove `list()` cast
Fix
13 13 | for i in list(foo_set): # PERF101
14 14 | pass
15 15 |
16 |-for i in list((1, 2, 3)): # PERF101
16 |+for i in (1, 2, 3): # PERF101
17 17 | pass
18 18 |
19 19 | for i in list([1, 2, 3]): # PERF101
PERF101.py:19:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
17 | pass
18 |
19 | for i in list([1, 2, 3]): # PERF101
| ^^^^^^^^^^^^^^^ PERF101
20 | pass
|
= help: Remove `list()` cast
Fix
16 16 | for i in list((1, 2, 3)): # PERF101
17 17 | pass
18 18 |
19 |-for i in list([1, 2, 3]): # PERF101
19 |+for i in [1, 2, 3]: # PERF101
20 20 | pass
21 21 |
22 22 | for i in list({1, 2, 3}): # PERF101
PERF101.py:22:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
20 | pass
21 |
22 | for i in list({1, 2, 3}): # PERF101
| ^^^^^^^^^^^^^^^ PERF101
23 | pass
|
= help: Remove `list()` cast
Fix
19 19 | for i in list([1, 2, 3]): # PERF101
20 20 | pass
21 21 |
22 |-for i in list({1, 2, 3}): # PERF101
22 |+for i in {1, 2, 3}: # PERF101
23 23 | pass
24 24 |
25 25 | for i in list(
PERF101.py:25:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
23 | pass
24 |
25 | for i in list(
| __________^
26 | | {
27 | | 1,
28 | | 2,
29 | | 3,
30 | | }
31 | | ):
| |_^ PERF101
32 | pass
|
= help: Remove `list()` cast
Fix
22 22 | for i in list({1, 2, 3}): # PERF101
23 23 | pass
24 24 |
25 |-for i in list(
26 |- {
25 |+for i in {
27 26 | 1,
28 27 | 2,
29 28 | 3,
30 |- }
31 |-):
29 |+ }:
32 30 | pass
33 31 |
34 32 | for i in list( # Comment
PERF101.py:34:10: PERF101 [*] Do not cast an iterable to `list` before iterating over it
|
32 | pass
33 |
34 | for i in list( # Comment
| __________^
35 | | {1, 2, 3}
36 | | ): # PERF101
| |_^ PERF101
37 | pass
|
= help: Remove `list()` cast
Fix
31 31 | ):
32 32 | pass
33 33 |
34 |-for i in list( # Comment
35 |- {1, 2, 3}
36 |-): # PERF101
34 |+for i in {1, 2, 3}: # PERF101
37 35 | pass
38 36 |
39 37 | for i in list(foo_dict): # Ok

View File

@@ -10,7 +10,21 @@ use crate::settings::Settings;
///
/// ## Why is this bad?
/// For flowing long blocks of text (docstrings or comments), overlong lines
/// can hurt readability.
/// can hurt readability. [PEP 8], for example, recommends that such lines be
/// limited to 72 characters.
///
/// In the context of this rule, a "doc line" is defined as a line consisting
/// of either a standalone comment or a standalone string, like a docstring.
///
/// In the interest of pragmatism, this rule makes a few exceptions when
/// determining whether a line is overlong. Namely, it ignores lines that
/// consist of a single "word" (i.e., without any whitespace between its
/// characters), and lines that end with a URL (as long as the URL starts
/// before the line-length threshold).
///
/// If `pycodestyle.ignore_overlong_task_comments` is `true`, this rule will
/// also ignore comments that start with any of the specified `task-tags`
/// (e.g., `# TODO:`).
///
/// ## Example
/// ```python
@@ -26,6 +40,13 @@ use crate::settings::Settings;
/// Duis auctor purus ut ex fermentum, at maximus est hendrerit.
/// """
/// ```
///
///
/// ## Options
/// - `task-tags`
/// - `pycodestyle.ignore-overlong-task-comments`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length
#[violation]
pub struct DocLineTooLong(pub usize, pub usize);

View File

@@ -108,8 +108,7 @@ pub(crate) fn invalid_escape_sequence(
let range = TextRange::at(location, next_char.text_len() + TextSize::from(1));
let mut diagnostic = Diagnostic::new(InvalidEscapeSequence(*next_char), range);
if autofix {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::insertion(
diagnostic.set_fix(Fix::automatic(Edit::insertion(
r"\".to_string(),
range.start() + TextSize::from(1),
)));

View File

@@ -9,7 +9,18 @@ use crate::settings::Settings;
/// Checks for lines that exceed the specified maximum character length.
///
/// ## Why is this bad?
/// Overlong lines can hurt readability.
/// Overlong lines can hurt readability. [PEP 8], for example, recommends
/// limiting lines to 79 characters.
///
/// In the interest of pragmatism, this rule makes a few exceptions when
/// determining whether a line is overlong. Namely, it ignores lines that
/// consist of a single "word" (i.e., without any whitespace between its
/// characters), and lines that end with a URL (as long as the URL starts
/// before the line-length threshold).
///
/// If `pycodestyle.ignore_overlong_task_comments` is `true`, this rule will
/// also ignore comments that start with any of the specified `task-tags`
/// (e.g., `# TODO:`).
///
/// ## Example
/// ```python
@@ -26,6 +37,9 @@ use crate::settings::Settings;
///
/// ## Options
/// - `task-tags`
/// - `pycodestyle.ignore-overlong-task-comments`
///
/// [PEP 8]: https://peps.python.org/pep-0008/#maximum-line-length
#[violation]
pub struct LineTooLong(pub usize, pub usize);

View File

@@ -92,8 +92,7 @@ pub(crate) fn missing_whitespace(
let mut diagnostic = Diagnostic::new(kind, token.range());
if autofix {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::insertion(
diagnostic.set_fix(Fix::automatic(Edit::insertion(
" ".to_string(),
token.end(),
)));

View File

@@ -65,8 +65,7 @@ pub(crate) fn whitespace_before_parameters(
let mut diagnostic = Diagnostic::new(kind, TextRange::new(start, end));
if autofix {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::deletion(start, end)));
diagnostic.set_fix(Fix::automatic(Edit::deletion(start, end)));
}
context.push_diagnostic(diagnostic);
}

View File

@@ -55,8 +55,7 @@ pub(crate) fn no_newline_at_end_of_file(
let mut diagnostic = Diagnostic::new(MissingNewlineAtEndOfFile, range);
if autofix {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::insertion(
diagnostic.set_fix(Fix::automatic(Edit::insertion(
stylist.line_ending().to_string(),
range.start(),
)));

View File

@@ -11,7 +11,7 @@ E21.py:2:5: E211 [*] Whitespace before '('
|
= help: Removed whitespace before '('
Suggested fix
Fix
1 1 | #: E211
2 |-spam (1)
2 |+spam(1)
@@ -30,7 +30,7 @@ E21.py:4:5: E211 [*] Whitespace before '['
|
= help: Removed whitespace before '['
Suggested fix
Fix
1 1 | #: E211
2 2 | spam (1)
3 3 | #: E211 E211
@@ -51,7 +51,7 @@ E21.py:4:20: E211 [*] Whitespace before '['
|
= help: Removed whitespace before '['
Suggested fix
Fix
1 1 | #: E211
2 2 | spam (1)
3 3 | #: E211 E211
@@ -72,7 +72,7 @@ E21.py:6:12: E211 [*] Whitespace before '['
|
= help: Removed whitespace before '['
Suggested fix
Fix
3 3 | #: E211 E211
4 4 | dict ['key'] = list [index]
5 5 | #: E211

View File

@@ -11,7 +11,7 @@ E23.py:2:7: E231 [*] Missing whitespace after ','
|
= help: Added missing whitespace after ','
Suggested fix
Fix
1 1 | #: E231
2 |-a = (1,2)
2 |+a = (1, 2)
@@ -30,7 +30,7 @@ E23.py:4:5: E231 [*] Missing whitespace after ','
|
= help: Added missing whitespace after ','
Suggested fix
Fix
1 1 | #: E231
2 2 | a = (1,2)
3 3 | #: E231
@@ -51,7 +51,7 @@ E23.py:6:10: E231 [*] Missing whitespace after ':'
|
= help: Added missing whitespace after ':'
Suggested fix
Fix
3 3 | #: E231
4 4 | a[b1,:]
5 5 | #: E231
@@ -71,7 +71,7 @@ E23.py:19:10: E231 [*] Missing whitespace after ','
|
= help: Added missing whitespace after ','
Suggested fix
Fix
16 16 |
17 17 | def foo() -> None:
18 18 | #: E231
@@ -91,7 +91,7 @@ E23.py:29:20: E231 [*] Missing whitespace after ':'
|
= help: Added missing whitespace after ':'
Suggested fix
Fix
26 26 | #: E231:2:20
27 27 | mdtypes_template = {
28 28 | 'tag_full': [('mdtype', 'u4'), ('byte_count', 'u4')],

View File

@@ -9,7 +9,7 @@ W292_0.py:2:9: W292 [*] No newline at end of file
|
= help: Add trailing newline
Suggested fix
Fix
1 1 | def fn() -> None:
2 |- pass
2 |+ pass

View File

@@ -11,7 +11,7 @@ W605_0.py:2:10: W605 [*] Invalid escape sequence: `\.`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
1 1 | #: W605:1:10
2 |-regex = '\.png$'
2 |+regex = '\\.png$'
@@ -29,7 +29,7 @@ W605_0.py:6:1: W605 [*] Invalid escape sequence: `\.`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
3 3 |
4 4 | #: W605:2:1
5 5 | regex = '''
@@ -49,7 +49,7 @@ W605_0.py:11:6: W605 [*] Invalid escape sequence: `\_`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
8 8 |
9 9 | #: W605:2:6
10 10 | f(
@@ -70,7 +70,7 @@ W605_0.py:18:6: W605 [*] Invalid escape sequence: `\_`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
15 15 | """
16 16 | multi-line
17 17 | literal

View File

@@ -11,7 +11,7 @@ W605_1.py:2:10: W605 [*] Invalid escape sequence: `\.`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
1 1 | #: W605:1:10
2 |-regex = '\.png$'
2 |+regex = '\\.png$'
@@ -29,7 +29,7 @@ W605_1.py:6:1: W605 [*] Invalid escape sequence: `\.`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
3 3 |
4 4 | #: W605:2:1
5 5 | regex = '''
@@ -49,7 +49,7 @@ W605_1.py:11:6: W605 [*] Invalid escape sequence: `\_`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
8 8 |
9 9 | #: W605:2:6
10 10 | f(
@@ -70,7 +70,7 @@ W605_1.py:18:6: W605 [*] Invalid escape sequence: `\_`
|
= help: Add backslash to escape sequence
Suggested fix
Fix
15 15 | """
16 16 | multi-line
17 17 | literal

View File

@@ -8,7 +8,7 @@ W292_4.py:1:2: W292 [*] No newline at end of file
|
= help: Add trailing newline
Suggested fix
Fix
1 |-
1 |+

View File

@@ -353,9 +353,59 @@ mod tests {
except Exception as x:
pass
# No error here, though it should arguably be an F821 error. `x` will
# be unbound after the `except` block (assuming an exception is raised
# and caught).
print(x)
"#,
"print_after_shadowing_except"
"print_in_body_after_shadowing_except"
)]
#[test_case(
r#"
def f():
x = 1
try:
1 / 0
except ValueError as x:
pass
except ImportError as x:
pass
# No error here, though it should arguably be an F821 error. `x` will
# be unbound after the `except` block (assuming an exception is raised
# and caught).
print(x)
"#,
"print_in_body_after_double_shadowing_except"
)]
#[test_case(
r#"
def f():
try:
x = 3
except ImportError as x:
print(x)
else:
print(x)
"#,
"print_in_try_else_after_shadowing_except"
)]
#[test_case(
r#"
def f():
list = [1, 2, 3]
for e in list:
if e % 2 == 0:
try:
pass
except Exception as e:
print(e)
else:
print(e)
"#,
"print_in_if_else_after_shadowing_except"
)]
#[test_case(
r#"
@@ -366,6 +416,79 @@ mod tests {
"#,
"double_del"
)]
#[test_case(
r#"
x = 1
def f():
try:
pass
except ValueError as x:
pass
# This should resolve to the `x` in `x = 1`.
print(x)
"#,
"load_after_unbind_from_module_scope"
)]
#[test_case(
r#"
x = 1
def f():
try:
pass
except ValueError as x:
pass
try:
pass
except ValueError as x:
pass
# This should resolve to the `x` in `x = 1`.
print(x)
"#,
"load_after_multiple_unbinds_from_module_scope"
)]
#[test_case(
r#"
x = 1
def f():
try:
pass
except ValueError as x:
pass
def g():
try:
pass
except ValueError as x:
pass
# This should resolve to the `x` in `x = 1`.
print(x)
"#,
"load_after_unbind_from_nested_module_scope"
)]
#[test_case(
r#"
class C:
x = 1
def f():
try:
pass
except ValueError as x:
pass
# This should raise an F821 error, rather than resolving to the
# `x` in `x = 1`.
print(x)
"#,
"load_after_unbind_from_class_scope"
)]
fn contents(contents: &str, snapshot: &str) {
let diagnostics = test_snippet(contents, &Settings::for_rules(&Linter::Pyflakes));
assert_messages!(snapshot, diagnostics);

View File

@@ -79,24 +79,6 @@ fn find_useless_f_strings<'a>(
})
}
fn unescape_f_string(content: &str) -> String {
content.replace("{{", "{").replace("}}", "}")
}
fn fix_f_string_missing_placeholders(
prefix_range: TextRange,
tok_range: TextRange,
checker: &mut Checker,
) -> Fix {
let content = &checker.locator.contents()[TextRange::new(prefix_range.end(), tok_range.end())];
#[allow(deprecated)]
Fix::unspecified(Edit::replacement(
unescape_f_string(content),
prefix_range.start(),
tok_range.end(),
))
}
/// F541
pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checker: &mut Checker) {
if !values
@@ -106,13 +88,51 @@ pub(crate) fn f_string_missing_placeholders(expr: &Expr, values: &[Expr], checke
for (prefix_range, tok_range) in find_useless_f_strings(expr, checker.locator) {
let mut diagnostic = Diagnostic::new(FStringMissingPlaceholders, tok_range);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.set_fix(fix_f_string_missing_placeholders(
diagnostic.set_fix(convert_f_string_to_regular_string(
prefix_range,
tok_range,
checker,
checker.locator,
));
}
checker.diagnostics.push(diagnostic);
}
}
}
/// Unescape an f-string body by replacing `{{` with `{` and `}}` with `}`.
///
/// In Python, curly-brace literals within f-strings must be escaped by doubling the braces.
/// When rewriting an f-string to a regular string, we need to unescape any curly-brace literals.
/// For example, given `{{Hello, world!}}`, return `{Hello, world!}`.
fn unescape_f_string(content: &str) -> String {
content.replace("{{", "{").replace("}}", "}")
}
/// Generate a [`Fix`] to rewrite an f-string as a regular string.
fn convert_f_string_to_regular_string(
prefix_range: TextRange,
tok_range: TextRange,
locator: &Locator,
) -> Fix {
// Extract the f-string body.
let mut content =
unescape_f_string(locator.slice(TextRange::new(prefix_range.end(), tok_range.end())));
// If the preceding character is equivalent to the quote character, insert a space to avoid a
// syntax error. For example, when removing the `f` prefix in `""f""`, rewrite to `"" ""`
// instead of `""""`.
if locator
.slice(TextRange::up_to(prefix_range.start()))
.chars()
.last()
.map_or(false, |char| content.starts_with(char))
{
content.insert(0, ' ');
}
Fix::automatic(Edit::replacement(
content,
prefix_range.start(),
tok_range.end(),
))
}

View File

@@ -11,7 +11,7 @@ F541.py:6:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
3 3 | b = f"ghi{'jkl'}"
4 4 |
5 5 | # Errors
@@ -32,7 +32,7 @@ F541.py:7:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
4 4 |
5 5 | # Errors
6 6 | c = f"def"
@@ -53,7 +53,7 @@ F541.py:9:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
6 6 | c = f"def"
7 7 | d = f"def" + "ghi"
8 8 | e = (
@@ -74,7 +74,7 @@ F541.py:13:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
10 10 | "ghi"
11 11 | )
12 12 | f = (
@@ -95,7 +95,7 @@ F541.py:14:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
11 11 | )
12 12 | f = (
13 13 | f"a"
@@ -116,7 +116,7 @@ F541.py:16:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
13 13 | f"a"
14 14 | F"b"
15 15 | "c"
@@ -137,7 +137,7 @@ F541.py:17:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
14 14 | F"b"
15 15 | "c"
16 16 | rf"d"
@@ -158,7 +158,7 @@ F541.py:19:5: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
16 16 | rf"d"
17 17 | fr"e"
18 18 | )
@@ -178,7 +178,7 @@ F541.py:25:13: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
22 22 | g = f"ghi{123:{45}}"
23 23 |
24 24 | # Error
@@ -198,7 +198,7 @@ F541.py:34:7: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
31 31 | f"{f'{v:0.2f}'}"
32 32 |
33 33 | # Errors
@@ -219,7 +219,7 @@ F541.py:35:4: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
32 32 |
33 33 | # Errors
34 34 | f"{v:{f'0.2f'}}"
@@ -240,7 +240,7 @@ F541.py:36:1: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
33 33 | # Errors
34 34 | f"{v:{f'0.2f'}}"
35 35 | f"{f''}"
@@ -261,7 +261,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
34 34 | f"{v:{f'0.2f'}}"
35 35 | f"{f''}"
36 36 | f"{{test}}"
@@ -269,7 +269,7 @@ F541.py:37:1: F541 [*] f-string without any placeholders
37 |+'{ 40 }'
38 38 | f"{{a {{x}}"
39 39 | f"{{{{x}}}}"
40 40 |
40 40 | ""f""
F541.py:38:1: F541 [*] f-string without any placeholders
|
@@ -278,18 +278,19 @@ F541.py:38:1: F541 [*] f-string without any placeholders
38 | f"{{a {{x}}"
| ^^^^^^^^^^^^ F541
39 | f"{{{{x}}}}"
40 | ""f""
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
35 35 | f"{f''}"
36 36 | f"{{test}}"
37 37 | f'{{ 40 }}'
38 |-f"{{a {{x}}"
38 |+"{a {x}"
39 39 | f"{{{{x}}}}"
40 40 |
41 41 | # To be fixed
40 40 | ""f""
41 41 | ''f""
F541.py:39:1: F541 [*] f-string without any placeholders
|
@@ -297,19 +298,81 @@ F541.py:39:1: F541 [*] f-string without any placeholders
38 | f"{{a {{x}}"
39 | f"{{{{x}}}}"
| ^^^^^^^^^^^^ F541
40 |
41 | # To be fixed
40 | ""f""
41 | ''f""
|
= help: Remove extraneous `f` prefix
Suggested fix
Fix
36 36 | f"{{test}}"
37 37 | f'{{ 40 }}'
38 38 | f"{{a {{x}}"
39 |-f"{{{{x}}}}"
39 |+"{{x}}"
40 40 |
41 41 | # To be fixed
42 42 | # Error: f-string: single '}' is not allowed at line 41 column 8
40 40 | ""f""
41 41 | ''f""
42 42 | (""f""r"")
F541.py:40:3: F541 [*] f-string without any placeholders
|
38 | f"{{a {{x}}"
39 | f"{{{{x}}}}"
40 | ""f""
| ^^^ F541
41 | ''f""
42 | (""f""r"")
|
= help: Remove extraneous `f` prefix
Fix
37 37 | f'{{ 40 }}'
38 38 | f"{{a {{x}}"
39 39 | f"{{{{x}}}}"
40 |-""f""
40 |+"" ""
41 41 | ''f""
42 42 | (""f""r"")
43 43 |
F541.py:41:3: F541 [*] f-string without any placeholders
|
39 | f"{{{{x}}}}"
40 | ""f""
41 | ''f""
| ^^^ F541
42 | (""f""r"")
|
= help: Remove extraneous `f` prefix
Fix
38 38 | f"{{a {{x}}"
39 39 | f"{{{{x}}}}"
40 40 | ""f""
41 |-''f""
41 |+''""
42 42 | (""f""r"")
43 43 |
44 44 | # To be fixed
F541.py:42:4: F541 [*] f-string without any placeholders
|
40 | ""f""
41 | ''f""
42 | (""f""r"")
| ^^^ F541
43 |
44 | # To be fixed
|
= help: Remove extraneous `f` prefix
Fix
39 39 | f"{{{{x}}}}"
40 40 | ""f""
41 41 | ''f""
42 |-(""f""r"")
42 |+("" ""r"")
43 43 |
44 44 | # To be fixed
45 45 | # Error: f-string: single '}' is not allowed at line 41 column 8

View File

@@ -0,0 +1,44 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
5 | try:
6 | pass
7 | except ValueError as x:
| ^ F841
8 | pass
|
= help: Remove assignment to unused variable `x`
Fix
4 4 | def f():
5 5 | try:
6 6 | pass
7 |- except ValueError as x:
7 |+ except ValueError:
8 8 | pass
9 9 |
10 10 | try:
<filename>:12:26: F841 [*] Local variable `x` is assigned to but never used
|
10 | try:
11 | pass
12 | except ValueError as x:
| ^ F841
13 | pass
|
= help: Remove assignment to unused variable `x`
Fix
9 9 |
10 10 | try:
11 11 | pass
12 |- except ValueError as x:
12 |+ except ValueError:
13 13 | pass
14 14 |
15 15 | # This should resolve to the `x` in `x = 1`.

View File

@@ -0,0 +1,32 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---
<filename>:8:30: F841 [*] Local variable `x` is assigned to but never used
|
6 | try:
7 | pass
8 | except ValueError as x:
| ^ F841
9 | pass
|
= help: Remove assignment to unused variable `x`
Fix
5 5 | def f():
6 6 | try:
7 7 | pass
8 |- except ValueError as x:
8 |+ except ValueError:
9 9 | pass
10 10 |
11 11 | # This should raise an F821 error, rather than resolving to the
<filename>:13:15: F821 Undefined name `x`
|
11 | # This should raise an F821 error, rather than resolving to the
12 | # `x` in `x = 1`.
13 | print(x)
| ^ F821
|

View File

@@ -0,0 +1,24 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
5 | try:
6 | pass
7 | except ValueError as x:
| ^ F841
8 | pass
|
= help: Remove assignment to unused variable `x`
Fix
4 4 | def f():
5 5 | try:
6 6 | pass
7 |- except ValueError as x:
7 |+ except ValueError:
8 8 | pass
9 9 |
10 10 | # This should resolve to the `x` in `x = 1`.

View File

@@ -0,0 +1,44 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
5 | try:
6 | pass
7 | except ValueError as x:
| ^ F841
8 | pass
|
= help: Remove assignment to unused variable `x`
Fix
4 4 | def f():
5 5 | try:
6 6 | pass
7 |- except ValueError as x:
7 |+ except ValueError:
8 8 | pass
9 9 |
10 10 | def g():
<filename>:13:30: F841 [*] Local variable `x` is assigned to but never used
|
11 | try:
12 | pass
13 | except ValueError as x:
| ^ F841
14 | pass
|
= help: Remove assignment to unused variable `x`
Fix
10 10 | def g():
11 11 | try:
12 12 | pass
13 |- except ValueError as x:
13 |+ except ValueError:
14 14 | pass
15 15 |
16 16 | # This should resolve to the `x` in `x = 1`.

View File

@@ -0,0 +1,45 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---
<filename>:7:26: F841 [*] Local variable `x` is assigned to but never used
|
5 | try:
6 | 1 / 0
7 | except ValueError as x:
| ^ F841
8 | pass
9 | except ImportError as x:
|
= help: Remove assignment to unused variable `x`
Fix
4 4 |
5 5 | try:
6 6 | 1 / 0
7 |- except ValueError as x:
7 |+ except ValueError:
8 8 | pass
9 9 | except ImportError as x:
10 10 | pass
<filename>:9:27: F841 [*] Local variable `x` is assigned to but never used
|
7 | except ValueError as x:
8 | pass
9 | except ImportError as x:
| ^ F841
10 | pass
|
= help: Remove assignment to unused variable `x`
Fix
6 6 | 1 / 0
7 7 | except ValueError as x:
8 8 | pass
9 |- except ImportError as x:
9 |+ except ImportError:
10 10 | pass
11 11 |
12 12 | # No error here, though it should arguably be an F821 error. `x` will

View File

@@ -19,14 +19,6 @@ source: crates/ruff/src/rules/pyflakes/mod.rs
7 |+ except Exception:
8 8 | pass
9 9 |
10 10 | print(x)
<filename>:10:11: F821 Undefined name `x`
|
8 | pass
9 |
10 | print(x)
| ^ F821
|
10 10 | # No error here, though it should arguably be an F821 error. `x` will

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---

View File

@@ -0,0 +1,4 @@
---
source: crates/ruff/src/rules/pyflakes/mod.rs
---

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of the builtin `eval()` function.
/// Checks for uses of the builtin `eval()` function.
///
/// ## Why is this bad?
/// The `eval()` function is insecure as it enables arbitrary code execution.

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of `await` outside of `async` functions.
/// Checks for uses of `await` outside of `async` functions.
///
/// ## Why is this bad?
/// Using `await` outside of an `async` function is a syntax error.

View File

@@ -7,49 +7,6 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub(crate) enum EmptyStringCmpOp {
Is,
IsNot,
Eq,
NotEq,
}
impl TryFrom<&CmpOp> for EmptyStringCmpOp {
type Error = anyhow::Error;
fn try_from(value: &CmpOp) -> Result<Self, Self::Error> {
match value {
CmpOp::Is => Ok(Self::Is),
CmpOp::IsNot => Ok(Self::IsNot),
CmpOp::Eq => Ok(Self::Eq),
CmpOp::NotEq => Ok(Self::NotEq),
_ => bail!("{value:?} cannot be converted to EmptyStringCmpOp"),
}
}
}
impl EmptyStringCmpOp {
pub(crate) fn into_unary(self) -> &'static str {
match self {
Self::Is | Self::Eq => "not ",
Self::IsNot | Self::NotEq => "",
}
}
}
impl std::fmt::Display for EmptyStringCmpOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repr = match self {
Self::Is => "is",
Self::IsNot => "is not",
Self::Eq => "==",
Self::NotEq => "!=",
};
write!(f, "{repr}")
}
}
/// ## What it does
/// Checks for comparisons to empty strings.
///
@@ -83,13 +40,15 @@ pub struct CompareToEmptyString {
impl Violation for CompareToEmptyString {
#[derive_message_formats]
fn message(&self) -> String {
format!(
"`{}` can be simplified to `{}` as an empty string is falsey",
self.existing, self.replacement,
)
let CompareToEmptyString {
existing,
replacement,
} = self;
format!("`{existing}` can be simplified to `{replacement}` as an empty string is falsey",)
}
}
/// PLC1901
pub(crate) fn compare_to_empty_string(
checker: &mut Checker,
left: &Expr,
@@ -98,10 +57,12 @@ pub(crate) fn compare_to_empty_string(
) {
// Omit string comparison rules within subscripts. This is mostly commonly used within
// DataFrame and np.ndarray indexing.
for parent in checker.semantic().expr_ancestors() {
if matches!(parent, Expr::Subscript(_)) {
return;
}
if checker
.semantic()
.expr_ancestors()
.any(|parent| parent.is_subscript_expr())
{
return;
}
let mut first = true;
@@ -153,3 +114,46 @@ pub(crate) fn compare_to_empty_string(
}
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
enum EmptyStringCmpOp {
Is,
IsNot,
Eq,
NotEq,
}
impl TryFrom<&CmpOp> for EmptyStringCmpOp {
type Error = anyhow::Error;
fn try_from(value: &CmpOp) -> Result<Self, Self::Error> {
match value {
CmpOp::Is => Ok(Self::Is),
CmpOp::IsNot => Ok(Self::IsNot),
CmpOp::Eq => Ok(Self::Eq),
CmpOp::NotEq => Ok(Self::NotEq),
_ => bail!("{value:?} cannot be converted to EmptyStringCmpOp"),
}
}
}
impl EmptyStringCmpOp {
fn into_unary(self) -> &'static str {
match self {
Self::Is | Self::Eq => "not ",
Self::IsNot | Self::NotEq => "",
}
}
}
impl std::fmt::Display for EmptyStringCmpOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let repr = match self {
Self::Is => "is",
Self::IsNot => "is not",
Self::Eq => "==",
Self::NotEq => "!=",
};
write!(f, "{repr}")
}
}

View File

@@ -191,8 +191,7 @@ pub(crate) fn invalid_string_characters(locator: &Locator, range: TextRange) ->
let location = range.start() + TextSize::try_from(column).unwrap();
let range = TextRange::at(location, c.text_len());
#[allow(deprecated)]
diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::unspecified(
diagnostics.push(Diagnostic::new(rule, range).with_fix(Fix::automatic(
Edit::range_replacement(replacement.to_string(), range),
)));
}

View File

@@ -7,7 +7,7 @@ use ruff_python_ast::source_code::OneIndexed;
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of names that are declared as `global` prior to the
/// Checks for uses of names that are declared as `global` prior to the
/// relevant `global` declaration.
///
/// ## Why is this bad?

View File

@@ -6,7 +6,7 @@ use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for usages of named expressions (e.g., `a := 42`) that can be
/// Checks for uses of named expressions (e.g., `a := 42`) that can be
/// replaced by regular assignment statements (e.g., `a = 42`).
///
/// ## Why is this bad?

View File

@@ -157,8 +157,7 @@ pub(crate) fn nested_min_max(
keywords: keywords.to_owned(),
range: TextRange::default(),
});
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
checker.generator().expr(&flattened_expr),
expr.range(),
)));

View File

@@ -82,8 +82,7 @@ pub(crate) fn sys_exit_alias(checker: &mut Checker, func: &Expr) {
checker.semantic(),
)?;
let reference_edit = Edit::range_replacement(binding, func.range());
#[allow(deprecated)]
Ok(Fix::unspecified_edits(import_edit, [reference_edit]))
Ok(Fix::suggested_edits(import_edit, [reference_edit]))
});
}
checker.diagnostics.push(diagnostic);

View File

@@ -49,8 +49,7 @@ pub(crate) fn useless_import_alias(checker: &mut Checker, alias: &Alias) {
let mut diagnostic = Diagnostic::new(UselessImportAlias, alias.range());
if checker.patch(diagnostic.kind.rule()) {
#[allow(deprecated)]
diagnostic.set_fix(Fix::unspecified(Edit::range_replacement(
diagnostic.set_fix(Fix::suggested(Edit::range_replacement(
asname.to_string(),
alias.range(),
)));

View File

@@ -12,7 +12,7 @@ invalid_characters.py:15:6: PLE2510 [*] Invalid unescaped character backspace, u
|
= help: Replace with escape sequence
Suggested fix
Fix
12 12 | # (Pylint, "C0414") => Rule::UselessImportAlias,
13 13 | # (Pylint, "C3002") => Rule::UnnecessaryDirectLambdaCall,
14 14 | #foo = 'hi'

View File

@@ -12,7 +12,7 @@ invalid_characters.py:21:12: PLE2512 [*] Invalid unescaped character SUB, use "\
|
= help: Replace with escape sequence
Suggested fix
Fix
18 18 |
19 19 | cr_ok = '\\r'
20 20 |

View File

@@ -12,7 +12,7 @@ invalid_characters.py:25:16: PLE2513 [*] Invalid unescaped character ESC, use "\
|
= help: Replace with escape sequence
Suggested fix
Fix
22 22 |
23 23 | sub_ok = '\x1a'
24 24 |

View File

@@ -12,7 +12,7 @@ invalid_characters.py:34:13: PLE2515 [*] Invalid unescaped character zero-width-
|
= help: Replace with escape sequence
Suggested fix
Fix
31 31 |
32 32 | nul_ok = '\0'
33 33 |
@@ -32,7 +32,7 @@ invalid_characters.py:38:36: PLE2515 [*] Invalid unescaped character zero-width-
|
= help: Replace with escape sequence
Suggested fix
Fix
35 35 |
36 36 | zwsp_ok = '\u200b'
37 37 |
@@ -48,7 +48,7 @@ invalid_characters.py:39:60: PLE2515 [*] Invalid unescaped character zero-width-
|
= help: Replace with escape sequence
Suggested fix
Fix
36 36 | zwsp_ok = '\u200b'
37 37 |
38 38 | zwsp_after_multibyte_character = "ಫ​"
@@ -63,7 +63,7 @@ invalid_characters.py:39:61: PLE2515 [*] Invalid unescaped character zero-width-
|
= help: Replace with escape sequence
Suggested fix
Fix
36 36 | zwsp_ok = '\u200b'
37 37 |
38 38 | zwsp_after_multibyte_character = "ಫ​"

View File

@@ -126,7 +126,7 @@ pub(crate) fn remove_import_members(contents: &str, members: &[&str]) -> String
}
#[cfg(test)]
mod test {
mod tests {
use crate::rules::pyupgrade::fixes::remove_import_members;
#[test]

View File

@@ -43,6 +43,12 @@ enum Deprecation {
/// Deprecated imports may be removed in future versions of Python, and
/// should be replaced with their new equivalents.
///
/// Note that, in some cases, it may be preferable to continue importing
/// members from `typing_extensions` even after they're added to the Python
/// standard library, as `typing_extensions` can backport bugfixes and
/// optimizations from later Python versions. This rule thus avoids flagging
/// imports from `typing_extensions` in such cases.
///
/// ## Example
/// ```python
/// from collections import Sequence
@@ -139,10 +145,12 @@ const TYPING_EXTENSIONS_TO_TYPING: &[&str] = &[
"ContextManager",
"Coroutine",
"DefaultDict",
"NewType",
"TYPE_CHECKING",
"Text",
"Type",
// Introduced in Python 3.5.2, but `typing_extensions` contains backported bugfixes and
// optimizations,
// "NewType",
];
// Python 3.7+
@@ -168,11 +176,13 @@ const MYPY_EXTENSIONS_TO_TYPING_38: &[&str] = &["TypedDict"];
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING_38: &[&str] = &[
"Final",
"Literal",
"OrderedDict",
"Protocol",
"SupportsIndex",
"runtime_checkable",
// Introduced in Python 3.8, but `typing_extensions` contains backported bugfixes and
// optimizations.
// "Literal",
// "Protocol",
// "SupportsIndex",
];
// Python 3.9+
@@ -243,6 +253,8 @@ const TYPING_TO_COLLECTIONS_ABC_310: &[&str] = &["Callable"];
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[
"Concatenate",
"Literal",
"NewType",
"ParamSpecArgs",
"ParamSpecKwargs",
"TypeAlias",
@@ -258,23 +270,28 @@ const TYPING_EXTENSIONS_TO_TYPING_310: &[&str] = &[
const TYPING_EXTENSIONS_TO_TYPING_311: &[&str] = &[
"Any",
"LiteralString",
"NamedTuple",
"Never",
"NotRequired",
"Required",
"Self",
"TypedDict",
"Unpack",
"assert_never",
"assert_type",
"clear_overloads",
"dataclass_transform",
"final",
"get_overloads",
"overload",
"reveal_type",
];
// Python 3.12+
// Members of `typing_extensions` that were moved to `typing`.
const TYPING_EXTENSIONS_TO_TYPING_312: &[&str] = &[
// Introduced in Python 3.11, but `typing_extensions` backports the `frozen_default` argument,
// which was introduced in Python 3.12.
"dataclass_transform",
];
struct ImportReplacer<'a> {
stmt: &'a Stmt,
module: &'a str,
@@ -359,6 +376,9 @@ impl<'a> ImportReplacer<'a> {
if self.version >= PythonVersion::Py311 {
typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_311);
}
if self.version >= PythonVersion::Py312 {
typing_extensions_to_typing.extend(TYPING_EXTENSIONS_TO_TYPING_312);
}
if let Some(operation) = self.try_replace(&typing_extensions_to_typing, "typing") {
operations.push(operation);
}

View File

@@ -477,7 +477,7 @@ pub(crate) fn printf_string_formatting(
}
#[cfg(test)]
mod test {
mod tests {
use test_case::test_case;
use super::*;

View File

@@ -11,6 +11,22 @@ use crate::rules::ruff::rules::confusables::CONFUSABLES;
use crate::rules::ruff::rules::Context;
use crate::settings::Settings;
/// ## What it does
/// Checks for ambiguous unicode characters in strings.
///
/// ## Why is this bad?
/// The use of ambiguous unicode characters can confuse readers and cause
/// subtle bugs.
///
/// ## Example
/// ```python
/// print("Ηello, world!") # "Η" is the Greek eta (`U+0397`).
/// ```
///
/// Use instead:
/// ```python
/// print("Hello, world!") # "H" is the Latin capital H (`U+0048`).
/// ```
#[violation]
pub struct AmbiguousUnicodeCharacterString {
confusable: char,
@@ -44,6 +60,22 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterString {
}
}
/// ## What it does
/// Checks for ambiguous unicode characters in docstrings.
///
/// ## Why is this bad?
/// The use of ambiguous unicode characters can confuse readers and cause
/// subtle bugs.
///
/// ## Example
/// ```python
/// """A lovely docstring (with a `U+FF09` parenthesis."""
/// ```
///
/// Use instead:
/// ```python
/// """A lovely docstring (with no strange parentheses)."""
/// ```
#[violation]
pub struct AmbiguousUnicodeCharacterDocstring {
confusable: char,
@@ -77,6 +109,22 @@ impl AlwaysAutofixableViolation for AmbiguousUnicodeCharacterDocstring {
}
}
/// ## What it does
/// Checks for ambiguous unicode characters in comments.
///
/// ## Why is this bad?
/// The use of ambiguous unicode characters can confuse readers and cause
/// subtle bugs.
///
/// ## Example
/// ```python
/// foo() # nоqa # "о" is Cyrillic (`U+043E`)
/// ```
///
/// Use instead:
/// ```python
/// foo() # noqa # "o" is Latin (`U+006F`)
/// ```
#[violation]
pub struct AmbiguousUnicodeCharacterComment {
confusable: char,

View File

@@ -8,6 +8,36 @@ use ruff_python_ast::helpers::has_comments;
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
/// ## What it does
/// Checks for uses of the `+` operator to concatenate collections.
///
/// ## Why is this bad?
/// In Python, the `+` operator can be used to concatenate collections (e.g.,
/// `x + y` to concatenate the lists `x` and `y`).
///
/// However, collections can be concatenated more efficiently using the
/// unpacking operator (e.g., `[*x, *y]` to concatenate `x` and `y`).
///
/// Prefer the unpacking operator to concatenate collections, as it is more
/// readable and flexible. The `*` operator can unpack any iterable, whereas
/// `+` operates only on particular sequences which, in many cases, must be of
/// the same type.
///
/// ## Example
/// ```python
/// foo = [2, 3, 4]
/// bar = [1] + foo + [5, 6]
/// ```
///
/// Use instead:
/// ```python
/// foo = [2, 3, 4]
/// bar = [1, *foo, 5, 6]
/// ```
///
/// ## References
/// - [PEP 448 Additional Unpacking Generalizations](https://peps.python.org/pep-0448/)
/// - [Python docs: Sequence Types — `list`, `tuple`, `range`](https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range)
#[violation]
pub struct CollectionLiteralConcatenation {
expr: String,

View File

@@ -6,7 +6,6 @@ use rustpython_parser::ast::{self, Expr, Ranged};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Edit, Fix};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::prelude::ConversionFlag;
use ruff_python_ast::source_code::{Locator, Stylist};
use crate::autofix::codemods::CodegenStylist;
@@ -22,62 +21,36 @@ use crate::registry::AsRule;
/// f-strings support dedicated conversion flags for these types, which are
/// more succinct and idiomatic.
///
/// In the case of `str()`, it's also redundant, since `str()` is the default
/// conversion.
/// Note that, in many cases, calling `str()` within an f-string is
/// unnecessary and can be removed entirely, as the value will be converted
/// to a string automatically, the notable exception being for classes that
/// implement a custom `__format__` method.
///
/// ## Example
/// ```python
/// a = "some string"
/// f"{str(a)}"
/// f"{repr(a)}"
/// ```
///
/// Use instead:
/// ```python
/// a = "some string"
/// f"{a}"
/// f"{a!r}"
/// ```
#[violation]
pub struct ExplicitFStringTypeConversion {
operation: Operation,
}
pub struct ExplicitFStringTypeConversion;
impl AlwaysAutofixableViolation for ExplicitFStringTypeConversion {
#[derive_message_formats]
fn message(&self) -> String {
let ExplicitFStringTypeConversion { operation } = self;
match operation {
Operation::ConvertCallToConversionFlag => {
format!("Use explicit conversion flag")
}
Operation::RemoveCall => format!("Remove unnecessary `str` conversion"),
Operation::RemoveConversionFlag => format!("Remove unnecessary conversion flag"),
}
format!("Use explicit conversion flag")
}
fn autofix_title(&self) -> String {
let ExplicitFStringTypeConversion { operation } = self;
match operation {
Operation::ConvertCallToConversionFlag => {
format!("Replace with conversion flag")
}
Operation::RemoveCall => format!("Remove `str` call"),
Operation::RemoveConversionFlag => format!("Remove conversion flag"),
}
"Replace with conversion flag".to_string()
}
}
#[derive(Debug, PartialEq, Eq)]
enum Operation {
/// Ex) Convert `f"{repr(bla)}"` to `f"{bla!r}"`
ConvertCallToConversionFlag,
/// Ex) Convert `f"{bla!s}"` to `f"{bla}"`
RemoveConversionFlag,
/// Ex) Convert `f"{str(bla)}"` to `f"{bla}"`
RemoveCall,
}
/// RUF010
pub(crate) fn explicit_f_string_type_conversion(
checker: &mut Checker,
@@ -96,156 +69,50 @@ pub(crate) fn explicit_f_string_type_conversion(
.enumerate()
{
let ast::ExprFormattedValue {
value,
conversion,
format_spec,
range: _,
value, conversion, ..
} = formatted_value;
match conversion {
ConversionFlag::Ascii | ConversionFlag::Repr => {
// Nothing to do.
continue;
}
ConversionFlag::Str => {
// Skip if there's a format spec.
if format_spec.is_some() {
continue;
}
// Remove the conversion flag entirely.
// Ex) `f"{bla!s}"`
let mut diagnostic = Diagnostic::new(
ExplicitFStringTypeConversion {
operation: Operation::RemoveConversionFlag,
},
value.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
remove_conversion_flag(expr, index, checker.locator, checker.stylist)
});
}
checker.diagnostics.push(diagnostic);
}
ConversionFlag::None => {
// Replace with the appropriate conversion flag.
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
..
}) = value.as_ref() else {
continue;
};
// Can't be a conversion otherwise.
if args.len() != 1 || !keywords.is_empty() {
continue;
}
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
continue;
};
if !matches!(id.as_str(), "str" | "repr" | "ascii") {
continue;
};
if !checker.semantic().is_builtin(id) {
continue;
}
if id == "str" && format_spec.is_none() {
// Ex) `f"{str(bla)}"`
let mut diagnostic = Diagnostic::new(
ExplicitFStringTypeConversion {
operation: Operation::RemoveCall,
},
value.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
remove_conversion_call(expr, index, checker.locator, checker.stylist)
});
}
checker.diagnostics.push(diagnostic);
} else {
// Ex) `f"{repr(bla)}"`
let mut diagnostic = Diagnostic::new(
ExplicitFStringTypeConversion {
operation: Operation::ConvertCallToConversionFlag,
},
value.range(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
convert_call_to_conversion_flag(
expr,
index,
checker.locator,
checker.stylist,
)
});
}
checker.diagnostics.push(diagnostic);
}
}
// Skip if there's already a conversion flag.
if !conversion.is_none() {
continue;
}
let Expr::Call(ast::ExprCall {
func,
args,
keywords,
..
}) = value.as_ref() else {
continue;
};
// Can't be a conversion otherwise.
if args.len() != 1 || !keywords.is_empty() {
continue;
}
let Expr::Name(ast::ExprName { id, .. }) = func.as_ref() else {
continue;
};
if !matches!(id.as_str(), "str" | "repr" | "ascii") {
continue;
};
if !checker.semantic().is_builtin(id) {
continue;
}
let mut diagnostic = Diagnostic::new(ExplicitFStringTypeConversion, value.range());
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
convert_call_to_conversion_flag(expr, index, checker.locator, checker.stylist)
});
}
checker.diagnostics.push(diagnostic);
}
}
/// Generate a [`Fix`] to remove a conversion flag from a formatted expression.
fn remove_conversion_flag(
expr: &Expr,
index: usize,
locator: &Locator,
stylist: &Stylist,
) -> Result<Fix> {
// Parenthesize the expression, to support implicit concatenation.
let range = expr.range();
let content = locator.slice(range);
let parenthesized_content = format!("({content})");
let mut expression = match_expression(&parenthesized_content)?;
// Replace the formatted call expression at `index` with a conversion flag.
let formatted_string_expression = match_part(index, &mut expression)?;
formatted_string_expression.conversion = None;
// Remove the parentheses (first and last characters).
let mut content = expression.codegen_stylist(stylist);
content.remove(0);
content.pop();
Ok(Fix::automatic(Edit::range_replacement(content, range)))
}
/// Generate a [`Fix`] to remove a call from a formatted expression.
fn remove_conversion_call(
expr: &Expr,
index: usize,
locator: &Locator,
stylist: &Stylist,
) -> Result<Fix> {
// Parenthesize the expression, to support implicit concatenation.
let range = expr.range();
let content = locator.slice(range);
let parenthesized_content = format!("({content})");
let mut expression = match_expression(&parenthesized_content)?;
// Replace the formatted call expression at `index` with a conversion flag.
let formatted_string_expression = match_part(index, &mut expression)?;
let call = match_call_mut(&mut formatted_string_expression.expression)?;
formatted_string_expression.expression = call.args[0].value.clone();
// Remove the parentheses (first and last characters).
let mut content = expression.codegen_stylist(stylist);
content.remove(0);
content.pop();
Ok(Fix::automatic(Edit::range_replacement(content, range)))
}
/// Generate a [`Fix`] to replace an explicit type conversion with a conversion flag.
fn convert_call_to_conversion_flag(
expr: &Expr,

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