Compare commits

...

146 Commits

Author SHA1 Message Date
Zanie Blue
fe9590f39f Bump version number to 0.0.283 (#6407) 2023-08-08 12:31:30 -05:00
konsti
e769c74899 Check .git in formatter progress checkouts for build (#6387)
From the formatter progress CI logs:
```
2023-08-07T03:49:02.5178602Z + mkdir -p /home/runner/work/ruff/ruff/target/progress_projects
2023-08-07T03:49:02.5193474Z + '[' '!' -d /home/runner/work/ruff/ruff/target/progress_projects/build ']'
2023-08-07T03:49:02.5194228Z + '[' '!' -d /home/runner/work/ruff/ruff/target/progress_projects/django ']'
2023-08-07T03:49:02.5194966Z + git clone --filter=tree:0 https://github.com/django/django /home/runner/work/ruff/ruff/target/progress_projects/django
2023-08-07T03:49:02.5209260Z Cloning into '/home/runner/work/ruff/ruff/target/progress_projects/django'...
```
```
2023-08-07T03:51:17.4726088Z 2023-08-07T03:51:17.472404Z ERROR Failed /home/runner/work/ruff/ruff/target/progress_projects/build: no python files in ["/home/runner/work/ruff/ruff/target/progress_projects/build"]
```

Seems that build exists but is an empty cached folder. These changes
should fix this by a) checking for `.git` instead of just the folder
existing b) running the commit checkout unconditionally. The latter is
also important if we ever want to update the SHAs.
2023-08-08 17:46:46 +02:00
Dhruv Manilawala
d815a25b11 Update StmtMatch formatting snapshots (#6427) 2023-08-08 16:45:02 +02:00
Dhruv Manilawala
001aa486df Add formatting for StmtMatch (#6286)
## Summary

This PR adds support for `StmtMatch` with subs for `MatchCase`.

## Test Plan

Add a few additional test cases around `match` statement, comments, line
breaks.

resolves: #6298
2023-08-08 18:48:49 +05:30
Charlie Marsh
87984e9ac7 Expand parents whenever open-parenthesis comments are present (#6389)
## Summary

This PR modifies our dangling-open-parenthesis handling to _always_
expand the parent expression.

So, for example, given:

```python
a = int(  # type: ignore
    int(  # type: ignore
        int(  # type: ignore
            6
        )
    )
)
```

We now retain that as stable formatting, instead of truncating like:

```python
a = int(int(int(6)))  # comment  # comment  # comment
```

Note that Black _does_ collapse comments like this _unless_ they're `#
type: ignore` comments, and perhaps in some other cases, so this is an
intentional deviation
([playground](https://black.vercel.app/?version=main&state=_Td6WFoAAATm1rRGAgAhARYAAAB0L-Wj4AFEAHpdAD2IimZxl1N_WlOfrjryFgvD4ScVsKPztqdHDGJUg5knO0JCdpUfW1IrWSNmIJPx95s0hP-pRNkCQNH64-eIznIvXjeWBQ5-qax0oNw4yMOuhwr2azvMRZaEB5r8IXVPHmRCJp7fe7y4290u1zzxqK_nAi6q_5sI-jsAAAAA8HgZ9V7hG3QAAZYBxQIAAGnCHXexxGf7AgAAAAAEWVo=)).
2023-08-08 08:45:20 -04:00
Piotr
6aefe71c56 Fix name of rule in example of extend-per-file-ignores in options.rs (#6417)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

-->

## Summary

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

Fix name of rule in example of `extend-per-file-ignores` in `options.rs`
file.

It was `E401` but in configuration example `E402` was listed. Just a
tiny mismatch.

## Test Plan

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

Just by my eyes :).
2023-08-08 11:24:41 +02:00
konsti
90ba40c23c Fix zulip unstable formatting with end-of-line comments (#6386)
## Bug

Given
```python
x = () - (#
)
```
the comment is a dangling comment of the empty tuple. This is an
end-of-line comment so it may move after the expression. It still
expands the parent, so the operator breaks:
```python
x = (
    ()
    - ()  #
)
```
In the next formatting pass, the comment is not a trailing tuple but a
trailing bin op comment, so the bin op doesn't break anymore. The
comment again expands the parent, so we still add the superfluous
parentheses
```python
x = (
    () - ()  #
)
```

## Fix

The new formatting is to keep the comment on the empty tuple. This is a
log uglier and again has additional outer parentheses, but it's stable:
```python
x = (
    ()
    - (  #
    )
)
```

## Alternatives

Black formats all the examples above as
```python
x = () - ()  #
```
which i find better. 

I would be happy about any suggestions for better solutions than the
current one. I'd mainly need a workaround for expand parent having an
effect on the bin op instead of first moving the comment to the end and
then applying expand parent to the assign statement.
2023-08-08 09:15:35 +00:00
Micha Reiser
2bd345358f Simplify parenthesized formatting (#6419) 2023-08-08 08:50:57 +00:00
Dhruv Manilawala
289d1e85bf Manually parenthesize tuple expr in B014 autofix (#6415)
## Summary

Manually add the parentheses around tuple expressions for the autofix in
`B014`.
This is also done in various other autofixes as well such as for
[`RUF005`](6df5ab4098/crates/ruff/src/rules/ruff/rules/collection_literal_concatenation.rs (L183-L184)),
[`UP024`](6df5ab4098/crates/ruff/src/rules/pyupgrade/rules/os_error_alias.rs (L137-L137)).

### Alternate Solution

An alternate solution would be to fix this in the `Generator` itself by
checking
if the tuple expression needs to be generated at the top-level or not.
If so,
then always add the parentheses.

```rust
                } else if level == 0 {
                    // Top-level tuples are always parenthesized.
                    self.p("(");
                    let mut first = true;
                    for elt in elts {
                        self.p_delim(&mut first, ", ");
                        self.unparse_expr(elt, precedence::COMMA);
                    }
                    self.p_if(elts.len() == 1, ",");
                    self.p(")");
```

## Test Plan

Add a regression test for this case in `B014`.

fixes: #6412
2023-08-08 09:14:18 +05:30
Anders Kaseorg
6df5ab4098 Remove duplicate line from project structure docs (#6408)
Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-08-07 20:08:51 -04:00
Zanie Blue
90c9aa2992 Add support for simple generic type variables to UP040 (#6314)
Extends #6289 to support moving type variable usage in type aliases to
use PEP-695.

Does not remove the possibly unused type variable declaration.
Presumably this is handled by other rules, but is not working for me.

Does not handle type variables with bounds or variance declarations yet.

Part of #4617
2023-08-07 16:22:06 -05:00
Charlie Marsh
927cfc9564 Respect file-level # ruff: noqa suppressions for unused-noqa rule (#6405)
## Summary

We weren't respecting `# ruff: noqa: RUF100`, i.e., file-level
suppressions for the `unused-noqa` rule itself.

Closes https://github.com/astral-sh/ruff/issues/6385.
2023-08-07 16:33:01 -04:00
Charlie Marsh
3d06fe743d Change model: &SemanticModel to semantic: &SemanticModel (#6406)
Use the same naming conventions everywhere. See:
https://github.com/astral-sh/ruff/pull/6314/files#r1284457874.
2023-08-07 16:32:55 -04:00
Charlie Marsh
404e334fec Rename ArgumentSeparator to ParameterSeparator (#6404)
To mirror the rename from `Arguments` to `Parameters`.
2023-08-07 15:46:28 -04:00
Charlie Marsh
26098b8d91 Extend nested union detection to handle bitwise or Union expressions (#6399)
## Summary

We have some logic in the expression analyzer method to avoid
re-checking the inner `Union` in `Union[Union[...]]`, since the methods
that analyze `Union` expressions already recurse. Elsewhere, we have
logic to avoid re-checking the inner `|` in `int | (int | str)`, for the
same reason.

This PR unifies that logic into a single method _and_ ensures that, just
as we recurse over both `Union` and `|`, we also detect that we're in
_either_ kind of nested union.

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

## Test Plan

Added some new snapshots.
2023-08-07 15:17:26 -04:00
Charlie Marsh
98d4657961 Avoid attempting to fix .format(...) calls with too-few-arguments (#6401)
## Summary

We can anticipate earlier that this will error, so we should avoid
flagging the error at all. Specifically, we're talking about cases like
`"{1} {0}".format(*args)"`, in which we'd need to reorder the arguments
in order to remove the `1` and `0`, but we _can't_ reorder the arguments
since they're not statically analyzable.

Closes https://github.com/astral-sh/ruff/issues/6388.
2023-08-07 19:13:22 +00:00
Charlie Marsh
8919b6ad9a Add a with_dangling_comments to the parenthesized formatter (#6402)
See: https://github.com/astral-sh/ruff/pull/6376#discussion_r1285514328.
2023-08-07 19:12:12 +00:00
Zanie Blue
bb96647d66 Assume Python 3.8 instead of 3.10 for target version (#6397)
The target version should be the oldest supported version instead of an
arbitary version. Since 3.7 is EOL, we should use 3.8. I would like to
follow this up with more comprehensive default detection based on the
environment.
2023-08-07 13:48:06 -05:00
Charlie Marsh
df1591b3c2 Remove outdated TODO (#6400)
See: https://github.com/astral-sh/ruff/pull/6376#discussion_r1285539278.
2023-08-07 18:33:18 +00:00
Charlie Marsh
a637b8b3a3 Fixup comment handling on opening parenthesis in function definition (#6381)
## Summary

I noticed some deviations in how we treat dangling comments that hug the
opening parenthesis for function definitions.

For example, given:

```python
def f(  # first
    # second
):  # third
    ...
```

We currently format as:

```python
def f(
      # first
    # second
):  # third
    ...
```

This PR adds the proper opening-parenthesis dangling comment handling
for function parameters. Specifically, as with all other parenthesized
nodes, we now detect that dangling comment in `placement.rs` and handle
it in `parameters.rs`. We have to take some care in that file, since we
have multiple "kinds" of dangling comments, but I added a bunch of test
cases that we now format identically to Black.

## Test Plan

`cargo test`

Before:

- `zulip`: 0.99388
- `django`: 0.99784
- `warehouse`: 0.99504
- `transformers`: 0.99404
- `cpython`: 0.75913
- `typeshed`: 0.74364

After:

- `zulip`: 0.99386
- `django`: 0.99784
- `warehouse`: 0.99504
- `transformers`: 0.99404
- `cpython`: 0.75913
- `typeshed`: 0.74409

Meaningful improvement on `typeshed`, minor decrease on `zulip`.
2023-08-07 14:04:56 -04:00
Charlie Marsh
3f0eea6d87 Rename JoinedStr to FString in the AST (#6379)
## Summary

Per the proposal in https://github.com/astral-sh/ruff/discussions/6183,
this PR renames the `JoinedStr` node to `FString`.
2023-08-07 17:33:17 +00:00
Zanie Blue
999d88e773 Fix formatting of chained boolean operations (#6394)
Closes https://github.com/astral-sh/ruff/issues/6068

These commits are kind of a mess as I did some stumbling around here. 

Unrolls formatting of chained boolean operations to prevent nested
grouping which gives us Black-compatible formatting where each boolean
operation is on a new line.
2023-08-07 12:22:33 -05:00
Charlie Marsh
63ffadf0b8 Avoid omitting parentheses for trailing attributes on call expressions (#6322)
## Summary

This PR modifies our `can_omit_optional_parentheses` rules to ensure
that if we see a call followed by an attribute, we treat that as an
attribute access rather than a splittable call expression.

This in turn ensures that we wrap like:

```python
ct_match = aaaaaaaaaaact_id == self.get_content_type(
    obj=rel_obj, using=instance._state.db
)
```

For calls, but:

```python
ct_match = (
    aaaaaaaaaaact_id == self.get_content_type(obj=rel_obj, using=instance._state.db).id
)
```

For calls with trailing attribute accesses.

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

## Test Plan

Similarity index before:

- `zulip`: 0.99436
- `django`: 0.99779
- `warehouse`: 0.99504
- `transformers`: 0.99403
- `cpython`: 0.75912
- `typeshed`: 0.72293

And after:

- `zulip`: 0.99436
- `django`: 0.99780
- `warehouse`: 0.99504
- `transformers`: 0.99404
- `cpython`: 0.75913
- `typeshed`: 0.72293
2023-08-07 13:18:58 -04:00
Charlie Marsh
c439435615 Use dedicated AST nodes on MemberKind (#6374)
## Summary

This PR leverages the unified function definition node to add precise
AST node types to `MemberKind`, which is used to power our docstring
definition tracking (e.g., classes and functions, whether they're
methods or functions or nested functions and so on, whether they have a
docstring, etc.). It was painful to do this in the past because the
function variants needed to support a union anyway, but storing precise
nodes removes like a dozen panics.

No behavior changes -- purely a refactor.

## Test Plan

`cargo test`
2023-08-07 17:17:58 +00:00
Charlie Marsh
daefa74e9a Remove async AST node variants for with, for, and def (#6369)
## Summary

Per the suggestion in
https://github.com/astral-sh/ruff/discussions/6183, this PR removes
`AsyncWith`, `AsyncFor`, and `AsyncFunctionDef`, replacing them with an
`is_async` field on the non-async variants of those structs. Unlike an
interpreter, we _generally_ have identical handling for these nodes, so
separating them into distinct variants adds complexity from which we
don't really benefit. This can be seen below, where we get to remove a
_ton_ of code related to adding generic `Any*` wrappers, and a ton of
duplicate branches for these cases.

## Test Plan

`cargo test` is unchanged, apart from parser snapshots.
2023-08-07 16:36:02 +00:00
Charlie Marsh
c895252aae Remove RefEquality (#6393)
## Summary

See discussion in
https://github.com/astral-sh/ruff/pull/6351#discussion_r1284996979. We
can remove `RefEquality` entirely and instead use a text offset for
statement keys, since no two statements can start at the same text
offset.

## Test Plan

`cargo test`
2023-08-07 16:04:50 +00:00
Charlie Marsh
9328606843 Remove Statements#parent (#6392)
Discussed in
https://github.com/astral-sh/ruff/pull/6351#discussion_r1284997065.
2023-08-07 15:41:02 +00:00
Dhruv Manilawala
e4a4660925 Support help end escape command with priority (#6272)
## Summary

This PR adds support for help end escape command in the lexer.

### What are "help end escape commands"?

First, the escape commands are special IPython syntax which enhances the
functionality for the IPython REPL. There are 9 types of escape kinds
which are recognized by the tokens which are present at the start of the
command (`?`, `??`, `!`, `!!`, etc.).

Here, the help command is using either the `?` or `??` token at the
start (`?str.replace` for example). Those 2 tokens are also supported
when they're at the end of the command (`str.replace?`), but the other
tokens aren't supported in that position.

There are mainly two types of help end escape commands:
1. Ending with either `?` or `??`, but it also starts with one of the
escape tokens (`%matplotlib?`)
2. On the other hand, there's a stricter version for (1) which doesn't
start with any escape tokens (`str.replace?`)

This PR adds support for (1) while (2) will be supported in the parser.

### Priority

Now, if the command starts and ends with an escape token, how do we
decide the kind of this command? This is where priority comes into
picture. This is simple as there's only one priority where `?`/`??` at
the end takes priority over any other escape token and all of the other
tokens are at the same priority. Remember that only `?`/`??` at the end
is considered valid.

This is mainly useful in the case where someone would want to invoke the
help command on the magic command itself. For example, in `%matplotlib?`
the help command takes priority which means that we want help for the
`matplotlib` magic function instead of calling the magic function
itself.

### Specification

Here's where things get a bit tricky. What if there are question mark
tokens at both ends. How do we decide if it's `Help` (`?`) kind or
`Help2` (`??`) kind?

|     | Magic       | Value     | Kind    |
| --- | ---         | ---       | ---     |
| 1   | `?foo?`     | `foo`     | `Help`  |
| 2   | `??foo?`    | `foo`     | `Help`  |
| 3   | `?foo??`    | `foo`     | `Help2` |
| 4   | `??foo??`   | `foo`     | `Help2` |
| 5   | `???foo??`  | `foo`     | `Help2` |
| 6   | `??foo???`  | `foo???`  | `Help2` |
| 7   | `???foo???` | `?foo???` | `Help2` |

Looking at the above table:

- The question mark tokens on the right takes priority over the ones on
the left but only if the number of question mark on the right is 1 or 2.
- If there are more than 2 question mark tokens on the right side, then
the left side is used to determine the same.
- If the right side is used to determine the kind, then all of the
question marks and whitespaces on the left side are ignored in the
`value`, but if it’s the other way around, then all of the extra
question marks are part of the `value`.

### References

- IPython implementation using the regex:
292e3a2345/IPython/core/inputtransformer2.py (L454-L462)
- Priorities:
292e3a2345/IPython/core/inputtransformer2.py (L466-L469)

## Test Plan

Add a bunch of test cases for the lexer and verify that it matches the
behavior of
IPython transformer.

resolves: #6357
2023-08-07 21:01:02 +05:30
Charlie Marsh
b21abe0a57 Use separate structs for expression and statement tracking (#6351)
## Summary

This PR fixes the performance degradation introduced in
https://github.com/astral-sh/ruff/pull/6345. Instead of using the
generic `Nodes` structs, we now use separate `Statement` and
`Expression` structs. Importantly, we can avoid tracking a bunch of
state for expressions that we need for parents: we don't need to track
reference-to-ID pointers (we just have no use-case for this -- I'd
actually like to remove this from statements too, but we need it for
branch detection right now), we don't need to track depth, etc.

In my testing, this entirely removes the regression on all-rules, and
gets us down to 2ms slower on the default rules (as a crude hyperfine
benchmark, so this is within margin of error IMO).

No behavioral changes.
2023-08-07 15:27:42 +00:00
Charlie Marsh
61d3977f95 Make the statement vector private on SemanticModel (#6348)
## Summary

Instead, expose these as methods, now that we can use a reasonable
nomenclature on the API.
2023-08-07 15:02:14 +00:00
Charlie Marsh
bae87fa016 Rename semantic model methods to use current_* prefix (#6347)
## Summary

This PR attempts to draw a clearer divide between "methods that take
(e.g.) an expression or statement as input" and "methods that rely on
the _current_ expression or statement" in the semantic model, by
renaming methods like `stmt()` to `current_statement()`.

This had led to confusion in the past. For example, prior to this PR, we
had `scope()` (which returns the current scope), and `parent_scope`,
which returns the parent _of a scope that's passed in_. Now, the API is
clearer: `current_scope` returns the current scope, and `parent_scope`
takes a scope as argument and returns its parent.

Per above, I also changed `stmt` to `statement` and `expr` to
`expression`.
2023-08-07 14:44:49 +00:00
Charlie Marsh
b763973357 Avoid hard line break after dangling open-parenthesis comments (#6380)
## Summary

Given:

```python
[  # comment
    first,
    second,
    third
]  # another comment
```

We were adding a hard line break as part of the formatting of `#
comment`, which led to the following formatting:

```python
[first, second, third]  # comment
  # another comment
```

Closes https://github.com/astral-sh/ruff/issues/6367.
2023-08-07 14:15:32 +00:00
Charlie Marsh
63692b3798 Use parenthesized_with_dangling_comments in arguments formatter (#6376)
## Summary

Fixes an instability whereby this:

```python
def get_recent_deployments(threshold_days: int) -> Set[str]:
    # Returns a list of deployments not older than threshold days
    # including `/root/zulip` directory if it exists.
    recent = set()
    threshold_date = datetime.datetime.now() - datetime.timedelta(  # noqa: DTZ005
        days=threshold_days
    )
```

Was being formatted as:

```python
def get_recent_deployments(threshold_days: int) -> Set[str]:
    # Returns a list of deployments not older than threshold days
    # including `/root/zulip` directory if it exists.
    recent = set()
    threshold_date = (
        datetime.datetime.now()
        - datetime.timedelta(days=threshold_days)  # noqa: DTZ005
    )
```

Which was in turn being formatted as:

```python
def get_recent_deployments(threshold_days: int) -> Set[str]:
    # Returns a list of deployments not older than threshold days
    # including `/root/zulip` directory if it exists.
    recent = set()
    threshold_date = (
        datetime.datetime.now() - datetime.timedelta(days=threshold_days)  # noqa: DTZ005
    )
```

The second-to-third formattings still differs from Black because we
aren't taking the line suffix into account when splitting
(https://github.com/astral-sh/ruff/issues/6377), but the first
formatting is correct and should be unchanged (i.e., the first-to-second
formattings is incorrect, and fixed here).

## Test Plan

`cargo run --bin ruff_dev -- format-dev --stability-check ../zulip`
2023-08-07 09:43:57 -04:00
Charlie Marsh
89e4e038b0 Store expression hierarchy in semantic model snapshots (#6345)
## Summary

When we iterate over the AST for analysis, we often process nodes in a
"deferred" manner. For example, if we're analyzing a function, we push
the function body onto a deferred stack, along with a snapshot of the
current semantic model state. Later, when we analyze the body, we
restore the semantic model state from the snapshot. This ensures that we
know the correct scope, hierarchy of statement parents, etc., when we go
to analyze the function body.

Historically, we _haven't_ included the _expression_ hierarchy in the
model snapshot -- so we track the current expression parents in the
visitor, but we never save and restore them when processing deferred
nodes. This can lead to subtle bugs, in that methods like
`expr_parent()` aren't guaranteed to be correct, if you're in a deferred
visitor.

This PR migrates expression tracking to mirror statement tracking
exactly. So we push all expressions onto an `IndexVec`, and include the
current expression on the snapshot. This ensures that `expr_parent()`
and related methods are "always correct" rather than "sometimes
correct".

There's a performance cost here, both at runtime and in terms of memory
consumption (we now store an additional pointer for every expression).
In my hyperfine testing, it's about a 1% performance decrease for
all-rules on CPython (up to 533.8ms, from 528.3ms) and a 4% performance
decrease for default-rules on CPython (up to 212ms, from 204ms).
However... I think this is worth it given the incorrectness of our
current approach. In the future, we may want to reconsider how we do
these upward traversals (e.g., with something like a red-green tree).
(**Note**: in https://github.com/astral-sh/ruff/pull/6351, the slowdown
seems to be entirely removed.)
2023-08-07 09:42:04 -04:00
Tom Kuson
5d2a4ebc99 Add documentation to subprocess-with[out]-shell-equals-true rules (#6373) 2023-08-07 03:48:36 +00:00
Harutaka Kawamura
9c3fbcdf4a Add PT011 and PT012 docs (#6362) 2023-08-06 21:28:24 -04:00
Konrad Listwan-Ciesielski
61532e8aad Add DTZ003 and DTZ004 docs (#6223)
Changes:
- Fixes typo and repeated phrase in `DTZ002`
- Adds docs for `DTZ003`
- Adds docs for `DTZ004`
- Adds example for <=Python3.10 in `DTZ001`

Related to: https://github.com/astral-sh/ruff/issues/2646
2023-08-07 01:21:14 +00:00
Charlie Marsh
9171e97d15 Avoid allocation in no-signature (#6375) 2023-08-06 15:27:56 +00:00
Charlie Marsh
a5a29bb8d6 Revert change to require_git(false) in WalkBuilder (#6368)
## Summary

This was changed to fix https://github.com/astral-sh/ruff/issues/5930
(respect `.gitignore` for unzipped source repositories), but led to
undesirable behavior whereby `.gitignore` files in parent directories
are respected regardless of whether you're working in a child git
repository (see: https://github.com/astral-sh/ruff/issues/6335). The
latter is a bigger problem than the former is an important use-case to
support, so pragmatically erring on the side of a revert.

Closes https://github.com/astral-sh/ruff/issues/6335.
2023-08-05 19:45:50 +00:00
Zixuan Li
be657f5e7e Respect typing_extensions imports of Annotated for B006. (#6361)
`typing_extensions.Annotated` should be treated the same way as
`typing.Annotated`.
2023-08-05 17:39:52 +00:00
Charlie Marsh
76148ddb76 Store call paths rather than stringified names (#6102)
## Summary

Historically, we've stored "qualified names" on our
`BindingKind::Import`, `BindingKind::SubmoduleImport`, and
`BindingKind::ImportFrom` structs. In Ruff, a "qualified name" is a
dot-separated path to a symbol. For example, given `import foo.bar`, the
"qualified name" would be `"foo.bar"`; and given `from foo.bar import
baz`, the "qualified name" would be `foo.bar.baz`.

This PR modifies the `BindingKind` structs to instead store _call paths_
rather than qualified names. So in the examples above, we'd store
`["foo", "bar"]` and `["foo", "bar", "baz"]`. It turns out that this
more efficient given our data access patterns. Namely, we frequently
need to convert the qualified name to a call path (whenever we call
`resolve_call_path`), and it turns out that we do this operation enough
that those conversations show up on benchmarks.

There are a few other advantages to using call paths, rather than
qualified names:

1. The size of `BindingKind` is reduced from 32 to 24 bytes, since we no
longer need to store a `String` (only a boxed slice).
2. All three import types are more consistent, since they now all store
a boxed slice, rather than some storing an `&str` and some storing a
`String` (for `BindingKind::ImportFrom`, we needed to allocate a
`String` to create the qualified name, but the call path is a slice of
static elements that don't require that allocation).
3. A lot of code gets simpler, in part because we now do call path
resolution "earlier". Most notably, for relative imports (`from .foo
import bar`), we store the _resolved_ call path rather than the relative
call path, so the semantic model doesn't have to deal with that
resolution. (See that `resolve_call_path` is simpler, fewer branches,
etc.)

In my testing, this change improves the all-rules benchmark by another
4-5% on top of the improvements mentioned in #6047.
2023-08-05 15:21:50 +00:00
Harutaka Kawamura
501f537cb8 Avoid auto-fixing UP031 if there are comments within the right-hand side (#6364) 2023-08-05 11:14:29 -04:00
Dhruv Manilawala
1ac2699b5e Update F841 autofix to not remove line magic expr (#6141)
## Summary

Update `F841` autofix to not remove line magic expr

## Test Plan

Added test case for assignment statement with and without type
annotation

fixes: #6116
2023-08-05 00:45:01 +00:00
Dhruv Manilawala
32fa05765a Use Jupyter mode while parsing Notebook files (#5552)
## Summary

Enable using the new `Mode::Jupyter` for the tokenizer/parser to parse
Jupyter line magic tokens.

The individual call to the lexer i.e., `lex_starts_at` done by various
rules should consider the context of the source code (is this content
from a Jupyter Notebook?). Thus, a new field `source_type` (of type
`PySourceType`) is added to `Checker` which is being passed around as an
argument to the relevant functions. This is then used to determine the
`Mode` for the lexer.

## Test Plan

Add new test cases to make sure that the magic statement is considered
while generating the diagnostic and autofix:
* For `I001`, if there's a magic statement in between two import blocks,
they should be sorted independently

fixes: #6090
2023-08-05 00:32:07 +00:00
Charlie Marsh
d788957ec4 Allow capitalized names for logger candidate heuristic match (#6356)
Closes https://github.com/astral-sh/ruff/issues/6353.
2023-08-04 23:25:34 +00:00
Victor Hugo Gomes
78a370303b [flake8-pyi] Add tests cases for bad imports from PYI027 to PYI022 (UP035) (#6354)
## Summary
As of version
[23.1.0](2a86db8271/CHANGELOG.md (L158-L160)),
`flake8-pyi` remove the rule `Y027`.

The errors that resulted in `PYI027` are now being emitted by `PYI022`
(`UP035`).

ref: #848 

## Test Plan

Add new tests cases.
2023-08-04 19:00:33 -04:00
Charlie Marsh
5e73345a1c Avoid panic with positional-only arguments in PYI019 (#6350)
## Summary

Previously, failed on methods like:

```python
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ...  # PYI019
```

Since we check if there are any positional-only or non-positional
arguments, but then do an unsafe access on `parameters.args`.

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

## Test Plan

`cargo test` (verified that `main` panics on the new fixtures)
2023-08-04 18:37:07 +00:00
Charlie Marsh
b8fd69311c Remove ruff_python_ast prefix in fixes.rs (#6346) 2023-08-04 16:48:20 +00:00
Charlie Marsh
fa5c9cced9 Ignore same-line docstrings for lines-before and lines-after rules (#6344)
These rules assume that the docstring is on its own line. pydocstyle
treats them inconsistently, so I'm just going to disable them in this
case.

Closes https://github.com/astral-sh/ruff/issues/6329.
2023-08-04 16:08:36 +00:00
Harutaka Kawamura
08dd87e04d Avoid auto-fixing UP032 if comments are present around format call arguments (#6342) 2023-08-04 15:37:23 +00:00
konsti
9bb21283ca More similarity index digits (#6343)
**Summary** We were at similarity index 0.998 for django, we need more
decimal places, now we're at 0.99779.

**Test Plan** n/a
2023-08-04 17:12:33 +02:00
Charlie Marsh
4d47dfd6c0 Tweak breaking groups for comprehensions (#6321)
## Summary

Fixes some comprehension formatting by avoiding creating the group for
the comprehension itself (so that if it breaks, all parts break on their
own lines, e.g. the `for` and the `if` clauses).

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

## Test Plan

Bunch of new fixtures.
2023-08-04 14:00:54 +00:00
konsti
99baad12d8 Call chain formatting in fluent style (#6151)
Implement fluent style/call chains. See the `call_chains.py` formatting
for examples.

This isn't fully like black because in `raise A from B` they allow `A`
breaking can influence the formatting of `B` even if it is already
multiline.

Similarity index:

| project      | main  | PR    |
|--------------|-------|-------|
| build        | ???   | 0.753 |
| django       | 0.991 | 0.998 |
| transformers | 0.993 | 0.994 |
| typeshed     | 0.723 | 0.723 |
| warehouse    | 0.978 | 0.994 |
| zulip        | 0.992 | 0.994 |

Call chain formatting is affected by
https://github.com/astral-sh/ruff/issues/627, but i'm cutting scope
here.

Closes #5343

**Test Plan**:
 * Added a dedicated call chains test file
 * The ecosystem checks found some bugs
 * I manually check django and zulip formatting

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-04 13:58:01 +00:00
Charlie Marsh
35bdbe43a8 Flag comparison-with-itself on builtin calls (#6324)
## Summary

Extends `comparison-with-itself` to cover simple function calls on
known-pure functions, like `id`. For example, we now flag `id(x) ==
id(x)`.

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

## Test Plan

`cargo test`
2023-08-04 09:51:41 -04:00
Charlie Marsh
3a985dd71e Rename CommentPlacement#then_with to or_else (#6341)
Per nits in the PR.
2023-08-04 13:50:57 +00:00
Charlie Marsh
1e3fe67ca5 Refactor and rename skip_trailing_trivia (#6312)
Based on feedback here:
https://github.com/astral-sh/ruff/pull/6274#discussion_r1282747964.
2023-08-04 13:30:53 +00:00
Charlie Marsh
38a96c88c1 Add missing enable check for bad-string-format-character (#6340) 2023-08-04 13:27:53 +00:00
Micha Reiser
f4831d5a26 Formatter comment handling nits (#6339) 2023-08-04 13:22:16 +00:00
konsti
1031bb6550 Formatter: Add SourceType to context to enable special formatting for stub files (#6331)
**Summary** This adds the information whether we're in a .py python
source file or in a .pyi stub file to enable people working on #5822 and
related issues.

I'm not completely happy with `Default` for something that depends on
the input.

**Test Plan** None, this is currently unused, i'm leaving this to first
implementation of stub file specific formatting.

---------

Co-authored-by: Micha Reiser <micha@reiser.io>
2023-08-04 11:52:26 +00:00
David Szotten
fe97a2a302 Fix panic with empty attribute inner comment (#6332)
Fixes https://github.com/astral-sh/ruff/issues/6181
2023-08-04 11:59:55 +02:00
konsti
a48d16e025 Replace Formatter<PyFormatContext<'_>> with PyFormatter (#6330)
This is a refactoring to use the type alias in more places. In the
process, I had to fix and run generate.py. There are no functional
changes.
2023-08-04 10:48:58 +02:00
Charlie Marsh
8a5bc93fdd Make the Nodes vector generic on node type (#6328) 2023-08-04 03:57:15 +00:00
Charlie Marsh
6da527170f Match left-hand side types() call in types-comparison (#6326)
Follow-up to https://github.com/astral-sh/ruff/pull/6325, to avoid false
positives in cases like:

```python
if x == int:
    ...
```

Which is valid, since we don't know that we're comparing the type _of_
something -- we're comparing the type objects directly.
2023-08-03 23:01:23 -04:00
Charlie Marsh
8cddb6c08d Include comparisons to builtin types in type-comparison rule (#6325)
## Summary

Extends `type-comparison` to flag:

```python
if type(obj) is int:
    pass
```

In addition to the existing cases, like:

```python
if type(obj) is type(1):
    pass
```

Closes https://github.com/astral-sh/ruff/issues/6260.
2023-08-04 02:25:19 +00:00
Victor Hugo Gomes
b8ca220eeb [flake8-pyi] Implement PYI055 (#6316) 2023-08-04 01:36:00 +00:00
Charlie Marsh
1d8759d5df Generalize comment-after-bracket handling to lists, sets, etc. (#6320)
## Summary

We already support preserving the end-of-line comment in calls and type
parameters, as in:

```python
foo(  # comment
    bar,
)
```

This PR adds the same behavior for lists, sets, comprehensions, etc.,
such that we preserve:

```python
[  # comment
    1,
    2,
    3,
]
```

And related cases.
2023-08-04 01:28:05 +00:00
Charlie Marsh
d3aa8b4ee0 Add API to chain comment placement operations (#6319)
## Summary

This PR adds an API for chaining comment placement methods based on the
[`then_with`](https://doc.rust-lang.org/std/cmp/enum.Ordering.html#method.then_with)
from `Ordering` in the standard library.

For example, you can now do:

```rust
try_some_case(comment).then_with(|comment| try_some_other_case_if_still_default(comment))
```

This lets us avoid this kind of pattern, which I've seen in
`placement.rs` and used myself before:

```rust
let comment = match handle_own_line_comment_between_branches(comment, preceding, locator) {
    CommentPlacement::Default(comment) => comment,
    placement => return placement,
};
```
2023-08-03 21:08:50 -04:00
Zanie Blue
9ae498595c Upgrade Rust to 1.71 (#6323)
Addresses
[CVE-2023-38497](https://blog.rust-lang.org/2023/08/03/cve-2023-38497.html)

See also the [version release
post](https://blog.rust-lang.org/2023/08/03/Rust-1.71.1.html)
2023-08-03 21:08:39 -04:00
Charlie Marsh
5f225b18ab Generalize bracketed end-of-line comment handling (#6315)
Micha suggested this in
https://github.com/astral-sh/ruff/pull/6274#discussion_r1282774151, and
it allows us to unify the implementations for arguments and type params.
2023-08-03 20:51:03 +00:00
Charlie Marsh
8276b26480 Omit formatter PRs from releases by default (#6317)
I end up removing these manually every time, seems easier to just omit
them for now.
2023-08-03 20:45:50 +00:00
Charlie Marsh
1705fcef36 Mark trailing comments in parenthesized tests (#6287)
## Summary

This ensures that we treat `# comment` as parenthesized in contexts
like:

```python
while (
    True
    # comment
):
    pass
```

The same logic applies equally to `for`, `async for`, `if`, `with`, and
`async with`. The general pattern is that you have an expression which
precedes a colon-separated suite.
2023-08-03 20:45:03 +00:00
konsti
51ff98f9e9 Make formatter ecosystem check failure output better understandable (#6300)
**Summary** Prompted by
https://github.com/astral-sh/ruff/pull/6257#issuecomment-1661308410, it
tried to make the ecosystem script output on failure better
understandable. All log messages are now written to a file, which is
printed on error. Running locally progress is still shown.

Looking through the log output i saw that we currently log syntax errors
in input, which is confusing because they aren't actual errors, but we
don't check that these files don't change due to parser regressions or
improvements. I added `--files-with-errors` to catch that.

**Test Plan** CI
2023-08-03 20:23:25 +02:00
Charlie Marsh
b3f3529499 Improve comments around Arguments handling in classes (#6310)
## Summary

Based on the confusion here:
https://github.com/astral-sh/ruff/pull/6274#discussion_r1282754515.

I looked into moving this logic into `placement.rs`, but I think it's
trickier than it may appear.
2023-08-03 12:34:03 -04:00
Charlie Marsh
2fa508793f Return a slice in StmtClassDef#bases (#6311)
Slices are strictly more flexible, since you can always convert to an
iterator, etc., but not the other way around. Suggested in
https://github.com/astral-sh/ruff/pull/6259#discussion_r1282730994.
2023-08-03 16:21:55 +00:00
Zanie Blue
718e3945e3 Add rule to upgrade type alias annotations to keyword (UP040) (#6289)
Adds rule to convert type aliases defined with annotations i.e. `x:
TypeAlias = int` to the new PEP-695 syntax e.g. `type x = int`.

Does not support using new generic syntax for type variables, will be
addressed in a follow-up.
Added as part of pyupgrade — ~the code 100 as chosen to avoid collision
with real pyupgrade codes~.

Part of #4617 
Builds on #5062
2023-08-03 16:13:06 +00:00
Charlie Marsh
c75e8a8dab Move ExprCall's NeedsParentheses impl into expr_call.rs (#6309)
Accidental move.
2023-08-03 16:01:01 +00:00
Harutaka Kawamura
74e734e962 More precise invalid expression check for UP032 (#6308) 2023-08-03 15:49:02 +00:00
Zanie Blue
0e18abcf95 Add is_ and is_not to excluded functions for FBT003 (#6307)
These methods are commonly used in SQLAlchemy.

See https://github.com/astral-sh/ruff/discussions/6302
2023-08-03 10:41:45 -05:00
Anders Kaseorg
7c8bcede5b Broaden appropriate flake8-pyi rules to check non-stub code too (#6297)
Of the rules that flake8-pyi enforces for `.pyi` type stubs, many of
them equally make sense to check in normal runtime code with type
annotations. Broaden these rules to check all files:

PYI013 ellipsis-in-non-empty-class-body
PYI016 duplicate-union-member
PYI018 unused-private-type-var
PYI019 custom-type-var-return-type
PYI024 collections-named-tuple
PYI025 unaliased-collections-abc-set-import
PYI030 unnecessary-literal-union
PYI032 any-eq-ne-annotation
PYI034 non-self-return-type
PYI036 bad-exit-annotation
PYI041 redundant-numeric-union
PYI042 snake-case-type-alias
PYI043 t-suffixed-type-alias
PYI045 iter-method-return-iterable
PYI046 unused-private-protocol
PYI047 unused-private-type-alias
PYI049 unused-private-typed-dict
PYI050 no-return-argument-annotation-in-stub (Python ≥ 3.11)
PYI051 redundant-literal-union
PYI056 unsupported-method-call-on-all

The other rules are stub-specific and remain enabled only in `.pyi`
files.

PYI001 unprefixed-type-param
PYI002 complex-if-statement-in-stub
PYI003 unrecognized-version-info-check
PYI004 patch-version-comparison
PYI005 wrong-tuple-length-version-comparison (could make sense to
broaden, see
https://github.com/astral-sh/ruff/pull/6297#issuecomment-1663314807)
PYI006 bad-version-info-comparison (same)
PYI007 unrecognized-platform-check
PYI008 unrecognized-platform-name
PYI009 pass-statement-stub-body
PYI010 non-empty-stub-body
PYI011 typed-argument-default-in-stub
PYI012 pass-in-class-body
PYI014 argument-default-in-stub
PYI015 assignment-default-in-stub
PYI017 complex-assignment-in-stub
PYI020 quoted-annotation-in-stub
PYI021 docstring-in-stub
PYI026 type-alias-without-annotation (could make sense to broaden, but
gives many false positives on runtime code as currently implemented)
PYI029 str-or-repr-defined-in-stub
PYI033 type-comment-in-stub
PYI035 unassigned-special-variable-in-stub
PYI044 future-annotations-in-stub
PYI048 stub-body-multiple-statements
PYI052 unannotated-assignment-in-stub
PYI053 string-or-bytes-too-long
PYI054 numeric-literal-too-long

Signed-off-by: Anders Kaseorg <andersk@mit.edu>
2023-08-03 11:40:42 -04:00
Harutaka Kawamura
30c2e9430e Update UP032 to support await expressions (#6304)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

In Python >= 3.7, `await` can be included in f-strings. 

https://bugs.python.org/issue28942

## Test Plan

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

Existing tests
2023-08-03 09:53:36 -05:00
Harutaka Kawamura
b6f0316d55 Add PT013 and PT015 docs (#6303)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

#2646

## Test Plan

<!-- How was it tested? -->
2023-08-03 09:51:52 -05:00
Charlie Marsh
9f3567dea6 Use range: _ in lieu of range: _range (#6296)
## Summary

`range: _range` is slightly inconvenient because you can't use it
multiple times within a single match, unlike `_`.
2023-08-02 22:11:13 -04:00
Charlie Marsh
9e2bbf4beb Add a simple tooltip to the sidebar (#6295)
## Summary

Not perfect, but IMO helpful:

<img width="1792" alt="Screen Shot 2023-08-02 at 9 29 24 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/e613e918-75cb-475e-9ea4-f833d1a0b5f6">

<img width="1792" alt="Screen Shot 2023-08-02 at 9 29 20 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/bb3efdfe-40e1-45b5-b774-082521b2d214">
2023-08-03 01:41:07 +00:00
Charlie Marsh
d7627c398c Add an icon for FIR (#6292)
It's not a very _good_ icon, but I prefer the consistency. I'm also
going to add tooltips to these.
2023-08-03 01:20:00 +00:00
Charlie Marsh
23e527e386 Increase icon opacity on-hover (#6291)
## Summary

Makes it clearer that these are clickable.
2023-08-03 01:05:38 +00:00
Charlie Marsh
a15b0a9102 Tweak background on theme button (#6290)
## Summary

It's now white on-hover as opposed to yellow, to match the copy button:

<img width="1792" alt="Screen Shot 2023-08-02 at 8 52 10 PM"
src="https://github.com/astral-sh/ruff/assets/1309177/96d5cbf9-ef33-4fba-8888-f2a4af9a6ec4">
2023-08-03 01:00:37 +00:00
qdegraaf
d40597a266 [flake8-pyi] Implement custom_type_var_return_type (PYI019) (#6204)
## Summary

Implements `Y019` from
[flake8-pyi](https://github.com/PyCQA/flake8-pyi).

The rule checks if

-  instance methods that return `self` 
-  class methods that return an instance of `cls`
- `__new__` methods

Return a custom `TypeVar` instead of `typing.Self` and raises a
violation if this is the case. The rule also covers
[PEP-695](https://peps.python.org/pep-0695/) syntax as introduced in
upstream in https://github.com/PyCQA/flake8-pyi/pull/402

## Test Plan

Added fixtures with test cases from upstream implementation (plus
additional one for an excluded edge case, mentioned in upstream
implementation)
2023-08-03 00:42:42 +00:00
Silvano Cerza
82410524d9 [pylint] Implement Pylint bad-format-character (E1300) (#6171)
## Summary

Relates to #970.

Add new `bad-format-character` Pylint rule.

I had to make a change in `crates/ruff_python_literal/src/format.rs` to
get a more detailed error in case the format character is not correct. I
chose to do this since most of the format spec parsing functions are
private. It would have required me reimplementing most of the parsing
logic just to know if the format char was correct.

This PR also doesn't reflect current Pylint functionality in two ways.

It supports new format strings correctly, Pylint as of now doesn't. See
pylint-dev/pylint#6085.

In case there are multiple adjacent string literals delimited by
whitespace the index of the wrong format char will relative to the
single string. Pylint will instead reported it relative to the
concatenated string.

Given this:
```
"%s" "%z" % ("hello", "world")
```

Ruff will report this:
```Unsupported format character 'z' (0x7a) at index 1```

Pylint instead:
```Unsupported format character 'z' (0x7a) at index 3```

I believe it's more sensible to report the index relative to the
individual string.

## Test Plan

Added new snapshot and a small test in
`crates/ruff_python_literal/src/format.rs`.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-08-02 21:32:43 +00:00
Zanie Blue
5b2e973fa5 Add formatting of type alias statements (#6162)
Part of #5062 
Extends https://github.com/astral-sh/ruff/pull/6161
Closes #5929
2023-08-02 20:40:32 +00:00
Zanie Blue
1a60d1e3c6 Add formatting of type parameters in class and function definitions (#6161)
Part of #5062 
Closes https://github.com/astral-sh/ruff/issues/5931

Implements formatting of a sequence of type parameters in a dedicated
struct for reuse by classes, functions, and type aliases (preparing for
#5929). Adds formatting of type parameters in class and function
definitions — previously, they were just elided.
2023-08-02 20:29:28 +00:00
Charlie Marsh
9425ed72a0 Break global and nonlocal statements over continuation lines (#6172)
## Summary

Builds on #6170 to break `global` and `nonlocal` statements, such that
we get:

```python
def f():
    global \
        analyze_featuremap_layer, \
        analyze_featuremapcompression_layer, \
        analyze_latencies_post, \
        analyze_motions_layer, \
        analyze_size_model
```

Instead of:

```python
def f():
    global analyze_featuremap_layer, analyze_featuremapcompression_layer, analyze_latencies_post, analyze_motions_layer, analyze_size_model
```

Notably, we avoid applying this formatting if the statement ends in a
comment. Otherwise, the comment would _need_ to be placed after the last
item, like:

```python
def f():
    global \
        analyze_featuremap_layer, \
        analyze_featuremapcompression_layer, \
        analyze_latencies_post, \
        analyze_motions_layer, \
        analyze_size_model  # noqa
```

To me, this seems wrong (and would break the `# noqa` comment). Ideally,
the items would be parenthesized, and the comment would be on the inner
parenthesis, like:

```python
def f():
    global (  # noqa
        analyze_featuremap_layer,
        analyze_featuremapcompression_layer,
        analyze_latencies_post,
        analyze_motions_layer,
        analyze_size_model
    )
```

But that's not valid syntax.
2023-08-02 19:55:00 +00:00
Victor Hugo Gomes
9f38dbd06e [flake8-pyi] Implement PYI051 (#6215)
## Summary
Checks for the presence of redundant `Literal` types and builtin super
types in an union. See [original
source](2a86db8271/pyi.py (L1261)).

This implementation has a couple of differences from the original. The
first one is, we support the `complex` and `float` builtin types. The
second is, when reporting diagnostic for a `Literal` with multiple
members of the same type, we print the entire `Literal` while `flak8`
only prints the `Literal` with its first member.
For example:
```python
from typing import Literal

x: Literal[1, 2] | int
```  
Ruff will show `Literal[1, 2]` while flake8 only shows `Literal[1]`.

```shell
$ ruff crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:4:18: PYI051 `Literal["foo"]` is redundant in an union with `str`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:5:37: PYI051 `Literal[b"bar", b"foo"]` is redundant in an union with `bytes`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:6:37: PYI051 `Literal[5]` is redundant in an union with `int`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:6:67: PYI051 `Literal["foo"]` is redundant in an union with `str`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:7:37: PYI051 `Literal[b"str_bytes"]` is redundant in an union with `bytes`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:7:51: PYI051 `Literal[42]` is redundant in an union with `int`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:9:31: PYI051 `Literal[1J]` is redundant in an union with `complex`
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:9:53: PYI051 `Literal[3.14]` is redundant in an union with `float`
Found 8 errors.
```

```shell
$ flake8 crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:4:18: Y051 "Literal['foo']" is redundant in a union with "str"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:5:37: Y051 "Literal[b'bar']" is redundant in a union with "bytes"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:6:37: Y051 "Literal[5]" is redundant in a unionwith "int"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:6:67: Y051 "Literal['foo']" is redundant in a union with "str"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:7:37: Y051 "Literal[b'str_bytes']" is redundantin a union with "bytes"
crates/ruff/resources/test/fixtures/flake8_pyi/PYI051.pyi:7:51: Y051 "Literal[42]" is redundant in a union with "int"
```

While implementing this rule, I found a bug in the `is_unchecked_union`
check. This is the new check.


1ab86bad35/crates/ruff/src/checkers/ast/analyze/expression.rs (L85-L102)

The purpose of the check was to prevent rules from navigating through
nested `Union`s, as they already handle nested `Union`s. The way it was
implemented, this was not happening, the rules were getting executed
more than one time and sometimes were receiving expressions that were
not `Union`. For example, with the following code:
 ```python
  typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
 ```

The rules were receiving the expressions in the following order:
- `typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]`
     - `Literal[5]`
     - `typing.Union[Literal["foo"], str]]`

This was causing `PYI030` to report redundant information, for example:
 ```python
typing.Union[Literal[5], int, typing.Union[Literal["foo"],
Literal["bar"]]]
 ```
This is the `PYI030` output for this code:
```shell
PYI030 Multiple literal members in a union. Use a single literal, e.g. `Literal[5, "foo", "bar"]`
YI030 Multiple literal members in a union. Use a single literal, e.g.`Literal[5, "foo"]`
```

If I haven't misinterpreted the rule, that looks incorrect. I didn't
have the time to check the `PYI016` rule.

The last thing is, I couldn't find a reason for the "Why is this bad?"
section for `PYI051`.

Ref: #848 

## Test Plan

Snapshots and manual runs of flake8.
\
2023-08-02 15:37:40 -04:00
Victor Hugo Gomes
7c5791fb77 Fix formatting of lambda star arguments (#6257)
## Summary
Previously, the ruff formatter was removing the star argument of
`lambda` expressions when formatting.

Given the following code snippet
```python
lambda *a: ()
lambda **b: ()
```
it would be formatted to
```python
lambda: ()
lambda: ()
```

We fix this by checking for the presence of `args`, `vararg` or `kwarg`
in the `lambda` expression, before we were only checking for the
presence of `args`.

Fixes #5894

## Test Plan

Add new tests cases.

---------

Co-authored-by: Charlie Marsh <charlie.r.marsh@gmail.com>
2023-08-02 19:31:20 +00:00
Harutaka Kawamura
c362ea7fd4 Add PT025 and PT026 docs (#6264) 2023-08-02 19:00:03 +00:00
Harutaka Kawamura
ec8fad5b02 Extend UP032 to support implicitly concatenated strings (#6263) 2023-08-02 18:56:24 +00:00
Harutaka Kawamura
bcc41ba062 Extend UP032 to support repeated format fields (#6266) 2023-08-02 14:23:25 -04:00
Charlie Marsh
556abf4bd3 Avoid PTH206 with maxsplit (#6283)
## Summary

Avoid suggesting `Path.parts` when a `maxsplit` is specified, since
these behavior differently.

## Test Plan

`cargo test`
2023-08-02 18:16:57 +00:00
Charlie Marsh
23b8fc4366 Move includes_arg_name onto Parameters (#6282)
## Summary

Like #6279, no reason for this to be a standalone method.
2023-08-02 18:05:26 +00:00
Charlie Marsh
fd40864924 Move find_keyword helpers onto Arguments struct (#6280)
## Summary

Similar to #6279, moving some helpers onto the struct in the name of
reducing the number of random undiscoverable utilities we have in
`helpers.rs`.

Most of the churn is migrating rules to take `ast::ExprCall` instead of
the spread call arguments.

## Test Plan

`cargo test`
2023-08-02 13:54:48 -04:00
Charlie Marsh
041946fb64 Remove CallArguments abstraction (#6279)
## Summary

This PR removes a now-unnecessary abstraction from `helper.rs`
(`CallArguments`), in favor of adding methods to `Arguments` directly,
which helps with discoverability.
2023-08-02 13:25:43 -04:00
Charlie Marsh
8a0f844642 Box type params and arguments fields on the class definition node (#6275)
## Summary

This PR boxes the `TypeParams` and `Arguments` fields on the class
definition node. These fields are optional and often emitted, and given
that class definition is our largest enum variant, we pay the cost of
including them for every statement in the AST. Boxing these types
reduces the statement size by 40 bytes, which seems like a good tradeoff
given how infrequently these are accessed.

## Test Plan

Need to benchmark, but no behavior changes.
2023-08-02 16:47:06 +00:00
Charlie Marsh
8c40886f87 Use Arguments node to power remove_argument method (#6278)
## Summary

Internal refactor to take advantage of the new `Arguments` node, to
power our `remove_argument` fix action.

## Test Plan

`cargo test`
2023-08-02 12:38:43 -04:00
Charlie Marsh
4c53bfe896 Add formatter support for call and class definition Arguments (#6274)
## Summary

This PR leverages the `Arguments` AST node introduced in #6259 in the
formatter, which ensures that we correctly handle trailing comments in
calls, like:

```python
f(
  1,
  # comment
)

pass
```

(Previously, this was treated as a leading comment on `pass`.)

This also allows us to unify the argument handling across calls and
class definitions.

## Test Plan

A bunch of new fixture tests, plus improved Black compatibility.
2023-08-02 11:54:22 -04:00
Charlie Marsh
b095b7204b Add a TypeParams node to the AST (#6261)
## Summary

Similar to #6259, this PR adds a `TypeParams` node to the AST, to
capture the list of type parameters with their surrounding brackets.

If a statement lacks type parameters, the `type_params` field will be
`None`.
2023-08-02 14:12:45 +00:00
Charlie Marsh
981e64f82b Introduce an Arguments AST node for function calls and class definitions (#6259)
## Summary

This PR adds a new `Arguments` AST node, which we can use for function
calls and class definitions.

The `Arguments` node spans from the left (open) to right (close)
parentheses inclusive.

In the case of classes, the `Arguments` is an option, to differentiate
between:

```python
# None
class C: ...

# Some, with empty vectors
class C(): ...
```

In this PR, we don't really leverage this change (except that a few
rules get much simpler, since we don't need to lex to find the start and
end ranges of the parentheses, e.g.,
`crates/ruff/src/rules/pyupgrade/rules/lru_cache_without_parameters.rs`,
`crates/ruff/src/rules/pyupgrade/rules/unnecessary_class_parentheses.rs`).

In future PRs, this will be especially helpful for the formatter, since
we can track comments enclosed on the node itself.

## Test Plan

`cargo test`
2023-08-02 10:01:13 -04:00
Ran Benita
0d62ad2480 Permit ClassVar and Final without subscript in RUF012 (#6273)
Fix #6267.
2023-08-02 12:58:44 +00:00
Harutaka Kawamura
b4f224ecea Fix links in docs (#6265)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

Before:

<img width="1031" alt="Screen Shot 2023-08-02 at 15 57 10"
src="https://github.com/astral-sh/ruff/assets/17039389/171a21d5-01a5-4aa5-8079-4e7f8a59ade8">

After:

<img width="1031" alt="Screen Shot 2023-08-02 at 15 58 03"
src="https://github.com/astral-sh/ruff/assets/17039389/afd1b9b7-89e0-4e38-a4a6-e3255b62f021">


## Test Plan

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

Manual inspection
2023-08-02 09:42:25 +02:00
Charlie Marsh
7842c82a0a Preserve end-of-line comments on import-from statements (#6216)
## Summary

Ensures that we keep comments at the end-of-line in cases like:

```python
from foo import (  # comment
  bar,
)
```

Closes https://github.com/astral-sh/ruff/issues/6067.
2023-08-01 18:58:05 +00:00
Charlie Marsh
9c708d8fc1 Rename Parameter#arg and ParameterWithDefault#def fields (#6255)
## Summary

This PR renames...

- `Parameter#arg` to `Parameter#name`
- `ParameterWithDefault#def` to `ParameterWithDefault#parameter` (such
that `ParameterWithDefault` has a `default` and a `parameter`)

## Test Plan

`cargo test`
2023-08-01 14:28:34 -04:00
Charlie Marsh
adc8bb7821 Rename Arguments to Parameters in the AST (#6253)
## Summary

This PR renames a few AST nodes for clarity:

- `Arguments` is now `Parameters`
- `Arg` is now `Parameter`
- `ArgWithDefault` is now `ParameterWithDefault`

For now, the attribute names that reference `Parameters` directly are
changed (e.g., on `StmtFunctionDef`), but the attributes on `Parameters`
itself are not (e.g., `vararg`). We may revisit that decision in the
future.

For context, the AST node formerly known as `Arguments` is used in
function definitions. Formally (outside of the Python context),
"arguments" typically refers to "the values passed to a function", while
"parameters" typically refers to "the variables used in a function
definition". E.g., if you Google "arguments vs parameters", you'll get
some explanation like:

> A parameter is a variable in a function definition. It is a
placeholder and hence does not have a concrete value. An argument is a
value passed during function invocation.

We're thus deviating from Python's nomenclature in favor of a scheme
that we find to be more precise.
2023-08-01 13:53:28 -04:00
Charlie Marsh
a82eb9544c Implement Black's rules around newlines before and after class docstrings (#6209)
## Summary

Black allows up to one blank line _before_ a class docstring, and
enforces one blank line _after_ a class docstring. This PR implements
that handling. The cases in
`crates/ruff_python_formatter/resources/test/fixtures/ruff/statement/class_definition.py`
match Black identically.
2023-08-01 13:33:01 -04:00
Zanie Blue
5e41f2fc7d Tweak pre-commit message (#6243)
Follow-up to https://github.com/astral-sh/ruff/pull/6153
2023-08-01 12:32:14 -05:00
konsti
1df7e9831b Replace .map_or(false, $closure) with .is_some_and(closure) (#6244)
**Summary**
[Option::is_some_and](https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.is_some_and)
and
[Result::is_ok_and](https://doc.rust-lang.org/std/result/enum.Result.html#method.is_ok_and)
are new methods is rust 1.70. I find them way more readable than
`.map_or(false, ...)`.

The changes are `s/.map_or(false,/.is_some_and(/g`, then manually
switching to `is_ok_and` where the value is a Result rather than an
Option.

**Test Plan** n/a^
2023-08-01 19:29:42 +02:00
Zanie Blue
2e1754e5fc Update ecosystem checks for bokeh to 3.3 (#6249)
Bokeh 3.3 is planned for release this month
(https://github.com/bokeh/bokeh/issues/13207) and is their default
branch now
2023-08-01 11:56:58 -05:00
Zanie Blue
67b88803d8 Use prettier to format yaml files in pre-commit (#6250)
Prompted by
https://github.com/astral-sh/ruff/pull/6248#discussion_r1280855848
2023-08-01 16:45:08 +00:00
konsti
ed45fcb1f7 Remove old CI comment (#6246)
We don't build abi3 wheels
2023-08-01 11:35:47 -05:00
Zanie Blue
adf227b8a9 Run ecosystem checks on changes to ecosystem test script (#6248)
e.g. https://github.com/astral-sh/ruff/pull/6245 should probably run
checks before merge
2023-08-01 11:35:29 -05:00
Micha Reiser
debfca3a11 Remove Parse trait (#6235) 2023-08-01 18:35:03 +02:00
Charlie Marsh
83fe103d6e Allow generic tuple and list calls in __all__ (#6247)
## Summary

Allows, e.g., `__all__ = list[str]()`.

Closes https://github.com/astral-sh/ruff/issues/6226.
2023-08-01 12:01:48 -04:00
Charlie Marsh
e08f873077 Add Poetry and FastAPI to ecosystem checks (#6245)
Poetry in particular would be useful to avoid issues like
https://github.com/astral-sh/ruff/issues/6233.
2023-08-01 11:48:34 -04:00
Charlie Marsh
928ab63a64 Add empty lines before nested functions and classes (#6206)
## Summary

This PR ensures that if a function or class is the first statement in a
nested suite that _isn't_ a function or class body, we insert a leading
newline.

For example, given:

```python
def f():
    if True:

        def register_type():
            pass
```

We _want_ to preserve the newline, whereas today, we remove it.

Note that this only applies when the function or class doesn't have any
leading comments.

Closes https://github.com/astral-sh/ruff/issues/6066.
2023-08-01 15:30:59 +00:00
Harutaka Kawamura
b68f76f0d9 Add pre-commit install in CONTRIBUTING.md (#6153)
<!--
Thank you for contributing to Ruff! To help us out with reviewing,
please consider the following:

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

## Summary

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

To enable the git hooks, `pre-commit install` needs to be executed.

## Test Plan

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

N/A
2023-08-01 09:28:50 -05:00
Charlie Marsh
1a85953129 Don't require docstrings in .pyi files (#6239)
Closes https://github.com/astral-sh/ruff/issues/6224.
2023-08-01 10:02:57 -04:00
Charlie Marsh
743118ae9a Bump version to 0.0.282 (#6241) 2023-08-01 13:21:33 +00:00
Charlie Marsh
0753017cf1 Revert "Expand scope of quoted-annotation rule (#5766)" (#6237)
This is causing some problems, so we'll just revert for now.

Closes https://github.com/astral-sh/ruff/issues/6189.
2023-08-01 09:03:02 -04:00
Charlie Marsh
29fb655e04 Fix logger-objects documentation (#6238)
Closes https://github.com/astral-sh/ruff/issues/6234.
2023-08-01 12:57:56 +00:00
Micha Reiser
f45e8645d7 Remove unused parser modes
<!--
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 removes the `Interactive` and `FunctionType` parser modes that are unused by ruff

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

## Test Plan

`cargo test`

<!-- How was it tested? -->
2023-08-01 13:10:07 +02:00
Micha Reiser
7c7231db2e Remove unsupported type_comment field
<!--
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 removes the `type_comment` field which our parser doesn't support.

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

## Test Plan

`cargo test`

<!-- How was it tested? -->
2023-08-01 12:53:13 +02:00
Micha Reiser
4ad5903ef6 Delete type-ignore node
<!--
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 removes the type ignore node from the AST because our parser doesn't support it, and just having it around is confusing.

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

## Test Plan

`cargo build`

<!-- How was it tested? -->
2023-08-01 12:34:50 +02:00
konsti
c6986ac95d Consistent CommentPlacement conversion signatures (#6231)
**Summary** Allow passing any node to `CommentPlacement::{leading,
trailing, dangling}` without manually converting. Conversely, Restrict
the comment to the only type we actually pass.

**Test Plan** No changes.
2023-08-01 12:01:17 +02:00
Micha Reiser
ecfdd8d58b Add static assertions to nodes (#6228) 2023-08-01 11:54:49 +02:00
David Szotten
07468f8be9 format ExprJoinedStr (#5932) 2023-08-01 08:26:30 +02:00
David Szotten
ba990b676f add DebugText for self-documenting f-strings (#6167) 2023-08-01 07:55:03 +02:00
Harutaka Kawamura
44a8d1c644 Add PT021, PT022 and PT023 docs (#6143) 2023-08-01 00:41:54 -04:00
Charlie Marsh
88b984e885 Avoid detecting continuations at non-start-of-line (#6219)
## Summary

Previously, given:

```python
a = \
  5;
```

When detecting continuations starting at the offset of the `;`, we'd
flag the previous line as a continuation. We should only flag a
continuation if there isn't leading content prior to the offset.

Closes https://github.com/astral-sh/ruff/issues/6214
2023-08-01 00:20:29 -04:00
Charlie Marsh
bf584c6d74 Remove use of SmallVec in unnecessary-literal-union (#6221)
I prefer to use this on an as-needed basis.
2023-08-01 04:03:58 +00:00
Konrad Listwan-Ciesielski
6ea3c178fd Add DTZ002 documentation (#6146)
## Summary

Adds documentation for DTZ002. Related to
https://github.com/astral-sh/ruff/issues/2646.

## Test Plan

`python scripts/test_docs_formatted.py`
2023-08-01 04:00:50 +00:00
Charlie Marsh
764d35667f Avoid PERF401 false positive on list access in loop (#6220)
Closes https://github.com/astral-sh/ruff/issues/6210.
2023-08-01 03:56:53 +00:00
Charlie Marsh
ff9ebbaa5f Skip trivia when searching for named exception (#6218)
Closes https://github.com/astral-sh/ruff/issues/6213.
2023-08-01 03:42:30 +00:00
Micha Reiser
38b5726948 formatter: WithNodeLevel helper (#6212) 2023-07-31 21:22:17 +00:00
Charlie Marsh
615337a54d Remove newline-insertion logic from JoinNodesBuilder (#6205)
## Summary

This PR moves the "insert empty lines" behavior out of
`JoinNodesBuilder` and into the `Suite` formatter. I find it a little
confusing that the logic is split between those two formatters right
now, and since this is _only_ used in that one place, IMO it is a bit
simpler to just inline it and use a single approach to tracking state
(right now, both are stateful).

The only other place this was used was for decorators. As a side effect,
we now remove blank lines in both of these cases, which is a known but
intentional deviation from Black (which preserves the empty line before
the comment in the first case):

```python
@foo

# Hello
@bar
def baz():
    pass

@foo

@bar
def baz():
    pass
```
2023-07-31 16:58:15 -04:00
Charlie Marsh
6ee5cb37c0 Reset model state when exiting deferred visitors (#6208)
## Summary

Very subtle bug related to the AST traversal. Given:

```python
from __future__ import annotations

from logging import getLogger

__all__ = ("getLogger",)


def foo() -> None:
    pass
```

We end up visiting the `-> None` annotation, then reusing the state
snapshot when we go to visit the `__all__` exports, so when we visit
`"getLogger"`, we think we're inside of a deferred type annotation.

This PR changes all the deferred visitors to snapshot and restore the
state, which is a lot safer -- that way, the visitors avoid modifying
the current visitor state. (Previously, they implicitly left the visitor
state set to the state of the _last_ thing they visited.)

Closes https://github.com/astral-sh/ruff/issues/6207.
2023-07-31 19:46:52 +00:00
konsti
0fddb31235 Use tracing for format_dev (#6177)
## Summary

[tracing](https://github.com/tokio-rs/tracing) is library for logging,
tracing and related features that has a large ecosystem. Using
[tracing-subscriber](https://docs.rs/tracing-subscriber) and
[tracing-indicatif](https://github.com/emersonford/tracing-indicatif),
we get a nice logging output that you can configure with `RUST_LOG`
(e.g. `RUST_LOG=debug`) and a live look into the formatter progress.

Default:
![Screenshot from 2023-07-30
13-59-53](https://github.com/astral-sh/ruff/assets/6826232/6432f835-9ff1-4771-955b-398e54c406dc)

`RUST_LOG=debug`:
![Screenshot from 2023-07-30
14-01-32](https://github.com/astral-sh/ruff/assets/6826232/5f2c87da-0867-4159-82e7-b5757eebb8eb)

It's easy to see in this output which files take a disproportionate
amount of time.

[Peek 2023-07-30
14-35.webm](https://github.com/astral-sh/ruff/assets/6826232/2c92db5c-1354-465b-a6bc-ddfb281d6f9d)

It opens up further integration with the tracing ecosystem,
[tracing-timing](https://docs.rs/tracing-timing/latest/tracing_timing/)
and [tokio-console](https://github.com/tokio-rs/console) can e.g. show
histograms and the json output allows us building better pipelines than
grepping a log file.

One caveat is using `parent: None` for the logging statements because
tracing subscriber does not allow deactivating the span without
reimplementing all the other log message formatting, too, and we don't
need span information, esp. since it would currently show the progress
bar span.

## Test Plan

n/a
2023-07-31 19:14:01 +00:00
konsti
a7aa3caaae Rename formatter_progress to formatter_ecosystem_checks (#6194)
Rename the `scripts/formatter_progress.sh` to
`formatter/formatter_ecosysytem_checks.sh` since it fits the actual task
better.
2023-07-31 18:33:12 +00:00
konsti
e52b636da0 Log configuration in ruff_dev (#6193)
**Summary** This includes two changes:
 * Allow setting `-v` in `ruff_dev`, using the `ruff_cli` implementation
 * `debug!` which ruff configuration strategy was used

This is a byproduct of debugging #6187.

**Test Plan** n/a
2023-07-31 17:52:38 +00:00
konsti
9063f4524d Fix formatting of trailing unescaped quotes in raw triple quoted strings (#6202)
**Summary** This prevents us from turning `r'''\""'''` into
`r"""\"""""`, which is invalid syntax.

This PR fixes CI, which is currently broken on main (in a way that still
passes on linter PRs and allows merging formatter PRs, but it's bad to
have a job be red). Once merged, i'll make the formatted ecosystem
checks a required check.

**Test Plan** Added a regression test.
2023-07-31 19:25:16 +02:00
764 changed files with 50122 additions and 44984 deletions

1
.github/release.yml vendored
View File

@@ -4,6 +4,7 @@ changelog:
labels:
- internal
- documentation
- formatter
categories:
- title: Breaking Changes
labels:

View File

@@ -3,12 +3,12 @@ name: Benchmark
on:
pull_request:
paths:
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain'
- 'crates/**'
- '!crates/ruff_dev'
- '!crates/ruff_shrinking'
- "Cargo.toml"
- "Cargo.lock"
- "rust-toolchain"
- "crates/**"
- "!crates/ruff_dev"
- "!crates/ruff_shrinking"
workflow_dispatch:
@@ -22,7 +22,7 @@ jobs:
name: "Run | ${{ matrix.os }}"
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:

View File

@@ -2,7 +2,7 @@ name: CI
on:
push:
branches: [ main ]
branches: [main]
pull_request:
workflow_dispatch:
@@ -16,7 +16,7 @@ env:
CARGO_TERM_COLOR: always
RUSTUP_MAX_RETRIES: 10
PACKAGE_NAME: ruff
PYTHON_VERSION: "3.11" # to build abi3 wheels
PYTHON_VERSION: "3.11"
jobs:
determine_changes:
@@ -42,6 +42,7 @@ jobs:
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/*
formatter:
- Cargo.toml
@@ -54,7 +55,8 @@ jobs:
- crates/ruff_python_index/**
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
cargo-fmt:
name: "cargo fmt"
@@ -83,7 +85,7 @@ jobs:
cargo-test:
strategy:
matrix:
os: [ ubuntu-latest, windows-latest ]
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
name: "cargo test | ${{ matrix.os }}"
steps:
@@ -235,7 +237,6 @@ jobs:
- name: "Run cargo-udeps"
run: cargo +nightly-2023-06-08 udeps
python-package:
name: "python package"
runs-on: ubuntu-latest
@@ -335,11 +336,8 @@ jobs:
- name: "Cache rust"
uses: Swatinem/rust-cache@v2
- name: "Formatter progress"
run: scripts/formatter_progress.sh
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: grep "similarity index" target/progress_projects_report.txt | sort > $GITHUB_STEP_SUMMARY
# CPython is not black formatted, so we run only the stability check
- name: "Clone CPython 3.10"
run: git clone --branch 3.10 --depth 1 https://github.com/python/cpython.git crates/ruff/resources/test/cpython
- name: "Check CPython stability"
run: cargo run --bin ruff_dev -- format-dev --stability-check crates/ruff/resources/test/cpython
run: grep "similarity index" target/progress_projects_log.txt | sort > $GITHUB_STEP_SUMMARY
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects

View File

@@ -3,7 +3,7 @@ name: mkdocs
on:
workflow_dispatch:
release:
types: [ published ]
types: [published]
jobs:
mkdocs:

View File

@@ -66,7 +66,7 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
target: [ x64, x86 ]
target: [x64, x86]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -94,7 +94,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target: [ x86_64, i686 ]
target: [x86_64, i686]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
@@ -121,7 +121,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
target: [ aarch64, armv7, s390x, ppc64le, ppc64 ]
target: [aarch64, armv7, s390x, ppc64le, ppc64]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4

View File

@@ -3,7 +3,7 @@ name: "[Playground] Release"
on:
workflow_dispatch:
release:
types: [ published ]
types: [published]
env:
CARGO_INCREMENTAL: 0

View File

@@ -2,8 +2,8 @@ name: PR Check Comment
on:
workflow_run:
workflows: [ CI, Benchmark ]
types: [ completed ]
workflows: [CI, Benchmark]
types: [completed]
workflow_dispatch:
inputs:
workflow_run_id:

View File

@@ -42,13 +42,13 @@ repos:
name: cargo fmt
entry: cargo fmt --
language: system
types: [ rust ]
types: [rust]
pass_filenames: false # This makes it a lot faster
- id: ruff
name: ruff
entry: cargo run --bin ruff -- check --no-cache --force-exclude --fix --exit-non-zero-on-fix
language: system
types_or: [ python, pyi ]
types_or: [python, pyi]
require_serial: true
exclude: |
(?x)^(
@@ -62,5 +62,12 @@ repos:
hooks:
- id: black
# Prettier
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v3.0.0
hooks:
- id: prettier
types: [yaml]
ci:
skip: [ cargo-fmt, dev-generate-all ]
skip: [cargo-fmt, dev-generate-all]

View File

@@ -1,5 +1,13 @@
# Breaking Changes
## 0.0.283
### The target Python version now defaults to 3.8 instead of 3.10 ([#6397](https://github.com/astral-sh/ruff/pull/6397))
Previously, when a target Python version was not specified, Ruff would use a default of Python 3.10. However, it is safer to default to an _older_ Python version to avoid assuming the availability of new features. We now default to the oldest supported Python version which is currently Python 3.8.
(We still support Python 3.7 but since [it has reached EOL](https://devguide.python.org/versions/#unsupported-versions) we've decided not to make it the default here.)
## 0.0.277
### `.ipynb_checkpoints`, `.pyenv`, `.pytest_cache`, and `.vscode` are now excluded by default ([#5513](https://github.com/astral-sh/ruff/pull/5513))

View File

@@ -69,6 +69,13 @@ and pre-commit to run some validation checks:
pipx install pre-commit # or `pip install pre-commit` if you have a virtualenv
```
You can optionally install pre-commit hooks to automatically run the validation checks
when making a commit:
```shell
pre-commit install
```
### Development
After cloning the repository, run Ruff locally from the repository root with:
@@ -124,7 +131,6 @@ At time of writing, the repository includes the following crates:
- `crates/ruff_macros`: proc macro crate containing macros used by Ruff.
- `crates/ruff_python_ast`: library crate containing Python-specific AST types and utilities.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_codegen`: library crate containing utilities for generating Python source code.
- `crates/ruff_python_formatter`: library crate implementing the Python formatter. Emits an
intermediate representation for each node, which `ruff_formatter` prints based on the configured
line length.

63
Cargo.lock generated
View File

@@ -133,6 +133,12 @@ dependencies = [
"os_str_bytes",
]
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "ascii-canvas"
version = "3.0.0"
@@ -794,7 +800,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.281"
version = "0.0.283"
dependencies = [
"anyhow",
"clap",
@@ -1027,6 +1033,7 @@ dependencies = [
"number_prefix",
"portable-atomic",
"unicode-width",
"vt100",
]
[[package]]
@@ -2035,7 +2042,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.281"
version = "0.0.283"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2134,7 +2141,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.281"
version = "0.0.283"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2194,7 +2201,6 @@ dependencies = [
"indoc",
"itertools",
"libcst",
"log",
"once_cell",
"pretty_assertions",
"rayon",
@@ -2218,6 +2224,9 @@ dependencies = [
"strum_macros",
"tempfile",
"toml",
"tracing",
"tracing-indicatif",
"tracing-subscriber",
]
[[package]]
@@ -2282,6 +2291,7 @@ dependencies = [
"rustc-hash",
"serde",
"smallvec",
"static_assertions",
]
[[package]]
@@ -3115,6 +3125,18 @@ dependencies = [
"valuable",
]
[[package]]
name = "tracing-indicatif"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b38ed3722d27705c3bd7ca0ccf29acc3d8e1c717b4cd87f97891a2c1834ea1af"
dependencies = [
"indicatif",
"tracing",
"tracing-core",
"tracing-subscriber",
]
[[package]]
name = "tracing-log"
version = "0.1.3"
@@ -3313,6 +3335,39 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "vt100"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84cd863bf0db7e392ba3bd04994be3473491b31e66340672af5d11943c6274de"
dependencies = [
"itoa",
"log",
"unicode-width",
"vte",
]
[[package]]
name = "vte"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5022b5fbf9407086c180e9557be968742d839e68346af7792b8592489732197"
dependencies = [
"arrayvec",
"utf8parse",
"vte_generate_state_changes",
]
[[package]]
name = "vte_generate_state_changes"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "wait-timeout"
version = "0.2.0"

View File

@@ -4,7 +4,7 @@ resolver = "2"
[workspace.package]
edition = "2021"
rust-version = "1.70"
rust-version = "1.71"
homepage = "https://beta.ruff.rs/docs"
documentation = "https://beta.ruff.rs/docs"
repository = "https://github.com/astral-sh/ruff"
@@ -46,6 +46,9 @@ syn = { version = "2.0.15" }
test-case = { version = "3.0.0" }
thiserror = { version = "1.0.43" }
toml = { version = "0.7.2" }
tracing = "0.1.37"
tracing-indicatif = "0.3.4"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
wsl = { version = "0.1.0" }
# v1.0.1

View File

@@ -140,7 +140,7 @@ Ruff can also be used as a [pre-commit](https://pre-commit.com) hook:
```yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.0.281
rev: v0.0.283
hooks:
- id: ruff
```
@@ -211,8 +211,8 @@ line-length = 88
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
# Assume Python 3.10.
target-version = "py310"
# Assume Python 3.8
target-version = "py38"
[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.

View File

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

View File

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

View File

@@ -67,7 +67,8 @@ cfg.getboolean("hello", True)
os.set_blocking(0, False)
g_action.set_enabled(True)
settings.set_enable_developer_extras(True)
foo.is_(True)
bar.is_not(False)
class Registry:
def __init__(self) -> None:

View File

@@ -240,12 +240,16 @@ def foo(f=lambda x: print(x)):
from collections import abc
from typing import Annotated, Dict, Optional, Sequence, Union, Set
import typing_extensions
def immutable_annotations(
a: Sequence[int] | None = [],
b: Optional[abc.Mapping[int, int]] = {},
c: Annotated[Union[abc.Set[str], abc.Sized], "annotation"] = set(),
d: typing_extensions.Annotated[
Union[abc.Set[str], abc.Sized], "annotation"
] = set(),
):
pass
@@ -254,5 +258,6 @@ def mutable_annotations(
a: list[int] | None = [],
b: Optional[Dict[int, int]] = {},
c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
):
pass

View File

@@ -74,3 +74,10 @@ try:
except (ValueError, binascii.Error):
# binascii.Error is a subclass of ValueError.
pass
# https://github.com/astral-sh/ruff/issues/6412
try:
pass
except (ValueError, ValueError, TypeError):
pass

View File

@@ -3,3 +3,6 @@ import logging
name = "world"
logging.info(f"Hello {name}")
logging.log(logging.INFO, f"Hello {name}")
_LOGGER = logging.getLogger()
_LOGGER.info(f"{__name__}")

View File

@@ -1,20 +1,19 @@
import typing
# Shouldn't affect non-union field types.
field1: str
# Should emit for duplicate field types.
field2: str | str # PYI016: Duplicate union member `str`
# Should emit for union types in arguments.
def func1(arg1: int | int): # PYI016: Duplicate union member `int`
print(arg1)
# Should emit for unions in return types.
def func2() -> str | str: # PYI016: Duplicate union member `str`
return "my string"
# Should emit in longer unions, even if not directly adjacent.
field3: str | str | int # PYI016: Duplicate union member `str`
field4: int | int | str # PYI016: Duplicate union member `int`
@@ -33,3 +32,55 @@ field10: (str | int) | str # PYI016: Duplicate union member `str`
# Should emit for nested unions.
field11: dict[int | int, str]
# Should emit for unions with more than two cases
field12: int | int | int # Error
field13: int | int | int | int # Error
# Should emit for unions with more than two cases, even if not directly adjacent
field14: int | int | str | int # Error
# Should emit for duplicate literal types; also covered by PYI030
field15: typing.Literal[1] | typing.Literal[1] # Error
# Shouldn't emit if in new parent type
field16: int | dict[int, str] # OK
# Shouldn't emit if not in a union parent
field17: dict[int, int] # OK
# Should emit in cases with newlines
field18: typing.Union[
set[
int # foo
],
set[
int # bar
],
] # Error, newline and comment will not be emitted in message
# Should emit in cases with `typing.Union` instead of `|`
field19: typing.Union[int, int] # Error
# Should emit in cases with nested `typing.Union`
field20: typing.Union[int, typing.Union[int, str]] # Error
# Should emit in cases with mixed `typing.Union` and `|`
field21: typing.Union[int, int | str] # Error
# Should emit only once in cases with multiple nested `typing.Union`
field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@@ -74,3 +74,13 @@ field22: typing.Union[int, typing.Union[int, typing.Union[int, int]]] # Error
# Should emit in cases with newlines
field23: set[ # foo
int] | set[int]
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field24: typing.Union[int, typing.Union[int, int]] # PYI016: Duplicate union member `int`
# Should emit twice (once for each `int` in the nested union, both of which are
# duplicates of the outer `int`), but not three times (which would indicate that
# we incorrectly re-checked the nested union).
field25: typing.Union[int, int | int] # PYI016: Duplicate union member `int`

View File

@@ -0,0 +1,46 @@
from typing import TypeVar, Self, Type
_S = TypeVar("_S", bound=BadClass)
_S2 = TypeVar("_S2", BadClass, GoodClass)
class BadClass:
def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
@classmethod
def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@classmethod
def excluded_edge_case(cls: Type[_S], arg: int) -> _S: ... # Ok
class GoodClass:
def __new__(cls: type[Self], *args: list[int], **kwargs: set[str]) -> Self: ...
def good_instance_method_1(self: Self, arg: bytes) -> Self: ...
def good_instance_method_2(self, arg1: _S2, arg2: _S2) -> _S2: ...
@classmethod
def good_cls_method_1(cls: type[Self], arg: int) -> Self: ...
@classmethod
def good_cls_method_2(cls, arg1: _S, arg2: _S) -> _S: ...
@staticmethod
def static_method(arg1: _S) -> _S: ...
# Python > 3.12
class PEP695BadDunderNew[T]:
def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
def generic_instance_method[S](self: S) -> S: ... # PYI019
class PEP695GoodDunderNew[T]:
def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...

View File

@@ -0,0 +1,46 @@
from typing import TypeVar, Self, Type
_S = TypeVar("_S", bound=BadClass)
_S2 = TypeVar("_S2", BadClass, GoodClass)
class BadClass:
def __new__(cls: type[_S], *args: str, **kwargs: int) -> _S: ... # PYI019
def bad_instance_method(self: _S, arg: bytes) -> _S: ... # PYI019
@classmethod
def bad_class_method(cls: type[_S], arg: int) -> _S: ... # PYI019
@classmethod
def bad_posonly_class_method(cls: type[_S], /) -> _S: ... # PYI019
@classmethod
def excluded_edge_case(cls: Type[_S], arg: int) -> _S: ... # Ok
class GoodClass:
def __new__(cls: type[Self], *args: list[int], **kwargs: set[str]) -> Self: ...
def good_instance_method_1(self: Self, arg: bytes) -> Self: ...
def good_instance_method_2(self, arg1: _S2, arg2: _S2) -> _S2: ...
@classmethod
def good_cls_method_1(cls: type[Self], arg: int) -> Self: ...
@classmethod
def good_cls_method_2(cls, arg1: _S, arg2: _S) -> _S: ...
@staticmethod
def static_method(arg1: _S) -> _S: ...
# Python > 3.12
class PEP695BadDunderNew[T]:
def __new__[S](cls: type[S], *args: Any, ** kwargs: Any) -> S: ... # PYI019
def generic_instance_method[S](self: S) -> S: ... # PYI019
class PEP695GoodDunderNew[T]:
def __new__(cls, *args: Any, **kwargs: Any) -> Self: ...

View File

@@ -1,9 +1,11 @@
import collections
person: collections.namedtuple # OK
person: collections.namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
from collections import namedtuple
person: namedtuple # OK
person: namedtuple # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"
person = namedtuple("Person", ["name", "age"]) # OK
person = namedtuple(
"Person", ["name", "age"]
) # Y024 Use "typing.NamedTuple" instead of "collections.namedtuple"

View File

@@ -1,24 +1,38 @@
import typing
import typing_extensions
from typing import Literal
# Shouldn't emit for any cases in the non-stub file for compatibility with flake8-pyi.
# Note that this rule could be applied here in the future.
# Shouldn't affect non-union field types.
field1: Literal[1] # OK
field2: Literal[1] | Literal[2] # OK
def func1(arg1: Literal[1] | Literal[2]): # OK
# Should emit for duplicate field types.
field2: Literal[1] | Literal[2] # Error
# Should emit for union types in arguments.
def func1(arg1: Literal[1] | Literal[2]): # Error
print(arg1)
def func2() -> Literal[1] | Literal[2]: # OK
# Should emit for unions in return types.
def func2() -> Literal[1] | Literal[2]: # Error
return "my Literal[1]ing"
field3: Literal[1] | Literal[2] | str # OK
field4: str | Literal[1] | Literal[2] # OK
field5: Literal[1] | str | Literal[2] # OK
field6: Literal[1] | bool | Literal[2] | str # OK
field7 = Literal[1] | Literal[2] # OK
field8: Literal[1] | (Literal[2] | str) # OK
field9: Literal[1] | (Literal[2] | str) # OK
field10: (Literal[1] | str) | Literal[2] # OK
field11: dict[Literal[1] | Literal[2], str] # OK
# Should emit in longer unions, even if not directly adjacent.
field3: Literal[1] | Literal[2] | str # Error
field4: str | Literal[1] | Literal[2] # Error
field5: Literal[1] | str | Literal[2] # Error
field6: Literal[1] | bool | Literal[2] | str # Error
# Should emit for non-type unions.
field7 = Literal[1] | Literal[2] # Error
# Should emit for parenthesized unions.
field8: Literal[1] | (Literal[2] | str) # Error
# Should handle user parentheses when fixing.
field9: Literal[1] | (Literal[2] | str) # Error
field10: (Literal[1] | str) | Literal[2] # Error
# Should emit for union in generic parent type.
field11: dict[Literal[1] | Literal[2], str] # Error

View File

@@ -3,8 +3,8 @@ import typing
class Bad:
def __eq__(self, other: Any) -> bool: ... # Fine because not a stub file
def __ne__(self, other: typing.Any) -> typing.Any: ... # Fine because not a stub file
def __eq__(self, other: Any) -> bool: ... # Y032
def __ne__(self, other: typing.Any) -> typing.Any: ... # Y032
class Good:

View File

@@ -9,16 +9,16 @@ from typing import (
just_literals_pipe_union: TypeAlias = (
Literal[True] | Literal["idk"]
) # not PYI042 (not a stubfile)
) # PYI042, since not camel case
PublicAliasT: TypeAlias = str | int
PublicAliasT2: TypeAlias = Union[str, bytes]
_ABCDEFGHIJKLMNOPQRST: TypeAlias = typing.Any
_PrivateAliasS: TypeAlias = Literal["I", "guess", "this", "is", "okay"]
_PrivateAliasS2: TypeAlias = Annotated[str, "also okay"]
snake_case_alias1: TypeAlias = str | int # not PYI042 (not a stubfile)
_snake_case_alias2: TypeAlias = Literal["whatever"] # not PYI042 (not a stubfile)
Snake_case_alias: TypeAlias = int | float # not PYI042 (not a stubfile)
snake_case_alias1: TypeAlias = str | int # PYI042, since not camel case
_snake_case_alias2: TypeAlias = Literal["whatever"] # PYI042, since not camel case
Snake_case_alias: TypeAlias = int | float # PYI042, since not camel case
# check that this edge case doesn't crash
_: TypeAlias = str | int

View File

@@ -7,11 +7,11 @@ from typing import (
Literal,
)
_PrivateAliasT: TypeAlias = str | int # not PYI043 (not a stubfile)
_PrivateAliasT2: TypeAlias = typing.Any # not PYI043 (not a stubfile)
_PrivateAliasT: TypeAlias = str | int # PYI043, since this ends in a T
_PrivateAliasT2: TypeAlias = typing.Any # PYI043, since this ends in a T
_PrivateAliasT3: TypeAlias = Literal[
"not", "a", "chance"
] # not PYI043 (not a stubfile)
] # PYI043, since this ends in a T
just_literals_pipe_union: TypeAlias = Literal[True] | Literal["idk"]
PublicAliasT: TypeAlias = str | int
PublicAliasT2: TypeAlias = Union[str, bytes]

View File

@@ -0,0 +1,17 @@
import typing
from typing import Literal, TypeAlias, Union
A: str | Literal["foo"]
B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
# OK
A: Literal["foo"]
B: TypeAlias = Literal[b"bar", b"foo"]
C: TypeAlias = typing.Union[Literal[5], Literal["foo"]]
D: TypeAlias = Literal[b"str_bytes", 42]
def func(x: Literal[1J], y: Literal[3.14]): ...

View File

@@ -0,0 +1,17 @@
import typing
from typing import Literal, TypeAlias, Union
A: str | Literal["foo"]
B: TypeAlias = typing.Union[Literal[b"bar", b"foo"], bytes, str]
C: TypeAlias = typing.Union[Literal[5], int, typing.Union[Literal["foo"], str]]
D: TypeAlias = typing.Union[Literal[b"str_bytes", 42], bytes, int]
def func(x: complex | Literal[1J], y: Union[Literal[3.14], float]): ...
# OK
A: Literal["foo"]
B: TypeAlias = Literal[b"bar", b"foo"]
C: TypeAlias = typing.Union[Literal[5], Literal["foo"]]
D: TypeAlias = Literal[b"str_bytes", 42]
def func(x: Literal[1J], y: Literal[3.14]): ...

View File

@@ -0,0 +1,20 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
def func(arg: type[int] | str | type[float]) -> None: ...
# OK
x: type[int, str, float]
y: builtins.type[int, str, complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...

View File

@@ -0,0 +1,20 @@
import builtins
from typing import Union
w: builtins.type[int] | builtins.type[str] | builtins.type[complex]
x: type[int] | type[str] | type[float]
y: builtins.type[int] | type[str] | builtins.type[complex]
z: Union[type[float], type[complex]]
z: Union[type[float, int], type[complex]]
def func(arg: type[int] | str | type[float]) -> None: ...
# OK
x: type[int, str, float]
y: builtins.type[int, str, complex]
z: Union[float, complex]
def func(arg: type[int, float] | str) -> None: ...

View File

@@ -0,0 +1,10 @@
"""Regression test: ensure that we don't treat the export entry as a typing-only reference."""
from __future__ import annotations
from logging import getLogger
__all__ = ("getLogger",)
def foo() -> None:
pass

View File

@@ -18,3 +18,5 @@ file_name.split(os.sep)
# OK
"foo/bar/".split("/")
"foo/bar/".split(os.sep, 1)
"foo/bar/".split(1, sep=os.sep)

View File

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

View File

@@ -25,6 +25,23 @@
"def foo():\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16214f6f-bb32-4594-81be-79fb27c6ec92",
"metadata": {},
"outputs": [],
"source": [
"from pathlib import Path\n",
"import sys\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import math\n",
"import abc"
]
}
],
"metadata": {

View File

@@ -27,6 +27,23 @@
"def foo():\n",
" pass"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6d6c55c6-4a34-4662-914b-4ee11c9c24a5",
"metadata": {},
"outputs": [],
"source": [
"import sys\n",
"from pathlib import Path\n",
"\n",
"%matplotlib \\\n",
" --inline\n",
"\n",
"import abc\n",
"import math"
]
}
],
"metadata": {

View File

@@ -0,0 +1,52 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "eab4754a-d6df-4b41-8ee8-7e23aef440f9",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"import os\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2b0e2986-1b87-4bb6-9b1d-c11ca1decd87",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,51 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "cad32845-44f9-4a53-8b8c-a6b1bb3f3378",
"metadata": {},
"outputs": [],
"source": [
"import math\n",
"\n",
"%matplotlib inline\n",
"\n",
"\n",
"_ = math.pi"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d7b8e967-8b4a-493b-b6f7-d5cecfb3a5c3",
"metadata": {},
"outputs": [],
"source": [
"%%timeit\n",
"import sys"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,49 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "a0efffbc-85f1-4513-bf49-5387ec3a2a4e",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" foo1 = %matplotlib --list\n",
" foo2: list[str] = %matplotlib --list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "6e0b2b50-43f2-4f59-951d-9404dd560ae4",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" bar1 = !pwd\n",
" bar2: str = !pwd"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -0,0 +1,49 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "24426ef2-046c-453e-b809-05b56e7355e0",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" %matplotlib --list\n",
" %matplotlib --list"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3d98fdae-b86b-476e-b4db-9d3ce5562682",
"metadata": {},
"outputs": [],
"source": [
"def f():\n",
" !pwd\n",
" !pwd"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python (ruff)",
"language": "python",
"name": "ruff"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@@ -45,3 +45,18 @@ def f():
for i in items:
if i not in result:
result.append(i) # OK
def f():
fibonacci = [0, 1]
for i in range(20):
fibonacci.append(sum(fibonacci[-2:])) # OK
print(fibonacci)
def f():
foo = object()
foo.fibonacci = [0, 1]
for i in range(20):
foo.fibonacci.append(sum(foo.fibonacci[-2:])) # OK
print(foo.fibonacci)

View File

@@ -66,3 +66,6 @@ while 1:
#: E703:2:1
0\
;
#: E701:2:3
a = \
5;

View File

@@ -58,3 +58,6 @@ assert type(res) == type(None)
types = StrEnum
if x == types.X:
pass
#: E721
assert type(res) is int

View File

@@ -634,3 +634,8 @@ def starts_with_this():
@expect('D404: First word of the docstring should not be "This"')
def starts_with_space_then_this():
""" This is a docstring that starts with a space.""" # noqa: D210
class SameLine: """This is a docstring on the same line"""
def same_line(): """This is a docstring on the same line"""

View File

@@ -147,3 +147,10 @@ def f() -> None:
global CONSTANT
CONSTANT = 1
CONSTANT = 2
def f() -> None:
try:
print("hello")
except A as e :
print("oh no!")

View File

@@ -0,0 +1,29 @@
# pylint: disable=missing-docstring,consider-using-f-string, pointless-statement
## Old style formatting
"%s %z" % ("hello", "world") # [bad-format-character]
"%s" "%z" % ("hello", "world") # [bad-format-character]
"""%s %z""" % ("hello", "world") # [bad-format-character]
"""%s""" """%z""" % ("hello", "world") # [bad-format-character]
## New style formatting
"{:s} {:y}".format("hello", "world") # [bad-format-character]
"{:*^30s}".format("centered")
## f-strings
H, W = "hello", "world"
f"{H} {W}"
f"{H:s} {W:z}" # [bad-format-character]
f"{1:z}" # [bad-format-character]
## False negatives
print(("%" "z") % 1)

View File

@@ -19,6 +19,10 @@ foo in foo
foo not in foo
id(foo) == id(foo)
len(foo) == len(foo)
# Non-errors.
"foo" == "foo" # This is flagged by `comparison-of-constant` instead.
@@ -43,3 +47,11 @@ foo is not bar
foo in bar
foo not in bar
x(foo) == y(foo)
id(foo) == id(bar)
id(foo, bar) == id(foo, bar)
id(foo, bar=1) == id(foo, bar=1)

View File

@@ -40,3 +40,4 @@ __all__ = __all__ + ["Hello"]
__all__ = __all__ + multiprocessing.__all__
__all__ = list[str](["Hello", "world"])

View File

@@ -32,3 +32,30 @@ print(
)
'{' '0}'.format(1)
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)
"{1}_{0}".format(1, 2, *args)
"{1}_{0}".format(1, 2)

View File

@@ -15,3 +15,17 @@ f"{0}".format(1)
print(f"{0}".format(1))
''.format(1)
'{1} {0}'.format(*args)
"{1}_{0}".format(*args, 1)
"{1}_{0}".format(*args, 1, 2)
"{1}_{0}".format(1, **kwargs)
"{1}_{0}".format(1, foo=2)
"{1}_{0}".format(1, 2, **kwargs)
"{1}_{0}".format(1, 2, foo=3, bar=4)

View File

@@ -1,28 +0,0 @@
# These SHOULD change
args = list(range(10))
kwargs = {x: x for x in range(10)}
"{0}".format(*args)
"{0}".format(**kwargs)
"{0}_{1}".format(*args)
"{0}_{1}".format(1, *args)
"{1}_{0}".format(*args)
"{1}_{0}".format(1, *args)
"{0}_{1}".format(1, 2, *args)
"{0}_{1}".format(*args, 1, 2)
"{0}_{1}_{2}".format(1, **kwargs)
"{0}_{1}_{2}".format(1, 2, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, **kwargs)
"{0}_{1}_{2}".format(1, 2, 3, *args, **kwargs)

View File

@@ -106,3 +106,7 @@ print('Hello %(arg)s' % bar['bop'])
"""
% (x,)
)
"%s" % (
x, # comment
)

View File

@@ -6,8 +6,12 @@
"{1} {0}".format(a, b)
"{0} {1} {0}".format(a, b)
"{x.y}".format(x=z)
"{x} {y} {x}".format(x=a, y=b)
"{.x} {.y}".format(a, b)
"{} {}".format(a.b, c.d)
@@ -72,6 +76,58 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
111111
)
"{a}" "{b}".format(a=1, b=1)
(
"{a}"
"{b}"
).format(a=1, b=1)
(
"{a}"
""
"{b}"
""
).format(a=1, b=1)
(
(
# comment
"{a}"
# comment
"{b}"
)
# comment
.format(a=1, b=1)
)
(
"{a}"
"b"
).format(a=1)
def d(osname, version, release):
return"{}-{}.{}".format(osname, version, release)
def e():
yield"{}".format(1)
assert"{}".format(1)
async def c():
return "{}".format(await 3)
async def c():
return "{}".format(1 + await 3)
"{}".format(1 * 2)
###
# Non-errors
###
@@ -85,8 +141,6 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
"{} {}".format(*a)
"{0} {0}".format(arg)
"{x} {x}".format(arg)
"{x.y} {x.z}".format(arg)
@@ -103,8 +157,6 @@ b"{} {}".format(a, b)
r'"\N{snowman} {}".format(a)'
"{a}" "{b}".format(a=1, b=1)
"123456789 {}".format(
11111111111111111111111111111111111111111111111111111111111111111111111111,
)
@@ -140,20 +192,13 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
]
)
async def c():
return "{}".format(await 3)
(
"{a}"
"{1 + 2}"
).format(a=1)
"{}".format(**c)
async def c():
return "{}".format(1 + await 3)
def d(osname, version, release):
return"{}-{}.{}".format(osname, version, release)
def e():
yield"{}".format(1)
assert"{}".format(1)
"{}".format(
1 # comment
)

View File

@@ -46,6 +46,35 @@ from typing import Callable, Match, Pattern, List, OrderedDict, AbstractSet, Con
if True: from collections import (
Mapping, Counter)
# Bad imports from PYI027 that are now handled by PYI022 (UP035)
from typing import ContextManager
from typing import OrderedDict
from typing_extensions import OrderedDict
from typing import Callable
from typing import ByteString
from typing import Container
from typing import Hashable
from typing import ItemsView
from typing import Iterable
from typing import Iterator
from typing import KeysView
from typing import Mapping
from typing import MappingView
from typing import MutableMapping
from typing import MutableSequence
from typing import MutableSet
from typing import Sequence
from typing import Sized
from typing import ValuesView
from typing import Awaitable
from typing import AsyncIterator
from typing import AsyncIterable
from typing import Coroutine
from typing import Collection
from typing import AsyncGenerator
from typing import Reversible
from typing import Generator
# OK
from a import b

View File

@@ -1,5 +1,3 @@
"""A mirror of UP037_1.py, with `from __future__ import annotations`."""
from __future__ import annotations
from typing import (

View File

@@ -1,108 +0,0 @@
"""A mirror of UP037_0.py, without `from __future__ import annotations`."""
from typing import (
Annotated,
Callable,
List,
Literal,
NamedTuple,
Tuple,
TypeVar,
TypedDict,
cast,
)
from mypy_extensions import Arg, DefaultArg, DefaultNamedArg, NamedArg, VarArg
def foo(var: "MyClass") -> "MyClass":
x: "MyClass"
def foo(*, inplace: "bool"):
pass
def foo(*args: "str", **kwargs: "int"):
pass
x: Tuple["MyClass"]
x: Callable[["MyClass"], None]
class Foo(NamedTuple):
x: "MyClass"
class D(TypedDict):
E: TypedDict("E", foo="int", total=False)
class D(TypedDict):
E: TypedDict("E", {"foo": "int"})
x: Annotated["str", "metadata"]
x: Arg("str", "name")
x: DefaultArg("str", "name")
x: NamedArg("str", "name")
x: DefaultNamedArg("str", "name")
x: DefaultNamedArg("str", name="name")
x: VarArg("str")
x: List[List[List["MyClass"]]]
x: NamedTuple("X", [("foo", "int"), ("bar", "str")])
x: NamedTuple("X", fields=[("foo", "int"), ("bar", "str")])
x: NamedTuple(typename="X", fields=[("foo", "int")])
X: MyCallable("X")
# OK
class D(TypedDict):
E: TypedDict("E")
x: Annotated[()]
x: DefaultNamedArg(name="name", quox="str")
x: DefaultNamedArg(name="name")
x: NamedTuple("X", [("foo",), ("bar",)])
x: NamedTuple("X", ["foo", "bar"])
x: NamedTuple()
x: Literal["foo", "bar"]
x = cast(x, "str")
def foo(x, *args, **kwargs):
...
def foo(*, inplace):
...
x: Annotated[1:2] = ...
x = TypeVar("x", "str", "int")
x = cast("str", x)
X = List["MyClass"]

View File

@@ -0,0 +1,47 @@
import typing
from typing import TypeAlias
# UP040
x: typing.TypeAlias = int
x: TypeAlias = int
# UP040 simple generic
T = typing.TypeVar["T"]
x: typing.TypeAlias = list[T]
# UP040 call style generic
T = typing.TypeVar("T")
x: typing.TypeAlias = list[T]
# UP040 bounded generic (todo)
T = typing.TypeVar("T", bound=int)
x: typing.TypeAlias = list[T]
T = typing.TypeVar("T", int, str)
x: typing.TypeAlias = list[T]
# UP040 contravariant generic (todo)
T = typing.TypeVar("T", contravariant=True)
x: typing.TypeAlias = list[T]
# UP040 covariant generic (todo)
T = typing.TypeVar("T", covariant=True)
x: typing.TypeAlias = list[T]
# UP040 in class scope
T = typing.TypeVar["T"]
class Foo:
# reference to global variable
x: typing.TypeAlias = list[T]
# reference to class variable
TCLS = typing.TypeVar["TCLS"]
y: typing.TypeAlias = list[TCLS]
# UP040 wont add generics in fix
T = typing.TypeVar(*args)
x: typing.TypeAlias = list[T]
# OK
x: TypeAlias
x: int = 1

View File

@@ -11,6 +11,8 @@ class A:
without_annotation = []
class_variable: ClassVar[list[int]] = []
final_variable: Final[list[int]] = []
class_variable_without_subscript: ClassVar = []
final_variable_without_subscript: Final = []
from dataclasses import dataclass, field

View File

@@ -0,0 +1,5 @@
# ruff: noqa: RUF100
import os # noqa: F401
print(os.sep)

View File

@@ -33,7 +33,7 @@ impl<'a, T: Codegen<'a>> CodegenStylist<'a> for T {
///
/// Returns `Ok(None)` if the statement is empty after removing the imports.
pub(crate) fn remove_imports<'a>(
imports: impl Iterator<Item = &'a str>,
member_names: impl Iterator<Item = &'a str>,
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
@@ -45,27 +45,20 @@ pub(crate) fn remove_imports<'a>(
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
names
} else if let ImportNames::Star(..) = &import_body.names {
// Special-case: if the import is a `from ... import *`, then we delete the
// entire statement.
let mut found_star = false;
for import in imports {
let qualified_name = match import_body.module.as_ref() {
Some(module_name) => format!("{}.*", compose_module_path(module_name)),
None => "*".to_string(),
};
if import == qualified_name {
for member in member_names {
if member == "*" {
found_star = true;
} else {
bail!("Expected \"*\" for unused import (got: \"{}\")", import);
bail!("Expected \"*\" for unused import (got: \"{}\")", member);
}
}
if !found_star {
@@ -82,30 +75,10 @@ pub(crate) fn remove_imports<'a>(
// Preserve the trailing comma (or not) from the last entry.
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
for import in imports {
let alias_index = aliases.iter().position(|alias| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == import
});
for member in member_names {
let alias_index = aliases
.iter()
.position(|alias| member == compose_module_path(&alias.name));
if let Some(index) = alias_index {
aliases.remove(index);
}
@@ -139,7 +112,7 @@ pub(crate) fn remove_imports<'a>(
///
/// Returns the modified import statement.
pub(crate) fn retain_imports(
imports: &[&str],
member_names: &[&str],
stmt: &Stmt,
locator: &Locator,
stylist: &Stylist,
@@ -151,14 +124,11 @@ pub(crate) fn retain_imports(
bail!("Expected Statement::Simple");
};
let (aliases, import_module) = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => (&mut import_body.names, None),
let aliases = match body.body.first_mut() {
Some(SmallStatement::Import(import_body)) => &mut import_body.names,
Some(SmallStatement::ImportFrom(import_body)) => {
if let ImportNames::Aliases(names) = &mut import_body.names {
(
names,
Some((&import_body.relative, import_body.module.as_ref())),
)
names
} else {
bail!("Expected: ImportNames::Aliases");
}
@@ -170,28 +140,9 @@ pub(crate) fn retain_imports(
let trailing_comma = aliases.last().and_then(|alias| alias.comma.clone());
aliases.retain(|alias| {
imports.iter().any(|import| {
let qualified_name = match import_module {
Some((relative, module)) => {
let module = module.map(compose_module_path);
let member = compose_module_path(&alias.name);
let mut qualified_name = String::with_capacity(
relative.len() + module.as_ref().map_or(0, String::len) + member.len() + 1,
);
for _ in 0..relative.len() {
qualified_name.push('.');
}
if let Some(module) = module {
qualified_name.push_str(&module);
qualified_name.push('.');
}
qualified_name.push_str(&member);
qualified_name
}
None => compose_module_path(&alias.name),
};
qualified_name == *import
})
member_names
.iter()
.any(|member| *member == compose_module_path(&alias.name))
});
// But avoid destroying any trailing comments.

View File

@@ -1,14 +1,17 @@
//! Interface for generating autofix edits from higher-level actions (e.g., "remove an argument").
use anyhow::{bail, Result};
use ruff_python_ast::{self as ast, ExceptHandler, Expr, Keyword, Ranged, Stmt};
use ruff_python_parser::{lexer, Mode};
use ruff_text_size::{TextLen, TextRange, TextSize};
use ruff_diagnostics::Edit;
use ruff_python_ast::{
self as ast, Arguments, ExceptHandler, Expr, Keyword, PySourceType, Ranged, Stmt,
};
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_parser::{lexer, AsMode};
use ruff_python_trivia::{has_leading_content, is_python_whitespace, PythonWhitespace};
use ruff_source_file::{Locator, NewlineWithTrailingNewline};
use ruff_text_size::{TextLen, TextRange, TextSize};
use crate::autofix::codemods;
@@ -55,118 +58,115 @@ pub(crate) fn delete_stmt(
/// Generate a `Fix` to remove the specified imports from an `import` statement.
pub(crate) fn remove_unused_imports<'a>(
unused_imports: impl Iterator<Item = &'a str>,
member_names: impl Iterator<Item = &'a str>,
stmt: &Stmt,
parent: Option<&Stmt>,
locator: &Locator,
stylist: &Stylist,
indexer: &Indexer,
) -> Result<Edit> {
match codemods::remove_imports(unused_imports, stmt, locator, stylist)? {
match codemods::remove_imports(member_names, stmt, locator, stylist)? {
None => Ok(delete_stmt(stmt, parent, locator, indexer)),
Some(content) => Ok(Edit::range_replacement(content, stmt.range())),
}
}
#[derive(Debug, Copy, Clone)]
pub(crate) enum Parentheses {
/// Remove parentheses, if the removed argument is the only argument left.
Remove,
/// Preserve parentheses, even if the removed argument is the only argument
Preserve,
}
/// Generic function to remove arguments or keyword arguments in function
/// calls and class definitions. (For classes `args` should be considered
/// `bases`)
///
/// Supports the removal of parentheses when this is the only (kw)arg left.
/// For this behavior, set `remove_parentheses` to `true`.
pub(crate) fn remove_argument(
pub(crate) fn remove_argument<T: Ranged>(
argument: &T,
arguments: &Arguments,
parentheses: Parentheses,
locator: &Locator,
call_at: TextSize,
expr_range: TextRange,
args: &[Expr],
keywords: &[Keyword],
remove_parentheses: bool,
source_type: PySourceType,
) -> Result<Edit> {
// TODO(sbrugman): Preserve trailing comments.
let contents = locator.after(call_at);
if arguments.keywords.len() + arguments.args.len() > 1 {
let mut fix_start = None;
let mut fix_end = None;
let mut fix_start = None;
let mut fix_end = None;
let n_arguments = keywords.len() + args.len();
if n_arguments == 0 {
bail!("No arguments or keywords to remove");
}
if n_arguments == 1 {
// Case 1: there is only one argument.
let mut count = 0u32;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
if tok.is_lpar() {
if count == 0 {
fix_start = Some(if remove_parentheses {
range.start()
} else {
range.start() + TextSize::from(1)
});
}
count = count.saturating_add(1);
}
if tok.is_rpar() {
count = count.saturating_sub(1);
if count == 0 {
fix_end = Some(if remove_parentheses {
if arguments
.args
.iter()
.map(Expr::start)
.chain(arguments.keywords.iter().map(Keyword::start))
.any(|location| location > argument.start())
{
// Case 1: argument or keyword is _not_ the last node, so delete from the start of the
// argument to the end of the subsequent comma.
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
arguments.start(),
)
.flatten()
{
if seen_comma {
if tok.is_non_logical_newline() {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if tok.is_newline() {
range.end()
} else {
range.end() - TextSize::from(1)
range.start()
});
break;
}
if range.start() == argument.start() {
fix_start = Some(range.start());
}
if fix_start.is_some() && tok.is_comma() {
seen_comma = true;
}
}
} else {
// Case 2: argument or keyword is the last node, so delete from the start of the
// previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
source_type.as_mode(),
arguments.start(),
)
.flatten()
{
if range.start() == argument.start() {
fix_end = Some(argument.end());
break;
}
if tok.is_comma() {
fix_start = Some(range.start());
}
}
}
} else if args
.iter()
.map(Expr::start)
.chain(keywords.iter().map(Keyword::start))
.any(|location| location > expr_range.start())
{
// Case 2: argument or keyword is _not_ the last node.
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
if seen_comma {
if tok.is_non_logical_newline() {
// Also delete any non-logical newlines after the comma.
continue;
}
fix_end = Some(if tok.is_newline() {
range.end()
} else {
range.start()
});
break;
}
if range.start() == expr_range.start() {
fix_start = Some(range.start());
}
if fix_start.is_some() && tok.is_comma() {
seen_comma = true;
match (fix_start, fix_end) {
(Some(start), Some(end)) => Ok(Edit::deletion(start, end)),
_ => {
bail!("No fix could be constructed")
}
}
} else {
// Case 3: argument or keyword is the last node, so we have to find the last
// comma in the stmt.
for (tok, range) in lexer::lex_starts_at(contents, Mode::Module, call_at).flatten() {
if range.start() == expr_range.start() {
fix_end = Some(expr_range.end());
break;
// Only one argument; remove it (but preserve parentheses, if needed).
Ok(match parentheses {
Parentheses::Remove => Edit::deletion(arguments.start(), arguments.end()),
Parentheses::Preserve => {
Edit::replacement("()".to_string(), arguments.start(), arguments.end())
}
if tok.is_comma() {
fix_start = Some(range.start());
}
}
}
match (fix_start, fix_end) {
(Some(start), Some(end)) => Ok(Edit::deletion(start, end)),
_ => {
bail!("No fix could be constructed")
}
})
}
}
@@ -179,16 +179,13 @@ fn is_only<T: PartialEq>(vec: &[T], value: &T) -> bool {
fn is_lone_child(child: &Stmt, parent: &Stmt) -> bool {
match parent {
Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. })
| Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. }) => {
if is_only(body, child) {
return true;
}
}
Stmt::For(ast::StmtFor { body, orelse, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, orelse, .. })
| Stmt::While(ast::StmtWhile { body, orelse, .. }) => {
if is_only(body, child) || is_only(orelse, child) {
return true;
@@ -294,24 +291,24 @@ fn next_stmt_break(semicolon: TextSize, locator: &Locator) -> TextSize {
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_python_ast::{Ranged, Suite};
use ruff_python_parser::Parse;
use ruff_text_size::TextSize;
use ruff_python_ast::Ranged;
use ruff_python_parser::parse_suite;
use ruff_source_file::Locator;
use ruff_text_size::TextSize;
use crate::autofix::edits::{next_stmt_break, trailing_semicolon};
#[test]
fn find_semicolon() -> Result<()> {
let contents = "x = 1";
let program = Suite::parse(contents, "<filename>")?;
let program = parse_suite(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(trailing_semicolon(stmt.end(), &locator), None);
let contents = "x = 1; y = 1";
let program = Suite::parse(contents, "<filename>")?;
let program = parse_suite(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(
@@ -320,7 +317,7 @@ mod tests {
);
let contents = "x = 1 ; y = 1";
let program = Suite::parse(contents, "<filename>")?;
let program = parse_suite(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(
@@ -333,7 +330,7 @@ x = 1 \
; y = 1
"
.trim();
let program = Suite::parse(contents, "<filename>")?;
let program = parse_suite(contents, "<filename>")?;
let stmt = program.first().unwrap();
let locator = Locator::new(contents);
assert_eq!(

View File

@@ -76,7 +76,7 @@ fn apply_fixes<'a>(
}
// If this fix overlaps with a fix we've already applied, skip it.
if last_pos.map_or(false, |last_pos| last_pos >= first.start()) {
if last_pos.is_some_and(|last_pos| last_pos >= first.start()) {
continue;
}
}

View File

@@ -56,13 +56,11 @@ pub(crate) fn bindings(checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) {
if let Some(diagnostic) =
flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding)
{
checker.diagnostics.push(diagnostic);
}
if checker.enabled(Rule::UnaliasedCollectionsAbcSetImport) {
if let Some(diagnostic) =
flake8_pyi::rules::unaliased_collections_abc_set_import(checker, binding)
{
checker.diagnostics.push(diagnostic);
}
}
}

View File

@@ -11,21 +11,18 @@ pub(crate) fn deferred_for_loops(checker: &mut Checker) {
for snapshot in for_loops {
checker.semantic.restore(snapshot);
if let Stmt::For(ast::StmtFor {
let Stmt::For(ast::StmtFor {
target, iter, body, ..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target, iter, body, ..
}) = &checker.semantic.stmt()
{
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
} else {
unreachable!("Expected Expr::For | Expr::AsyncFor");
}) = checker.semantic.current_statement()
else {
unreachable!("Expected Stmt::For");
};
if checker.enabled(Rule::UnusedLoopControlVariable) {
flake8_bugbear::rules::unused_loop_control_variable(checker, target, body);
}
if checker.enabled(Rule::IncorrectDictIterator) {
perflint::rules::incorrect_dict_iterator(checker, target, iter);
}
}
}

View File

@@ -1,5 +1,4 @@
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::{branch_detection, visibility};
use ruff_python_semantic::{Binding, BindingKind, ScopeKind};
@@ -37,7 +36,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// Identify any valid runtime imports. If a module is imported at runtime, and
// used at runtime, then by default, we avoid flagging any other
// imports from that model as typing-only.
let enforce_typing_imports = !checker.is_stub
let enforce_typing_imports = !checker.source_type.is_stub()
&& checker.any_enabled(&[
Rule::RuntimeImportInTypeCheckingBlock,
Rule::TypingOnlyFirstPartyImport,
@@ -112,7 +111,11 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &checker.semantic.stmts)
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
})
}) {
continue;
@@ -168,16 +171,25 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
continue;
}
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition()
&& visibility::is_overload(
cast::decorator_list(
checker.semantic.stmts[shadowed.source.unwrap()],
),
&checker.semantic,
)
{
let Some(statement_id) = shadowed.source else {
continue;
};
// If this is an overloaded function, abort.
if shadowed.kind.is_function_definition() {
if checker
.semantic
.statement(statement_id)
.as_function_def_stmt()
.is_some_and(|function| {
visibility::is_overload(
&function.decorator_list,
&checker.semantic,
)
})
{
continue;
}
}
} else {
// Only enforce cross-scope shadowing for imports.
@@ -195,7 +207,11 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
// If the bindings are in different forks, abort.
if shadowed.source.map_or(true, |left| {
binding.source.map_or(true, |right| {
branch_detection::different_forks(left, right, &checker.semantic.stmts)
branch_detection::different_forks(
left,
right,
checker.semantic.statements(),
)
})
}) {
continue;
@@ -218,25 +234,20 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}
if checker.is_stub {
if checker.enabled(Rule::UnusedPrivateTypeVar) {
flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateProtocol) {
flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypeAlias) {
flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypedDict) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypeVar) {
flake8_pyi::rules::unused_private_type_var(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateProtocol) {
flake8_pyi::rules::unused_private_protocol(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypeAlias) {
flake8_pyi::rules::unused_private_type_alias(checker, scope, &mut diagnostics);
}
if checker.enabled(Rule::UnusedPrivateTypedDict) {
flake8_pyi::rules::unused_private_typed_dict(checker, scope, &mut diagnostics);
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Lambda(_)
) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Lambda(_)) {
if checker.enabled(Rule::UnusedVariable) {
pyflakes::rules::unused_variable(checker, scope, &mut diagnostics);
}
@@ -245,7 +256,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
pyflakes::rules::unused_annotation(checker, scope, &mut diagnostics);
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnusedClassMethodArgument,
Rule::UnusedFunctionArgument,
@@ -262,10 +273,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
}
}
if matches!(
scope.kind,
ScopeKind::Function(_) | ScopeKind::AsyncFunction(_) | ScopeKind::Module
) {
if matches!(scope.kind, ScopeKind::Function(_) | ScopeKind::Module) {
if enforce_typing_imports {
let runtime_imports: Vec<&Binding> = checker
.semantic

View File

@@ -30,8 +30,8 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
]);
let enforce_stubs = checker.is_stub
&& checker.any_enabled(&[Rule::DocstringInStub, Rule::IterMethodReturnIterable]);
let enforce_stubs = checker.source_type.is_stub() && checker.enabled(Rule::DocstringInStub);
let enforce_stubs_and_runtime = checker.enabled(Rule::IterMethodReturnIterable);
let enforce_docstrings = checker.any_enabled(&[
Rule::BlankLineAfterLastSection,
Rule::BlankLineAfterSummary,
@@ -81,7 +81,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::UndocumentedPublicPackage,
]);
if !enforce_annotations && !enforce_docstrings && !enforce_stubs {
if !enforce_annotations && !enforce_docstrings && !enforce_stubs && !enforce_stubs_and_runtime {
return;
}
@@ -117,7 +117,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
// interfaces, without any AST nodes in between. Right now, we
// only error when traversing definition boundaries (functions,
// classes, etc.).
if !overloaded_name.map_or(false, |overloaded_name| {
if !overloaded_name.is_some_and(|overloaded_name| {
flake8_annotations::helpers::is_overload_impl(
definition,
&overloaded_name,
@@ -141,6 +141,8 @@ pub(crate) fn definitions(checker: &mut Checker) {
if checker.enabled(Rule::DocstringInStub) {
flake8_pyi::rules::docstring_in_stubs(checker, docstring);
}
}
if enforce_stubs_and_runtime {
if checker.enabled(Rule::IterMethodReturnIterable) {
flake8_pyi::rules::iter_method_return_iterable(checker, definition);
}

View File

@@ -1,4 +1,4 @@
use ruff_python_ast::{self as ast, Constant, Expr, ExprContext, Operator, Ranged};
use ruff_python_ast::{self as ast, Arguments, Constant, Expr, ExprContext, Operator, Ranged};
use ruff_python_literal::cformat::{CFormatError, CFormatErrorType};
use ruff_diagnostics::Diagnostic;
@@ -31,7 +31,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if let Some(operator) = typing::to_pep604_operator(value, slice, &checker.semantic)
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py310
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -44,7 +44,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP604Annotation) {
if checker.is_stub
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py310
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -59,7 +59,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
// Ex) list[...]
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
@@ -74,24 +74,27 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
// Ex) Union[...]
if checker.any_enabled(&[Rule::UnnecessaryLiteralUnion, Rule::DuplicateUnionMember]) {
// Determine if the current expression is an union
// Avoid duplicate checks if the parent is an `Union[...]` since these rules traverse nested unions
let is_unchecked_union = checker
.semantic
.expr_grandparent()
.and_then(Expr::as_subscript_expr)
.map_or(true, |parent| {
!checker.semantic.match_typing_expr(&parent.value, "Union")
});
if is_unchecked_union {
if checker.any_enabled(&[
Rule::UnnecessaryLiteralUnion,
Rule::DuplicateUnionMember,
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
]) {
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::DuplicateUnionMember) {
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
}
}
@@ -152,10 +155,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::NumpyDeprecatedFunction) {
numpy::rules::deprecated_function(checker, expr);
}
if checker.is_stub {
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
// Ex) List[...]
@@ -167,20 +168,18 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
typing::to_pep585_generic(expr, &checker.semantic)
{
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
&& !checker.settings.pyupgrade.keep_runtime_typing
{
flake8_future_annotations::rules::future_rewritable_type_annotation(
checker, expr,
);
flake8_future_annotations::rules::future_rewritable_type_annotation(checker, expr);
}
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.is_stub
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -199,13 +198,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
ExprContext::Store => {
if checker.enabled(Rule::NonLowercaseVariableInFunction) {
if checker.semantic.scope().kind.is_any_function() {
if checker.semantic.current_scope().kind.is_function() {
// Ignore globals.
if !checker
.semantic
.scope()
.current_scope()
.get(id)
.map_or(false, |binding_id| {
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
@@ -216,16 +215,19 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
if let ScopeKind::Class(ast::StmtClassDef { bases, .. }) =
&checker.semantic.scope().kind
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
&checker.semantic.current_scope().kind
{
pep8_naming::rules::mixed_case_variable_in_class_scope(
checker, expr, id, bases,
checker,
expr,
id,
arguments.as_deref(),
);
}
}
if checker.enabled(Rule::MixedCaseVariableInGlobalScope) {
if matches!(checker.semantic.scope().kind, ScopeKind::Module) {
if matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
pep8_naming::rules::mixed_case_variable_in_global_scope(
checker, expr, id,
);
@@ -238,7 +240,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_attribute_shadowing(
checker, class_def, id, *range,
@@ -267,7 +269,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
]) {
if let Some(replacement) = typing::to_pep585_generic(expr, &checker.semantic) {
if checker.enabled(Rule::FutureRewritableTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py39
&& checker.settings.target_version >= PythonVersion::Py37
&& !checker.semantic.future_annotations()
@@ -280,7 +282,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::NonPEP585Annotation) {
if checker.is_stub
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py39
|| (checker.settings.target_version >= PythonVersion::Py37
&& checker.semantic.future_annotations()
@@ -318,22 +320,26 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::PrivateMemberAccess) {
flake8_self::rules::private_member_access(checker, expr);
}
if checker.is_stub {
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
if checker.enabled(Rule::CollectionsNamedTuple) {
flake8_pyi::rules::collections_named_tuple(checker, expr);
}
pandas_vet::rules::attr(checker, attr, value, expr);
}
Expr::Call(
call @ ast::ExprCall {
func,
args,
keywords,
arguments:
Arguments {
args,
keywords,
range: _,
},
range: _,
},
) => {
if checker.any_enabled(&[
// pylint
Rule::BadStringFormatCharacter,
// pyflakes
Rule::StringDotFormatInvalidFormat,
Rule::StringDotFormatExtraNamedArguments,
@@ -382,9 +388,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
.enabled(Rule::StringDotFormatExtraPositionalArguments)
{
pyflakes::rules::string_dot_format_extra_positional_arguments(
checker,
&summary, args, location,
);
checker, &summary, args, location,
);
}
if checker.enabled(Rule::StringDotFormatMissingArguments) {
pyflakes::rules::string_dot_format_missing_argument(
@@ -397,7 +402,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::FormatLiterals) {
pyupgrade::rules::format_literals(checker, &summary, expr);
pyupgrade::rules::format_literals(checker, &summary, call);
}
if checker.enabled(Rule::FString) {
pyupgrade::rules::f_strings(
@@ -410,6 +415,14 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
}
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::call(
checker,
val.as_str(),
location,
);
}
}
}
}
@@ -424,10 +437,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::super_call_with_parameters(checker, expr, func, args);
}
if checker.enabled(Rule::UnnecessaryEncodeUTF8) {
pyupgrade::rules::unnecessary_encode_utf8(checker, expr, func, args, keywords);
pyupgrade::rules::unnecessary_encode_utf8(checker, call);
}
if checker.enabled(Rule::RedundantOpenModes) {
pyupgrade::rules::redundant_open_modes(checker, expr);
pyupgrade::rules::redundant_open_modes(checker, call);
}
if checker.enabled(Rule::NativeLiterals) {
pyupgrade::rules::native_literals(checker, expr, func, args, keywords);
@@ -436,10 +449,10 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyupgrade::rules::open_alias(checker, expr, func);
}
if checker.enabled(Rule::ReplaceUniversalNewlines) {
pyupgrade::rules::replace_universal_newlines(checker, func, keywords);
pyupgrade::rules::replace_universal_newlines(checker, call);
}
if checker.enabled(Rule::ReplaceStdoutStderr) {
pyupgrade::rules::replace_stdout_stderr(checker, expr, func, args, keywords);
pyupgrade::rules::replace_stdout_stderr(checker, call);
}
if checker.enabled(Rule::OSErrorAlias) {
pyupgrade::rules::os_error_alias_call(checker, func);
@@ -459,7 +472,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_async::rules::blocking_os_call(checker, expr);
}
if checker.any_enabled(&[Rule::Print, Rule::PPrint]) {
flake8_print::rules::print_call(checker, func, keywords);
flake8_print::rules::print_call(checker, call);
}
if checker.any_enabled(&[
Rule::SuspiciousPickleUsage,
@@ -511,13 +524,11 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
if checker.enabled(Rule::ZipWithoutExplicitStrict) {
if checker.settings.target_version >= PythonVersion::Py310 {
flake8_bugbear::rules::zip_without_explicit_strict(
checker, expr, func, args, keywords,
);
flake8_bugbear::rules::zip_without_explicit_strict(checker, call);
}
}
if checker.enabled(Rule::NoExplicitStacklevel) {
flake8_bugbear::rules::no_explicit_stacklevel(checker, func, keywords);
flake8_bugbear::rules::no_explicit_stacklevel(checker, call);
}
if checker.enabled(Rule::UnnecessaryDictKwargs) {
flake8_pie::rules::unnecessary_dict_kwargs(checker, expr, keywords);
@@ -526,22 +537,22 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bandit::rules::exec_used(checker, func);
}
if checker.enabled(Rule::BadFilePermissions) {
flake8_bandit::rules::bad_file_permissions(checker, func, args, keywords);
flake8_bandit::rules::bad_file_permissions(checker, call);
}
if checker.enabled(Rule::RequestWithNoCertValidation) {
flake8_bandit::rules::request_with_no_cert_validation(checker, func, keywords);
flake8_bandit::rules::request_with_no_cert_validation(checker, call);
}
if checker.enabled(Rule::UnsafeYAMLLoad) {
flake8_bandit::rules::unsafe_yaml_load(checker, func, args, keywords);
flake8_bandit::rules::unsafe_yaml_load(checker, call);
}
if checker.enabled(Rule::SnmpInsecureVersion) {
flake8_bandit::rules::snmp_insecure_version(checker, func, keywords);
flake8_bandit::rules::snmp_insecure_version(checker, call);
}
if checker.enabled(Rule::SnmpWeakCryptography) {
flake8_bandit::rules::snmp_weak_cryptography(checker, func, args, keywords);
flake8_bandit::rules::snmp_weak_cryptography(checker, call);
}
if checker.enabled(Rule::Jinja2AutoescapeFalse) {
flake8_bandit::rules::jinja2_autoescape_false(checker, func, keywords);
flake8_bandit::rules::jinja2_autoescape_false(checker, call);
}
if checker.enabled(Rule::HardcodedPasswordFuncArg) {
flake8_bandit::rules::hardcoded_password_func_arg(checker, keywords);
@@ -550,18 +561,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_bandit::rules::hardcoded_sql_expression(checker, expr);
}
if checker.enabled(Rule::HashlibInsecureHashFunction) {
flake8_bandit::rules::hashlib_insecure_hash_functions(
checker, func, args, keywords,
);
flake8_bandit::rules::hashlib_insecure_hash_functions(checker, call);
}
if checker.enabled(Rule::RequestWithoutTimeout) {
flake8_bandit::rules::request_without_timeout(checker, func, keywords);
flake8_bandit::rules::request_without_timeout(checker, call);
}
if checker.enabled(Rule::ParamikoCall) {
flake8_bandit::rules::paramiko_call(checker, func);
}
if checker.enabled(Rule::LoggingConfigInsecureListen) {
flake8_bandit::rules::logging_config_insecure_listen(checker, func, keywords);
flake8_bandit::rules::logging_config_insecure_listen(checker, call);
}
if checker.any_enabled(&[
Rule::SubprocessWithoutShellEqualsTrue,
@@ -572,7 +581,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::StartProcessWithPartialPath,
Rule::UnixCommandWildcardInjection,
]) {
flake8_bandit::rules::shell_injection(checker, func, args, keywords);
flake8_bandit::rules::shell_injection(checker, call);
}
if checker.enabled(Rule::UnnecessaryGeneratorList) {
flake8_comprehensions::rules::unnecessary_generator_list(
@@ -656,7 +665,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_comprehensions::rules::unnecessary_map(
checker,
expr,
checker.semantic.expr_parent(),
checker.semantic.current_expression_parent(),
func,
args,
);
@@ -675,23 +684,17 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_debugger::rules::debugger_call(checker, expr, func);
}
if checker.enabled(Rule::PandasUseOfInplaceArgument) {
pandas_vet::rules::inplace_argument(checker, expr, func, args, keywords);
pandas_vet::rules::inplace_argument(checker, call);
}
pandas_vet::rules::call(checker, func);
if checker.enabled(Rule::PandasUseOfDotReadTable) {
pandas_vet::rules::use_of_read_table(checker, func, keywords);
pandas_vet::rules::use_of_read_table(checker, call);
}
if checker.enabled(Rule::PandasUseOfPdMerge) {
pandas_vet::rules::use_of_pd_merge(checker, func);
}
if checker.enabled(Rule::CallDatetimeWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_without_tzinfo(
checker,
func,
args,
keywords,
expr.range(),
);
flake8_datetimez::rules::call_datetime_without_tzinfo(checker, call);
}
if checker.enabled(Rule::CallDatetimeToday) {
flake8_datetimez::rules::call_datetime_today(checker, func, expr.range());
@@ -707,30 +710,13 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
if checker.enabled(Rule::CallDatetimeNowWithoutTzinfo) {
flake8_datetimez::rules::call_datetime_now_without_tzinfo(
checker,
func,
args,
keywords,
expr.range(),
);
flake8_datetimez::rules::call_datetime_now_without_tzinfo(checker, call);
}
if checker.enabled(Rule::CallDatetimeFromtimestamp) {
flake8_datetimez::rules::call_datetime_fromtimestamp(
checker,
func,
args,
keywords,
expr.range(),
);
flake8_datetimez::rules::call_datetime_fromtimestamp(checker, call);
}
if checker.enabled(Rule::CallDatetimeStrptimeWithoutZone) {
flake8_datetimez::rules::call_datetime_strptime_without_zone(
checker,
func,
args,
expr.range(),
);
flake8_datetimez::rules::call_datetime_strptime_without_zone(checker, call);
}
if checker.enabled(Rule::CallDateToday) {
flake8_datetimez::rules::call_date_today(checker, func, expr.range());
@@ -754,18 +740,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::bad_str_strip_call(checker, func, args);
}
if checker.enabled(Rule::InvalidEnvvarDefault) {
pylint::rules::invalid_envvar_default(checker, func, args, keywords);
pylint::rules::invalid_envvar_default(checker, call);
}
if checker.enabled(Rule::InvalidEnvvarValue) {
pylint::rules::invalid_envvar_value(checker, func, args, keywords);
pylint::rules::invalid_envvar_value(checker, call);
}
if checker.enabled(Rule::NestedMinMax) {
pylint::rules::nested_min_max(checker, expr, func, args, keywords);
}
if checker.enabled(Rule::PytestPatchWithLambda) {
if let Some(diagnostic) =
flake8_pytest_style::rules::patch_with_lambda(func, args, keywords)
{
if let Some(diagnostic) = flake8_pytest_style::rules::patch_with_lambda(call) {
checker.diagnostics.push(diagnostic);
}
}
@@ -777,16 +761,16 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::SubprocessPopenPreexecFn) {
pylint::rules::subprocess_popen_preexec_fn(checker, func, keywords);
pylint::rules::subprocess_popen_preexec_fn(checker, call);
}
if checker.any_enabled(&[
Rule::PytestRaisesWithoutException,
Rule::PytestRaisesTooBroad,
]) {
flake8_pytest_style::rules::raises_call(checker, func, args, keywords);
flake8_pytest_style::rules::raises_call(checker, call);
}
if checker.enabled(Rule::PytestFailWithoutMessage) {
flake8_pytest_style::rules::fail_call(checker, func, args, keywords);
flake8_pytest_style::rules::fail_call(checker, call);
}
if checker.enabled(Rule::PairwiseOverZipped) {
if checker.settings.target_version >= PythonVersion::Py310 {
@@ -857,7 +841,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_use_pathlib::rules::path_constructor_current_directory(checker, expr, func);
}
if checker.enabled(Rule::OsSepSplit) {
flake8_use_pathlib::rules::os_sep_split(checker, func, args, keywords);
flake8_use_pathlib::rules::os_sep_split(checker, call);
}
if checker.enabled(Rule::NumpyLegacyRandom) {
numpy::rules::legacy_random(checker, func);
@@ -872,15 +856,15 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::LoggingExcInfo,
Rule::LoggingRedundantExcInfo,
]) {
flake8_logging_format::rules::logging_call(checker, func, args, keywords);
flake8_logging_format::rules::logging_call(checker, call);
}
if checker.any_enabled(&[Rule::LoggingTooFewArgs, Rule::LoggingTooManyArgs]) {
pylint::rules::logging_call(checker, func, args, keywords);
pylint::rules::logging_call(checker, call);
}
if checker.enabled(Rule::DjangoLocalsInRenderFunction) {
flake8_django::rules::locals_in_render_function(checker, func, args, keywords);
flake8_django::rules::locals_in_render_function(checker, call);
}
if checker.is_stub && checker.enabled(Rule::UnsupportedMethodCallOnAll) {
if checker.enabled(Rule::UnsupportedMethodCallOnAll) {
flake8_pyi::rules::unsupported_method_call_on_all(checker, func);
}
}
@@ -931,7 +915,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pylint::rules::await_outside_async(checker, expr);
}
}
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, range: _ }) => {
if checker.enabled(Rule::FStringMissingPlaceholders) {
pyflakes::rules::f_string_missing_placeholders(expr, values, checker);
}
@@ -1045,6 +1029,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
checker.locator,
);
}
if checker.enabled(Rule::BadStringFormatCharacter) {
pylint::rules::bad_string_format_character::percent(checker, expr);
}
if checker.enabled(Rule::BadStringFormatType) {
pylint::rules::bad_string_format_type(checker, expr, right);
}
@@ -1076,7 +1063,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}) => {
// Ex) `str | None`
if checker.enabled(Rule::FutureRequiredTypeAnnotation) {
if !checker.is_stub
if !checker.source_type.is_stub()
&& checker.settings.target_version < PythonVersion::Py310
&& !checker.semantic.future_annotations()
&& checker.semantic.in_annotation()
@@ -1088,26 +1075,24 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
);
}
}
if checker.is_stub {
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
// Avoid duplicate checks if the parent is an `|`
&& !matches!(
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, ..}))
)
&& checker.semantic.in_type_definition()
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion)
// Avoid duplicate checks if the parent is an `|`
&& !matches!(
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp { op: Operator::BitOr, ..}))
)
{
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
}
}
}
Expr::UnaryOp(ast::ExprUnaryOp {
@@ -1142,12 +1127,14 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
flake8_simplify::rules::double_negation(checker, expr, *op, operand);
}
}
Expr::Compare(ast::ExprCompare {
left,
ops,
comparators,
range: _,
}) => {
Expr::Compare(
compare @ ast::ExprCompare {
left,
ops,
comparators,
range: _,
},
) => {
let check_none_comparisons = checker.enabled(Rule::NoneComparison);
let check_true_false_comparisons = checker.enabled(Rule::TrueFalseComparison);
if check_none_comparisons || check_true_false_comparisons {
@@ -1165,7 +1152,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
pyflakes::rules::invalid_literal_comparison(checker, left, ops, comparators, expr);
}
if checker.enabled(Rule::TypeComparison) {
pycodestyle::rules::type_comparison(checker, expr, ops, comparators);
pycodestyle::rules::type_comparison(checker, compare);
}
if checker.any_enabled(&[
Rule::SysVersionCmpStr3,
@@ -1216,7 +1203,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _,
range: _,
}) => {
if checker.is_stub && checker.enabled(Rule::NumericLiteralTooLong) {
if checker.source_type.is_stub() && checker.enabled(Rule::NumericLiteralTooLong) {
flake8_pyi::rules::numeric_literal_too_long(checker, expr);
}
}
@@ -1225,7 +1212,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
kind: _,
range: _,
}) => {
if checker.is_stub && checker.enabled(Rule::StringOrBytesTooLong) {
if checker.source_type.is_stub() && checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
}
@@ -1253,7 +1240,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
if checker.enabled(Rule::UnicodeKindPrefix) {
pyupgrade::rules::unicode_kind_prefix(checker, expr, kind.as_deref());
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::StringOrBytesTooLong) {
flake8_pyi::rules::string_or_bytes_too_long(checker, expr);
}
@@ -1261,7 +1248,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
Expr::Lambda(
lambda @ ast::ExprLambda {
args: _,
parameters: _,
body: _,
range: _,
},

View File

@@ -1,5 +1,3 @@
pub(super) use argument::argument;
pub(super) use arguments::arguments;
pub(super) use bindings::bindings;
pub(super) use comprehension::comprehension;
pub(super) use deferred_for_loops::deferred_for_loops;
@@ -8,12 +6,12 @@ pub(super) use definitions::definitions;
pub(super) use except_handler::except_handler;
pub(super) use expression::expression;
pub(super) use module::module;
pub(super) use parameter::parameter;
pub(super) use parameters::parameters;
pub(super) use statement::statement;
pub(super) use suite::suite;
pub(super) use unresolved_references::unresolved_references;
mod argument;
mod arguments;
mod bindings;
mod comprehension;
mod deferred_for_loops;
@@ -22,6 +20,8 @@ mod definitions;
mod except_handler;
mod expression;
mod module;
mod parameter;
mod parameters;
mod statement;
mod suite;
mod unresolved_references;

View File

@@ -1,27 +1,28 @@
use ruff_python_ast::{Arg, Ranged};
use ruff_python_ast::{Parameter, Ranged};
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_builtins, pep8_naming, pycodestyle};
/// Run lint rules over an [`Arg`] syntax node.
pub(crate) fn argument(arg: &Arg, checker: &mut Checker) {
/// Run lint rules over a [`Parameter`] syntax node.
pub(crate) fn parameter(parameter: &Parameter, checker: &mut Checker) {
if checker.enabled(Rule::AmbiguousVariableName) {
if let Some(diagnostic) = pycodestyle::rules::ambiguous_variable_name(&arg.arg, arg.range())
if let Some(diagnostic) =
pycodestyle::rules::ambiguous_variable_name(&parameter.name, parameter.range())
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::InvalidArgumentName) {
if let Some(diagnostic) = pep8_naming::rules::invalid_argument_name(
&arg.arg,
arg,
&parameter.name,
parameter,
&checker.settings.pep8_naming.ignore_names,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::BuiltinArgumentShadowing) {
flake8_builtins::rules::builtin_argument_shadowing(checker, arg);
flake8_builtins::rules::builtin_argument_shadowing(checker, parameter);
}
}

View File

@@ -1,26 +1,26 @@
use ruff_python_ast::Arguments;
use ruff_python_ast::Parameters;
use crate::checkers::ast::Checker;
use crate::codes::Rule;
use crate::rules::{flake8_bugbear, flake8_pyi, ruff};
/// Run lint rules over a [`Arguments`] syntax node.
pub(crate) fn arguments(arguments: &Arguments, checker: &mut Checker) {
/// Run lint rules over a [`Parameters`] syntax node.
pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.enabled(Rule::MutableArgumentDefault) {
flake8_bugbear::rules::mutable_argument_default(checker, arguments);
flake8_bugbear::rules::mutable_argument_default(checker, parameters);
}
if checker.enabled(Rule::FunctionCallInDefaultArgument) {
flake8_bugbear::rules::function_call_in_argument_default(checker, arguments);
flake8_bugbear::rules::function_call_in_argument_default(checker, parameters);
}
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
ruff::rules::implicit_optional(checker, arguments);
ruff::rules::implicit_optional(checker, parameters);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::TypedArgumentDefaultInStub) {
flake8_pyi::rules::typed_argument_simple_defaults(checker, arguments);
flake8_pyi::rules::typed_argument_simple_defaults(checker, parameters);
}
if checker.enabled(Rule::ArgumentDefaultInStub) {
flake8_pyi::rules::argument_simple_defaults(checker, arguments);
flake8_pyi::rules::argument_simple_defaults(checker, parameters);
}
}
}

View File

@@ -53,7 +53,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::BreakOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::break_outside_loop(
stmt,
&mut checker.semantic.parents().skip(1),
&mut checker.semantic.current_statements().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
@@ -63,27 +63,21 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ContinueOutsideLoop) {
if let Some(diagnostic) = pyflakes::rules::continue_outside_loop(
stmt,
&mut checker.semantic.parents().skip(1),
&mut checker.semantic.current_statements().skip(1),
) {
checker.diagnostics.push(diagnostic);
}
}
}
Stmt::FunctionDef(ast::StmtFunctionDef {
is_async,
name,
decorator_list,
returns,
args,
parameters,
body,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
decorator_list,
returns,
args,
body,
..
type_params,
range: _,
}) => {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
@@ -111,10 +105,10 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if let Some(diagnostic) =
pep8_naming::rules::invalid_first_argument_name_for_class_method(
checker,
checker.semantic.scope(),
checker.semantic.current_scope(),
name,
decorator_list,
args,
parameters,
)
{
checker.diagnostics.push(diagnostic);
@@ -123,15 +117,15 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::InvalidFirstArgumentNameForMethod) {
if let Some(diagnostic) = pep8_naming::rules::invalid_first_argument_name_for_method(
checker,
checker.semantic.scope(),
checker.semantic.current_scope(),
name,
decorator_list,
args,
parameters,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
@@ -141,41 +135,52 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::StubBodyMultipleStatements) {
flake8_pyi::rules::stub_body_multiple_statements(checker, stmt, body);
}
if checker.enabled(Rule::AnyEqNeAnnotation) {
flake8_pyi::rules::any_eq_ne_annotation(checker, name, args);
}
if checker.enabled(Rule::NonSelfReturnType) {
flake8_pyi::rules::non_self_return_type(
checker,
stmt,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
args,
stmt.is_async_function_def_stmt(),
);
}
}
if checker.enabled(Rule::AnyEqNeAnnotation) {
flake8_pyi::rules::any_eq_ne_annotation(checker, name, parameters);
}
if checker.enabled(Rule::NonSelfReturnType) {
flake8_pyi::rules::non_self_return_type(
checker,
stmt,
*is_async,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
);
}
if checker.enabled(Rule::CustomTypeVarReturnType) {
flake8_pyi::rules::custom_type_var_return_type(
checker,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
type_params.as_ref(),
);
}
if checker.source_type.is_stub() {
if checker.enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
}
}
if checker.source_type.is_stub()
|| checker.settings.target_version >= PythonVersion::Py311
{
if checker.enabled(Rule::NoReturnArgumentAnnotationInStub) {
flake8_pyi::rules::no_return_argument_annotation(checker, args);
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(
checker,
stmt.is_async_function_def_stmt(),
name,
args,
);
}
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, args);
flake8_pyi::rules::no_return_argument_annotation(checker, parameters);
}
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(checker, *is_async, name, parameters);
}
if checker.enabled(Rule::RedundantNumericUnion) {
flake8_pyi::rules::redundant_numeric_union(checker, parameters);
}
if checker.enabled(Rule::DunderFunctionName) {
if let Some(diagnostic) = pep8_naming::rules::dunder_function_name(
checker.semantic.scope(),
checker.semantic.current_scope(),
stmt,
name,
&checker.settings.pep8_naming.ignore_names,
@@ -230,13 +235,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
if checker.enabled(Rule::HardcodedPasswordDefault) {
flake8_bandit::rules::hardcoded_password_default(checker, args);
flake8_bandit::rules::hardcoded_password_default(checker, parameters);
}
if checker.enabled(Rule::PropertyWithParameters) {
pylint::rules::property_with_parameters(checker, stmt, decorator_list, args);
pylint::rules::property_with_parameters(checker, stmt, decorator_list, parameters);
}
if checker.enabled(Rule::TooManyArguments) {
pylint::rules::too_many_arguments(checker, args, stmt);
pylint::rules::too_many_arguments(checker, parameters, stmt);
}
if checker.enabled(Rule::TooManyReturnStatements) {
if let Some(diagnostic) = pylint::rules::too_many_return_statements(
@@ -282,7 +287,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker,
stmt,
name,
args,
parameters,
decorator_list,
body,
);
@@ -304,7 +309,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker,
name,
decorator_list,
args,
parameters,
);
}
if checker.enabled(Rule::BooleanDefaultValueInFunctionDefinition) {
@@ -312,7 +317,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker,
name,
decorator_list,
args,
parameters,
);
}
if checker.enabled(Rule::UnexpectedSpecialMethodSignature) {
@@ -321,7 +326,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
name,
decorator_list,
args,
parameters,
);
}
if checker.enabled(Rule::FStringDocstring) {
@@ -330,7 +335,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::YieldInForLoop) {
pyupgrade::rules::yield_in_for_loop(checker, stmt);
}
if let ScopeKind::Class(class_def) = checker.semantic.scope().kind {
if let ScopeKind::Class(class_def) = checker.semantic.current_scope().kind {
if checker.enabled(Rule::BuiltinAttributeShadowing) {
flake8_builtins::rules::builtin_method_shadowing(
checker,
@@ -363,8 +368,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
Stmt::ClassDef(
class_def @ ast::StmtClassDef {
name,
bases,
keywords,
arguments,
type_params: _,
decorator_list,
body,
@@ -375,23 +379,29 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_django::rules::nullable_model_string_field(checker, body);
}
if checker.enabled(Rule::DjangoExcludeWithModelForm) {
if let Some(diagnostic) =
flake8_django::rules::exclude_with_model_form(checker, bases, body)
{
if let Some(diagnostic) = flake8_django::rules::exclude_with_model_form(
checker,
arguments.as_deref(),
body,
) {
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::DjangoAllWithModelForm) {
if let Some(diagnostic) =
flake8_django::rules::all_with_model_form(checker, bases, body)
flake8_django::rules::all_with_model_form(checker, arguments.as_deref(), body)
{
checker.diagnostics.push(diagnostic);
}
}
if checker.enabled(Rule::DjangoUnorderedBodyContentInModel) {
flake8_django::rules::unordered_body_content_in_model(checker, bases, body);
flake8_django::rules::unordered_body_content_in_model(
checker,
arguments.as_deref(),
body,
);
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.enabled(Rule::DjangoModelWithoutDunderStr) {
flake8_django::rules::model_without_dunder_str(checker, class_def);
}
@@ -425,33 +435,37 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::ErrorSuffixOnExceptionName) {
if let Some(diagnostic) = pep8_naming::rules::error_suffix_on_exception_name(
stmt,
bases,
arguments.as_deref(),
name,
&checker.settings.pep8_naming.ignore_names,
) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::AbstractBaseClassWithoutAbstractMethod,
Rule::EmptyMethodWithoutAbstractDecorator,
]) {
flake8_bugbear::rules::abstract_base_class(
checker, stmt, name, bases, keywords, body,
checker,
stmt,
name,
arguments.as_deref(),
body,
);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
if checker.enabled(Rule::PassInClassBody) {
flake8_pyi::rules::pass_in_class_body(checker, stmt, body);
}
if checker.enabled(Rule::EllipsisInNonEmptyClassBody) {
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, stmt, body);
}
}
if checker.enabled(Rule::EllipsisInNonEmptyClassBody) {
flake8_pyi::rules::ellipsis_in_non_empty_class_body(checker, stmt, body);
}
if checker.enabled(Rule::PytestIncorrectMarkParenthesesStyle) {
flake8_pytest_style::rules::marks(checker, decorator_list);
@@ -478,7 +492,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_builtins::rules::builtin_variable_shadowing(checker, name, name.range());
}
if checker.enabled(Rule::DuplicateBases) {
pylint::rules::duplicate_bases(checker, name, bases);
pylint::rules::duplicate_bases(checker, name, arguments.as_deref());
}
if checker.enabled(Rule::NoSlotsInStrSubclass) {
flake8_slots::rules::no_slots_in_str_subclass(checker, stmt, class_def);
@@ -544,7 +558,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
alias,
);
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
}
@@ -719,7 +733,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::FutureAnnotationsInStub) {
flake8_pyi::rules::from_future_import(checker, import_from);
}
@@ -739,7 +753,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
} else if &alias.name == "*" {
if checker.enabled(Rule::UndefinedLocalWithNestedImportStarUsage) {
if !matches!(checker.semantic.scope().kind, ScopeKind::Module) {
if !matches!(checker.semantic.current_scope().kind, ScopeKind::Module) {
checker.diagnostics.push(Diagnostic::new(
pyflakes::rules::UndefinedLocalWithNestedImportStarUsage {
name: helpers::format_import_from(level, module),
@@ -864,7 +878,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if !checker.is_stub {
if !checker.source_type.is_stub() {
if checker.enabled(Rule::UselessImportAlias) {
pylint::rules::useless_import_alias(checker, alias);
}
@@ -955,7 +969,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_simplify::rules::nested_if_statements(
checker,
if_,
checker.semantic.stmt_parent(),
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::IfWithSameArms) {
@@ -977,7 +991,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
tryceratops::rules::type_check_without_type_error(
checker,
if_,
checker.semantic.stmt_parent(),
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::OutdatedVersionBlock) {
@@ -988,7 +1002,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
checker.diagnostics.push(diagnostic);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnrecognizedVersionInfoCheck,
Rule::PatchVersionComparison,
@@ -1070,8 +1084,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
pygrep_hooks::rules::non_existent_mock_method(checker, test);
}
}
Stmt::With(ast::StmtWith { items, body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { items, body, .. }) => {
Stmt::With(with_ @ ast::StmtWith { items, body, .. }) => {
if checker.enabled(Rule::AssertRaisesException) {
flake8_bugbear::rules::assert_raises_exception(checker, items);
}
@@ -1081,9 +1094,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::MultipleWithStatements) {
flake8_simplify::rules::multiple_with_statements(
checker,
stmt,
body,
checker.semantic.stmt_parent(),
with_,
checker.semantic.current_statement_parent(),
);
}
if checker.enabled(Rule::RedefinedLoopName) {
@@ -1107,13 +1119,6 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
iter,
orelse,
..
})
| Stmt::AsyncFor(ast::StmtAsyncFor {
target,
body,
iter,
orelse,
..
}) => {
if checker.any_enabled(&[Rule::UnusedLoopControlVariable, Rule::IncorrectDictIterator])
{
@@ -1300,7 +1305,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::TypeBivariance) {
pylint::rules::type_bivariance(checker, value);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.any_enabled(&[
Rule::UnprefixedTypeParam,
Rule::AssignmentDefaultInStub,
@@ -1311,8 +1316,8 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.scopes()
.any(|scope| scope.kind.is_any_function())
.current_scopes()
.any(|scope| scope.kind.is_function())
{
if checker.enabled(Rule::UnprefixedTypeParam) {
flake8_pyi::rules::prefix_type_params(checker, value, targets);
@@ -1337,12 +1342,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
}
}
}
Stmt::AnnAssign(ast::StmtAnnAssign {
target,
value,
annotation,
..
}) => {
Stmt::AnnAssign(
assign_stmt @ ast::StmtAnnAssign {
target,
value,
annotation,
..
},
) => {
if let Some(value) = value {
if checker.enabled(Rule::LambdaAssignment) {
pycodestyle::rules::lambda_assignment(
@@ -1365,14 +1372,17 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
stmt,
);
}
if checker.is_stub {
if checker.enabled(Rule::NonPEP695TypeAlias) {
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
}
if checker.source_type.is_stub() {
if let Some(value) = value {
if checker.enabled(Rule::AssignmentDefaultInStub) {
// Ignore assignments in function bodies; those are covered by other rules.
if !checker
.semantic
.scopes()
.any(|scope| scope.kind.is_any_function())
.current_scopes()
.any(|scope| scope.kind.is_function())
{
flake8_pyi::rules::annotated_assignment_default_in_stub(
checker, target, value, annotation,
@@ -1386,13 +1396,13 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
);
}
}
if checker.semantic.match_typing_expr(annotation, "TypeAlias") {
if checker.enabled(Rule::SnakeCaseTypeAlias) {
flake8_pyi::rules::snake_case_type_alias(checker, target);
}
if checker.enabled(Rule::TSuffixedTypeAlias) {
flake8_pyi::rules::t_suffixed_type_alias(checker, target);
}
}
if checker.semantic.match_typing_expr(annotation, "TypeAlias") {
if checker.enabled(Rule::SnakeCaseTypeAlias) {
flake8_pyi::rules::snake_case_type_alias(checker, target);
}
if checker.enabled(Rule::TSuffixedTypeAlias) {
flake8_pyi::rules::t_suffixed_type_alias(checker, target);
}
}
}

View File

@@ -31,18 +31,21 @@ use std::path::Path;
use itertools::Itertools;
use log::error;
use ruff_python_ast::{
self as ast, Arg, ArgWithDefault, Arguments, Comprehension, Constant, ElifElseClause,
ExceptHandler, Expr, ExprContext, Keyword, Pattern, Ranged, Stmt, Suite, UnaryOp,
self as ast, Arguments, Comprehension, Constant, ElifElseClause, ExceptHandler, Expr,
ExprContext, Keyword, Parameter, ParameterWithDefault, Parameters, Pattern, Ranged, Stmt,
Suite, UnaryOp,
};
use ruff_text_size::{TextRange, TextSize};
use ruff_diagnostics::{Diagnostic, IsolationLevel};
use ruff_python_ast::all::{extract_all_names, DunderAllFlags};
use ruff_python_ast::helpers::{extract_handled_exceptions, to_module_path};
use ruff_python_ast::helpers::{
collect_import_from_member, extract_handled_exceptions, to_module_path,
};
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::str::trailing_quote;
use ruff_python_ast::visitor::{walk_except_handler, walk_pattern, Visitor};
use ruff_python_ast::{helpers, str, visitor};
use ruff_python_ast::{helpers, str, visitor, PySourceType};
use ruff_python_codegen::{Generator, Quote, Stylist};
use ruff_python_index::Indexer;
use ruff_python_parser::typing::{parse_type_annotation, AnnotationKind};
@@ -52,7 +55,6 @@ use ruff_python_semantic::{
ModuleKind, ScopeId, ScopeKind, SemanticModel, SemanticModelFlags, StarImport, SubmoduleImport,
};
use ruff_python_stdlib::builtins::{BUILTINS, MAGIC_GLOBALS};
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator;
use crate::checkers::ast::deferred::Deferred;
@@ -74,8 +76,8 @@ pub(crate) struct Checker<'a> {
package: Option<&'a Path>,
/// The module representation of the current file (e.g., `foo.bar`).
module_path: Option<&'a [String]>,
/// Whether the current file is a stub (`.pyi`) file.
is_stub: bool,
/// The [`PySourceType`] of the current file.
pub(crate) source_type: PySourceType,
/// The [`flags::Noqa`] for the current analysis (i.e., whether to respect suppression
/// comments).
noqa: flags::Noqa,
@@ -117,6 +119,7 @@ impl<'a> Checker<'a> {
stylist: &'a Stylist,
indexer: &'a Indexer,
importer: Importer<'a>,
source_type: PySourceType,
) -> Checker<'a> {
Checker {
settings,
@@ -125,7 +128,7 @@ impl<'a> Checker<'a> {
path,
package,
module_path: module.path(),
is_stub: is_python_stub_file(path),
source_type,
locator,
stylist,
indexer,
@@ -173,13 +176,12 @@ impl<'a> Checker<'a> {
///
/// If the current expression in the context is not an f-string, returns ``None``.
pub(crate) fn f_string_quote_style(&self) -> Option<Quote> {
let model = &self.semantic;
if !model.in_f_string() {
if !self.semantic.in_f_string() {
return None;
}
// Find the quote character used to start the containing f-string.
let expr = model.expr()?;
let expr = self.semantic.current_expression()?;
let string_range = self.indexer.f_string_range(expr.start())?;
let trailing_quote = trailing_quote(self.locator.slice(string_range))?;
@@ -199,7 +201,7 @@ impl<'a> Checker<'a> {
/// thus be applied whenever we delete a statement, but can otherwise be omitted.
pub(crate) fn isolation(&self, parent: Option<&Stmt>) -> IsolationLevel {
parent
.and_then(|stmt| self.semantic.stmts.node_id(stmt))
.and_then(|stmt| self.semantic.statement_id(stmt))
.map_or(IsolationLevel::default(), |node_id| {
IsolationLevel::Group(node_id.into())
})
@@ -261,7 +263,7 @@ where
{
fn visit_stmt(&mut self, stmt: &'b Stmt) {
// Step 0: Pre-processing
self.semantic.push_stmt(stmt);
self.semantic.push_statement(stmt);
// Track whether we've seen docstrings, non-imports, etc.
match stmt {
@@ -285,7 +287,7 @@ where
self.semantic.flags |= SemanticModelFlags::FUTURES_BOUNDARY;
if !self.semantic.seen_import_boundary()
&& !helpers::is_assignment_to_a_dunder(stmt)
&& !helpers::in_nested_block(self.semantic.parents())
&& !helpers::in_nested_block(self.semantic.current_statements())
{
self.semantic.flags |= SemanticModelFlags::IMPORT_BOUNDARY;
}
@@ -319,11 +321,11 @@ where
// Given `import foo.bar`, `name` would be "foo", and `qualified_name` would be
// "foo.bar".
let name = alias.name.split('.').next().unwrap();
let qualified_name = &alias.name;
let call_path: Box<[&str]> = alias.name.split('.').collect();
self.add_binding(
name,
alias.identifier(),
BindingKind::SubmoduleImport(SubmoduleImport { qualified_name }),
BindingKind::SubmoduleImport(SubmoduleImport { call_path }),
BindingFlags::EXTERNAL,
);
} else {
@@ -334,17 +336,17 @@ where
if alias
.asname
.as_ref()
.map_or(false, |asname| asname.as_str() == alias.name.as_str())
.is_some_and(|asname| asname.as_str() == alias.name.as_str())
{
flags |= BindingFlags::EXPLICIT_EXPORT;
}
let name = alias.asname.as_ref().unwrap_or(&alias.name);
let qualified_name = &alias.name;
let call_path: Box<[&str]> = alias.name.split('.').collect();
self.add_binding(
name,
alias.identifier(),
BindingKind::Import(Import { qualified_name }),
BindingKind::Import(Import { call_path }),
flags,
);
}
@@ -369,7 +371,7 @@ where
);
} else if &alias.name == "*" {
self.semantic
.scope_mut()
.current_scope_mut()
.add_star_import(StarImport { level, module });
} else {
let mut flags = BindingFlags::EXTERNAL;
@@ -379,7 +381,7 @@ where
if alias
.asname
.as_ref()
.map_or(false, |asname| asname.as_str() == alias.name.as_str())
.is_some_and(|asname| asname.as_str() == alias.name.as_str())
{
flags |= BindingFlags::EXPLICIT_EXPORT;
}
@@ -388,12 +390,16 @@ where
// be "foo.bar". Given `from foo import bar as baz`, `name` would be "baz"
// and `qualified_name` would be "foo.bar".
let name = alias.asname.as_ref().unwrap_or(&alias.name);
let qualified_name =
helpers::format_import_from_member(level, module, &alias.name);
// Attempt to resolve any relative imports; but if we don't know the current
// module path, or the relative import extends beyond the package root,
// fallback to a literal representation (e.g., `[".", "foo"]`).
let call_path = collect_import_from_member(level, module, &alias.name)
.into_boxed_slice();
self.add_binding(
name,
alias.identifier(),
BindingKind::FromImport(FromImport { qualified_name }),
BindingKind::FromImport(FromImport { call_path }),
flags,
);
}
@@ -414,7 +420,7 @@ where
BindingKind::Global,
BindingFlags::GLOBAL,
);
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(name, binding_id);
}
}
@@ -437,7 +443,7 @@ where
BindingKind::Nonlocal(scope_id),
BindingFlags::NONLOCAL,
);
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(name, binding_id);
}
}
@@ -448,22 +454,16 @@ where
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
body,
args,
decorator_list,
returns,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body,
args,
decorator_list,
type_params,
returns,
..
}) => {
Stmt::FunctionDef(
function_def @ ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
},
) => {
// Visit the decorators and arguments, but avoid the body, which will be
// deferred.
for decorator in decorator_list {
@@ -476,28 +476,28 @@ where
self.semantic.push_scope(ScopeKind::Type);
for type_param in type_params {
self.visit_type_param(type_param);
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
for arg_with_default in args
for parameter_with_default in parameters
.posonlyargs
.iter()
.chain(&args.args)
.chain(&args.kwonlyargs)
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &arg_with_default.def.annotation {
if let Some(expr) = &parameter_with_default.parameter.annotation {
if runtime_annotation {
self.visit_runtime_annotation(expr);
} else {
self.visit_annotation(expr);
};
}
if let Some(expr) = &arg_with_default.default {
if let Some(expr) = &parameter_with_default.default {
self.visit_expr(expr);
}
}
if let Some(arg) = &args.vararg {
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_runtime_annotation(expr);
@@ -506,7 +506,7 @@ where
};
}
}
if let Some(arg) = &args.kwarg {
if let Some(arg) = &parameters.kwarg {
if let Some(expr) = &arg.annotation {
if runtime_annotation {
self.visit_runtime_annotation(expr);
@@ -524,8 +524,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Function,
stmt,
ExtractionTarget::Function(function_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -533,8 +532,7 @@ where
self.semantic.push_scope(match &stmt {
Stmt::FunctionDef(stmt) => ScopeKind::Function(stmt),
Stmt::AsyncFunctionDef(stmt) => ScopeKind::AsyncFunction(stmt),
_ => unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef"),
_ => unreachable!("Expected Stmt::FunctionDef"),
});
self.deferred.functions.push(self.semantic.snapshot());
@@ -547,8 +545,7 @@ where
Stmt::ClassDef(
class_def @ ast::StmtClassDef {
body,
bases,
keywords,
arguments,
decorator_list,
type_params,
..
@@ -560,19 +557,16 @@ where
self.semantic.push_scope(ScopeKind::Type);
for type_param in type_params {
self.visit_type_param(type_param);
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
for expr in bases {
self.visit_expr(expr);
}
for keyword in keywords {
self.visit_keyword(keyword);
if let Some(arguments) = arguments {
self.visit_arguments(arguments);
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Class,
stmt,
ExtractionTarget::Class(class_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -587,14 +581,14 @@ where
self.visit_body(body);
}
Stmt::TypeAlias(ast::StmtTypeAlias {
range: _range,
range: _,
name,
type_params,
value,
}) => {
self.semantic.push_scope(ScopeKind::Type);
for type_param in type_params {
self.visit_type_param(type_param);
if let Some(type_params) = type_params {
self.visit_type_params(type_params);
}
self.visit_expr(value);
self.semantic.pop_scope();
@@ -653,7 +647,7 @@ where
// available at runtime.
// See: https://docs.python.org/3/reference/simple_stmts.html#annotated-assignment-statements
let runtime_annotation = if self.semantic.future_annotations() {
if self.semantic.scope().kind.is_class() {
if self.semantic.current_scope().kind.is_class() {
let baseclasses = &self
.settings
.flake8_type_checking
@@ -672,7 +666,7 @@ where
}
} else {
matches!(
self.semantic.scope().kind,
self.semantic.current_scope().kind,
ScopeKind::Class(_) | ScopeKind::Module
)
};
@@ -739,8 +733,7 @@ where
// Step 3: Clean-up
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { name, .. }) => {
Stmt::FunctionDef(ast::StmtFunctionDef { name, .. }) => {
let scope_id = self.semantic.scope_id;
self.deferred.scopes.push(scope_id);
self.semantic.pop_scope(); // Function scope
@@ -773,7 +766,7 @@ where
analyze::statement(stmt, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_stmt();
self.semantic.pop_statement();
}
fn visit_annotation(&mut self, expr: &'b Expr) {
@@ -809,7 +802,7 @@ where
return;
}
self.semantic.push_expr(expr);
self.semantic.push_expression(expr);
// Store the flags prior to any further descent, so that we can restore them after visiting
// the node.
@@ -832,13 +825,12 @@ where
match expr {
Expr::Call(ast::ExprCall {
func,
args: _,
keywords: _,
arguments: _,
range: _,
}) => {
if let Expr::Name(ast::ExprName { id, ctx, range: _ }) = func.as_ref() {
if id == "locals" && ctx.is_load() {
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.set_uses_locals();
}
}
@@ -883,21 +875,21 @@ where
}
Expr::Lambda(
lambda @ ast::ExprLambda {
args,
parameters,
body: _,
range: _,
},
) => {
// Visit the default arguments, but avoid the body, which will be deferred.
for ArgWithDefault {
for ParameterWithDefault {
default,
def: _,
parameter: _,
range: _,
} in args
} in parameters
.posonlyargs
.iter()
.chain(&args.args)
.chain(&args.kwonlyargs)
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
{
if let Some(expr) = &default {
self.visit_expr(expr);
@@ -919,8 +911,12 @@ where
}
Expr::Call(ast::ExprCall {
func,
args,
keywords,
arguments:
Arguments {
args,
keywords,
range: _,
},
range: _,
}) => {
self.visit_expr(func);
@@ -1098,7 +1094,7 @@ where
arg,
range: _,
} = keyword;
if arg.as_ref().map_or(false, |arg| arg == "type") {
if arg.as_ref().is_some_and(|arg| arg == "type") {
self.visit_type_definition(value);
} else {
self.visit_non_type_definition(value);
@@ -1200,7 +1196,7 @@ where
));
}
}
Expr::JoinedStr(_) => {
Expr::FString(_) => {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
@@ -1224,7 +1220,7 @@ where
analyze::expression(expr, self);
self.semantic.flags = flags_snapshot;
self.semantic.pop_expr();
self.semantic.pop_expression();
}
fn visit_except_handler(&mut self, except_handler: &'b ExceptHandler) {
@@ -1279,7 +1275,7 @@ where
fn visit_format_spec(&mut self, format_spec: &'b Expr) {
match format_spec {
Expr::JoinedStr(ast::ExprJoinedStr { values, range: _ }) => {
Expr::FString(ast::ExprFString { values, range: _ }) => {
for value in values {
self.visit_expr(value);
}
@@ -1288,43 +1284,43 @@ where
}
}
fn visit_arguments(&mut self, arguments: &'b Arguments) {
fn visit_parameters(&mut self, parameters: &'b Parameters) {
// Step 1: Binding.
// Bind, but intentionally avoid walking default expressions, as we handle them
// upstream.
for arg_with_default in &arguments.posonlyargs {
self.visit_arg(&arg_with_default.def);
for parameter_with_default in &parameters.posonlyargs {
self.visit_parameter(&parameter_with_default.parameter);
}
for arg_with_default in &arguments.args {
self.visit_arg(&arg_with_default.def);
for parameter_with_default in &parameters.args {
self.visit_parameter(&parameter_with_default.parameter);
}
if let Some(arg) = &arguments.vararg {
self.visit_arg(arg);
if let Some(arg) = &parameters.vararg {
self.visit_parameter(arg);
}
for arg_with_default in &arguments.kwonlyargs {
self.visit_arg(&arg_with_default.def);
for parameter_with_default in &parameters.kwonlyargs {
self.visit_parameter(&parameter_with_default.parameter);
}
if let Some(arg) = &arguments.kwarg {
self.visit_arg(arg);
if let Some(arg) = &parameters.kwarg {
self.visit_parameter(arg);
}
// Step 4: Analysis
analyze::arguments(arguments, self);
analyze::parameters(parameters, self);
}
fn visit_arg(&mut self, arg: &'b Arg) {
fn visit_parameter(&mut self, parameter: &'b Parameter) {
// Step 1: Binding.
// Bind, but intentionally avoid walking the annotation, as we handle it
// upstream.
self.add_binding(
&arg.arg,
arg.identifier(),
&parameter.name,
parameter.identifier(),
BindingKind::Argument,
BindingFlags::empty(),
);
// Step 4: Analysis
analyze::argument(arg, self);
analyze::parameter(parameter, self);
}
fn visit_pattern(&mut self, pattern: &'b Pattern) {
@@ -1604,7 +1600,7 @@ impl<'a> Checker<'a> {
}
fn handle_node_store(&mut self, id: &'a str, expr: &Expr) {
let parent = self.semantic.stmt();
let parent = self.semantic.current_statement();
if matches!(
parent,
@@ -1619,7 +1615,7 @@ impl<'a> Checker<'a> {
return;
}
if matches!(parent, Stmt::For(_) | Stmt::AsyncFor(_)) {
if parent.is_for_stmt() {
self.add_binding(
id,
expr.range(),
@@ -1639,7 +1635,7 @@ impl<'a> Checker<'a> {
return;
}
let scope = self.semantic.scope();
let scope = self.semantic.current_scope();
if scope.kind.is_module()
&& match parent {
@@ -1691,8 +1687,8 @@ impl<'a> Checker<'a> {
if self
.semantic
.expr_ancestors()
.any(|expr| expr.is_named_expr_expr())
.current_expressions()
.any(Expr::is_named_expr_expr)
{
self.add_binding(
id,
@@ -1718,7 +1714,7 @@ impl<'a> Checker<'a> {
self.semantic.resolve_del(id, expr.range());
if helpers::on_conditional_branch(&mut self.semantic.parents()) {
if helpers::on_conditional_branch(&mut self.semantic.current_statements()) {
return;
}
@@ -1726,11 +1722,12 @@ impl<'a> Checker<'a> {
let binding_id =
self.semantic
.push_binding(expr.range(), BindingKind::Deletion, BindingFlags::empty());
let scope = self.semantic.scope_mut();
let scope = self.semantic.current_scope_mut();
scope.add(id, binding_id);
}
fn visit_deferred_future_type_definitions(&mut self) {
let snapshot = self.semantic.snapshot();
while !self.deferred.future_type_definitions.is_empty() {
let type_definitions = std::mem::take(&mut self.deferred.future_type_definitions);
for (expr, snapshot) in type_definitions {
@@ -1741,9 +1738,11 @@ impl<'a> Checker<'a> {
self.visit_expr(expr);
}
}
self.semantic.restore(snapshot);
}
fn visit_deferred_type_param_definitions(&mut self) {
let snapshot = self.semantic.snapshot();
while !self.deferred.type_param_definitions.is_empty() {
let type_params = std::mem::take(&mut self.deferred.type_param_definitions);
for (type_param, snapshot) in type_params {
@@ -1757,9 +1756,11 @@ impl<'a> Checker<'a> {
}
}
}
self.semantic.restore(snapshot);
}
fn visit_deferred_string_type_definitions(&mut self, allocator: &'a typed_arena::Arena<Expr>) {
let snapshot = self.semantic.snapshot();
while !self.deferred.string_type_definitions.is_empty() {
let type_definitions = std::mem::take(&mut self.deferred.string_type_definitions);
for (range, value, snapshot) in type_definitions {
@@ -1770,12 +1771,12 @@ impl<'a> Checker<'a> {
self.semantic.restore(snapshot);
if self.semantic.in_typing_only_annotation() {
if self.semantic.in_annotation() && self.semantic.future_annotations() {
if self.enabled(Rule::QuotedAnnotation) {
pyupgrade::rules::quoted_annotation(self, value, range);
}
}
if self.is_stub {
if self.source_type.is_stub() {
if self.enabled(Rule::QuotedAnnotationInStub) {
flake8_pyi::rules::quoted_annotation_in_stub(self, value, range);
}
@@ -1803,51 +1804,57 @@ impl<'a> Checker<'a> {
}
}
}
self.semantic.restore(snapshot);
}
fn visit_deferred_functions(&mut self) {
let snapshot = self.semantic.snapshot();
while !self.deferred.functions.is_empty() {
let deferred_functions = std::mem::take(&mut self.deferred.functions);
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
match &self.semantic.stmt() {
Stmt::FunctionDef(ast::StmtFunctionDef { body, args, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, args, .. }) => {
self.visit_arguments(args);
self.visit_body(body);
}
_ => {
unreachable!("Expected Stmt::FunctionDef | Stmt::AsyncFunctionDef")
}
if let Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
}) = self.semantic.current_statement()
{
self.visit_parameters(parameters);
self.visit_body(body);
} else {
unreachable!("Expected Stmt::FunctionDef")
}
}
}
self.semantic.restore(snapshot);
}
fn visit_deferred_lambdas(&mut self) {
let snapshot = self.semantic.snapshot();
while !self.deferred.lambdas.is_empty() {
let lambdas = std::mem::take(&mut self.deferred.lambdas);
for (expr, snapshot) in lambdas {
self.semantic.restore(snapshot);
if let Expr::Lambda(ast::ExprLambda {
args,
parameters,
body,
range: _,
}) = expr
{
self.visit_arguments(args);
self.visit_parameters(parameters);
self.visit_expr(body);
} else {
unreachable!("Expected Expr::Lambda");
}
}
}
self.semantic.restore(snapshot);
}
/// Run any lint rules that operate over the module exports (i.e., members of `__all__`).
fn visit_exports(&mut self) {
let snapshot = self.semantic.snapshot();
let exports: Vec<(&str, TextRange)> = self
.semantic
.global_scope()
@@ -1890,6 +1897,8 @@ impl<'a> Checker<'a> {
}
}
}
self.semantic.restore(snapshot);
}
}
@@ -1904,6 +1913,7 @@ pub(crate) fn check_ast(
noqa: flags::Noqa,
path: &Path,
package: Option<&Path>,
source_type: PySourceType,
) -> Vec<Diagnostic> {
let module_path = package.and_then(|package| to_module_path(package, path));
let module = Module {
@@ -1931,6 +1941,7 @@ pub(crate) fn check_ast(
stylist,
indexer,
Importer::new(python_ast, locator, stylist),
source_type,
);
checker.bind_builtins();

View File

@@ -2,7 +2,7 @@
use std::borrow::Cow;
use std::path::Path;
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite};
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::helpers::to_module_path;
@@ -10,7 +10,7 @@ use ruff_python_ast::imports::{ImportMap, ModuleImport};
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::Locator;
use crate::directives::IsortDirectives;
@@ -87,12 +87,12 @@ pub(crate) fn check_imports(
path: &Path,
package: Option<&Path>,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> (Vec<Diagnostic>, Option<ImportMap>) {
let is_stub = is_python_stub_file(path);
// Extract all import blocks from the AST.
let tracker = {
let mut tracker = BlockBuilder::new(locator, directives, is_stub, source_kind);
let mut tracker =
BlockBuilder::new(locator, directives, source_type.is_stub(), source_kind);
tracker.visit_body(python_ast);
tracker
};
@@ -104,7 +104,13 @@ pub(crate) fn check_imports(
for block in &blocks {
if !block.imports.is_empty() {
if let Some(diagnostic) = isort::rules::organize_imports(
block, locator, stylist, indexer, settings, package,
block,
locator,
stylist,
indexer,
settings,
package,
source_type,
) {
diagnostics.push(diagnostic);
}
@@ -113,7 +119,11 @@ pub(crate) fn check_imports(
}
if settings.rules.enabled(Rule::MissingRequiredImport) {
diagnostics.extend(isort::rules::add_required_imports(
python_ast, locator, stylist, settings, is_stub,
python_ast,
locator,
stylist,
settings,
source_type,
));
}

View File

@@ -94,8 +94,15 @@ pub(crate) fn check_noqa(
}
}
// Enforce that the noqa directive was actually used (RUF100).
if analyze_directives && settings.rules.enabled(Rule::UnusedNOQA) {
// Enforce that the noqa directive was actually used (RUF100), unless RUF100 was itself
// suppressed.
if settings.rules.enabled(Rule::UnusedNOQA)
&& analyze_directives
&& !exemption.is_some_and(|exemption| match exemption {
FileExemption::All => true,
FileExemption::Codes(codes) => codes.contains(&Rule::UnusedNOQA.noqa_code()),
})
{
for line in noqa_directives.lines() {
match &line.directive {
Directive::All(directive) => {

View File

@@ -191,6 +191,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pylint, "E1142") => (RuleGroup::Unspecified, rules::pylint::rules::AwaitOutsideAsync),
(Pylint, "E1205") => (RuleGroup::Unspecified, rules::pylint::rules::LoggingTooManyArgs),
(Pylint, "E1206") => (RuleGroup::Unspecified, rules::pylint::rules::LoggingTooFewArgs),
(Pylint, "E1300") => (RuleGroup::Unspecified, rules::pylint::rules::BadStringFormatCharacter),
(Pylint, "E1307") => (RuleGroup::Unspecified, rules::pylint::rules::BadStringFormatType),
(Pylint, "E1310") => (RuleGroup::Unspecified, rules::pylint::rules::BadStrStripCall),
(Pylint, "E1507") => (RuleGroup::Unspecified, rules::pylint::rules::InvalidEnvvarValue),
@@ -438,6 +439,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Pyupgrade, "037") => (RuleGroup::Unspecified, rules::pyupgrade::rules::QuotedAnnotation),
(Pyupgrade, "038") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP604Isinstance),
(Pyupgrade, "039") => (RuleGroup::Unspecified, rules::pyupgrade::rules::UnnecessaryClassParentheses),
(Pyupgrade, "040") => (RuleGroup::Unspecified, rules::pyupgrade::rules::NonPEP695TypeAlias),
// pydocstyle
(Pydocstyle, "100") => (RuleGroup::Unspecified, rules::pydocstyle::rules::UndocumentedPublicModule),
@@ -636,6 +638,7 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "016") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DuplicateUnionMember),
(Flake8Pyi, "017") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::ComplexAssignmentInStub),
(Flake8Pyi, "018") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnusedPrivateTypeVar),
(Flake8Pyi, "019") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CustomTypeVarReturnType),
(Flake8Pyi, "020") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::QuotedAnnotationInStub),
(Flake8Pyi, "021") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::DocstringInStub),
(Flake8Pyi, "024") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::CollectionsNamedTuple),
@@ -658,9 +661,11 @@ pub fn code_to_rule(linter: Linter, code: &str) -> Option<(RuleGroup, Rule)> {
(Flake8Pyi, "048") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StubBodyMultipleStatements),
(Flake8Pyi, "049") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnusedPrivateTypedDict),
(Flake8Pyi, "050") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NoReturnArgumentAnnotationInStub),
(Flake8Pyi, "051") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::RedundantLiteralUnion),
(Flake8Pyi, "052") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnannotatedAssignmentInStub),
(Flake8Pyi, "054") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::NumericLiteralTooLong),
(Flake8Pyi, "053") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::StringOrBytesTooLong),
(Flake8Pyi, "055") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnnecessaryTypeUnion),
(Flake8Pyi, "056") => (RuleGroup::Unspecified, rules::flake8_pyi::rules::UnsupportedMethodCallOnAll),
// flake8-pytest-style

View File

@@ -1,7 +1,6 @@
//! Extract docstrings from an AST.
use ruff_python_ast::{self as ast, Constant, Expr, Stmt};
use ruff_python_semantic::{Definition, DefinitionId, Definitions, Member, MemberKind};
/// Extract a docstring from a function or class body.
@@ -28,63 +27,48 @@ pub(crate) fn docstring_from(suite: &[Stmt]) -> Option<&Expr> {
pub(crate) fn extract_docstring<'a>(definition: &'a Definition<'a>) -> Option<&'a Expr> {
match definition {
Definition::Module(module) => docstring_from(module.python_ast),
Definition::Member(member) => {
if let Stmt::ClassDef(ast::StmtClassDef { body, .. })
| Stmt::FunctionDef(ast::StmtFunctionDef { body, .. })
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef { body, .. }) = &member.stmt
{
docstring_from(body)
} else {
None
}
}
Definition::Member(member) => docstring_from(member.body()),
}
}
#[derive(Copy, Clone)]
pub(crate) enum ExtractionTarget {
Class,
Function,
pub(crate) enum ExtractionTarget<'a> {
Class(&'a ast::StmtClassDef),
Function(&'a ast::StmtFunctionDef),
}
/// Extract a `Definition` from the AST node defined by a `Stmt`.
pub(crate) fn extract_definition<'a>(
target: ExtractionTarget,
stmt: &'a Stmt,
target: ExtractionTarget<'a>,
parent: DefinitionId,
definitions: &Definitions<'a>,
) -> Member<'a> {
match target {
ExtractionTarget::Function => match &definitions[parent] {
ExtractionTarget::Function(function) => match &definitions[parent] {
Definition::Module(..) => Member {
parent,
kind: MemberKind::Function,
stmt,
kind: MemberKind::Function(function),
},
Definition::Member(Member {
kind: MemberKind::Class | MemberKind::NestedClass,
kind: MemberKind::Class(_) | MemberKind::NestedClass(_),
..
}) => Member {
parent,
kind: MemberKind::Method,
stmt,
kind: MemberKind::Method(function),
},
Definition::Member(..) => Member {
Definition::Member(_) => Member {
parent,
kind: MemberKind::NestedFunction,
stmt,
kind: MemberKind::NestedFunction(function),
},
},
ExtractionTarget::Class => match &definitions[parent] {
Definition::Module(..) => Member {
ExtractionTarget::Class(class) => match &definitions[parent] {
Definition::Module(_) => Member {
parent,
kind: MemberKind::Class,
stmt,
kind: MemberKind::Class(class),
},
Definition::Member(..) => Member {
Definition::Member(_) => Member {
parent,
kind: MemberKind::NestedClass,
stmt,
kind: MemberKind::NestedClass(class),
},
},
}

View File

@@ -402,7 +402,7 @@ fn is_docstring_section(
}
// Determine whether the next line is an underline, e.g., "-----".
let next_line_is_underline = next_line.map_or(false, |next_line| {
let next_line_is_underline = next_line.is_some_and(|next_line| {
let next_line = next_line.trim();
if next_line.is_empty() {
false

View File

@@ -1,8 +1,8 @@
//! Insert statements into Python code.
use std::ops::Add;
use ruff_python_ast::{Ranged, Stmt};
use ruff_python_parser::{lexer, Mode, Tok};
use ruff_python_ast::{PySourceType, Ranged, Stmt};
use ruff_python_parser::{lexer, AsMode, Tok};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
@@ -137,6 +137,7 @@ impl<'a> Insertion<'a> {
mut location: TextSize,
locator: &Locator<'a>,
stylist: &Stylist,
source_type: PySourceType,
) -> Insertion<'a> {
enum Awaiting {
Colon(u32),
@@ -146,7 +147,7 @@ impl<'a> Insertion<'a> {
let mut state = Awaiting::Colon(0);
for (tok, range) in
lexer::lex_starts_at(locator.after(location), Mode::Module, location).flatten()
lexer::lex_starts_at(locator.after(location), source_type.as_mode(), location).flatten()
{
match state {
// Iterate until we find the colon indicating the start of the block body.
@@ -299,21 +300,21 @@ fn match_leading_semicolon(s: &str) -> Option<TextSize> {
#[cfg(test)]
mod tests {
use anyhow::Result;
use ruff_python_ast::Suite;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::Parse;
use ruff_text_size::TextSize;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::{parse_suite, Mode};
use ruff_source_file::{LineEnding, Locator};
use ruff_text_size::TextSize;
use super::Insertion;
#[test]
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
let program = Suite::parse(contents, "<filename>")?;
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents);
let program = parse_suite(contents, "<filename>")?;
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Ok(Insertion::start_of_file(&program, &locator, &stylist))
@@ -424,10 +425,10 @@ x = 1
#[test]
fn start_of_block() {
fn insert(contents: &str, offset: TextSize) -> Insertion {
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, Mode::Module);
let locator = Locator::new(contents);
let stylist = Stylist::from_tokens(&tokens, &locator);
Insertion::start_of_block(offset, &locator, &stylist)
Insertion::start_of_block(offset, &locator, &stylist, PySourceType::default())
}
let contents = "if True: pass";

View File

@@ -7,7 +7,7 @@ use std::error::Error;
use anyhow::Result;
use libcst_native::{ImportAlias, Name, NameOrAttribute};
use ruff_python_ast::{self as ast, Ranged, Stmt, Suite};
use ruff_python_ast::{self as ast, PySourceType, Ranged, Stmt, Suite};
use ruff_text_size::TextSize;
use ruff_diagnostics::Edit;
@@ -87,13 +87,13 @@ impl<'a> Importer<'a> {
/// import statement.
pub(crate) fn runtime_import_edit(
&self,
import: &StmtImports,
import: &ImportedMembers,
at: TextSize,
) -> Result<RuntimeImportEdit> {
// Generate the modified import statement.
let content = autofix::codemods::retain_imports(
&import.qualified_names,
import.stmt,
&import.names,
import.statement,
self.locator,
self.stylist,
)?;
@@ -118,14 +118,15 @@ impl<'a> Importer<'a> {
/// `TYPE_CHECKING` block.
pub(crate) fn typing_import_edit(
&self,
import: &StmtImports,
import: &ImportedMembers,
at: TextSize,
semantic: &SemanticModel,
source_type: PySourceType,
) -> Result<TypingImportEdit> {
// Generate the modified import statement.
let content = autofix::codemods::retain_imports(
&import.qualified_names,
import.stmt,
&import.names,
import.statement,
self.locator,
self.stylist,
)?;
@@ -140,7 +141,7 @@ impl<'a> Importer<'a> {
// Add the import to a `TYPE_CHECKING` block.
let add_import_edit = if let Some(block) = self.preceding_type_checking_block(at) {
// Add the import to the `TYPE_CHECKING` block.
self.add_to_type_checking_block(&content, block.start())
self.add_to_type_checking_block(&content, block.start(), source_type)
} else {
// Add the import to a new `TYPE_CHECKING` block.
self.add_type_checking_block(
@@ -305,7 +306,7 @@ impl<'a> Importer<'a> {
}) = stmt
{
if level.map_or(true, |level| level.to_u32() == 0)
&& name.as_ref().map_or(false, |name| name == module)
&& name.as_ref().is_some_and(|name| name == module)
{
import_from = Some(*stmt);
}
@@ -353,8 +354,13 @@ impl<'a> Importer<'a> {
}
/// Add an import statement to an existing `TYPE_CHECKING` block.
fn add_to_type_checking_block(&self, content: &str, at: TextSize) -> Edit {
Insertion::start_of_block(at, self.locator, self.stylist).into_edit(content)
fn add_to_type_checking_block(
&self,
content: &str,
at: TextSize,
source_type: PySourceType,
) -> Edit {
Insertion::start_of_block(at, self.locator, self.stylist, source_type).into_edit(content)
}
/// Return the import statement that precedes the given position, if any.
@@ -446,11 +452,11 @@ impl<'a> ImportRequest<'a> {
}
/// An existing list of module or member imports, located within an import statement.
pub(crate) struct StmtImports<'a> {
pub(crate) struct ImportedMembers<'a> {
/// The import statement.
pub(crate) stmt: &'a Stmt,
/// The "qualified names" of the imported modules or members.
pub(crate) qualified_names: Vec<&'a str>,
pub(crate) statement: &'a Stmt,
/// The "names" of the imported members.
pub(crate) names: Vec<&'a str>,
}
/// The result of an [`Importer::get_or_import_symbol`] call.

View File

@@ -24,8 +24,6 @@ use crate::IOError;
pub const JUPYTER_NOTEBOOK_EXT: &str = "ipynb";
const MAGIC_PREFIX: [&str; 3] = ["%", "!", "?"];
/// Run round-trip source code generation on a given Jupyter notebook file path.
pub fn round_trip(path: &Path) -> anyhow::Result<String> {
let mut notebook = Notebook::read(path).map_err(|err| {
@@ -78,26 +76,21 @@ impl Cell {
/// Return `true` if it's a valid code cell.
///
/// A valid code cell is a cell where the cell type is [`Cell::Code`] and the
/// source doesn't contain a magic, shell or help command.
/// source doesn't contain a cell magic.
fn is_valid_code_cell(&self) -> bool {
let source = match self {
Cell::Code(cell) => &cell.source,
_ => return false,
};
// Ignore a cell if it contains a magic command. There could be valid
// Python code as well, but we'll ignore that for now.
// TODO(dhruvmanila): https://github.com/psf/black/blob/main/src/black/handle_ipynb_magics.py
// Ignore cells containing cell magic. This is different from line magic
// which is allowed and ignored by the parser.
!match source {
SourceValue::String(string) => string.lines().any(|line| {
MAGIC_PREFIX
.iter()
.any(|prefix| line.trim_start().starts_with(prefix))
}),
SourceValue::StringArray(string_array) => string_array.iter().any(|line| {
MAGIC_PREFIX
.iter()
.any(|prefix| line.trim_start().starts_with(prefix))
}),
SourceValue::String(string) => string
.lines()
.any(|line| line.trim_start().starts_with("%%")),
SourceValue::StringArray(string_array) => string_array
.iter()
.any(|line| line.trim_start().starts_with("%%")),
}
}
}
@@ -376,7 +369,7 @@ impl Notebook {
1
} else {
let trailing_newline =
usize::from(string_array.last().map_or(false, |s| s.ends_with('\n')));
usize::from(string_array.last().is_some_and(|s| s.ends_with('\n')));
u32::try_from(string_array.len() + trailing_newline).unwrap()
}
}
@@ -513,9 +506,10 @@ mod tests {
}
#[test_case(Path::new("markdown.json"), false; "markdown")]
#[test_case(Path::new("only_magic.json"), false; "only_magic")]
#[test_case(Path::new("code_and_magic.json"), false; "code_and_magic")]
#[test_case(Path::new("only_magic.json"), true; "only_magic")]
#[test_case(Path::new("code_and_magic.json"), true; "code_and_magic")]
#[test_case(Path::new("only_code.json"), true; "only_code")]
#[test_case(Path::new("cell_magic.json"), false; "cell_magic")]
fn test_is_valid_code_cell(path: &Path, expected: bool) -> Result<()> {
assert_eq!(read_jupyter_cell(path)?.is_valid_code_cell(), expected);
Ok(())
@@ -567,7 +561,7 @@ print("after empty cells")
#[test]
fn test_import_sorting() -> Result<()> {
let path = "isort.ipynb".to_string();
let (diagnostics, source_kind) = test_notebook_path(
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("isort_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnsortedImports),
@@ -576,10 +570,34 @@ print("after empty cells")
Ok(())
}
#[test]
fn test_line_magics() -> Result<()> {
let path = "line_magics.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("line_magics_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),
)?;
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_unused_variable() -> Result<()> {
let path = "unused_variable.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("unused_variable_expected.ipynb"),
&settings::Settings::for_rule(Rule::UnusedVariable),
)?;
assert_messages!(diagnostics, path, source_kind);
Ok(())
}
#[test]
fn test_json_consistency() -> Result<()> {
let path = "before_fix.ipynb".to_string();
let (_, source_kind) = test_notebook_path(
let (_, _, source_kind) = test_notebook_path(
path,
Path::new("after_fix.ipynb"),
&settings::Settings::for_rule(Rule::UnusedImport),

View File

@@ -47,4 +47,43 @@ isort.ipynb:cell 2:1:1: I001 [*] Import block is un-sorted or un-formatted
7 9 | def foo():
8 10 | pass
isort.ipynb:cell 3:1:1: I001 [*] Import block is un-sorted or un-formatted
|
1 | / from pathlib import Path
2 | | import sys
3 | |
4 | | %matplotlib \
| |_^ I001
5 | --inline
|
= help: Organize imports
Fix
6 6 | # Newline should be added here
7 7 | def foo():
8 8 | pass
9 |+import sys
9 10 | from pathlib import Path
10 |-import sys
11 11 |
12 12 | %matplotlib \
13 13 | --inline
isort.ipynb:cell 3:7:1: I001 [*] Import block is un-sorted or un-formatted
|
5 | --inline
6 |
7 | / import math
8 | | import abc
|
= help: Organize imports
Fix
12 12 | %matplotlib \
13 13 | --inline
14 14 |
15 |+import abc
15 16 | import math
16 |-import abc

View File

@@ -0,0 +1,23 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
line_magics.ipynb:cell 1:5:8: F401 [*] `os` imported but unused
|
3 | %matplotlib inline
4 |
5 | import os
| ^^ F401
6 |
7 | _ = math.pi
|
= help: Remove unused import: `os`
Fix
2 2 |
3 3 | %matplotlib inline
4 4 |
5 |-import os
6 5 |
7 6 | _ = math.pi

View File

@@ -0,0 +1,72 @@
---
source: crates/ruff/src/jupyter/notebook.rs
---
unused_variable.ipynb:cell 1:2:5: F841 [*] Local variable `foo1` is assigned to but never used
|
1 | def f():
2 | foo1 = %matplotlib --list
| ^^^^ F841
3 | foo2: list[str] = %matplotlib --list
|
= help: Remove assignment to unused variable `foo1`
Suggested fix
1 1 | def f():
2 |- foo1 = %matplotlib --list
2 |+ %matplotlib --list
3 3 | foo2: list[str] = %matplotlib --list
4 4 | def f():
5 5 | bar1 = !pwd
unused_variable.ipynb:cell 1:3:5: F841 [*] Local variable `foo2` is assigned to but never used
|
1 | def f():
2 | foo1 = %matplotlib --list
3 | foo2: list[str] = %matplotlib --list
| ^^^^ F841
|
= help: Remove assignment to unused variable `foo2`
Suggested fix
1 1 | def f():
2 2 | foo1 = %matplotlib --list
3 |- foo2: list[str] = %matplotlib --list
3 |+ %matplotlib --list
4 4 | def f():
5 5 | bar1 = !pwd
6 6 | bar2: str = !pwd
unused_variable.ipynb:cell 2:2:5: F841 [*] Local variable `bar1` is assigned to but never used
|
1 | def f():
2 | bar1 = !pwd
| ^^^^ F841
3 | bar2: str = !pwd
|
= help: Remove assignment to unused variable `bar1`
Suggested fix
2 2 | foo1 = %matplotlib --list
3 3 | foo2: list[str] = %matplotlib --list
4 4 | def f():
5 |- bar1 = !pwd
5 |+ !pwd
6 6 | bar2: str = !pwd
unused_variable.ipynb:cell 2:3:5: F841 [*] Local variable `bar2` is assigned to but never used
|
1 | def f():
2 | bar1 = !pwd
3 | bar2: str = !pwd
| ^^^^ F841
|
= help: Remove assignment to unused variable `bar2`
Suggested fix
3 3 | foo2: list[str] = %matplotlib --list
4 4 | def f():
5 5 | bar1 = !pwd
6 |- bar2: str = !pwd
6 |+ !pwd

View File

@@ -7,14 +7,15 @@ use colored::Colorize;
use itertools::Itertools;
use log::error;
use ruff_python_parser::lexer::LexResult;
use ruff_python_parser::ParseError;
use ruff_python_parser::{AsMode, ParseError};
use rustc_hash::FxHashMap;
use ruff_diagnostics::Diagnostic;
use ruff_python_ast::imports::ImportMap;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_index::Indexer;
use ruff_python_stdlib::path::is_python_stub_file;
use ruff_source_file::{Locator, SourceFileBuilder};
use crate::autofix::{fix_file, FixResult};
@@ -81,6 +82,7 @@ pub fn check_path(
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Diagnostic>, Option<ImportMap>)> {
// Aggregate all diagnostics.
let mut diagnostics = vec![];
@@ -101,9 +103,13 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_tokens())
{
let is_stub = is_python_stub_file(path);
diagnostics.extend(check_tokens(
&tokens, path, locator, indexer, settings, is_stub,
&tokens,
path,
locator,
indexer,
settings,
source_type.is_stub(),
));
}
@@ -138,7 +144,11 @@ pub fn check_path(
.iter_enabled()
.any(|rule_code| rule_code.lint_source().is_imports());
if use_ast || use_imports || use_doc_lines {
match ruff_python_parser::parse_program_tokens(tokens, &path.to_string_lossy()) {
match ruff_python_parser::parse_program_tokens(
tokens,
&path.to_string_lossy(),
source_type.is_jupyter(),
) {
Ok(python_ast) => {
if use_ast {
diagnostics.extend(check_ast(
@@ -151,6 +161,7 @@ pub fn check_path(
noqa,
path,
package,
source_type,
));
}
if use_imports {
@@ -164,6 +175,7 @@ pub fn check_path(
path,
package,
source_kind,
source_type,
);
imports = module_imports;
diagnostics.extend(import_diagnostics);
@@ -256,11 +268,13 @@ const MAX_ITERATIONS: usize = 100;
/// Add any missing `# noqa` pragmas to the source code at the given `Path`.
pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings) -> Result<usize> {
let source_type = PySourceType::from(path);
// Read the file from disk.
let contents = std::fs::read_to_string(path)?;
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(&contents);
@@ -294,6 +308,7 @@ pub fn add_noqa_to_path(path: &Path, package: Option<&Path>, settings: &Settings
settings,
flags::Noqa::Disabled,
None,
source_type,
);
// Log any parse errors.
@@ -326,9 +341,10 @@ pub fn lint_only(
settings: &Settings,
noqa: flags::Noqa,
source_kind: Option<&SourceKind>,
source_type: PySourceType,
) -> LinterResult<(Vec<Message>, Option<ImportMap>)> {
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents);
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(contents, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(contents);
@@ -359,6 +375,7 @@ pub fn lint_only(
settings,
noqa,
source_kind,
source_type,
);
result.map(|(diagnostics, imports)| {
@@ -405,6 +422,7 @@ pub fn lint_fix<'a>(
noqa: flags::Noqa,
settings: &Settings,
source_kind: &mut SourceKind,
source_type: PySourceType,
) -> Result<FixerResult<'a>> {
let mut transformed = Cow::Borrowed(contents);
@@ -420,7 +438,8 @@ pub fn lint_fix<'a>(
// Continuously autofix until the source code stabilizes.
loop {
// Tokenize once.
let tokens: Vec<LexResult> = ruff_python_parser::tokenize(&transformed);
let tokens: Vec<LexResult> =
ruff_python_parser::tokenize(&transformed, source_type.as_mode());
// Map row and column locations to byte slices (lazily).
let locator = Locator::new(&transformed);
@@ -451,6 +470,7 @@ pub fn lint_fix<'a>(
settings,
noqa,
Some(source_kind),
source_type,
);
if iterations == 0 {

View File

@@ -141,7 +141,7 @@ impl<'a> EmitterContext<'a> {
pub fn is_jupyter_notebook(&self, name: &str) -> bool {
self.source_kind
.get(name)
.map_or(false, SourceKind::is_jupyter)
.is_some_and(SourceKind::is_jupyter)
}
pub fn source_kind(&self, name: &str) -> Option<&SourceKind> {

View File

@@ -60,7 +60,7 @@ impl<'a> Directive<'a> {
if text[..comment_start]
.chars()
.last()
.map_or(false, |c| c != '#')
.is_some_and(|c| c != '#')
{
continue;
}

View File

@@ -231,7 +231,7 @@ impl Renamer {
}
BindingKind::SubmoduleImport(import) => {
// Ex) Rename `import pandas.core` to `import pandas as pd`.
let module_name = import.qualified_name.split('.').next().unwrap();
let module_name = import.call_path.first().unwrap();
Some(Edit::range_replacement(
format!("{module_name} as {target}"),
binding.range,

View File

@@ -262,7 +262,6 @@ pub fn python_files_in_path(
builder.add(path);
}
builder.standard_filters(pyproject_config.settings.lib.respect_gitignore);
builder.require_git(false);
builder.hidden(false);
let walker = builder.build_parallel();
@@ -304,7 +303,7 @@ pub fn python_files_in_path(
if let Ok(entry) = &result {
if entry
.file_type()
.map_or(false, |file_type| file_type.is_dir())
.is_some_and(|file_type| file_type.is_dir())
{
match settings_toml(entry.path()) {
Ok(Some(pyproject)) => match resolve_scoped_settings(

View File

@@ -1,10 +1,8 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::helpers::find_keyword;
use ruff_python_ast as ast;
use ruff_python_ast::Constant;
use ruff_python_ast::{Expr, Ranged};
use crate::checkers::ast::Checker;
@@ -60,7 +58,10 @@ pub(crate) fn variable_name_task_id(
};
// If the value is not a call, we can't do anything.
let Expr::Call(ast::ExprCall { func, keywords, .. }) = value else {
let Expr::Call(ast::ExprCall {
func, arguments, ..
}) = value
else {
return None;
};
@@ -68,13 +69,13 @@ pub(crate) fn variable_name_task_id(
if !checker
.semantic()
.resolve_call_path(func)
.map_or(false, |call_path| matches!(call_path[0], "airflow"))
.is_some_and(|call_path| matches!(call_path[0], "airflow"))
{
return None;
}
// If the call doesn't have a `task_id` keyword argument, we can't do anything.
let keyword = find_keyword(keywords, "task_id")?;
let keyword = arguments.find_keyword("task_id")?;
// If the keyword argument is not a string, we can't do anything.
let task_id = match &keyword.value {

View File

@@ -1,8 +1,8 @@
/// See: [eradicate.py](https://github.com/myint/eradicate/blob/98f199940979c94447a461d50d27862b118b282d/eradicate.py)
use once_cell::sync::Lazy;
use regex::Regex;
use ruff_python_ast::Suite;
use ruff_python_parser::Parse;
use ruff_python_parser::parse_suite;
static ALLOWLIST_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(
@@ -79,7 +79,7 @@ pub(crate) fn comment_contains_code(line: &str, task_tags: &[String]) -> bool {
}
// Finally, compile the source code.
Suite::parse(&line, "<filename>").is_ok()
parse_suite(&line, "<filename>").is_ok()
}
/// Returns `true` if a line is probably part of some multiline code.

View File

@@ -5,5 +5,5 @@ use ruff_python_semantic::SemanticModel;
pub(super) fn is_sys(expr: &Expr, target: &str, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(expr)
.map_or(false, |call_path| call_path.as_slice() == ["sys", target])
.is_some_and(|call_path| call_path.as_slice() == ["sys", target])
}

View File

@@ -49,9 +49,7 @@ pub(crate) fn name_or_attribute(checker: &mut Checker, expr: &Expr) {
if checker
.semantic()
.resolve_call_path(expr)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["six", "PY3"])
})
.is_some_and(|call_path| matches!(call_path.as_slice(), ["six", "PY3"]))
{
checker
.diagnostics

View File

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

View File

@@ -1,53 +1,11 @@
use ruff_python_ast::{self as ast, Arguments, Expr, Stmt};
use ruff_python_ast::cast;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind, SemanticModel};
pub(super) fn match_function_def(
stmt: &Stmt,
) -> (&str, &Arguments, Option<&Expr>, &[Stmt], &[ast::Decorator]) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
args,
returns,
body,
decorator_list,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
args,
returns,
body,
decorator_list,
..
}) => (
name,
args,
returns.as_ref().map(AsRef::as_ref),
body,
decorator_list,
),
_ => panic!("Found non-FunctionDef in match_function_def"),
}
}
use ruff_python_semantic::{Definition, SemanticModel};
/// Return the name of the function, if it's overloaded.
pub(crate) fn overloaded_name(definition: &Definition, semantic: &SemanticModel) -> Option<String> {
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(cast::decorator_list(stmt), semantic) {
let (name, ..) = match_function_def(stmt);
Some(name.to_string())
} else {
None
}
let function = definition.as_function_def()?;
if visibility::is_overload(&function.decorator_list, semantic) {
Some(function.name.to_string())
} else {
None
}
@@ -60,19 +18,12 @@ pub(crate) fn is_overload_impl(
overloaded_name: &str,
semantic: &SemanticModel,
) -> bool {
if let Definition::Member(Member {
kind: MemberKind::Function | MemberKind::NestedFunction | MemberKind::Method,
stmt,
..
}) = definition
{
if visibility::is_overload(cast::decorator_list(stmt), semantic) {
false
} else {
let (name, ..) = match_function_def(stmt);
name == overloaded_name
}
} else {
let Some(function) = definition.as_function_def() else {
return false;
};
if visibility::is_overload(&function.decorator_list, semantic) {
false
} else {
function.name.as_str() == overloaded_name
}
}

View File

@@ -1,23 +1,19 @@
use ruff_python_ast::{self as ast, ArgWithDefault, Constant, Expr, Ranged, Stmt};
use ruff_diagnostics::{AlwaysAutofixableViolation, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::cast;
use ruff_python_ast::helpers::ReturnStatementVisitor;
use ruff_python_ast::identifier::Identifier;
use ruff_python_ast::statement_visitor::StatementVisitor;
use ruff_python_ast::{self as ast, Constant, Expr, ParameterWithDefault, Ranged, Stmt};
use ruff_python_parser::typing::parse_type_annotation;
use ruff_python_semantic::analyze::visibility;
use ruff_python_semantic::{Definition, Member, MemberKind};
use ruff_python_semantic::Definition;
use ruff_python_stdlib::typing::simple_magic_return_type;
use crate::checkers::ast::Checker;
use crate::registry::{AsRule, Rule};
use crate::rules::flake8_annotations::fixes;
use crate::rules::ruff::typing::type_hint_resolves_to_any;
use super::super::fixes;
use super::super::helpers::match_function_def;
/// ## What it does
/// Checks that function arguments have type annotations.
///
@@ -498,20 +494,23 @@ pub(crate) fn definition(
definition: &Definition,
visibility: visibility::Visibility,
) -> Vec<Diagnostic> {
// TODO(charlie): Consider using the AST directly here rather than `Definition`.
// We could adhere more closely to `flake8-annotations` by defining public
// vs. secret vs. protected.
let Definition::Member(Member { kind, stmt, .. }) = definition else {
let Some(function) = definition.as_function_def() else {
return vec![];
};
let is_method = match kind {
MemberKind::Method => true,
MemberKind::Function | MemberKind::NestedFunction => false,
_ => return vec![],
};
let ast::StmtFunctionDef {
range: _,
is_async: _,
decorator_list,
name,
type_params: _,
parameters,
returns,
body,
} = function;
let is_method = definition.is_method();
let (name, arguments, returns, body, decorator_list) = match_function_def(stmt);
// Keep track of whether we've seen any typed arguments or return values.
let mut has_any_typed_arg = false; // Any argument has been typed?
let mut has_typed_return = false; // Return value has been typed?
@@ -524,44 +523,46 @@ pub(crate) fn definition(
let is_overridden = visibility::is_override(decorator_list, checker.semantic());
// ANN001, ANN401
for ArgWithDefault {
def,
for ParameterWithDefault {
parameter,
default: _,
range: _,
} in arguments
} in parameters
.posonlyargs
.iter()
.chain(&arguments.args)
.chain(&arguments.kwonlyargs)
.chain(&parameters.args)
.chain(&parameters.kwonlyargs)
.skip(
// If this is a non-static method, skip `cls` or `self`.
usize::from(
is_method
&& !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()),
is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()),
),
)
{
// ANN401 for dynamically typed arguments
if let Some(annotation) = &def.annotation {
// ANN401 for dynamically typed parameters
if let Some(annotation) = &parameter.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) && !is_overridden {
check_dynamically_typed(
checker,
annotation,
|| def.arg.to_string(),
|| parameter.name.to_string(),
&mut diagnostics,
);
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&def.arg))
&& checker
.settings
.dummy_variable_rgx
.is_match(&parameter.name))
{
if checker.enabled(Rule::MissingTypeFunctionArgument) {
diagnostics.push(Diagnostic::new(
MissingTypeFunctionArgument {
name: def.arg.to_string(),
name: parameter.name.to_string(),
},
def.range(),
parameter.range(),
));
}
}
@@ -569,23 +570,23 @@ pub(crate) fn definition(
}
// ANN002, ANN401
if let Some(arg) = &arguments.vararg {
if let Some(arg) = &parameters.vararg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.enabled(Rule::AnyType) && !is_overridden {
let name = &arg.arg;
let name = &arg.name;
check_dynamically_typed(checker, expr, || format!("*{name}"), &mut diagnostics);
}
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.arg))
&& checker.settings.dummy_variable_rgx.is_match(&arg.name))
{
if checker.enabled(Rule::MissingTypeArgs) {
diagnostics.push(Diagnostic::new(
MissingTypeArgs {
name: arg.arg.to_string(),
name: arg.name.to_string(),
},
arg.range(),
));
@@ -595,12 +596,12 @@ pub(crate) fn definition(
}
// ANN003, ANN401
if let Some(arg) = &arguments.kwarg {
if let Some(arg) = &parameters.kwarg {
if let Some(expr) = &arg.annotation {
has_any_typed_arg = true;
if !checker.settings.flake8_annotations.allow_star_arg_any {
if checker.enabled(Rule::AnyType) && !is_overridden {
let name = &arg.arg;
let name = &arg.name;
check_dynamically_typed(
checker,
expr,
@@ -611,12 +612,12 @@ pub(crate) fn definition(
}
} else {
if !(checker.settings.flake8_annotations.suppress_dummy_args
&& checker.settings.dummy_variable_rgx.is_match(&arg.arg))
&& checker.settings.dummy_variable_rgx.is_match(&arg.name))
{
if checker.enabled(Rule::MissingTypeKwargs) {
diagnostics.push(Diagnostic::new(
MissingTypeKwargs {
name: arg.arg.to_string(),
name: arg.name.to_string(),
},
arg.range(),
));
@@ -626,33 +627,33 @@ pub(crate) fn definition(
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) {
if let Some(ArgWithDefault {
def,
if is_method && !visibility::is_staticmethod(decorator_list, checker.semantic()) {
if let Some(ParameterWithDefault {
parameter,
default: _,
range: _,
}) = arguments
}) = parameters
.posonlyargs
.first()
.or_else(|| arguments.args.first())
.or_else(|| parameters.args.first())
{
if def.annotation.is_none() {
if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if parameter.annotation.is_none() {
if visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
name: def.arg.to_string(),
name: parameter.name.to_string(),
},
def.range(),
parameter.range(),
));
}
} else {
if checker.enabled(Rule::MissingTypeSelf) {
diagnostics.push(Diagnostic::new(
MissingTypeSelf {
name: def.arg.to_string(),
name: parameter.name.to_string(),
},
def.range(),
parameter.range(),
));
}
}
@@ -673,24 +674,22 @@ pub(crate) fn definition(
// (explicitly or implicitly).
checker.settings.flake8_annotations.suppress_none_returning && is_none_returning(body)
) {
if is_method && visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if is_method && visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeClassMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeClassMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
} else if is_method
&& visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic())
{
} else if is_method && visibility::is_staticmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingReturnTypeStaticMethod) {
diagnostics.push(Diagnostic::new(
MissingReturnTypeStaticMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
} else if is_method && visibility::is_init(name) {
@@ -702,12 +701,17 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(checker.locator(), stmt, "None")
.map(Fix::suggested)
fixes::add_return_annotation(
function,
"None",
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
}
diagnostics.push(diagnostic);
@@ -719,13 +723,18 @@ pub(crate) fn definition(
MissingReturnTypeSpecialMethod {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
);
if checker.patch(diagnostic.kind.rule()) {
if let Some(return_type) = simple_magic_return_type(name) {
diagnostic.try_set_fix(|| {
fixes::add_return_annotation(checker.locator(), stmt, return_type)
.map(Fix::suggested)
fixes::add_return_annotation(
function,
return_type,
checker.source_type,
checker.locator(),
)
.map(Fix::suggested)
});
}
}
@@ -739,7 +748,7 @@ pub(crate) fn definition(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
}
@@ -749,7 +758,7 @@ pub(crate) fn definition(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
}

View File

@@ -68,7 +68,7 @@ pub(crate) fn blocking_http_call(checker: &mut Checker, expr: &Expr) {
.semantic()
.resolve_call_path(func)
.as_ref()
.map_or(false, is_blocking_http_call)
.is_some_and(is_blocking_http_call)
{
checker.diagnostics.push(Diagnostic::new(
BlockingHttpCallInAsyncFunction,

View File

@@ -48,7 +48,7 @@ pub(crate) fn blocking_os_call(checker: &mut Checker, expr: &Expr) {
.semantic()
.resolve_call_path(func)
.as_ref()
.map_or(false, is_unsafe_os_method)
.is_some_and(is_unsafe_os_method)
{
checker
.diagnostics

View File

@@ -48,7 +48,7 @@ pub(crate) fn open_sleep_or_subprocess_call(checker: &mut Checker, expr: &Expr)
.semantic()
.resolve_call_path(func)
.as_ref()
.map_or(false, is_open_sleep_or_subprocess_call)
.is_some_and(is_open_sleep_or_subprocess_call)
{
checker.diagnostics.push(Diagnostic::new(
OpenSleepOrSubprocessInAsyncFunction,

View File

@@ -26,18 +26,14 @@ pub(super) fn is_untyped_exception(type_: Option<&Expr>, semantic: &SemanticMode
type_.map_or(true, |type_| {
if let Expr::Tuple(ast::ExprTuple { elts, .. }) = &type_ {
elts.iter().any(|type_| {
semantic
.resolve_call_path(type_)
.map_or(false, |call_path| {
matches!(call_path.as_slice(), ["", "Exception" | "BaseException"])
})
})
} else {
semantic
.resolve_call_path(type_)
.map_or(false, |call_path| {
semantic.resolve_call_path(type_).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["", "Exception" | "BaseException"])
})
})
} else {
semantic.resolve_call_path(type_).is_some_and(|call_path| {
matches!(call_path.as_slice(), ["", "Exception" | "BaseException"])
})
}
})
}

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