Compare commits

...

74 Commits

Author SHA1 Message Date
Zanie Blue
3ecd263b4d Bump version to 0.0.284 (#6453)
## What's Changed

This release fixes a few bugs, notably the previous release announced a
breaking change where the default target
Python version changed from 3.10 to 3.8 but it was not applied. Thanks
to @rco-ableton for fixing this in
https://github.com/astral-sh/ruff/pull/6444

### Bug Fixes
* Do not trigger `S108` if path is inside `tempfile.*` call by
@dhruvmanila in https://github.com/astral-sh/ruff/pull/6416
* Do not allow on zero tab width by @tjkuson in
https://github.com/astral-sh/ruff/pull/6429
* Fix false-positive in submodule resolution by @charliermarsh in
https://github.com/astral-sh/ruff/pull/6435

## New Contributors
* @rco-ableton made their first contribution in
https://github.com/astral-sh/ruff/pull/6444

**Full Changelog**:
https://github.com/astral-sh/ruff/compare/v0.0.283...v0.0.284
2023-08-09 13:32:33 -05:00
Charlie Marsh
6acf07c5c4 Use latest Python version by default in tests (#6448)
## Summary

Use the same Python version by default for all tests (our
latest-supported version).

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-08-09 15:22:39 +00:00
Charlie Marsh
38b9fb8bbd Set a default on PythonVersion (#6446)
## Summary

I think it makes sense for `PythonVersion::default()` to return our
minimum-supported non-EOL version.

## Test Plan

`cargo test`

---------

Co-authored-by: Zanie <contact@zanie.dev>
2023-08-09 15:19:27 +00:00
dependabot[bot]
e4f57434a2 ci(deps): bump cloudflare/wrangler-action from 2.0.0 to 3.0.0 (#6398)
Bumps
[cloudflare/wrangler-action](https://github.com/cloudflare/wrangler-action)
from 2.0.0 to 3.0.0.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="089567dec4"><code>089567d</code></a>
feat: rewrite Wrangler Action in TypeScript</li>
<li>See full diff in <a
href="https://github.com/cloudflare/wrangler-action/compare/2.0.0...3.0.0">compare
view</a></li>
</ul>
</details>
<br />


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

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

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

---

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

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


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-09 10:17:43 -05:00
Dhruv Manilawala
6a64f2289b Rename Magic* to IpyEscape* (#6395)
## Summary

This PR renames the `MagicCommand` token to `IpyEscapeCommand` token and
`MagicKind` to `IpyEscapeKind` type to better reflect the purpose of the
token and type. Similarly, it renames the AST nodes from `LineMagic` to
`IpyEscapeCommand` prefixed with `Stmt`/`Expr` wherever necessary.

It also makes renames from using `jupyter_magic` to
`ipython_escape_commands` in various function names.

The mode value is still `Mode::Jupyter` because the escape commands are
part of the IPython syntax but the lexing/parsing is done for a Jupyter
notebook.

### Motivation behind the rename:
* IPython codebase defines it as "EscapeCommand" / "Escape Sequences":
* Escape Sequences:
292e3a2345/IPython/core/inputtransformer2.py (L329-L333)
* Escape command:
292e3a2345/IPython/core/inputtransformer2.py (L410-L411)
* The word "magic" is used mainly for the actual magic commands i.e.,
the ones starting with `%`/`%%`
(https://ipython.readthedocs.io/en/stable/interactive/reference.html#magic-command-system).
So, this avoids any confusion between the Magic token (`%`, `%%`) and
the escape command itself.
## Test Plan

* `cargo test` to make sure all renames are done correctly.
* `grep` for `jupyter_escape`/`magic` to make sure all renames are done
correctly.
2023-08-09 13:28:18 +00:00
Charlie Marsh
3bf1c66cda Group function definition parameters with return type annotations (#6410)
## Summary

This PR removes the group around function definition parameters, instead
grouping the parameters with the type parameters and return type
annotation.

This increases Zulip's similarity score from 0.99385 to 0.99699, so it's
a meaningful improvement. However, there's at least one stability error
that I'm working on, and I'm really just looking for high-level feedback
at this point, because I'm not happy with the solution.

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

## Test Plan

Before:

- `zulip`: 0.99396
- `django`: 0.99784
- `warehouse`: 0.99578
- `build`: 0.75436
- `transformers`: 0.99407
- `cpython`: 0.75987
- `typeshed`: 0.74432

After:

- `zulip`: 0.99702
- `django`: 0.99784
- `warehouse`: 0.99585
- `build`: 0.75623
- `transformers`: 0.99470
- `cpython`: 0.75988
- `typeshed`: 0.74853
2023-08-09 12:13:58 +00:00
rco-ableton
eaada0345c Set default version to py38 (#6444)
## Summary

In https://github.com/astral-sh/ruff/pull/6397, the documentation was
updated stating that the default target-version is now "py38", but the
actual default value wasn't updated and remained py310. This commit
updates the default value to match what the documentation says.
2023-08-09 12:08:47 +00:00
Micha Reiser
a39dd76d95 Add enter and leave_node methods to Preoder visitor (#6422) 2023-08-09 09:09:00 +00:00
Dhruv Manilawala
e257c5af32 Add support for help end IPython escape commands (#6358)
## Summary

This PR adds support for a stricter version of help end escape
commands[^1] in the parser. By stricter, I mean that the escape tokens
are only at the end of the command and there are no tokens at the start.
This makes it difficult to implement it in the lexer without having to
do a lot of look aheads or keeping track of previous tokens.

Now, as we're adding this in the parser, the lexer needs to recognize
and emit a new token for `?`. So, `Question` token is added which will
be recognized only in `Jupyter` mode.

The conditions applied are the same as the ones in the original
implementation in IPython codebase (which is a regex):
* There can only be either 1 or 2 question mark(s) at the end
* The node before the question mark can be a `Name`, `Attribute`,
`Subscript` (only with integer constants in slice position), or any
combination of the 3 nodes.

## Test Plan

Added test cases for various combination of the possible nodes in the
command value position and update the snapshots.

fixes: #6359
fixes: #5030 (This is the final piece)

[^1]: https://github.com/astral-sh/ruff/pull/6272#issue-1833094281
2023-08-09 10:28:52 +05:30
Dhruv Manilawala
887a47cad9 Avoid S108 if path is inside tempfile.* call (#6416) 2023-08-09 10:22:31 +05:30
Charlie Marsh
a2758513de Fix false-positive in submodule resolution (#6435)
Closes https://github.com/astral-sh/ruff/issues/6433.
2023-08-09 02:36:39 +00:00
Tom Kuson
1b9fed8397 Error on zero tab width (#6429)
## Summary

Error if `tab-size` is set to zero (it is used as a divisor). Closes
#6423.

Also fixes a typo.

## Test Plan

Running ruff with a config

```toml
[tool.ruff]
tab-size = 0
```

returns an error message to the user saying that `tab-size` must be
greater than zero.
2023-08-08 16:51:37 -04:00
Charlie Marsh
55d6fd53cd Treat comments on open parentheses in return annotations as dangling (#6413)
## Summary

Given:

```python
def double(a: int) -> ( # Hello
    int
):
    return 2*a
```

We currently treat `# Hello` as a trailing comment on the parameters
(`(a: int)`). This PR adds a placement method to instead treat it as a
dangling comment on the function definition itself, so that it gets
formatted at the end of the definition, like:

```python
def double(a: int) -> int:  # Hello
    return 2*a
```

The formatting in this case is unchanged, but it's incorrect IMO for
that to be a trailing comment on the parameters, and that placement
leads to an instability after changing the grouping in #6410.

Fixing this led to a _different_ instability related to tuple return
type annotations, like:

```python
def zrevrangebylex(self, name: _Key, max: _Value, min: _Value, start: int | None = None, num: int | None = None) -> (  # type: ignore[override]
):
    ...
```

(This is a real example.)

To fix, I had to special-case tuples in that spot, though I'm not
certain that's correct.
2023-08-08 16:48:38 -04:00
Zanie Blue
d33618062e Improve documentation for PLE1300 (#6430) 2023-08-08 20:16:36 +00:00
Charlie Marsh
c7703e205d Move empty_parenthesized into the parentheses.rs (#6403)
## Summary

This PR moves `empty_parenthesized` such that it's peer to
`parenthesized`, and changes the API to better match that of
`parenthesized` (takes `&str` rather than `StaticText`, has a
`with_dangling_comments` method, etc.).

It may be intentionally _not_ part of `parentheses.rs`, but to me
they're so similar that it makes more sense for them to be in the same
module, with the same API, etc.
2023-08-08 19:17:17 +00:00
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
420 changed files with 50618 additions and 41251 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- "!crates/ruff_formatter/**"
- "!crates/ruff_dev/**"
- "!crates/ruff_shrinking/**"
- scripts/check_ecosystem.py
- scripts/*
formatter:
- Cargo.toml
@@ -56,6 +56,7 @@ jobs:
- crates/ruff_text_size/**
- crates/ruff_python_parser/**
- crates/ruff_dev/**
- scripts/*
cargo-fmt:
name: "cargo fmt"
@@ -338,8 +339,5 @@ jobs:
run: scripts/formatter_ecosystem_checks.sh
- name: "Github step summary"
run: grep "similarity index" target/progress_projects_log.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
- name: "Remove checkouts from cache"
run: rm -r target/progress_projects

View File

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

View File

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

View File

@@ -1,5 +1,15 @@
# Breaking Changes
## 0.0.283 / 0.284
### 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.)
Note this change was announced in 0.0.283 but not active until 0.0.284.
## 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

@@ -131,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.

6
Cargo.lock generated
View File

@@ -800,7 +800,7 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flake8-to-ruff"
version = "0.0.282"
version = "0.0.284"
dependencies = [
"anyhow",
"clap",
@@ -2042,7 +2042,7 @@ dependencies = [
[[package]]
name = "ruff"
version = "0.0.282"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",
@@ -2141,7 +2141,7 @@ dependencies = [
[[package]]
name = "ruff_cli"
version = "0.0.282"
version = "0.0.284"
dependencies = [
"annotate-snippets 0.9.1",
"anyhow",

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.282
rev: v0.0.284
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.282"
version = "0.0.284"
description = """
Convert Flake8 configuration files to Ruff configuration files.
"""

View File

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

View File

@@ -14,3 +14,19 @@ with open("/dev/shm/unit/test", "w") as f:
# not ok by config
with open("/foo/bar", "w") as f:
f.write("def")
# Using `tempfile` module should be ok
import tempfile
from tempfile import TemporaryDirectory
with tempfile.NamedTemporaryFile(dir="/tmp") as f:
f.write(b"def")
with tempfile.NamedTemporaryFile(dir="/var/tmp") as f:
f.write(b"def")
with tempfile.TemporaryDirectory(dir="/dev/shm") as d:
pass
with TemporaryDirectory(dir="/tmp") as d:
pass

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

@@ -14,6 +14,10 @@ class BadClass:
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

View File

@@ -14,6 +14,10 @@ class BadClass:
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

View File

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

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

@@ -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,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

@@ -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

@@ -92,3 +92,10 @@ match *0, 1, *2:
case 0,:
import x
import y
# Test: access a sub-importation via an alias.
import foo.bar as bop
import foo.bar.baz
print(bop.baz.read_csv("test.csv"))

View File

@@ -70,3 +70,13 @@ import requests_mock as rm
def requests_mock(requests_mock: rm.Mocker):
print(rm.ANY)
import sklearn.base
import mlflow.sklearn
def f():
import sklearn
mlflow

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

@@ -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

@@ -198,3 +198,7 @@ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
).format(a=1)
"{}".format(**c)
"{}".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

@@ -5,11 +5,42 @@ from typing import TypeAlias
x: typing.TypeAlias = int
x: TypeAlias = int
# UP040 with generics (todo)
# 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

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

@@ -3,10 +3,12 @@
use anyhow::{bail, Result};
use ruff_diagnostics::Edit;
use ruff_python_ast::{self as ast, Arguments, ExceptHandler, Expr, Keyword, Ranged, Stmt};
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, Mode};
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};
@@ -56,14 +58,14 @@ 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())),
}
@@ -88,6 +90,7 @@ pub(crate) fn remove_argument<T: Ranged>(
arguments: &Arguments,
parentheses: Parentheses,
locator: &Locator,
source_type: PySourceType,
) -> Result<Edit> {
// TODO(sbrugman): Preserve trailing comments.
if arguments.keywords.len() + arguments.args.len() > 1 {
@@ -106,7 +109,7 @@ pub(crate) fn remove_argument<T: Ranged>(
let mut seen_comma = false;
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
Mode::Module,
source_type.as_mode(),
arguments.start(),
)
.flatten()
@@ -135,7 +138,7 @@ pub(crate) fn remove_argument<T: Ranged>(
// previous comma to the end of the argument.
for (tok, range) in lexer::lex_starts_at(
locator.slice(arguments.range()),
Mode::Module,
source_type.as_mode(),
arguments.start(),
)
.flatten()
@@ -176,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;

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;
@@ -231,10 +247,7 @@ pub(crate) fn deferred_scopes(checker: &mut Checker) {
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);
}
@@ -243,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,
@@ -260,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,7 +30,7 @@ pub(crate) fn definitions(checker: &mut Checker) {
Rule::MissingTypeKwargs,
Rule::MissingTypeSelf,
]);
let enforce_stubs = checker.is_stub && checker.enabled(Rule::DocstringInStub);
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,

View File

@@ -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()
@@ -80,17 +80,9 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
Rule::RedundantLiteralUnion,
Rule::UnnecessaryTypeUnion,
]) {
// Avoid duplicate checks if the parent is an `Union[...]` since these rules
// Avoid duplicate checks if the parent is a union, since these rules already
// 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.semantic.in_nested_union() {
if checker.enabled(Rule::UnnecessaryLiteralUnion) {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
@@ -176,7 +168,7 @@ 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()
@@ -187,7 +179,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()
@@ -206,11 +198,16 @@ 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().get(id).is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
}) {
if !checker
.semantic
.current_scope()
.get(id)
.is_some_and(|binding_id| {
checker.semantic.binding(binding_id).is_global()
})
{
pep8_naming::rules::non_lowercase_variable_in_function(
checker, expr, id,
);
@@ -219,7 +216,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
if checker.enabled(Rule::MixedCaseVariableInClassScope) {
if let ScopeKind::Class(ast::StmtClassDef { arguments, .. }) =
&checker.semantic.scope().kind
&checker.semantic.current_scope().kind
{
pep8_naming::rules::mixed_case_variable_in_class_scope(
checker,
@@ -230,7 +227,7 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
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,
);
@@ -243,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,
@@ -272,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()
@@ -285,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()
@@ -341,6 +338,8 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
},
) => {
if checker.any_enabled(&[
// pylint
Rule::BadStringFormatCharacter,
// pyflakes
Rule::StringDotFormatInvalidFormat,
Rule::StringDotFormatExtraNamedArguments,
@@ -403,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(
@@ -666,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,
);
@@ -916,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);
}
@@ -1064,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()
@@ -1077,29 +1076,23 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
// Avoid duplicate checks if the parent is an `|` since these rules
// Avoid duplicate checks if the parent is a union, since these rules already
// traverse nested unions.
let is_unchecked_union = !matches!(
checker.semantic.expr_parent(),
Some(Expr::BinOp(ast::ExprBinOp {
op: Operator::BitOr,
..
}))
);
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
&& is_unchecked_union
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
if checker.enabled(Rule::UnnecessaryLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_literal_union(checker, expr);
}
if checker.enabled(Rule::RedundantLiteralUnion) && is_unchecked_union {
flake8_pyi::rules::redundant_literal_union(checker, expr);
}
if checker.enabled(Rule::UnnecessaryTypeUnion) && is_unchecked_union {
flake8_pyi::rules::unnecessary_type_union(checker, expr);
if !checker.semantic.in_nested_union() {
if checker.enabled(Rule::DuplicateUnionMember)
&& checker.semantic.in_type_definition()
{
flake8_pyi::rules::duplicate_union_member(checker, expr);
}
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 {
@@ -1210,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);
}
}
@@ -1219,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);
}
}
@@ -1236,18 +1229,12 @@ pub(crate) fn expression(expr: &Expr, checker: &mut Checker) {
}
}
if checker.enabled(Rule::HardcodedTempFile) {
if let Some(diagnostic) = flake8_bandit::rules::hardcoded_tmp_directory(
expr,
value,
&checker.settings.flake8_bandit.hardcoded_tmp_directory,
) {
checker.diagnostics.push(diagnostic);
}
flake8_bandit::rules::hardcoded_tmp_directory(checker, expr, value);
}
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);
}

View File

@@ -15,7 +15,7 @@ pub(crate) fn parameters(parameters: &Parameters, checker: &mut Checker) {
if checker.settings.rules.enabled(Rule::ImplicitOptional) {
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, 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,29 +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,
parameters,
body,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
decorator_list,
returns,
parameters,
body,
type_params,
..
range: _,
}) => {
if checker.enabled(Rule::DjangoNonLeadingReceiverDecorator) {
flake8_django::rules::non_leading_receiver_decorator(checker, decorator_list);
@@ -113,7 +105,7 @@ 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,
parameters,
@@ -125,7 +117,7 @@ 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,
parameters,
@@ -133,7 +125,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::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
@@ -151,11 +143,11 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
flake8_pyi::rules::non_self_return_type(
checker,
stmt,
*is_async,
name,
decorator_list,
returns.as_ref().map(AsRef::as_ref),
parameters,
stmt.is_async_function_def_stmt(),
);
}
if checker.enabled(Rule::CustomTypeVarReturnType) {
@@ -168,30 +160,27 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
type_params.as_ref(),
);
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::StrOrReprDefinedInStub) {
flake8_pyi::rules::str_or_repr_defined_in_stub(checker, stmt);
}
}
if checker.is_stub || checker.settings.target_version >= PythonVersion::Py311 {
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, parameters);
}
}
if checker.enabled(Rule::BadExitAnnotation) {
flake8_pyi::rules::bad_exit_annotation(
checker,
stmt.is_async_function_def_stmt(),
name,
parameters,
);
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,
@@ -346,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,
@@ -412,7 +401,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
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);
}
@@ -453,7 +442,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::AbstractBaseClassWithoutAbstractMethod,
Rule::EmptyMethodWithoutAbstractDecorator,
@@ -467,7 +456,7 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
);
}
}
if checker.is_stub {
if checker.source_type.is_stub() {
if checker.enabled(Rule::PassStatementStubBody) {
flake8_pyi::rules::pass_statement_stub_body(checker, body);
}
@@ -569,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);
}
@@ -744,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);
}
@@ -764,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),
@@ -889,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);
}
@@ -980,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) {
@@ -1002,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) {
@@ -1013,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,
@@ -1095,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);
}
@@ -1106,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) {
@@ -1132,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])
{
@@ -1325,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,
@@ -1336,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);
@@ -1395,14 +1375,14 @@ pub(crate) fn statement(stmt: &Stmt, checker: &mut Checker) {
if checker.enabled(Rule::NonPEP695TypeAlias) {
pyupgrade::rules::non_pep695_type_alias(checker, assign_stmt);
}
if checker.is_stub {
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,

View File

@@ -39,11 +39,13 @@ 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};
@@ -53,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;
@@ -75,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,
@@ -118,6 +119,7 @@ impl<'a> Checker<'a> {
stylist: &'a Stylist,
indexer: &'a Indexer,
importer: Importer<'a>,
source_type: PySourceType,
) -> Checker<'a> {
Checker {
settings,
@@ -126,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,
@@ -174,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))?;
@@ -200,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())
})
@@ -233,11 +234,6 @@ impl<'a> Checker<'a> {
&self.semantic
}
/// Return `true` if the current file is a stub file (`.pyi`).
pub(crate) const fn is_stub(&self) -> bool {
self.is_stub
}
/// The [`Path`] to the file under analysis.
pub(crate) const fn path(&self) -> &'a Path {
self.path
@@ -267,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 {
@@ -291,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;
}
@@ -325,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 {
@@ -346,11 +342,11 @@ where
}
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,
);
}
@@ -375,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;
@@ -394,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,
);
}
@@ -420,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);
}
}
@@ -443,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);
}
}
@@ -454,22 +454,16 @@ where
// Step 2: Traversal
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
body,
parameters,
decorator_list,
returns,
type_params,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body,
parameters,
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 {
@@ -530,8 +524,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Function,
stmt,
ExtractionTarget::Function(function_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -539,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());
@@ -574,8 +566,7 @@ where
}
let definition = docstrings::extraction::extract_definition(
ExtractionTarget::Class,
stmt,
ExtractionTarget::Class(class_def),
self.semantic.definition_id,
&self.semantic.definitions,
);
@@ -656,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
@@ -675,7 +666,7 @@ where
}
} else {
matches!(
self.semantic.scope().kind,
self.semantic.current_scope().kind,
ScopeKind::Class(_) | ScopeKind::Module
)
};
@@ -742,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
@@ -776,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) {
@@ -812,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.
@@ -840,7 +830,7 @@ where
}) => {
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();
}
}
@@ -1206,7 +1196,7 @@ where
));
}
}
Expr::JoinedStr(_) => {
Expr::FString(_) => {
self.semantic.flags |= SemanticModelFlags::F_STRING;
visitor::walk_expr(self, expr);
}
@@ -1230,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) {
@@ -1285,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);
}
@@ -1610,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,
@@ -1625,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(),
@@ -1645,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 {
@@ -1697,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,
@@ -1724,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;
}
@@ -1732,7 +1722,7 @@ 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);
}
@@ -1786,7 +1776,7 @@ impl<'a> Checker<'a> {
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);
}
@@ -1824,19 +1814,14 @@ impl<'a> Checker<'a> {
for snapshot in deferred_functions {
self.semantic.restore(snapshot);
match &self.semantic.stmt() {
Stmt::FunctionDef(ast::StmtFunctionDef {
body, parameters, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
body, parameters, ..
}) => {
self.visit_parameters(parameters);
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")
}
}
}
@@ -1928,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 {
@@ -1955,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

@@ -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

@@ -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.
@@ -300,12 +301,12 @@ fn match_leading_semicolon(s: &str) -> Option<TextSize> {
mod tests {
use anyhow::Result;
use ruff_python_parser::lexer::LexResult;
use ruff_text_size::TextSize;
use ruff_python_ast::PySourceType;
use ruff_python_codegen::Stylist;
use ruff_python_parser::parse_suite;
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;
@@ -313,7 +314,7 @@ mod tests {
fn start_of_file() -> Result<()> {
fn insert(contents: &str) -> Result<Insertion> {
let program = parse_suite(contents, "<filename>")?;
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);
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(
@@ -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("%%")),
}
}
}
@@ -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_ipy_escape_command() -> Result<()> {
let path = "ipy_escape_command.ipynb".to_string();
let (diagnostics, source_kind, _) = test_notebook_path(
&path,
Path::new("ipy_escape_command_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
---
ipy_escape_command.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

@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
use std::num::NonZeroU8;
use unicode_width::UnicodeWidthChar;
use ruff_macros::CacheKey;
@@ -83,7 +84,7 @@ impl LineWidth {
}
fn update(mut self, chars: impl Iterator<Item = char>) -> Self {
let tab_size: usize = self.tab_size.into();
let tab_size: usize = self.tab_size.as_usize();
for c in chars {
match c {
'\t' => {
@@ -144,22 +145,22 @@ impl PartialOrd<LineLength> for LineWidth {
/// The size of a tab.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, CacheKey)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabSize(pub u8);
pub struct TabSize(pub NonZeroU8);
impl TabSize {
fn as_usize(self) -> usize {
self.0.get() as usize
}
}
impl Default for TabSize {
fn default() -> Self {
Self(4)
Self(NonZeroU8::new(4).unwrap())
}
}
impl From<u8> for TabSize {
fn from(tab_size: u8) -> Self {
impl From<NonZeroU8> for TabSize {
fn from(tab_size: NonZeroU8) -> Self {
Self(tab_size)
}
}
impl From<TabSize> for usize {
fn from(tab_size: TabSize) -> Self {
tab_size.0 as usize
}
}

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

@@ -293,12 +293,10 @@ impl Display for MessageCodeFrame<'_> {
}
fn replace_whitespace(source: &str, annotation_range: TextRange) -> SourceCode {
static TAB_SIZE: TabSize = TabSize(4); // TODO(jonathan): use `tab-size`
let mut result = String::new();
let mut last_end = 0;
let mut range = annotation_range;
let mut line_width = LineWidth::new(TAB_SIZE);
let mut line_width = LineWidth::new(TabSize::default());
for (index, c) in source.char_indices() {
let old_width = line_width.get();

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();

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, Expr, Parameters, 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, &Parameters, Option<&Expr>, &[Stmt], &[ast::Decorator]) {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
name,
parameters,
returns,
body,
decorator_list,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
name,
parameters,
returns,
body,
decorator_list,
..
}) => (
name,
parameters,
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, Constant, Expr, ParameterWithDefault, 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?
@@ -528,20 +527,19 @@ pub(crate) fn definition(
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
// ANN401 for dynamically typed parameters
if let Some(annotation) = &parameter.annotation {
has_any_typed_arg = true;
if checker.enabled(Rule::AnyType) && !is_overridden {
@@ -572,7 +570,7 @@ 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 {
@@ -598,7 +596,7 @@ 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 {
@@ -629,18 +627,18 @@ pub(crate) fn definition(
}
// ANN101, ANN102
if is_method && !visibility::is_staticmethod(cast::decorator_list(stmt), checker.semantic()) {
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 parameter.annotation.is_none() {
if visibility::is_classmethod(cast::decorator_list(stmt), checker.semantic()) {
if visibility::is_classmethod(decorator_list, checker.semantic()) {
if checker.enabled(Rule::MissingTypeCls) {
diagnostics.push(Diagnostic::new(
MissingTypeCls {
@@ -676,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) {
@@ -705,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);
@@ -722,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)
});
}
}
@@ -742,7 +748,7 @@ pub(crate) fn definition(
MissingReturnTypeUndocumentedPublicFunction {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
}
@@ -752,7 +758,7 @@ pub(crate) fn definition(
MissingReturnTypePrivateFunction {
name: name.to_string(),
},
stmt.identifier(),
function.identifier(),
));
}
}

View File

@@ -112,21 +112,21 @@ fn py_stat(call_path: &CallPath) -> Option<u16> {
}
}
fn int_value(expr: &Expr, model: &SemanticModel) -> Option<u16> {
fn int_value(expr: &Expr, semantic: &SemanticModel) -> Option<u16> {
match expr {
Expr::Constant(ast::ExprConstant {
value: Constant::Int(value),
..
}) => value.to_u16(),
Expr::Attribute(_) => model.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::Attribute(_) => semantic.resolve_call_path(expr).as_ref().and_then(py_stat),
Expr::BinOp(ast::ExprBinOp {
left,
op,
right,
range: _,
}) => {
let left_value = int_value(left, model)?;
let right_value = int_value(right, model)?;
let left_value = int_value(left, semantic)?;
let right_value = int_value(right, semantic)?;
match op {
Operator::BitAnd => Some(left_value & right_value),
Operator::BitOr => Some(left_value | right_value),

View File

@@ -53,7 +53,7 @@ fn matches_sql_statement(string: &str) -> bool {
SQL_REGEX.is_match(string)
}
fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool {
fn matches_string_format_expression(expr: &Expr, semantic: &SemanticModel) -> bool {
match expr {
// "select * from table where val = " + "str" + ...
// "select * from table where val = %s" % ...
@@ -62,8 +62,8 @@ fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool
..
}) => {
// Only evaluate the full BinOp, not the nested components.
if model
.expr_parent()
if semantic
.current_expression_parent()
.map_or(true, |parent| !parent.is_bin_op_expr())
{
if any_over_expr(expr, &has_string_literal) {
@@ -80,7 +80,7 @@ fn matches_string_format_expression(expr: &Expr, model: &SemanticModel) -> bool
attr == "format" && string_literal(value).is_some()
}
// f"select * from table where val = {val}"
Expr::JoinedStr(_) => true,
Expr::FString(_) => true,
_ => false,
}
}

View File

@@ -1,8 +1,10 @@
use ruff_python_ast::{Expr, Ranged};
use ruff_python_ast::{self as ast, Expr, Ranged};
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use crate::checkers::ast::Checker;
/// ## What it does
/// Checks for the use of hardcoded temporary file or directory paths.
///
@@ -49,19 +51,33 @@ impl Violation for HardcodedTempFile {
}
/// S108
pub(crate) fn hardcoded_tmp_directory(
expr: &Expr,
value: &str,
prefixes: &[String],
) -> Option<Diagnostic> {
if prefixes.iter().any(|prefix| value.starts_with(prefix)) {
Some(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
))
} else {
None
pub(crate) fn hardcoded_tmp_directory(checker: &mut Checker, expr: &Expr, value: &str) {
if !checker
.settings
.flake8_bandit
.hardcoded_tmp_directory
.iter()
.any(|prefix| value.starts_with(prefix))
{
return;
}
if let Some(Expr::Call(ast::ExprCall { func, .. })) =
checker.semantic().current_expression_parent()
{
if checker
.semantic()
.resolve_call_path(func)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["tempfile", ..]))
{
return;
}
}
checker.diagnostics.push(Diagnostic::new(
HardcodedTempFile {
string: value.to_string(),
},
expr.range(),
));
}

View File

@@ -10,6 +10,31 @@ use crate::{
checkers::ast::Checker, registry::Rule, rules::flake8_bandit::helpers::string_literal,
};
/// ## What it does
/// Check for method calls that initiate a subprocess with a shell.
///
/// ## Why is this bad?
/// Starting a subprocess with a shell can allow attackers to execute arbitrary
/// shell commands. Consider starting the process without a shell call and
/// sanitize the input to mitigate the risk of shell injection.
///
/// ## Example
/// ```python
/// import subprocess
///
/// subprocess.run("ls -l", shell=True)
/// ```
///
/// Use instead:
/// ```python
/// import subprocess
///
/// subprocess.run(["ls", "-l"])
/// ```
///
/// ## References
/// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html)
/// - [Common Weakness Enumeration: CWE-78](https://cwe.mitre.org/data/definitions/78.html)
#[violation]
pub struct SubprocessPopenWithShellEqualsTrue {
seems_safe: bool,
@@ -28,6 +53,30 @@ impl Violation for SubprocessPopenWithShellEqualsTrue {
}
}
/// ## What it does
/// Check for method calls that initiate a subprocess without a shell.
///
/// ## Why is this bad?
/// Starting a subprocess without a shell can prevent attackers from executing
/// arbitrary shell commands; however, it is still error-prone. Consider
/// validating the input.
///
/// ## Known problems
/// Prone to false positives as it is difficult to determine whether the
/// passed arguments have been validated ([#4045]).
///
/// ## Example
/// ```python
/// import subprocess
///
/// cmd = input("Enter a command: ").split()
/// subprocess.run(cmd)
/// ```
///
/// ## References
/// - [Python documentation: `subprocess` — Subprocess management](https://docs.python.org/3/library/subprocess.html)
///
/// [#4045]: https://github.com/astral-sh/ruff/issues/4045
#[violation]
pub struct SubprocessWithoutShellEqualsTrue;

View File

@@ -49,7 +49,6 @@ mod tests {
#[test_case(Rule::UselessComparison, Path::new("B015.py"))]
#[test_case(Rule::UselessContextlibSuppress, Path::new("B022.py"))]
#[test_case(Rule::UselessExpression, Path::new("B018.py"))]
#[test_case(Rule::ZipWithoutExplicitStrict, Path::new("B905.py"))]
fn rules(rule_code: Rule, path: &Path) -> Result<()> {
let snapshot = format!("{}_{}", rule_code.noqa_code(), path.to_string_lossy());
let diagnostics = test_path(
@@ -60,6 +59,17 @@ mod tests {
Ok(())
}
#[test]
fn zip_without_explicit_strict() -> Result<()> {
let snapshot = "B905.py";
let diagnostics = test_path(
Path::new("flake8_bugbear").join(snapshot).as_path(),
&Settings::for_rule(Rule::ZipWithoutExplicitStrict),
)?;
assert_messages!(snapshot, diagnostics);
Ok(())
}
#[test]
fn extend_immutable_calls() -> Result<()> {
let snapshot = "extend_immutable_calls".to_string();
@@ -72,7 +82,7 @@ mod tests {
"fastapi.Query".to_string(),
],
},
..Settings::for_rules(vec![Rule::FunctionCallInDefaultArgument])
..Settings::for_rule(Rule::FunctionCallInDefaultArgument)
},
)?;
assert_messages!(snapshot, diagnostics);

View File

@@ -159,18 +159,12 @@ pub(crate) fn abstract_base_class(
continue;
}
let (Stmt::FunctionDef(ast::StmtFunctionDef {
let Stmt::FunctionDef(ast::StmtFunctionDef {
decorator_list,
body,
name: method_name,
..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
decorator_list,
body,
name: method_name,
..
})) = stmt
}) = stmt
else {
continue;
};

View File

@@ -77,7 +77,7 @@ fn is_cache_func(expr: &Expr, semantic: &SemanticModel) -> bool {
/// B019
pub(crate) fn cached_instance_method(checker: &mut Checker, decorator_list: &[Decorator]) {
if !checker.semantic().scope().kind.is_class() {
if !checker.semantic().current_scope().kind.is_class() {
return;
}
for decorator in decorator_list {

View File

@@ -150,7 +150,9 @@ fn duplicate_handler_exceptions<'a>(
if unique_elts.len() == 1 {
checker.generator().expr(unique_elts[0])
} else {
checker.generator().expr(&type_pattern(unique_elts))
// Multiple exceptions must always be parenthesized. This is done
// manually as the generator never parenthesizes lone tuples.
format!("({})", checker.generator().expr(&type_pattern(unique_elts)))
},
expr.range(),
)));

View File

@@ -50,7 +50,7 @@ pub(crate) fn f_string_docstring(checker: &mut Checker, body: &[Stmt]) {
let Stmt::Expr(ast::StmtExpr { value, range: _ }) = stmt else {
return;
};
if !value.is_joined_str_expr() {
if !value.is_f_string_expr() {
return;
}
checker

View File

@@ -67,9 +67,9 @@ struct ArgumentDefaultVisitor<'a> {
}
impl<'a> ArgumentDefaultVisitor<'a> {
fn new(model: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
fn new(semantic: &'a SemanticModel<'a>, extend_immutable_calls: Vec<CallPath<'a>>) -> Self {
Self {
semantic: model,
semantic,
extend_immutable_calls,
diagnostics: Vec::new(),
}

View File

@@ -86,9 +86,6 @@ impl<'a> Visitor<'a> for SuspiciousVariablesVisitor<'a> {
match stmt {
Stmt::FunctionDef(ast::StmtFunctionDef {
parameters, body, ..
})
| Stmt::AsyncFunctionDef(ast::StmtAsyncFunctionDef {
parameters, body, ..
}) => {
// Collect all loaded variable names.
let mut visitor = LoadedNamesVisitor::default();
@@ -236,7 +233,7 @@ struct AssignedNamesVisitor<'a> {
/// `Visitor` to collect all used identifiers in a statement.
impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
fn visit_stmt(&mut self, stmt: &'a Stmt) {
if matches!(stmt, Stmt::FunctionDef(_) | Stmt::AsyncFunctionDef(_)) {
if stmt.is_function_def_stmt() {
// Don't recurse.
return;
}
@@ -251,8 +248,7 @@ impl<'a> Visitor<'a> for AssignedNamesVisitor<'a> {
}
Stmt::AugAssign(ast::StmtAugAssign { target, .. })
| Stmt::AnnAssign(ast::StmtAnnAssign { target, .. })
| Stmt::For(ast::StmtFor { target, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { target, .. }) => {
| Stmt::For(ast::StmtFor { target, .. }) => {
let mut visitor = NamesFromAssignmentsVisitor::default();
visitor.visit_expr(target);
self.names.extend(visitor.names);

View File

@@ -69,16 +69,13 @@ fn walk_stmt(checker: &mut Checker, body: &[Stmt], f: fn(&Stmt) -> bool) {
));
}
match stmt {
Stmt::While(ast::StmtWhile { body, .. })
| Stmt::For(ast::StmtFor { body, .. })
| Stmt::AsyncFor(ast::StmtAsyncFor { body, .. }) => {
Stmt::While(ast::StmtWhile { body, .. }) | Stmt::For(ast::StmtFor { body, .. }) => {
walk_stmt(checker, body, Stmt::is_return_stmt);
}
Stmt::If(ast::StmtIf { body, .. })
| Stmt::Try(ast::StmtTry { body, .. })
| Stmt::TryStar(ast::StmtTryStar { body, .. })
| Stmt::With(ast::StmtWith { body, .. })
| Stmt::AsyncWith(ast::StmtAsyncWith { body, .. }) => {
| Stmt::With(ast::StmtWith { body, .. }) => {
walk_stmt(checker, body, f);
}
Stmt::Match(ast::StmtMatch { cases, .. }) => {

View File

@@ -97,7 +97,7 @@ pub(crate) fn setattr_with_constant(
if let Stmt::Expr(ast::StmtExpr {
value: child,
range: _,
}) = checker.semantic().stmt()
}) = checker.semantic().current_statement()
{
if expr == child.as_ref() {
let mut diagnostic = Diagnostic::new(SetAttrWithConstant, expr.range());

View File

@@ -159,7 +159,7 @@ pub(crate) fn unused_loop_control_variable(checker: &mut Checker, target: &Expr,
if certainty.into() {
// Avoid fixing if the variable, or any future bindings to the variable, are
// used _after_ the loop.
let scope = checker.semantic().scope();
let scope = checker.semantic().current_scope();
if scope
.get_all(name)
.map(|binding_id| checker.semantic().binding(binding_id))

View File

@@ -54,7 +54,7 @@ pub(crate) fn useless_expression(checker: &mut Checker, value: &Expr) {
// Ignore strings, to avoid false positives with docstrings.
if matches!(
value,
Expr::JoinedStr(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Ellipsis,
..

View File

@@ -81,33 +81,43 @@ B006_B008.py:221:20: B006 Do not use mutable data structures for argument defaul
222 | pass
|
B006_B008.py:254:27: B006 Do not use mutable data structures for argument defaults
B006_B008.py:258:27: B006 Do not use mutable data structures for argument defaults
|
253 | def mutable_annotations(
254 | a: list[int] | None = [],
257 | def mutable_annotations(
258 | a: list[int] | None = [],
| ^^ B006
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
B006_B008.py:255:35: B006 Do not use mutable data structures for argument defaults
B006_B008.py:259:35: B006 Do not use mutable data structures for argument defaults
|
253 | def mutable_annotations(
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
257 | def mutable_annotations(
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
| ^^ B006
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
257 | ):
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
|
B006_B008.py:256:62: B006 Do not use mutable data structures for argument defaults
B006_B008.py:260:62: B006 Do not use mutable data structures for argument defaults
|
254 | a: list[int] | None = [],
255 | b: Optional[Dict[int, int]] = {},
256 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
258 | a: list[int] | None = [],
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
257 | ):
258 | pass
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
262 | ):
|
B006_B008.py:261:80: B006 Do not use mutable data structures for argument defaults
|
259 | b: Optional[Dict[int, int]] = {},
260 | c: Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
261 | d: typing_extensions.Annotated[Union[Set[str], abc.Sized], "annotation"] = set(),
| ^^^^^ B006
262 | ):
263 | pass
|

View File

@@ -64,4 +64,22 @@ B014.py:49:8: B014 [*] Exception handler with duplicate exception: `re.error`
51 51 | pass
52 52 |
B014.py:82:8: B014 [*] Exception handler with duplicate exception: `ValueError`
|
80 | try:
81 | pass
82 | except (ValueError, ValueError, TypeError):
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ B014
83 | pass
|
= help: De-duplicate exceptions
Fix
79 79 | # https://github.com/astral-sh/ruff/issues/6412
80 80 | try:
81 81 | pass
82 |-except (ValueError, ValueError, TypeError):
82 |+except (ValueError, TypeError):
83 83 | pass

View File

@@ -131,7 +131,7 @@ pub(crate) fn builtin_method_shadowing(
fn is_standard_library_override(
name: &str,
class_def: &ast::StmtClassDef,
model: &SemanticModel,
semantic: &SemanticModel,
) -> bool {
let Some(Arguments { args: bases, .. }) = class_def.arguments.as_deref() else {
return false;
@@ -139,13 +139,13 @@ fn is_standard_library_override(
match name {
// Ex) `Event#set`
"set" => bases.iter().any(|base| {
model
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["threading", "Event"]))
}),
// Ex) `Filter#filter`
"filter" => bases.iter().any(|base| {
model
semantic
.resolve_call_path(base)
.is_some_and(|call_path| matches!(call_path.as_slice(), ["logging", "Filter"]))
}),

View File

@@ -7,7 +7,7 @@ use libcst_native::{
RightParen, RightSquareBracket, Set, SetComp, SimpleString, SimpleWhitespace,
TrailingWhitespace, Tuple,
};
use ruff_python_ast::Ranged;
use ruff_python_ast::{Expr, Ranged};
use ruff_text_size::TextRange;
use ruff_diagnostics::{Edit, Fix};
@@ -28,7 +28,7 @@ use crate::{
pub(crate) fn fix_unnecessary_generator_list(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
// Expr(Call(GeneratorExp)))) -> Expr(ListComp)))
let module_text = locator.slice(expr.range());
@@ -58,10 +58,7 @@ pub(crate) fn fix_unnecessary_generator_list(
}
/// (C401) Convert `set(x for x in y)` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_generator_set(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
pub(crate) fn fix_unnecessary_generator_set(checker: &Checker, expr: &Expr) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -96,10 +93,7 @@ pub(crate) fn fix_unnecessary_generator_set(
/// (C402) Convert `dict((x, x) for x in range(3))` to `{x: x for x in
/// range(3)}`.
pub(crate) fn fix_unnecessary_generator_dict(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
pub(crate) fn fix_unnecessary_generator_dict(checker: &Checker, expr: &Expr) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -141,7 +135,7 @@ pub(crate) fn fix_unnecessary_generator_dict(
/// (C403) Convert `set([x for x in y])` to `{x for x in y}`.
pub(crate) fn fix_unnecessary_list_comprehension_set(
checker: &Checker,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -177,7 +171,7 @@ pub(crate) fn fix_unnecessary_list_comprehension_set(
/// range(3)}`.
pub(crate) fn fix_unnecessary_list_comprehension_dict(
checker: &Checker,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -262,10 +256,7 @@ fn drop_trailing_comma<'a>(
}
/// (C405) Convert `set((1, 2))` to `{1, 2}`.
pub(crate) fn fix_unnecessary_literal_set(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
pub(crate) fn fix_unnecessary_literal_set(checker: &Checker, expr: &Expr) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -306,10 +297,7 @@ pub(crate) fn fix_unnecessary_literal_set(
}
/// (C406) Convert `dict([(1, 2)])` to `{1: 2}`.
pub(crate) fn fix_unnecessary_literal_dict(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
pub(crate) fn fix_unnecessary_literal_dict(checker: &Checker, expr: &Expr) -> Result<Edit> {
let locator = checker.locator();
let stylist = checker.stylist();
@@ -372,10 +360,7 @@ pub(crate) fn fix_unnecessary_literal_dict(
}
/// (C408)
pub(crate) fn fix_unnecessary_collection_call(
checker: &Checker,
expr: &ruff_python_ast::Expr,
) -> Result<Edit> {
pub(crate) fn fix_unnecessary_collection_call(checker: &Checker, expr: &Expr) -> Result<Edit> {
enum Collection {
Tuple,
List,
@@ -535,7 +520,7 @@ fn pad_expression(content: String, range: TextRange, checker: &Checker) -> Strin
pub(crate) fn fix_unnecessary_literal_within_tuple_call(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -585,7 +570,7 @@ pub(crate) fn fix_unnecessary_literal_within_tuple_call(
pub(crate) fn fix_unnecessary_literal_within_list_call(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -637,7 +622,7 @@ pub(crate) fn fix_unnecessary_literal_within_list_call(
pub(crate) fn fix_unnecessary_list_call(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
// Expr(Call(List|Tuple)))) -> Expr(List|Tuple)))
let module_text = locator.slice(expr.range());
@@ -659,7 +644,7 @@ pub(crate) fn fix_unnecessary_list_call(
pub(crate) fn fix_unnecessary_call_around_sorted(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -771,7 +756,7 @@ pub(crate) fn fix_unnecessary_call_around_sorted(
pub(crate) fn fix_unnecessary_double_cast_or_process(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -801,7 +786,7 @@ pub(crate) fn fix_unnecessary_double_cast_or_process(
pub(crate) fn fix_unnecessary_comprehension(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -888,8 +873,8 @@ pub(crate) fn fix_unnecessary_comprehension(
pub(crate) fn fix_unnecessary_map(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
parent: Option<&ruff_python_ast::Expr>,
expr: &Expr,
parent: Option<&Expr>,
object_type: ObjectType,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
@@ -1018,7 +1003,7 @@ pub(crate) fn fix_unnecessary_map(
// If the expression is embedded in an f-string, surround it with spaces to avoid
// syntax errors.
if matches!(object_type, ObjectType::Set | ObjectType::Dict) {
if parent.is_some_and(ruff_python_ast::Expr::is_formatted_value_expr) {
if parent.is_some_and(Expr::is_formatted_value_expr) {
content = format!(" {content} ");
}
}
@@ -1033,7 +1018,7 @@ pub(crate) fn fix_unnecessary_map(
pub(crate) fn fix_unnecessary_literal_within_dict_call(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Edit> {
let module_text = locator.slice(expr.range());
let mut tree = match_expression(module_text)?;
@@ -1052,7 +1037,7 @@ pub(crate) fn fix_unnecessary_literal_within_dict_call(
pub(crate) fn fix_unnecessary_comprehension_any_all(
locator: &Locator,
stylist: &Stylist,
expr: &ruff_python_ast::Expr,
expr: &Expr,
) -> Result<Fix> {
// Expr(ListComp) -> Expr(GeneratorExp)
let module_text = locator.slice(expr.range());

View File

@@ -43,8 +43,8 @@ pub(crate) fn call_datetime_strptime_without_zone(checker: &mut Checker, call: &
};
let (Some(grandparent), Some(parent)) = (
checker.semantic().expr_grandparent(),
checker.semantic().expr_parent(),
checker.semantic().current_expression_grandparent(),
checker.semantic().current_expression_parent(),
) else {
checker.diagnostics.push(Diagnostic::new(
CallDatetimeStrptimeWithoutZone,

View File

@@ -18,9 +18,8 @@ use super::helpers;
/// `datetime` objects are preferred, as they represent a specific moment in
/// time, unlike "naive" objects.
///
/// `datetime.datetime.today()` crates a "naive" object; instead, use
/// instead, use `datetime.datetime.now(tz=)` to create a timezone-aware
/// object.
/// `datetime.datetime.today()` creates a "naive" object; instead, use
/// `datetime.datetime.now(tz=)` to create a timezone-aware object.
///
/// ## Example
/// ```python

View File

@@ -8,6 +8,43 @@ use crate::checkers::ast::Checker;
use super::helpers;
/// ## What it does
/// Checks for usage of `datetime.datetime.utcfromtimestamp()`.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.utcfromtimestamp()` returns a naive datetime object;
/// instead, use `datetime.datetime.fromtimestamp(ts, tz=)` to return a
/// timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.utcfromtimestamp()
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.fromtimestamp(946684800, tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDatetimeUtcfromtimestamp;
@@ -21,15 +58,6 @@ impl Violation for CallDatetimeUtcfromtimestamp {
}
}
/// Checks for `datetime.datetime.utcfromtimestamp()`. (DTZ004)
///
/// ## Why is this bad?
///
/// Because naive `datetime` objects are treated by many `datetime` methods as
/// local times, it is preferred to use aware datetimes to represent times in
/// UTC. As such, the recommended way to create an object representing a
/// specific timestamp in UTC is by calling `datetime.fromtimestamp(timestamp,
/// tz=timezone.utc)`.
pub(crate) fn call_datetime_utcfromtimestamp(
checker: &mut Checker,
func: &Expr,

View File

@@ -8,6 +8,42 @@ use crate::checkers::ast::Checker;
use super::helpers;
/// ## What it does
/// Checks for usage of `datetime.datetime.utcnow()`.
///
/// ## Why is this bad?
/// Python datetime objects can be naive or timezone-aware. While an aware
/// object represents a specific moment in time, a naive object does not
/// contain enough information to unambiguously locate itself relative to other
/// datetime objects. Since this can lead to errors, it is recommended to
/// always use timezone-aware objects.
///
/// `datetime.datetime.utcnow()` returns a naive datetime object; instead, use
/// `datetime.datetime.now(tz=)` to return a timezone-aware object.
///
/// ## Example
/// ```python
/// import datetime
///
/// datetime.datetime.utcnow()
/// ```
///
/// Use instead:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime.now(tz=datetime.UTC)
/// ```
///
/// ## References
/// - [Python documentation: Aware and Naive Objects](https://docs.python.org/3/library/datetime.html#aware-and-naive-objects)
#[violation]
pub struct CallDatetimeUtcnow;
@@ -21,14 +57,6 @@ impl Violation for CallDatetimeUtcnow {
}
}
/// Checks for `datetime.datetime.today()`. (DTZ003)
///
/// ## Why is this bad?
///
/// Because naive `datetime` objects are treated by many `datetime` methods as
/// local times, it is preferred to use aware datetimes to represent times in
/// UTC. As such, the recommended way to create an object representing the
/// current time in UTC is by calling `datetime.now(timezone.utc)`.
pub(crate) fn call_datetime_utcnow(checker: &mut Checker, func: &Expr, location: TextRange) {
if !checker
.semantic()

View File

@@ -31,6 +31,13 @@ use super::helpers;
/// ```python
/// import datetime
///
/// datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.timezone.utc)
/// ```
///
/// Or, for Python 3.11 and later:
/// ```python
/// import datetime
///
/// datetime.datetime(2000, 1, 1, 0, 0, 0, tzinfo=datetime.UTC)
/// ```
#[violation]

View File

@@ -6,7 +6,7 @@ use crate::checkers::ast::Checker;
/// Check if the parent expression is a call to `astimezone`. This assumes that
/// the current expression is a `datetime.datetime` object.
pub(super) fn parent_expr_is_astimezone(checker: &Checker) -> bool {
checker.semantic().expr_parent().is_some_and( |parent| {
checker.semantic().current_expression_parent().is_some_and( |parent| {
matches!(parent, Expr::Attribute(ExprAttribute { attr, .. }) if attr.as_str() == "astimezone")
})
}

View File

@@ -210,7 +210,7 @@ pub(crate) fn string_in_exception(checker: &mut Checker, stmt: &Stmt, exc: &Expr
}
}
// Check for f-strings.
Expr::JoinedStr(_) => {
Expr::FString(_) => {
if checker.enabled(Rule::FStringInException) {
let mut diagnostic = Diagnostic::new(FStringInException, first.range());
if checker.patch(diagnostic.kind.rule()) {

View File

@@ -52,7 +52,7 @@ impl Violation for FStringInGetTextFuncCall {
/// INT001
pub(crate) fn f_string_in_gettext_func_call(checker: &mut Checker, args: &[Expr]) {
if let Some(first) = args.first() {
if first.is_joined_str_expr() {
if first.is_f_string_expr() {
checker
.diagnostics
.push(Diagnostic::new(FStringInGetTextFuncCall {}, first.range()));

View File

@@ -50,14 +50,14 @@ pub(crate) fn explicit(expr: &Expr, locator: &Locator) -> Option<Diagnostic> {
if matches!(op, Operator::Add) {
if matches!(
left.as_ref(),
Expr::JoinedStr(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Bytes(..),
..
})
) && matches!(
right.as_ref(),
Expr::JoinedStr(_)
Expr::FString(_)
| Expr::Constant(ast::ExprConstant {
value: Constant::Str(..) | Constant::Bytes(..),
..

View File

@@ -2,7 +2,7 @@ use rustc_hash::FxHashMap;
use ruff_diagnostics::{AutofixKind, Diagnostic, Fix, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_semantic::Binding;
use ruff_python_semantic::{Binding, Imported};
use crate::checkers::ast::Checker;
use crate::registry::AsRule;
@@ -56,11 +56,13 @@ pub(crate) fn unconventional_import_alias(
binding: &Binding,
conventions: &FxHashMap<String, String>,
) -> Option<Diagnostic> {
let Some(qualified_name) = binding.qualified_name() else {
let Some(import) = binding.as_any_import() else {
return None;
};
let Some(expected_alias) = conventions.get(qualified_name) else {
let qualified_name = import.qualified_name();
let Some(expected_alias) = conventions.get(qualified_name.as_str()) else {
return None;
};
@@ -71,7 +73,7 @@ pub(crate) fn unconventional_import_alias(
let mut diagnostic = Diagnostic::new(
UnconventionalImportAlias {
name: qualified_name.to_string(),
name: qualified_name,
asname: expected_alias.to_string(),
},
binding.range,

View File

@@ -62,7 +62,7 @@ fn check_msg(checker: &mut Checker, msg: &Expr) {
_ => {}
},
// Check for f-strings.
Expr::JoinedStr(_) => {
Expr::FString(_) => {
if checker.enabled(Rule::LoggingFString) {
checker
.diagnostics

View File

@@ -15,6 +15,15 @@ G004.py:5:27: G004 Logging statement uses f-string
4 | logging.info(f"Hello {name}")
5 | logging.log(logging.INFO, f"Hello {name}")
| ^^^^^^^^^^^^^^^ G004
6 |
7 | _LOGGER = logging.getLogger()
|
G004.py:8:14: G004 Logging statement uses f-string
|
7 | _LOGGER = logging.getLogger()
8 | _LOGGER.info(f"{__name__}")
| ^^^^^^^^^^^^^ G004
|

View File

@@ -66,7 +66,7 @@ pub(crate) fn any_eq_ne_annotation(checker: &mut Checker, name: &str, parameters
return;
};
if !checker.semantic().scope().kind.is_class() {
if !checker.semantic().current_scope().kind.is_class() {
return;
}

View File

@@ -2,9 +2,7 @@ use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast as ast;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_ast::{
Decorator, Expr, ParameterWithDefault, Parameters, Ranged, TypeParam, TypeParams,
};
use ruff_python_ast::{Decorator, Expr, Parameters, Ranged, TypeParam, TypeParams};
use ruff_python_semantic::analyze::visibility::{
is_abstract, is_classmethod, is_new, is_overload, is_staticmethod,
};
@@ -77,15 +75,23 @@ pub(crate) fn custom_type_var_return_type(
args: &Parameters,
type_params: Option<&TypeParams>,
) {
if args.args.is_empty() && args.posonlyargs.is_empty() {
return;
}
let Some(returns) = returns else {
// Given, e.g., `def foo(self: _S, arg: bytes) -> _T`, extract `_T`.
let Some(return_annotation) = returns else {
return;
};
if !checker.semantic().scope().kind.is_class() {
// Given, e.g., `def foo(self: _S, arg: bytes)`, extract `_S`.
let Some(self_or_cls_annotation) = args
.posonlyargs
.iter()
.chain(args.args.iter())
.next()
.and_then(|parameter_with_default| parameter_with_default.parameter.annotation.as_ref())
else {
return;
};
if !checker.semantic().current_scope().kind.is_class() {
return;
};
@@ -97,14 +103,12 @@ pub(crate) fn custom_type_var_return_type(
return;
}
let returns = map_subscript(returns);
let uses_custom_var: bool =
if is_classmethod(decorator_list, checker.semantic()) || is_new(name) {
class_method(args, returns, type_params)
class_method(self_or_cls_annotation, return_annotation, type_params)
} else {
// If not static, or a class method or __new__ we know it is an instance method
instance_method(args, returns, type_params)
instance_method(self_or_cls_annotation, return_annotation, type_params)
};
if uses_custom_var {
@@ -112,7 +116,7 @@ pub(crate) fn custom_type_var_return_type(
CustomTypeVarReturnType {
method_name: name.to_string(),
},
returns.range(),
return_annotation.range(),
));
}
}
@@ -120,17 +124,11 @@ pub(crate) fn custom_type_var_return_type(
/// Returns `true` if the class method is annotated with a custom `TypeVar` that is likely
/// private.
fn class_method(
args: &Parameters,
cls_annotation: &Expr,
return_annotation: &Expr,
type_params: Option<&TypeParams>,
) -> bool {
let ParameterWithDefault { parameter, .. } = &args.args[0];
let Some(annotation) = &parameter.annotation else {
return false;
};
let Expr::Subscript(ast::ExprSubscript { slice, value, .. }) = annotation.as_ref() else {
let Expr::Subscript(ast::ExprSubscript { slice, value, .. }) = cls_annotation else {
return false;
};
@@ -148,7 +146,7 @@ fn class_method(
return false;
};
let Expr::Name(return_annotation) = return_annotation else {
let Expr::Name(return_annotation) = map_subscript(return_annotation) else {
return false;
};
@@ -162,26 +160,20 @@ fn class_method(
/// Returns `true` if the instance method is annotated with a custom `TypeVar` that is likely
/// private.
fn instance_method(
args: &Parameters,
self_annotation: &Expr,
return_annotation: &Expr,
type_params: Option<&TypeParams>,
) -> bool {
let ParameterWithDefault { parameter, .. } = &args.args[0];
let Some(annotation) = &parameter.annotation else {
return false;
};
let Expr::Name(ast::ExprName {
id: first_arg_type, ..
}) = annotation.as_ref()
}) = self_annotation
else {
return false;
};
let Expr::Name(ast::ExprName {
id: return_type, ..
}) = return_annotation
}) = map_subscript(return_annotation)
else {
return false;
};

View File

@@ -242,11 +242,11 @@ fn check_positional_args(
/// Return the non-`None` annotation element of a PEP 604-style union or `Optional` annotation.
fn non_none_annotation_element<'a>(
annotation: &'a Expr,
model: &SemanticModel,
semantic: &SemanticModel,
) -> Option<&'a Expr> {
// E.g., `typing.Union` or `typing.Optional`
if let Expr::Subscript(ExprSubscript { value, slice, .. }) = annotation {
if model.match_typing_expr(value, "Optional") {
if semantic.match_typing_expr(value, "Optional") {
return if is_const_none(slice) {
None
} else {
@@ -254,7 +254,7 @@ fn non_none_annotation_element<'a>(
};
}
if !model.match_typing_expr(value, "Union") {
if !semantic.match_typing_expr(value, "Union") {
return None;
}
@@ -297,8 +297,8 @@ fn non_none_annotation_element<'a>(
}
/// Return `true` if the [`Expr`] is the `object` builtin or the `_typeshed.Unused` type.
fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool {
model
fn is_object_or_unused(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(expr)
.as_ref()
.is_some_and(|call_path| {
@@ -310,34 +310,34 @@ fn is_object_or_unused(expr: &Expr, model: &SemanticModel) -> bool {
}
/// Return `true` if the [`Expr`] is `BaseException`.
fn is_base_exception(expr: &Expr, model: &SemanticModel) -> bool {
model
fn is_base_exception(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(expr)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "BaseException"]))
}
/// Return `true` if the [`Expr`] is the `types.TracebackType` type.
fn is_traceback_type(expr: &Expr, model: &SemanticModel) -> bool {
model
fn is_traceback_type(expr: &Expr, semantic: &SemanticModel) -> bool {
semantic
.resolve_call_path(expr)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["types", "TracebackType"]))
}
/// Return `true` if the [`Expr`] is, e.g., `Type[BaseException]`.
fn is_base_exception_type(expr: &Expr, model: &SemanticModel) -> bool {
fn is_base_exception_type(expr: &Expr, semantic: &SemanticModel) -> bool {
let Expr::Subscript(ExprSubscript { value, slice, .. }) = expr else {
return false;
};
if model.match_typing_expr(value, "Type")
|| model
if semantic.match_typing_expr(value, "Type")
|| semantic
.resolve_call_path(value)
.as_ref()
.is_some_and(|call_path| matches!(call_path.as_slice(), ["" | "builtins", "type"]))
{
is_base_exception(slice, model)
is_base_exception(slice, semantic)
} else {
false
}

View File

@@ -1,9 +1,9 @@
use ruff_python_ast as ast;
use ruff_python_ast::{Ranged, Stmt};
use ruff_python_ast::Ranged;
use ruff_diagnostics::{Diagnostic, Violation};
use ruff_macros::{derive_message_formats, violation};
use ruff_python_ast::Expr;
use ruff_python_ast::helpers::map_subscript;
use ruff_python_semantic::{Definition, Member, MemberKind};
use crate::checkers::ast::Checker;
@@ -49,14 +49,14 @@ use crate::checkers::ast::Checker;
/// ```
#[violation]
pub struct IterMethodReturnIterable {
async_: bool,
is_async: bool,
}
impl Violation for IterMethodReturnIterable {
#[derive_message_formats]
fn message(&self) -> String {
let IterMethodReturnIterable { async_ } = self;
if *async_ {
let IterMethodReturnIterable { is_async } = self;
if *is_async {
format!("`__aiter__` methods should return an `AsyncIterator`, not an `AsyncIterable`")
} else {
format!("`__iter__` methods should return an `Iterator`, not an `Iterable`")
@@ -67,41 +67,31 @@ impl Violation for IterMethodReturnIterable {
/// PYI045
pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &Definition) {
let Definition::Member(Member {
kind: MemberKind::Method,
stmt,
kind: MemberKind::Method(function),
..
}) = definition
else {
return;
};
let Stmt::FunctionDef(ast::StmtFunctionDef { name, returns, .. }) = stmt else {
let Some(returns) = function.returns.as_ref() else {
return;
};
let Some(returns) = returns else {
return;
};
let annotation = if let Expr::Subscript(ast::ExprSubscript { value, .. }) = returns.as_ref() {
// Ex) `Iterable[T]`
value
} else {
// Ex) `Iterable`, `typing.Iterable`
returns
};
let async_ = match name.as_str() {
let is_async = match function.name.as_str() {
"__iter__" => false,
"__aiter__" => true,
_ => return,
};
// Support both `Iterable` and `Iterable[T]`.
let annotation = map_subscript(returns);
if checker
.semantic()
.resolve_call_path(annotation)
.resolve_call_path(map_subscript(annotation))
.is_some_and(|call_path| {
if async_ {
if is_async {
matches!(
call_path.as_slice(),
["typing", "AsyncIterable"] | ["collections", "abc", "AsyncIterable"]
@@ -115,7 +105,7 @@ pub(crate) fn iter_method_return_iterable(checker: &mut Checker, definition: &De
})
{
checker.diagnostics.push(Diagnostic::new(
IterMethodReturnIterable { async_ },
IterMethodReturnIterable { is_async },
returns.range(),
));
}

View File

@@ -113,13 +113,13 @@ impl Violation for NonSelfReturnType {
pub(crate) fn non_self_return_type(
checker: &mut Checker,
stmt: &Stmt,
is_async: bool,
name: &str,
decorator_list: &[Decorator],
returns: Option<&Expr>,
parameters: &Parameters,
async_: bool,
) {
let ScopeKind::Class(class_def) = checker.semantic().scope().kind else {
let ScopeKind::Class(class_def) = checker.semantic().current_scope().kind else {
return;
};
@@ -138,7 +138,7 @@ pub(crate) fn non_self_return_type(
return;
}
if async_ {
if is_async {
if name == "__aenter__"
&& is_name(returns, &class_def.name)
&& !is_final(&class_def.decorator_list, checker.semantic())

View File

@@ -121,7 +121,7 @@ impl fmt::Display for ExprType {
/// Return the [`ExprType`] of an [`Expr]` if it is a builtin type (e.g. `int`, `bool`, `float`,
/// `str`, `bytes`, or `complex`).
fn match_builtin_type(expr: &Expr, model: &SemanticModel) -> Option<ExprType> {
fn match_builtin_type(expr: &Expr, semantic: &SemanticModel) -> Option<ExprType> {
let name = expr.as_name_expr()?;
let result = match name.id.as_str() {
"int" => ExprType::Int,
@@ -132,7 +132,7 @@ fn match_builtin_type(expr: &Expr, model: &SemanticModel) -> Option<ExprType> {
"complex" => ExprType::Complex,
_ => return None,
};
if !model.is_builtin(name.id.as_str()) {
if !semantic.is_builtin(name.id.as_str()) {
return None;
}
Some(result)

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